1# frozen_string_literal: true 2 3# == Participable concern 4# 5# Contains functionality related to objects that can have participants, such as 6# an author, an assignee and people mentioned in its description or comments. 7# 8# Usage: 9# 10# class Issue < ApplicationRecord 11# include Participable 12# 13# # ... 14# 15# participant :author 16# participant :assignee 17# participant :notes 18# 19# participant -> (current_user, ext) do 20# ext.analyze('...') 21# end 22# end 23# 24# issue = Issue.last 25# users = issue.participants 26module Participable 27 extend ActiveSupport::Concern 28 class_methods do 29 # Adds a list of participant attributes. Attributes can either be symbols or 30 # Procs. 31 # 32 # When using a Proc instead of a Symbol the Proc will be given two 33 # arguments: 34 # 35 # 1. The current user (as an instance of User) 36 # 2. An instance of `Gitlab::ReferenceExtractor` 37 # 38 # It is expected that a Proc populates the given reference extractor 39 # instance with data. The return value of the Proc is ignored. 40 # 41 # attr - The name of the attribute or a Proc 42 def participant(attr) 43 participant_attrs << attr 44 end 45 end 46 47 included do 48 # Accessor for participant attributes. 49 cattr_accessor :participant_attrs, instance_accessor: false do 50 [] 51 end 52 end 53 54 # Returns the users participating in a discussion. 55 # 56 # This method processes attributes of objects in breadth-first order. 57 # 58 # Returns an Array of User instances. 59 def participants(user = nil) 60 filtered_participants_hash[user] 61 end 62 63 # Returns only participants visible for the user 64 # 65 # Returns an Array of User instances. 66 def visible_participants(user) 67 return participants(user) unless Feature.enabled?(:verify_participants_access, project, default_enabled: :yaml) 68 69 filter_by_ability(raw_participants(user, verify_access: true)) 70 end 71 72 # Checks if the user is a participant in a discussion. 73 # 74 # This method processes attributes of objects in breadth-first order. 75 # 76 # Returns a Boolean. 77 def participant?(user) 78 can_read_participable?(user) && 79 all_participants_hash[user].include?(user) 80 end 81 82 private 83 84 def all_participants_hash 85 @all_participants_hash ||= Hash.new do |hash, user| 86 hash[user] = raw_participants(user) 87 end 88 end 89 90 def filtered_participants_hash 91 @filtered_participants_hash ||= Hash.new do |hash, user| 92 hash[user] = filter_by_ability(all_participants_hash[user]) 93 end 94 end 95 96 def raw_participants(current_user = nil, verify_access: false) 97 ext = Gitlab::ReferenceExtractor.new(project, current_user) 98 participants = Set.new 99 process = [self] 100 101 until process.empty? 102 source = process.pop 103 104 case source 105 when User 106 participants << source 107 when Participable 108 next unless !verify_access || source_visible_to_user?(source, current_user) 109 110 source.class.participant_attrs.each do |attr| 111 if attr.respond_to?(:call) 112 source.instance_exec(current_user, ext, &attr) 113 else 114 process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend 115 end 116 end 117 when Enumerable, ActiveRecord::Relation 118 # This uses reverse_each so we can use "pop" to get the next value to 119 # process (in order). Using unshift instead of pop would require 120 # moving all Array values one index to the left (which can be 121 # expensive). 122 source.reverse_each { |obj| process << obj } 123 end 124 end 125 126 participants.merge(ext.users) 127 end 128 129 def source_visible_to_user?(source, user) 130 Ability.allowed?(user, "read_#{source.model_name.element}".to_sym, source) 131 end 132 133 def filter_by_ability(participants) 134 case self 135 when PersonalSnippet 136 Ability.users_that_can_read_personal_snippet(participants.to_a, self) 137 else 138 Ability.users_that_can_read_project(participants.to_a, project) 139 end 140 end 141 142 def can_read_participable?(participant) 143 case self 144 when PersonalSnippet 145 participant.can?(:read_snippet, self) 146 else 147 participant.can?(:read_project, project) 148 end 149 end 150end 151 152Participable.prepend_mod_with('Participable') 153