Commit 2d084dd8 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'separate-banzai-references' into 'master'

Separate reference gathering from rendering

This is a required step to allow batch processing when gathering references. This in turn would allow grabbing (for example) all mentioned users of an issue/merge request using a single query.

cc @rspeicher @DouweM 

See merge request !3969
parents ef6fe42e 580d2501
...@@ -91,8 +91,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -91,8 +91,8 @@ class Projects::WikisController < Projects::ApplicationController
def markdown_preview def markdown_preview
text = params[:text] text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text) ext.analyze(text, author: current_user)
render json: { render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
......
...@@ -197,8 +197,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -197,8 +197,8 @@ class ProjectsController < Projects::ApplicationController
def markdown_preview def markdown_preview
text = params[:text] text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text) ext.analyze(text, author: current_user)
render json: { render json: {
body: view_context.markdown(text), body: view_context.markdown(text),
......
...@@ -23,6 +23,28 @@ class Ability ...@@ -23,6 +23,28 @@ class Ability
end.concat(global_abilities(user)) end.concat(global_abilities(user))
end end
# Given a list of users and a project this method returns the users that can
# read the given project.
def users_that_can_read_project(users, project)
if project.public?
users
else
users.select do |user|
if user.admin?
true
elsif project.internal? && !user.external?
true
elsif project.owner == user
true
elsif project.team.members.include?(user)
true
else
false
end
end
end
end
# List of possible abilities for anonymous user # List of possible abilities for anonymous user
def anonymous_abilities(user, subject) def anonymous_abilities(user, subject)
if subject.is_a?(PersonalSnippet) if subject.is_a?(PersonalSnippet)
......
...@@ -8,7 +8,10 @@ class Commit ...@@ -8,7 +8,10 @@ class Commit
include StaticModel include StaticModel
attr_mentionable :safe_message, pipeline: :single_line attr_mentionable :safe_message, pipeline: :single_line
participant :author, :committer, :notes
participant :author
participant :committer
participant :notes_with_associations
attr_accessor :project attr_accessor :project
...@@ -194,6 +197,10 @@ class Commit ...@@ -194,6 +197,10 @@ class Commit
project.notes.for_commit_id(self.id) project.notes.for_commit_id(self.id)
end end
def notes_with_associations
notes.includes(:author, :project)
end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
@raw.send(m, *args, &block) @raw.send(m, *args, &block)
end end
...@@ -219,7 +226,7 @@ class Commit ...@@ -219,7 +226,7 @@ class Commit
def revert_branch_name def revert_branch_name
"revert-#{short_id}" "revert-#{short_id}"
end end
def cherry_pick_branch_name def cherry_pick_branch_name
project.repository.next_branch("cherry-pick-#{short_id}", mild: true) project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end end
...@@ -251,11 +258,13 @@ class Commit ...@@ -251,11 +258,13 @@ class Commit
end end
def has_been_reverted?(current_user = nil, noteable = self) def has_been_reverted?(current_user = nil, noteable = self)
Gitlab::ReferenceExtractor.lazily do ext = all_references(current_user)
noteable.notes.system.flat_map do |note|
note.all_references(current_user).commits noteable.notes_with_associations.system.each do |note|
end note.all_references(current_user, extractor: ext)
end.any? { |commit_ref| commit_ref.reverts_commit?(self) } end
ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end end
def change_type_title def change_type_title
......
...@@ -62,7 +62,7 @@ class CommitRange ...@@ -62,7 +62,7 @@ class CommitRange
def initialize(range_string, project) def initialize(range_string, project)
@project = project @project = project
range_string.strip! range_string = range_string.strip
unless range_string =~ /\A#{PATTERN}\z/ unless range_string =~ /\A#{PATTERN}\z/
raise ArgumentError, "invalid CommitRange string format: #{range_string}" raise ArgumentError, "invalid CommitRange string format: #{range_string}"
......
...@@ -59,8 +59,12 @@ module Issuable ...@@ -59,8 +59,12 @@ module Issuable
prefix: true prefix: true
attr_mentionable :title, pipeline: :single_line attr_mentionable :title, pipeline: :single_line
attr_mentionable :description, cache: true attr_mentionable :description
participant :author, :assignee, :notes_with_associations
participant :author
participant :assignee
participant :notes_with_associations
strip_attributes :title strip_attributes :title
acts_as_paranoid acts_as_paranoid
......
...@@ -23,7 +23,7 @@ module Mentionable ...@@ -23,7 +23,7 @@ module Mentionable
included do included do
if self < Participable if self < Participable
participant ->(current_user) { mentioned_users(current_user) } participant -> (user, ext) { all_references(user, extractor: ext) }
end end
end end
...@@ -43,23 +43,22 @@ module Mentionable ...@@ -43,23 +43,22 @@ module Mentionable
self self
end end
def all_references(current_user = nil, text = nil) def all_references(current_user = nil, text = nil, extractor: nil)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user || self.author, self.author) extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user || author)
if text if text
ext.analyze(text) extractor.analyze(text, author: author)
else else
self.class.mentionable_attrs.each do |attr, options| self.class.mentionable_attrs.each do |attr, options|
text = send(attr) text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
context = options.dup extractor.analyze(text, options)
context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
ext.analyze(text, context)
end end
end end
ext extractor
end end
def mentioned_users(current_user = nil) def mentioned_users(current_user = nil)
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
# Contains functionality related to objects that can have participants, such as # Contains functionality related to objects that can have participants, such as
# an author, an assignee and people mentioned in its description or comments. # an author, an assignee and people mentioned in its description or comments.
# #
# Used by Issue, Note, MergeRequest, Snippet and Commit.
#
# Usage: # Usage:
# #
# class Issue < ActiveRecord::Base # class Issue < ActiveRecord::Base
...@@ -12,22 +10,36 @@ ...@@ -12,22 +10,36 @@
# #
# # ... # # ...
# #
# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) } # participant :author
# participant :assignee
# participant :notes
#
# participant -> (current_user, ext) do
# ext.analyze('...')
# end
# end # end
# #
# issue = Issue.last # issue = Issue.last
# users = issue.participants # users = issue.participants
# # `users` will contain the issue's author, its assignee,
# # all users returned by its #mentioned_users method,
# # as well as all participants to all of the issue's notes,
# # since Note implements Participable as well.
#
module Participable module Participable
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods module ClassMethods
def participant(*attrs) # Adds a list of participant attributes. Attributes can either be symbols or
participant_attrs.concat(attrs) # Procs.
#
# When using a Proc instead of a Symbol the Proc will be given two
# arguments:
#
# 1. The current user (as an instance of User)
# 2. An instance of `Gitlab::ReferenceExtractor`
#
# It is expected that a Proc populates the given reference extractor
# instance with data. The return value of the Proc is ignored.
#
# attr - The name of the attribute or a Proc
def participant(attr)
participant_attrs << attr
end end
def participant_attrs def participant_attrs
...@@ -35,42 +47,42 @@ module Participable ...@@ -35,42 +47,42 @@ module Participable
end end
end end
# Be aware that this method makes a lot of sql queries. # Returns the users participating in a discussion.
# Save result into variable if you are going to reuse it inside same request #
def participants(current_user = self.author) # This method processes attributes of objects in breadth-first order.
participants = #
Gitlab::ReferenceExtractor.lazily do # Returns an Array of User instances.
self.class.participant_attrs.flat_map do |attr| def participants(current_user = nil)
value = current_user ||= author
if attr.respond_to?(:call) ext = Gitlab::ReferenceExtractor.new(project, current_user)
instance_exec(current_user, &attr) participants = Set.new
else process = [self]
send(attr)
end
participants_for(value, current_user) until process.empty?
end.compact.uniq source = process.pop
end
unless Gitlab::ReferenceExtractor.lazy? case source
participants.select! do |user| when User
user.can?(:read_project, project) participants << source
when Participable
source.class.participant_attrs.each do |attr|
if attr.respond_to?(:call)
source.instance_exec(current_user, ext, &attr)
else
process << source.__send__(attr)
end
end
when Enumerable, ActiveRecord::Relation
# This uses reverse_each so we can use "pop" to get the next value to
# process (in order). Using unshift instead of pop would require
# moving all Array values one index to the left (which can be
# expensive).
source.reverse_each { |obj| process << obj }
end end
end end
participants participants.merge(ext.users)
end
private
def participants_for(value, current_user = nil) Ability.users_that_can_read_project(participants.to_a, project)
case value
when User, Banzai::LazyReference
[value]
when Enumerable, ActiveRecord::Relation
value.flat_map { |v| participants_for(v, current_user) }
when Participable
value.participants(current_user)
end
end end
end end
...@@ -95,14 +95,13 @@ class Issue < ActiveRecord::Base ...@@ -95,14 +95,13 @@ class Issue < ActiveRecord::Base
end end
def referenced_merge_requests(current_user = nil) def referenced_merge_requests(current_user = nil)
@referenced_merge_requests ||= {} ext = all_references(current_user)
@referenced_merge_requests[current_user] ||= begin
Gitlab::ReferenceExtractor.lazily do notes_with_associations.each do |object|
[self, *notes].flat_map do |note| object.all_references(current_user, extractor: ext)
note.all_references(current_user).merge_requests
end
end.sort_by(&:iid).uniq
end end
ext.merge_requests.sort_by(&:iid)
end end
# All branches containing the current issue's ID, except for # All branches containing the current issue's ID, except for
...@@ -139,9 +138,13 @@ class Issue < ActiveRecord::Base ...@@ -139,9 +138,13 @@ class Issue < ActiveRecord::Base
def closed_by_merge_requests(current_user = nil) def closed_by_merge_requests(current_user = nil)
return [] unless open? return [] unless open?
notes.system.flat_map do |note| ext = all_references(current_user)
note.all_references(current_user).merge_requests
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } notes.system.each do |note|
note.all_references(current_user, extractor: ext)
end
ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
end end
def moved? def moved?
......
...@@ -6,7 +6,7 @@ class Note < ActiveRecord::Base ...@@ -6,7 +6,7 @@ class Note < ActiveRecord::Base
default_value_for :system, false default_value_for :system, false
attr_mentionable :note, cache: true, pipeline: :note attr_mentionable :note, pipeline: :note
participant :author participant :author
belongs_to :project belongs_to :project
......
...@@ -7,5 +7,6 @@ class ProjectSnippet < Snippet ...@@ -7,5 +7,6 @@ class ProjectSnippet < Snippet
# Scopes # Scopes
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
participant :author, :notes participant :author
participant :notes_with_associations
end end
...@@ -30,7 +30,8 @@ class Snippet < ActiveRecord::Base ...@@ -30,7 +30,8 @@ class Snippet < ActiveRecord::Base
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
participant :author, :notes participant :author
participant :notes_with_associations
def self.reference_prefix def self.reference_prefix
'$' '$'
...@@ -100,6 +101,10 @@ class Snippet < ActiveRecord::Base ...@@ -100,6 +101,10 @@ class Snippet < ActiveRecord::Base
content.lines.count > 1000 content.lines.count > 1000
end end
def notes_with_associations
notes.includes(:author, :project)
end
class << self class << self
# Searches for snippets with a matching title or file name. # Searches for snippets with a matching title or file name.
# #
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.light.small .light.small
= time_ago_with_tooltip(abuse_report.created_at) = time_ago_with_tooltip(abuse_report.created_at)
%td %td
= markdown(abuse_report.message.squish!, pipeline: :single_line) = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td %td
- if user - if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
.commit-row-title .commit-row-title
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id]) = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
&middot; &middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
= markdown(issue.description, pipeline: :atom, project: issue.project) = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author)
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
= markdown(merge_request.description, pipeline: :atom, project: merge_request.project) = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author)
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
= markdown(note.note, pipeline: :atom, project: note.project) = markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
...@@ -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]), pipeline: :atom, project: event.project) %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author)
- if event.commits_count > 15 - if event.commits_count > 15
%p %p
%i %i
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%ul.well-list.event_commits %ul.well-list.event_commits
- few_commits = event.commits[0...2] - few_commits = event.commits[0...2]
- few_commits.each do |commit| - few_commits.each do |commit|
= render "events/commit", commit: commit, project: project = render "events/commit", commit: commit, project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project)
- if event.commits_count > 1 - if event.commits_count > 1
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
%div %div
#{link_to @note.author_name, user_url(@note.author)} wrote: #{link_to @note.author_name, user_url(@note.author)} wrote:
%div %div
= markdown(@note.note, pipeline: :email) = markdown(@note.note, pipeline: :email, author: @note.author)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%div %div
#{link_to @issue.author_name, user_url(@issue.author)} wrote: #{link_to @issue.author_name, user_url(@issue.author)} wrote:
-if @issue.description -if @issue.description
= markdown(@issue.description, pipeline: :email) = markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present? - if @issue.assignee_id.present?
%p %p
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,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, pipeline: :email) = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
...@@ -63,10 +63,10 @@ ...@@ -63,10 +63,10 @@
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
- if @commit.description.present? - if @commit.description.present?
%pre.commit-description %pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
:javascript :javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
- if commit.description? - if commit.description?
.commit-row-description.js-toggle-content .commit-row-description.js-toggle-content
%pre %pre
= preserve(markdown(escape_once(commit.description), pipeline: :single_line)) = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
.commit-row-info .commit-row-info
by by
......
...@@ -52,12 +52,12 @@ ...@@ -52,12 +52,12 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@issue.title), pipeline: :single_line = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki .wiki
= preserve do = preserve do
= markdown(@issue.description, cache_key: [@issue, "description"]) = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @issue.description = @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
......
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@merge_request.title), pipeline: :single_line = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
%div %div
- if @merge_request.description.present? - if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki .wiki
= preserve do = preserve do
= markdown(@merge_request.description, cache_key: [@merge_request, "description"]) = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @merge_request.description = @merge_request.description
......
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
%i.fa.fa-check %i.fa.fa-check
Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)}
= succeed '.' do = succeed '.' do
!= markdown issues_sentence(@closes_issues), pipeline: :gfm != markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
.note-body{class: note_editable ? 'js-task-list-container' : ''} .note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"]) = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id %code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
= markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
%td %td
%span.pull-right.cgray %span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date) = time_ago_with_tooltip(commit.committed_date)
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if issue.description.present? - if issue.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project })) = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
%span.light %span.light
#{issue.project.name_with_namespace} #{issue.project.name_with_namespace}
- if issue.closed? - if issue.closed?
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if merge_request.description.present? - if merge_request.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(merge_request.description, { project: merge_request.project })) = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
%span.light %span.light
#{merge_request.project.name_with_namespace} #{merge_request.project.name_with_namespace}
.pull-right .pull-right
......
...@@ -19,4 +19,4 @@ ...@@ -19,4 +19,4 @@
.note-search-result .note-search-result
.term .term
= preserve do = preserve do
= search_md_sanitize(markdown(note.note, {no_header_anchors: true})) = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
...@@ -21,4 +21,4 @@ ...@@ -21,4 +21,4 @@
.content-block.second-block .content-block.second-block
%h2.snippet-title.prepend-top-0.append-bottom-0 %h2.snippet-title.prepend-top-0.append-bottom-0
= markdown escape_once(@snippet.title), pipeline: :single_line = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
...@@ -18,10 +18,6 @@ module Banzai ...@@ -18,10 +18,6 @@ module Banzai
@object_sym ||= object_name.to_sym @object_sym ||= object_name.to_sym
end end
def self.data_reference
@data_reference ||= "data-#{object_name.dasherize}"
end
def self.object_class_title def self.object_class_title
@object_title ||= object_class.name.titleize @object_title ||= object_class.name.titleize
end end
...@@ -45,10 +41,6 @@ module Banzai ...@@ -45,10 +41,6 @@ module Banzai
end end
end end
def self.referenced_by(node)
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
end
def object_class def object_class
self.class.object_class self.class.object_class
end end
......
...@@ -4,6 +4,8 @@ module Banzai ...@@ -4,6 +4,8 @@ module Banzai
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range
def self.object_class def self.object_class
CommitRange CommitRange
end end
...@@ -14,34 +16,18 @@ module Banzai ...@@ -14,34 +16,18 @@ module Banzai
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 = find_object(project, id)
return unless range
{ commit_range: range }
end
def initialize(*args) def initialize(*args)
super super
@commit_map = {} @commit_map = {}
end end
def self.find_object(project, id) def find_object(project, id)
range = CommitRange.new(id, project) range = CommitRange.new(id, project)
range.valid_commits? ? range : nil range.valid_commits? ? range : nil
end end
def find_object(*args)
self.class.find_object(*args)
end
def url_for_object(range, project) def url_for_object(range, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project, h.namespace_project_compare_url(project.namespace, project,
......
...@@ -4,6 +4,8 @@ module Banzai ...@@ -4,6 +4,8 @@ module Banzai
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit
def self.object_class def self.object_class
Commit Commit
end end
...@@ -14,28 +16,12 @@ module Banzai ...@@ -14,28 +16,12 @@ module Banzai
end end
end end
def self.referenced_by(node) def find_object(project, id)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-commit")
commit = find_object(project, id)
return unless commit
{ commit: commit }
end
def self.find_object(project, id)
if project && project.valid_repo? if project && project.valid_repo?
project.commit(id) project.commit(id)
end end
end end
def find_object(*args)
self.class.find_object(*args)
end
def url_for_object(commit, project) def url_for_object(commit, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit, h.namespace_project_commit_url(project.namespace, project, commit,
......
...@@ -4,6 +4,8 @@ module Banzai ...@@ -4,6 +4,8 @@ module Banzai
# References are ignored if the project doesn't use an external issue # References are ignored if the project doesn't use an external issue
# tracker. # tracker.
class ExternalIssueReferenceFilter < ReferenceFilter class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
# Public: Find `JIRA-123` issue references in text # Public: Find `JIRA-123` issue references in text
# #
# ExternalIssueReferenceFilter.references_in(text) do |match, issue| # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
...@@ -21,18 +23,6 @@ module Banzai ...@@ -21,18 +23,6 @@ module Banzai
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-external-issue")
external_issue = ExternalIssue.new(id, project)
return unless external_issue
{ external_issue: external_issue }
end
def call def call
# Early return if the project isn't using an external tracker # Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker? return doc if project.nil? || default_issues_tracker?
......
...@@ -5,18 +5,12 @@ module Banzai ...@@ -5,18 +5,12 @@ module Banzai
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class IssueReferenceFilter < AbstractReferenceFilter class IssueReferenceFilter < AbstractReferenceFilter
self.reference_type = :issue
def self.object_class def self.object_class
Issue Issue
end end
def self.user_can_see_reference?(user, node, context)
# It is not possible to check access rights for external issue trackers
return true if context[:project].try(:external_issue_tracker)
issue = Issue.find(node.attr('data-issue')) rescue nil
Ability.abilities.allowed?(user, :read_issue, issue)
end
def find_object(project, id) def find_object(project, id)
project.get_issue(id) project.get_issue(id)
end end
......
...@@ -2,6 +2,8 @@ module Banzai ...@@ -2,6 +2,8 @@ module Banzai
module Filter module Filter
# HTML filter that replaces label references with links. # HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label
def self.object_class def self.object_class
Label Label
end end
......
...@@ -5,6 +5,8 @@ module Banzai ...@@ -5,6 +5,8 @@ module Banzai
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class MergeRequestReferenceFilter < AbstractReferenceFilter class MergeRequestReferenceFilter < AbstractReferenceFilter
self.reference_type = :merge_request
def self.object_class def self.object_class
MergeRequest MergeRequest
end end
......
...@@ -2,6 +2,8 @@ module Banzai ...@@ -2,6 +2,8 @@ module Banzai
module Filter module Filter
# HTML filter that replaces milestone references with links. # HTML filter that replaces milestone references with links.
class MilestoneReferenceFilter < AbstractReferenceFilter class MilestoneReferenceFilter < AbstractReferenceFilter
self.reference_type = :milestone
def self.object_class def self.object_class
Milestone Milestone
end end
......
...@@ -7,8 +7,11 @@ module Banzai ...@@ -7,8 +7,11 @@ module Banzai
# #
class RedactorFilter < HTML::Pipeline::Filter class RedactorFilter < HTML::Pipeline::Filter
def call def call
Querying.css(doc, 'a.gfm').each do |node| nodes = Querying.css(doc, 'a.gfm[data-reference-type]')
unless user_can_see_reference?(node) visible = nodes_visible_to_user(nodes)
nodes.each do |node|
unless visible.include?(node)
# The reference should be replaced by the original text, # The reference should be replaced by the original text,
# which is not always the same as the rendered text. # which is not always the same as the rendered text.
text = node.attr('data-original') || node.text text = node.attr('data-original') || node.text
...@@ -21,20 +24,30 @@ module Banzai ...@@ -21,20 +24,30 @@ module Banzai
private private
def user_can_see_reference?(node) def nodes_visible_to_user(nodes)
if node.has_attribute?('data-reference-filter') per_type = Hash.new { |h, k| h[k] = [] }
reference_type = node.attr('data-reference-filter') visible = Set.new
reference_filter = Banzai::Filter.const_get(reference_type)
nodes.each do |node|
per_type[node.attr('data-reference-type')] << node
end
per_type.each do |type, nodes|
parser = Banzai::ReferenceParser[type].new(project, current_user)
reference_filter.user_can_see_reference?(current_user, node, context) visible.merge(parser.nodes_visible_to_user(current_user, nodes))
else
true
end end
visible
end end
def current_user def current_user
context[:current_user] context[:current_user]
end end
def project
context[:project]
end
end end
end end
end end
...@@ -8,24 +8,8 @@ module Banzai ...@@ -8,24 +8,8 @@ module Banzai
# :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.
class ReferenceFilter < HTML::Pipeline::Filter class ReferenceFilter < HTML::Pipeline::Filter
def self.user_can_see_reference?(user, node, context) class << self
if node.has_attribute?('data-project') attr_accessor :reference_type
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
def self.user_can_reference?(user, node, context)
true
end
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
...@@ -43,7 +27,9 @@ module Banzai ...@@ -43,7 +27,9 @@ module Banzai
# #
# Returns a String # Returns a String
def data_attribute(attributes = {}) def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize attributes = attributes.reject { |_, v| v.nil? }
attributes[:reference_type] = self.class.reference_type
attributes.delete(:original) if context[:no_original_data] attributes.delete(:original) if context[:no_original_data]
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end end
......
module Banzai
module Filter
# 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
Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
load_lazy_references unless ReferenceExtractor.lazy?
doc
end
private
def gather_references(node)
return unless node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = Banzai::Filter.const_get(reference_type)
return if context[:reference_filter] && reference_filter != context[:reference_filter]
return if author && !reference_filter.user_can_reference?(author, node, context)
return unless reference_filter.user_can_see_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
def load_lazy_references
refs = result[:references]
refs.each do |type, values|
refs[type] = ReferenceExtractor.lazily(values)
end
end
def current_user
context[:current_user]
end
def author
context[:author]
end
end
end
end
...@@ -5,6 +5,8 @@ module Banzai ...@@ -5,6 +5,8 @@ module Banzai
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet
def self.object_class def self.object_class
Snippet Snippet
end end
......
...@@ -4,6 +4,8 @@ module Banzai ...@@ -4,6 +4,8 @@ module Banzai
# #
# A special `@all` reference is also supported. # A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter class UserReferenceFilter < ReferenceFilter
self.reference_type = :user
# Public: Find `@user` user references in text # Public: Find `@user` user references in text
# #
# UserReferenceFilter.references_in(text) do |match, username| # UserReferenceFilter.references_in(text) do |match, username|
...@@ -21,43 +23,6 @@ module Banzai ...@@ -21,43 +23,6 @@ module Banzai
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_see_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 self.user_can_reference?(user, node, context)
# Only team members can reference `@all`
if node.has_attribute?('data-project')
project = Project.find(node.attr('data-project')) rescue nil
return false unless project
user && project.team.member?(user)
else
super
end
end
def call def call
return doc if project.nil? return doc if project.nil?
...@@ -114,9 +79,12 @@ module Banzai ...@@ -114,9 +79,12 @@ module Banzai
def link_to_all(link_text: nil) def link_to_all(link_text: nil)
project = context[:project] project = context[:project]
author = context[:author]
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)
data = data_attribute(project: project.id, author: author.try(:id))
text = link_text || User.reference_prefix + 'all' text = link_text || User.reference_prefix + 'all'
link_tag(url, data, text) link_tag(url, data, text)
......
module Banzai
class LazyReference
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
attr_reader :klass, :ids
def initialize(klass, ids)
@klass = klass
@ids = Array.wrap(ids).map(&:to_i)
end
def load
self.klass.where(id: self.ids)
end
end
end
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
def self.filters
FilterArray[
Filter::ReferenceGathererFilter
]
end
end
end
end
module Banzai module Banzai
# 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
class << self
LAZY_KEY = :banzai_reference_extractor_lazy
def lazy?
Thread.current[LAZY_KEY]
end
def lazily(values = nil, &block)
return (values || block.call).uniq if lazy?
begin
Thread.current[LAZY_KEY] = true
values ||= block.call
Banzai::LazyReference.load(values.uniq).uniq
ensure
Thread.current[LAZY_KEY] = false
end
end
end
def initialize def initialize
@texts = [] @texts = []
end end
...@@ -31,23 +9,21 @@ module Banzai ...@@ -31,23 +9,21 @@ module Banzai
@texts << Renderer.render(text, context) @texts << Renderer.render(text, context)
end end
def references(type, context = {}) def references(type, project, current_user = nil)
filter = Banzai::Filter["#{type}_reference"] processor = Banzai::ReferenceParser[type].
new(project, current_user)
processor.process(html_documents)
end
context.merge!( private
pipeline: :reference_extraction,
# ReferenceGathererFilter def html_documents
reference_filter: filter # This ensures that we don't memoize anything until we have a number of
) # text blobs to parse.
return [] if @texts.empty?
self.class.lazily do @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) }
@texts.flat_map do |html|
text_context = context.dup
result = Renderer.render_result(html, text_context)
result[:references][type]
end.uniq
end
end end
end end
end end
module Banzai
module ReferenceParser
# Returns the reference parser class for the given type
#
# Example:
#
# Banzai::ReferenceParser['issue']
#
# This would return the `Banzai::ReferenceParser::IssueParser` class.
def self.[](name)
const_get("#{name.to_s.camelize}Parser")
end
end
end
module Banzai
module ReferenceParser
# Base class for reference parsing classes.
#
# Each parser should also specify its reference type by calling
# `self.reference_type = ...` in the body of the class. The value of this
# method should be a symbol such as `:issue` or `:merge_request`. For
# example:
#
# class IssueParser < BaseParser
# self.reference_type = :issue
# end
#
# The reference type is used to determine what nodes to pass to the
# `referenced_by` method.
#
# Parser classes should either implement the instance method
# `references_relation` or overwrite `referenced_by`. The
# `references_relation` method is supposed to return an
# ActiveRecord::Relation used as a base relation for retrieving the objects
# referenced in a set of HTML nodes.
#
# Each class can implement two additional methods:
#
# * `nodes_user_can_reference`: returns an Array of nodes the given user can
# refer to.
# * `nodes_visible_to_user`: returns an Array of nodes that are visible to
# the given user.
#
# You only need to overwrite these methods if you want to tweak who can see
# which references. For example, the IssueParser class defines its own
# `nodes_visible_to_user` method so it can ensure users can only see issues
# they have access to.
class BaseParser
class << self
attr_accessor :reference_type
end
# Returns the attribute name containing the value for every object to be
# parsed by the current parser.
#
# For example, for a parser class that returns "Animal" objects this
# attribute would be "data-animal".
def self.data_attribute
@data_attribute ||= "data-#{reference_type.to_s.dasherize}"
end
def initialize(project = nil, current_user = nil)
@project = project
@current_user = current_user
end
# Returns all the nodes containing references that the user can refer to.
def nodes_user_can_reference(user, nodes)
nodes
end
# Returns all the nodes that are visible to the given user.
def nodes_visible_to_user(user, nodes)
projects = lazy { projects_for_nodes(nodes) }
project_attr = 'data-project'
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
if project && project.id == node_id
true
else
can?(user, :read_project, projects[node_id])
end
else
true
end
end
end
# Returns an Array of objects referenced by any of the given HTML nodes.
def referenced_by(nodes)
ids = unique_attribute_values(nodes, self.class.data_attribute)
references_relation.where(id: ids)
end
# Returns the ActiveRecord::Relation to use for querying references in the
# DB.
def references_relation
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
# Returns a Hash containing attribute values per project ID.
#
# The returned Hash uses the following format:
#
# { project id => [value1, value2, ...] }
#
# nodes - An Array of HTML nodes to process.
# attribute - The name of the attribute (as a String) for which to gather
# values.
#
# Returns a Hash.
def gather_attributes_per_project(nodes, attribute)
per_project = Hash.new { |hash, key| hash[key] = Set.new }
nodes.each do |node|
project_id = node.attr('data-project').to_i
id = node.attr(attribute)
per_project[project_id] << id if id
end
per_project
end
# Returns a Hash containing objects for an attribute grouped per their
# IDs.
#
# The returned Hash uses the following format:
#
# { id value => row }
#
# nodes - An Array of HTML nodes to process.
#
# collection - The model or ActiveRecord relation to use for retrieving
# rows from the database.
#
# attribute - The name of the attribute containing the primary key values
# for every row.
#
# Returns a Hash.
def grouped_objects_for_nodes(nodes, collection, attribute)
return {} if nodes.empty?
ids = unique_attribute_values(nodes, attribute)
collection.where(id: ids).each_with_object({}) do |row, hash|
hash[row.id] = row
end
end
# Returns an Array containing all unique values of an attribute of the
# given nodes.
def unique_attribute_values(nodes, attribute)
values = Set.new
nodes.each do |node|
if node.has_attribute?(attribute)
values << node.attr(attribute)
end
end
values.to_a
end
# Processes the list of HTML documents and returns an Array containing all
# the references.
def process(documents)
type = self.class.reference_type
nodes = documents.flat_map do |document|
Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
end
gather_references(nodes)
end
# Gathers the references for the given HTML nodes.
def gather_references(nodes)
nodes = nodes_user_can_reference(current_user, nodes)
nodes = nodes_visible_to_user(current_user, nodes)
referenced_by(nodes)
end
# Returns a Hash containing the projects for a given list of HTML nodes.
#
# The returned Hash uses the following format:
#
# { project ID => project }
#
def projects_for_nodes(nodes)
@projects_for_nodes ||=
grouped_objects_for_nodes(nodes, Project, 'data-project')
end
def can?(user, permission, subject)
Ability.abilities.allowed?(user, permission, subject)
end
def find_projects_for_hash_keys(hash)
Project.where(id: hash.keys)
end
private
attr_reader :current_user, :project
def lazy(&block)
Gitlab::Lazy.new(&block)
end
end
end
end
module Banzai
module ReferenceParser
class CommitParser < BaseParser
self.reference_type = :commit
def referenced_by(nodes)
commit_ids = commit_ids_per_project(nodes)
projects = find_projects_for_hash_keys(commit_ids)
projects.flat_map do |project|
find_commits(project, commit_ids[project.id])
end
end
def commit_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
def find_commits(project, ids)
commits = []
return commits unless project.valid_repo?
ids.each do |id|
commit = project.commit(id)
commits << commit if commit
end
commits
end
end
end
end
module Banzai
module ReferenceParser
class CommitRangeParser < BaseParser
self.reference_type = :commit_range
def referenced_by(nodes)
range_ids = commit_range_ids_per_project(nodes)
projects = find_projects_for_hash_keys(range_ids)
projects.flat_map do |project|
find_ranges(project, range_ids[project.id])
end
end
def commit_range_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
def find_ranges(project, range_ids)
ranges = []
range_ids.each do |id|
range = find_object(project, id)
ranges << range if range
end
ranges
end
def find_object(project, id)
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
end
end
end
end
module Banzai
module ReferenceParser
class ExternalIssueParser < BaseParser
self.reference_type = :external_issue
def referenced_by(nodes)
issue_ids = issue_ids_per_project(nodes)
projects = find_projects_for_hash_keys(issue_ids)
issues = []
projects.each do |project|
issue_ids[project.id].each do |id|
issues << ExternalIssue.new(id, project)
end
end
issues
end
def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
end
end
end
module Banzai
module ReferenceParser
class IssueParser < BaseParser
self.reference_type = :issue
def nodes_visible_to_user(user, nodes)
# It is not possible to check access rights for external issue trackers
return nodes if project && project.external_issue_tracker
issues = issues_for_nodes(nodes)
nodes.select do |node|
issue = issue_for_node(issues, node)
issue ? can?(user, :read_issue, issue) : false
end
end
def referenced_by(nodes)
issues = issues_for_nodes(nodes)
nodes.map { |node| issue_for_node(issues, node) }.uniq
end
def issues_for_nodes(nodes)
@issues_for_nodes ||= grouped_objects_for_nodes(
nodes,
Issue.all.includes(:author, :assignee, :project),
self.class.data_attribute
)
end
private
def issue_for_node(issues, node)
issues[node.attr(self.class.data_attribute).to_i]
end
end
end
end
module Banzai
module ReferenceParser
class LabelParser < BaseParser
self.reference_type = :label
def references_relation
Label
end
end
end
end
module Banzai
module ReferenceParser
class MergeRequestParser < BaseParser
self.reference_type = :merge_request
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
end
end
end
end
module Banzai
module ReferenceParser
class MilestoneParser < BaseParser
self.reference_type = :milestone
def references_relation
Milestone
end
end
end
end
module Banzai
module ReferenceParser
class SnippetParser < BaseParser
self.reference_type = :snippet
def references_relation
Snippet
end
end
end
end
module Banzai
module ReferenceParser
class UserParser < BaseParser
self.reference_type = :user
def referenced_by(nodes)
group_ids = []
user_ids = []
project_ids = []
nodes.each do |node|
if node.has_attribute?('data-group')
group_ids << node.attr('data-group').to_i
elsif node.has_attribute?(self.class.data_attribute)
user_ids << node.attr(self.class.data_attribute).to_i
elsif node.has_attribute?('data-project')
project_ids << node.attr('data-project').to_i
end
end
find_users_for_groups(group_ids) | find_users(user_ids) |
find_users_for_projects(project_ids)
end
def nodes_visible_to_user(user, nodes)
group_attr = 'data-group'
groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) }
visible = []
remaining = []
nodes.each do |node|
if node.has_attribute?(group_attr)
node_group = groups[node.attr(group_attr).to_i]
if node_group &&
can?(user, :read_group, node_group)
visible << node
end
# Remaining nodes will be processed by the parent class'
# implementation of this method.
else
remaining << node
end
end
visible + super(current_user, remaining)
end
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
projects = lazy { projects_for_nodes(nodes) }
users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) }
nodes.select do |node|
project_id = node.attr(project_attr)
user_id = node.attr(author_attr)
if project && project_id && project.id == project_id.to_i
true
elsif project_id && user_id
project = projects[project_id.to_i]
user = users[user_id.to_i]
project && user ? project.team.member?(user) : false
else
true
end
end
end
def find_users(ids)
return [] if ids.empty?
User.where(id: ids).to_a
end
def find_users_for_groups(ids)
return [] if ids.empty?
User.joins(:group_members).where(members: { source_id: ids }).to_a
end
def find_users_for_projects(ids)
return [] if ids.empty?
Project.where(id: ids).flat_map { |p| p.team.members.to_a }
end
end
end
end
module Gitlab
# A class that can be wrapped around an expensive method call so it's only
# executed when actually needed.
#
# Usage:
#
# object = Gitlab::Lazy.new { some_expensive_work_here }
#
# object['foo']
# object.bar
class Lazy < BasicObject
def initialize(&block)
@block = block
end
def method_missing(name, *args, &block)
__evaluate__
@result.__send__(name, *args, &block)
end
def respond_to_missing?(name, include_private = false)
__evaluate__
@result.respond_to?(name, include_private) || super
end
private
def __evaluate__
@result = @block.call unless defined?(@result)
end
end
end
...@@ -4,10 +4,9 @@ module Gitlab ...@@ -4,10 +4,9 @@ module Gitlab
REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range) REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
attr_accessor :project, :current_user, :author attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil, author = nil) def initialize(project, current_user = nil)
@project = project @project = project
@current_user = current_user @current_user = current_user
@author = author
@references = {} @references = {}
...@@ -18,17 +17,21 @@ module Gitlab ...@@ -18,17 +17,21 @@ module Gitlab
super(text, context.merge(project: project)) super(text, context.merge(project: project))
end end
def references(type)
super(type, project, current_user)
end
REFERABLES.each do |type| REFERABLES.each do |type|
define_method("#{type}s") do define_method("#{type}s") do
@references[type] ||= references(type, reference_context) @references[type] ||= references(type)
end end
end end
def issues def issues
if project && project.jira_tracker? if project && project.jira_tracker?
@references[:external_issue] ||= references(:external_issue, reference_context) @references[:external_issue] ||= references(:external_issue)
else else
@references[:issue] ||= references(:issue, reference_context) @references[:issue] ||= references(:issue)
end end
end end
...@@ -46,11 +49,5 @@ module Gitlab ...@@ -46,11 +49,5 @@ module Gitlab
@pattern = Regexp.union(patterns.compact) @pattern = Regexp.union(patterns.compact)
end end
private
def reference_context
{ project: project, current_user: current_user, author: author }
end
end end
end end
...@@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do ...@@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end 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
context 'cross-project reference' do context 'cross-project reference' do
...@@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do ...@@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end 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
context 'cross-project URL reference' do context 'cross-project URL reference' do
...@@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do ...@@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end 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
end end
...@@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do ...@@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end 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
context 'cross-project reference' do context 'cross-project reference' do
...@@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do ...@@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
exp = act = "Committed #{invalidate_reference(reference)}" exp = act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end 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
context 'cross-project URL reference' do context 'cross-project URL reference' do
...@@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do ...@@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
act = "Committed #{invalidate_reference(reference)}" act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end 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
end end
...@@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
it 'does not process links containing issue numbers followed by text' do it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st" href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>") doc = reference_filter("<a href='#{href}'></a>")
...@@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
context 'cross-project URL reference' do context 'cross-project URL reference' do
...@@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)") doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
context 'cross-project reference in link href' do context 'cross-project reference in link href' do
...@@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)") doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
context 'cross-project URL in link href' do context 'cross-project URL in link href' do
...@@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)") doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
end end
...@@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label]
end
describe 'label span element' do describe 'label span element' do
it 'includes default classes' do it 'includes default classes' do
doc = reference_filter("Label #{reference}") doc = reference_filter("Label #{reference}")
...@@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to have_attribute('data-label') expect(link).to have_attribute('data-label')
expect(link.attr('data-label')).to eq label.id.to_s expect(link.attr('data-label')).to eq label.id.to_s
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label]
end
end end
describe 'cross project label references' do describe 'cross project label references' do
......
...@@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do ...@@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end 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
context 'cross-project reference' do context 'cross-project reference' do
...@@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do ...@@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end 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
context 'cross-project URL reference' do context 'cross-project URL reference' do
...@@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do ...@@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
doc = reference_filter("Merge (#{reference}.)") doc = reference_filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end 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
end end
...@@ -48,11 +48,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do ...@@ -48,11 +48,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
namespace_project_milestone_path(project.namespace, project, milestone) namespace_project_milestone_path(project.namespace, project, milestone)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Milestone #{reference}")
expect(result[:references][:milestone]).to eq [milestone]
end
context 'Integer-based references' do context 'Integer-based references' do
it 'links to a valid reference' do it 'links to a valid reference' do
doc = reference_filter("See #{reference}") doc = reference_filter("See #{reference}")
...@@ -151,11 +146,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do ...@@ -151,11 +146,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
expect(link).to have_attribute('data-milestone') expect(link).to have_attribute('data-milestone')
expect(link.attr('data-milestone')).to eq milestone.id.to_s expect(link.attr('data-milestone')).to eq milestone.id.to_s
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Milestone #{reference}")
expect(result[:references][:milestone]).to eq [milestone]
end
end end
describe 'cross project milestone references' do describe 'cross project milestone references' do
......
...@@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end end
context 'with data-project' do context 'with data-project' do
let(:parser_class) do
Class.new(Banzai::ReferenceParser::BaseParser) do
self.reference_type = :test
end
end
before do
allow(Banzai::ReferenceParser).to receive(:[]).
with('test').
and_return(parser_class)
end
it 'removes unpermitted Project references' do it 'removes unpermitted Project references' do
user = create(:user) user = create(:user)
project = create(:empty_project) project = create(:empty_project)
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user) doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0 expect(doc.css('a').length).to eq 0
...@@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project) project = create(:empty_project)
project.team << [user, :master] project.team << [user, :master]
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user) doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
it 'handles invalid Project references' do it 'handles invalid Project references' do
link = reference_link(project: 12345, reference_filter: 'ReferenceFilter') link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error expect { filter(link) }.not_to raise_error
end end
...@@ -51,7 +63,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -51,7 +63,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public) project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project) issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: non_member) doc = filter(link, current_user: non_member)
expect(doc.css('a').length).to eq 0 expect(doc.css('a').length).to eq 0
...@@ -62,7 +74,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -62,7 +74,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public) project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, author: author) issue = create(:issue, :confidential, project: project, author: author)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: author) doc = filter(link, current_user: author)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
...@@ -73,7 +85,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -73,7 +85,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public) project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, assignee: assignee) issue = create(:issue, :confidential, project: project, assignee: assignee)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: assignee) doc = filter(link, current_user: assignee)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
...@@ -85,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -85,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project.team << [member, :developer] project.team << [member, :developer]
issue = create(:issue, :confidential, project: project) issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: member) doc = filter(link, current_user: member)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
...@@ -96,7 +108,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -96,7 +108,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public) project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project) issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: admin) doc = filter(link, current_user: admin)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
...@@ -108,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -108,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public) project = create(:empty_project, :public)
issue = create(:issue, project: project) issue = create(:issue, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: user) doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
...@@ -121,7 +133,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -121,7 +133,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
user = create(:user) user = create(:user)
group = create(:group, :private) group = create(:group, :private)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user) doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0 expect(doc.css('a').length).to eq 0
...@@ -132,14 +144,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -132,14 +144,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
group = create(:group, :private) group = create(:group, :private)
group.add_developer(user) group.add_developer(user)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user) doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
it 'handles invalid Group references' do it 'handles invalid Group references' do
link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') link = reference_link(group: 12345, reference_type: 'user')
expect { filter(link) }.not_to raise_error expect { filter(link) }.not_to raise_error
end end
...@@ -149,7 +161,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -149,7 +161,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows any User reference' do it 'allows any User reference' do
user = create(:user) user = create(:user)
link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') link = reference_link(user: user.id, reference_type: 'user')
doc = filter(link) doc = filter(link)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
......
require 'spec_helper'
describe Banzai::Filter::ReferenceGathererFilter, lib: true 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: 'IssueReferenceFilter')
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: 'IssueReferenceFilter')
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: 'IssueReferenceFilter')
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: 'UserReferenceFilter')
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: 'UserReferenceFilter')
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: 'UserReferenceFilter')
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: 'UserReferenceFilter')
result = pipeline_result(link)
expect(result[:references][:user]).to eq([user])
end
end
end
end
...@@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do ...@@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do ...@@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end end
context 'cross-project URL reference' do context 'cross-project URL reference' do
...@@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do ...@@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end end
end end
...@@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end end
it 'supports a special @all mention' do it 'supports a special @all mention' do
doc = reference_filter("Hey #{reference}") doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href')) expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project) .to eq urls.namespace_project_url(project.namespace, project)
end end
context "when the author is a member of the project" do it 'includes a data-author attribute when there is an author' do
doc = reference_filter(reference, author: user)
it 'adds to the results hash' do expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
result = reference_pipeline_result("Hey #{reference}", author: project.creator)
expect(result[:references][:user]).to eq [project.creator]
end
end end
context "when the author is not a member of the project" do it 'does not include a data-author attribute when there is no author' do
doc = reference_filter(reference)
let(:other_user) { create(:user) }
it "doesn't add to the results hash" do expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
result = reference_pipeline_result("Hey #{reference}", author: other_user)
expect(result[:references][:user]).to eq []
end
end end
end end
...@@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user') expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).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
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end end
context 'mentioning a group' do context 'mentioning a group' do
...@@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-group') expect(link).to have_attribute('data-group')
expect(link.attr('data-group')).to eq group.id.to_s expect(link.attr('data-group')).to eq group.id.to_s
end end
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq group.users
end
end end
it 'links with adjacent text' do it 'links with adjacent text' do
...@@ -151,10 +135,5 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -151,10 +135,5 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user') expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).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
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end end
end end
require 'spec_helper'
describe Banzai::ReferenceParser::BaseParser, lib: true do
include ReferenceParserHelpers
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
subject do
klass = Class.new(described_class) do
self.reference_type = :foo
end
klass.new(project, user)
end
describe '.reference_type=' do
it 'sets the reference type' do
dummy = Class.new(described_class)
dummy.reference_type = :foo
expect(dummy.reference_type).to eq(:foo)
end
end
describe '#nodes_visible_to_user' do
let(:link) { empty_html_link }
context 'when the link has a data-project attribute' do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns the nodes if the user can read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the attribute value is empty' do
link['data-project'] = ''
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
it 'returns an empty Array when the user can not read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-project attribute' do
it 'returns the nodes' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
describe '#nodes_user_can_reference' do
it 'returns the nodes' do
link = double(:link)
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
end
describe '#referenced_by' do
context 'when references_relation is implemented' do
it 'returns a collection of objects' do
links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>").
children
expect(subject).to receive(:references_relation).and_return(User)
expect(subject.referenced_by(links)).to eq([user])
end
end
context 'when references_relation is not implemented' do
it 'raises NotImplementedError' do
links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children
expect { subject.referenced_by(links) }.
to raise_error(NotImplementedError)
end
end
end
describe '#references_relation' do
it 'raises NotImplementedError' do
expect { subject.references_relation }.to raise_error(NotImplementedError)
end
end
describe '#gather_attributes_per_project' do
it 'returns a Hash containing attribute values per project' do
link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>').
children[0]
hash = subject.gather_attributes_per_project([link], 'data-foo')
expect(hash).to be_an_instance_of(Hash)
expect(hash[1].to_a).to eq(['2'])
end
end
describe '#grouped_objects_for_nodes' do
it 'returns a Hash grouping objects per ID' do
nodes = [double(:node)]
expect(subject).to receive(:unique_attribute_values).
with(nodes, 'data-user').
and_return([user.id])
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
expect(hash).to eq({ user.id => user })
end
it 'returns an empty Hash when the list of nodes is empty' do
expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
end
end
describe '#unique_attribute_values' do
it 'returns an Array of unique values' do
link = double(:link)
expect(link).to receive(:has_attribute?).
with('data-foo').
twice.
and_return(true)
expect(link).to receive(:attr).
with('data-foo').
twice.
and_return('1')
nodes = [link, link]
expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1'])
end
end
describe '#process' do
it 'gathers the references for every node matching the reference type' do
dummy = Class.new(described_class) do
self.reference_type = :test
end
instance = dummy.new(project, user)
document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
expect(instance).to receive(:gather_references).
with([document.children[1]]).
and_return([user])
expect(instance.process([document])).to eq([user])
end
end
describe '#gather_references' do
let(:link) { double(:link) }
it 'does not process links a user can not reference' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([])
expect(subject).to receive(:referenced_by).with([])
subject.gather_references([link])
end
it 'does not process links a user can not see' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([link])
expect(subject).to receive(:nodes_visible_to_user).
with(user, [link]).
and_return([])
expect(subject).to receive(:referenced_by).with([])
subject.gather_references([link])
end
it 'returns the references if a user can reference and see a link' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([link])
expect(subject).to receive(:nodes_visible_to_user).
with(user, [link]).
and_return([link])
expect(subject).to receive(:referenced_by).with([link])
subject.gather_references([link])
end
end
describe '#can?' do
it 'delegates the permissions check to the Ability class' do
user = double(:user)
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, project)
subject.can?(user, :read_project, project)
end
end
describe '#find_projects_for_hash_keys' do
it 'returns a list of Projects' do
expect(subject.find_projects_for_hash_keys(project.id => project)).
to eq([project])
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::CommitParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link has a data-commit attribute' do
before do
link['data-commit'] = '123'
end
it 'returns an Array of commits' do
commit = double(:commit)
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject).to receive(:find_commits).
with(project, ['123']).
and_return([commit])
expect(subject.referenced_by([link])).to eq([commit])
end
it 'returns an empty Array when the commit could not be found' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject).to receive(:find_commits).
with(project, ['123']).
and_return([])
expect(subject.referenced_by([link])).to eq([])
end
it 'skips projects without valid repositories' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(false)
expect(subject.referenced_by([link])).to eq([])
end
end
context 'when the link does not have a data-commit attribute' do
it 'returns an empty Array' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#commit_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing commit IDs per project' do
link['data-commit'] = '123'
hash = subject.commit_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123'])
end
it 'does not add a project when the data-commit attribute is empty' do
hash = subject.commit_ids_per_project([link])
expect(hash).to be_empty
end
end
describe '#find_commits' do
it 'returns an Array of commit objects' do
commit = double(:commit)
expect(project).to receive(:commit).with('123').and_return(commit)
expect(project).to receive(:valid_repo?).and_return(true)
expect(subject.find_commits(project, %w{123})).to eq([commit])
end
it 'skips commit IDs for which no commit could be found' do
expect(project).to receive(:commit).with('123').and_return(nil)
expect(project).to receive(:valid_repo?).and_return(true)
expect(subject.find_commits(project, %w{123})).to eq([])
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link as a data-commit-range attribute' do
before do
link['data-commit-range'] = '123..456'
end
it 'returns an Array of commit ranges' do
range = double(:range)
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(range)
expect(subject.referenced_by([link])).to eq([range])
end
it 'returns an empty Array when the commit range could not be found' do
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(nil)
expect(subject.referenced_by([link])).to eq([])
end
end
context 'when the link does not have a data-commit-range attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#commit_range_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing range IDs per project' do
link['data-commit-range'] = '123..456'
hash = subject.commit_range_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123..456'])
end
it 'does not add a project when the data-commit-range attribute is empty' do
hash = subject.commit_range_ids_per_project([link])
expect(hash).to be_empty
end
end
describe '#find_ranges' do
it 'returns an Array of range objects' do
range = double(:commit)
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(range)
expect(subject.find_ranges(project, ['123..456'])).to eq([range])
end
it 'skips ranges that could not be found' do
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(nil)
expect(subject.find_ranges(project, ['123..456'])).to eq([])
end
end
describe '#find_object' do
let(:range) { double(:range) }
before do
expect(CommitRange).to receive(:new).and_return(range)
end
context 'when the range has valid commits' do
it 'returns the commit range' do
expect(range).to receive(:valid_commits?).and_return(true)
expect(subject.find_object(project, '123..456')).to eq(range)
end
end
context 'when the range does not have any valid commits' do
it 'returns nil' do
expect(range).to receive(:valid_commits?).and_return(false)
expect(subject.find_object(project, '123..456')).to be_nil
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link has a data-external-issue attribute' do
it 'returns an Array of ExternalIssue instances' do
link['data-external-issue'] = '123'
refs = subject.referenced_by([link])
expect(refs).to eq([ExternalIssue.new('123', project)])
end
end
context 'when the link does not have a data-external-issue attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#issue_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing range IDs per project' do
link['data-external-issue'] = '123'
hash = subject.issue_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123'])
end
it 'does not add a project when the data-external-issue attribute is empty' do
hash = subject.issue_ids_per_project([link])
expect(hash).to be_empty
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::IssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before do
link['data-issue'] = issue.id.to_s
end
it 'returns the nodes when the user can read the issue' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_issue, issue).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the user can not read the issue' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_issue, issue).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-issue attribute' do
it 'returns an empty Array' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the project uses an external issue tracker' do
it 'returns all nodes' do
link = double(:link)
expect(project).to receive(:external_issue_tracker).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
describe '#referenced_by' do
context 'when the link has a data-issue attribute' do
context 'using an existing issue ID' do
before do
link['data-issue'] = issue.id.to_s
end
it 'returns an Array of issues' do
expect(subject.referenced_by([link])).to eq([issue])
end
it 'returns an empty Array when the list of nodes is empty' do
expect(subject.referenced_by([link])).to eq([issue])
expect(subject.referenced_by([])).to eq([])
end
end
end
end
describe '#issues_for_nodes' do
it 'returns a Hash containing the issues for a list of nodes' do
link['data-issue'] = issue.id.to_s
nodes = [link]
expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue })
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::LabelParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-label attribute' do
context 'using an existing label ID' do
it 'returns an Array of labels' do
link['data-label'] = label.id.to_s
expect(subject.referenced_by([link])).to eq([label])
end
end
context 'using a non-existing label ID' do
it 'returns an empty Array' do
link['data-label'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
include ReferenceParserHelpers
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
subject { described_class.new(merge_request.target_project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
it 'returns an Array of merge requests' do
link['data-merge-request'] = merge_request.id.to_s
expect(subject.referenced_by([link])).to eq([merge_request])
end
end
context 'using a non-existing merge request ID' do
it 'returns an empty Array' do
link['data-merge-request'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::MilestoneParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-milestone attribute' do
context 'using an existing milestone ID' do
it 'returns an Array of milestones' do
link['data-milestone'] = milestone.id.to_s
expect(subject.referenced_by([link])).to eq([milestone])
end
end
context 'using a non-existing milestone ID' do
it 'returns an empty Array' do
link['data-milestone'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::SnippetParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:snippet) { create(:snippet, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
it 'returns an Array of snippets' do
link['data-snippet'] = snippet.id.to_s
expect(subject.referenced_by([link])).to eq([snippet])
end
end
context 'using a non-existing snippet ID' do
it 'returns an empty Array' do
link['data-snippet'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::UserParser, lib: true do
include ReferenceParserHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public, group: group, creator: user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-group attribute' do
context 'using an existing group ID' do
before do
link['data-group'] = project.group.id.to_s
end
it 'returns the users of the group' do
create(:group_member, group: group, user: user)
expect(subject.referenced_by([link])).to eq([user])
end
it 'returns an empty Array when the group has no users' do
expect(subject.referenced_by([link])).to eq([])
end
end
context 'using a non-existing group ID' do
it 'returns an empty Array' do
link['data-group'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link has a data-user attribute' do
it 'returns an Array of users' do
link['data-user'] = user.id.to_s
expect(subject.referenced_by([link])).to eq([user])
end
end
context 'when the link has a data-project attribute' do
context 'using an existing project ID' do
let(:contributor) { create(:user) }
before do
project.team << [user, :developer]
project.team << [contributor, :developer]
end
it 'returns the members of a project' do
link['data-project'] = project.id.to_s
# This uses an explicit sort to make sure this spec doesn't randomly
# fail when objects are returned in a different order.
refs = subject.referenced_by([link]).sort_by(&:id)
expect(refs).to eq([user, contributor])
end
end
context 'using a non-existing project ID' do
it 'returns an empty Array' do
link['data-project'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
describe '#nodes_visible_to_use?' do
context 'when the link has a data-group attribute' do
context 'using an existing group ID' do
before do
link['data-group'] = group.id.to_s
end
it 'returns the nodes if the user can read the group' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_group, group).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array if the user can not read the group' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_group, group).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-group attribute' do
context 'with a data-project attribute' do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns the nodes if the user can read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array if the user can not read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'without a data-project attribute' do
it 'returns the nodes' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
end
end
describe '#nodes_user_can_reference' do
context 'when the link has a data-author attribute' do
it 'returns the nodes when the user is a member of the project' do
other_project = create(:project)
other_project.team << [user, :developer]
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
it 'returns an empty Array when the project could not be found' do
link['data-project'] = ''
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
it 'returns an empty Array when the user could not be found' do
other_project = create(:project)
link['data-project'] = other_project.id.to_s
link['data-author'] = ''
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
it 'returns an empty Array when the user is not a team member' do
other_project = create(:project)
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
end
context 'when the link does not have a data-author attribute' do
it 'returns the nodes' do
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
end
end
end
require 'spec_helper'
describe Gitlab::Lazy, lib: true do
let(:dummy) { double(:dummy) }
context 'when not calling any methods' do
it 'does not call the supplied block' do
expect(dummy).not_to receive(:foo)
described_class.new { dummy.foo }
end
end
context 'when calling a method on the object' do
it 'lazy loads the value returned by the block' do
expect(dummy).to receive(:foo).and_return('foo')
lazy = described_class.new { dummy.foo }
expect(lazy.to_s).to eq('foo')
end
end
describe '#respond_to?' do
it 'returns true for a method defined on the wrapped object' do
lazy = described_class.new { 'foo' }
expect(lazy).to respond_to(:downcase)
end
it 'returns false for a method not defined on the wrapped object' do
lazy = described_class.new { 'foo' }
expect(lazy).not_to respond_to(:quack)
end
end
end
require 'spec_helper'
describe Ability, lib: true do
describe '.users_that_can_read_project' do
context 'using a public project' do
it 'returns all the users' do
project = create(:project, :public)
user = build(:user)
expect(described_class.users_that_can_read_project([user], project)).
to eq([user])
end
end
context 'using an internal project' do
let(:project) { create(:project, :internal) }
it 'returns users that are administrators' do
user = build(:user, admin: true)
expect(described_class.users_that_can_read_project([user], project)).
to eq([user])
end
it 'returns internal users while skipping external users' do
user1 = build(:user)
user2 = build(:user, external: true)
users = [user1, user2]
expect(described_class.users_that_can_read_project(users, project)).
to eq([user1])
end
it 'returns external users if they are the project owner' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(project).to receive(:owner).twice.and_return(user1)
expect(described_class.users_that_can_read_project(users, project)).
to eq([user1])
end
it 'returns external users if they are project members' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(project.team).to receive(:members).twice.and_return([user1])
expect(described_class.users_that_can_read_project(users, project)).
to eq([user1])
end
it 'returns an empty Array if all users are external users without access' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(described_class.users_that_can_read_project(users, project)).
to eq([])
end
end
context 'using a private project' do
let(:project) { create(:project, :private) }
it 'returns users that are administrators' do
user = build(:user, admin: true)
expect(described_class.users_that_can_read_project([user], project)).
to eq([user])
end
it 'returns external users if they are the project owner' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(project).to receive(:owner).twice.and_return(user1)
expect(described_class.users_that_can_read_project(users, project)).
to eq([user1])
end
it 'returns external users if they are project members' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(project.team).to receive(:members).twice.and_return([user1])
expect(described_class.users_that_can_read_project(users, project)).
to eq([user1])
end
it 'returns an empty Array if all users are internal users without access' do
user1 = build(:user)
user2 = build(:user)
users = [user1, user2]
expect(described_class.users_that_can_read_project(users, project)).
to eq([])
end
it 'returns an empty Array if all users are external users without access' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
users = [user1, user2]
expect(described_class.users_that_can_read_project(users, project)).
to eq([])
end
end
end
end
...@@ -24,6 +24,16 @@ describe CommitRange, models: true do ...@@ -24,6 +24,16 @@ describe CommitRange, models: true do
expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end end
describe '#initialize' do
it 'does not modify strings in-place' do
input = "#{sha_from}...#{sha_to} "
described_class.new(input, project)
expect(input).to eq("#{sha_from}...#{sha_to} ")
end
end
describe '#to_s' do describe '#to_s' do
it 'is correct for three-dot syntax' do it 'is correct for three-dot syntax' do
expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}" expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
...@@ -135,4 +145,27 @@ describe CommitRange, models: true do ...@@ -135,4 +145,27 @@ describe CommitRange, models: true do
end end
end end
end end
describe '#has_been_reverted?' do
it 'returns true if the commit has been reverted' do
issue = create(:issue)
create(:note_on_issue,
noteable_id: issue.id,
system: true,
note: commit1.revert_description)
expect_any_instance_of(Commit).to receive(:reverts_commit?).
with(commit1).
and_return(true)
expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
end
it 'returns false a commit has not been reverted' do
issue = create(:issue)
expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Commit, models: true do describe Commit, models: true do
let(:project) { create(:project) } let(:project) { create(:project, :public) }
let(:commit) { project.commit } let(:commit) { project.commit }
describe 'modules' do describe 'modules' do
...@@ -171,4 +171,40 @@ eos ...@@ -171,4 +171,40 @@ eos
describe '#status' do describe '#status' do
# TODO: kamil # TODO: kamil
end end
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
let!(:note1) do
create(:note_on_commit,
commit_id: commit.id,
project: project,
note: 'foo')
end
let!(:note2) do
create(:note_on_commit,
commit_id: commit.id,
project: project,
note: 'bar')
end
before do
allow(commit).to receive(:author).and_return(user1)
allow(commit).to receive(:committer).and_return(user2)
end
it 'includes the commit author' do
expect(commit.participants).to include(commit.author)
end
it 'includes the committer' do
expect(commit.participants).to include(commit.committer)
end
it 'includes the authors of the commit notes' do
expect(commit.participants).to include(note1.author, note2.author)
end
end
end end
require 'spec_helper'
describe Participable, models: true do
let(:model) do
Class.new do
include Participable
end
end
describe '.participant' do
it 'adds the participant attributes to the existing list' do
model.participant(:foo)
model.participant(:bar)
expect(model.participant_attrs).to eq([:foo, :bar])
end
end
describe '#participants' do
it 'returns the list of participants' do
model.participant(:foo)
model.participant(:bar)
user1 = build(:user)
user2 = build(:user)
user3 = build(:user)
project = build(:project, :public)
instance = model.new
expect(instance).to receive(:foo).and_return(user2)
expect(instance).to receive(:bar).and_return(user3)
expect(instance).to receive(:project).twice.and_return(project)
participants = instance.participants(user1)
expect(participants).to include(user2)
expect(participants).to include(user3)
end
it 'supports attributes returning another Participable' do
other_model = Class.new { include Participable }
other_model.participant(:bar)
model.participant(:foo)
instance = model.new
other = other_model.new
user1 = build(:user)
user2 = build(:user)
project = build(:project, :public)
expect(instance).to receive(:foo).and_return(other)
expect(other).to receive(:bar).and_return(user2)
expect(instance).to receive(:project).twice.and_return(project)
expect(instance.participants(user1)).to eq([user2])
end
context 'when using a Proc as an attribute' do
it 'calls the supplied Proc' do
user1 = build(:user)
project = build(:project, :public)
user_arg = nil
ext_arg = nil
model.participant -> (user, ext) do
user_arg = user
ext_arg = ext
end
instance = model.new
expect(instance).to receive(:project).twice.and_return(project)
instance.participants(user1)
expect(user_arg).to eq(user1)
expect(ext_arg).to be_an_instance_of(Gitlab::ReferenceExtractor)
end
end
end
end
...@@ -231,4 +231,42 @@ describe Issue, models: true do ...@@ -231,4 +231,42 @@ describe Issue, models: true do
expect(issue.to_branch_name).to match /confidential-issue\z/ expect(issue.to_branch_name).to match /confidential-issue\z/
end end
end end
describe '#participants' do
context 'using a public project' do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:note1) do
create(:note_on_issue, noteable: issue, project: project, note: 'a')
end
let!(:note2) do
create(:note_on_issue, noteable: issue, project: project, note: 'b')
end
it 'includes the issue author' do
expect(issue.participants).to include(issue.author)
end
it 'includes the authors of the notes' do
expect(issue.participants).to include(note1.author, note2.author)
end
end
context 'using a private project' do
it 'does not include mentioned users that do not have access to the project' do
project = create(:project)
user = create(:user)
issue = create(:issue, project: project)
create(:note_on_issue,
noteable: issue,
project: project,
note: user.to_reference)
expect(issue.participants).not_to include(user)
end
end
end
end end
...@@ -414,4 +414,28 @@ describe MergeRequest, models: true do ...@@ -414,4 +414,28 @@ describe MergeRequest, models: true do
end end
end end
end end
describe '#participants' do
let(:project) { create(:project, :public) }
let(:mr) do
create(:merge_request, source_project: project, target_project: project)
end
let!(:note1) do
create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
end
let!(:note2) do
create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
end
it 'includes the merge request author' do
expect(mr.participants).to include(mr.author)
end
it 'includes the authors of the notes' do
expect(mr.participants).to include(note1.author, note2.author)
end
end
end end
...@@ -121,8 +121,19 @@ describe Note, models: true do ...@@ -121,8 +121,19 @@ describe Note, models: true do
let!(:note2) { create(:note_on_issue) } let!(:note2) { create(:note_on_issue) }
it "reads the rendered note body from the cache" do it "reads the rendered note body from the cache" do
expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project) expect(Banzai::Renderer).to receive(:render).
expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project) with(note1.note,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
author: note1.author)
expect(Banzai::Renderer).to receive(:render).
with(note2.note,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
author: note2.author)
note1.all_references note1.all_references
note2.all_references note2.all_references
...@@ -248,4 +259,14 @@ describe Note, models: true do ...@@ -248,4 +259,14 @@ describe Note, models: true do
expect { note.valid? }.to change(note, :line_code).to(nil) expect { note.valid? }.to change(note, :line_code).to(nil)
end end
end end
describe '#participants' do
it 'includes the note author' do
project = create(:project, :public)
issue = create(:issue, project: project)
note = create(:note_on_issue, noteable: issue, project: project)
expect(note.participants).to include(note.author)
end
end
end end
...@@ -87,4 +87,31 @@ describe Snippet, models: true do ...@@ -87,4 +87,31 @@ describe Snippet, models: true do
expect(described_class.search_code('FOO')).to eq([snippet]) expect(described_class.search_code('FOO')).to eq([snippet])
end end
end end
describe '#participants' do
let(:project) { create(:project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
let!(:note1) do
create(:note_on_project_snippet,
noteable: snippet,
project: project,
note: 'a')
end
let!(:note2) do
create(:note_on_project_snippet,
noteable: snippet,
project: project,
note: 'b')
end
it 'includes the snippet author' do
expect(snippet.participants).to include(snippet.author)
end
it 'includes the note authors' do
expect(snippet.participants).to include(note1.author, note2.author)
end
end
end end
...@@ -40,8 +40,7 @@ module FilterSpecHelper ...@@ -40,8 +40,7 @@ module FilterSpecHelper
filters = [ filters = [
Banzai::Filter::AutolinkFilter, Banzai::Filter::AutolinkFilter,
described_class, described_class
Banzai::Filter::ReferenceGathererFilter
] ]
HTML::Pipeline.new(filters, context) HTML::Pipeline.new(filters, context)
......
module ReferenceParserHelpers
def empty_html_link
Nokogiri::HTML.fragment('<a></a>').children[0]
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment