# frozen_string_literal: true # == Participable concern # # Contains functionality related to objects that can have participants, such as # an author, an assignee and people mentioned in its description or comments. # # Usage: # # class Issue < ApplicationRecord # include Participable # # # ... # # participant :author # participant :assignee # participant :notes # # participant -> (current_user, ext) do # ext.analyze('...') # end # end # # issue = Issue.last # users = issue.participants module Participable extend ActiveSupport::Concern class_methods do # Adds a list of participant attributes. Attributes can either be symbols or # 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 included do # Accessor for participant attributes. cattr_accessor :participant_attrs, instance_accessor: false do [] end end # Returns the users participating in a discussion. # # This method processes attributes of objects in breadth-first order. # # Returns an Array of User instances. def participants(user = nil) filtered_participants_hash[user] end # Returns only participants visible for the user # # Returns an Array of User instances. def visible_participants(user) filter_by_ability(raw_participants(user, verify_access: true)) end # Checks if the user is a participant in a discussion. # # This method processes attributes of objects in breadth-first order. # # Returns a Boolean. def participant?(user) can_read_participable?(user) && all_participants_hash[user].include?(user) end private def all_participants_hash @all_participants_hash ||= Hash.new do |hash, user| hash[user] = raw_participants(user) end end def filtered_participants_hash @filtered_participants_hash ||= Hash.new do |hash, user| hash[user] = filter_by_ability(all_participants_hash[user]) end end def raw_participants(current_user = nil, verify_access: false) ext = Gitlab::ReferenceExtractor.new(project, current_user) participants = Set.new process = [self] until process.empty? source = process.pop case source when User participants << source when Participable next unless !verify_access || source_visible_to_user?(source, current_user) source.class.participant_attrs.each do |attr| if attr.respond_to?(:call) source.instance_exec(current_user, ext, &attr) else process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend 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 participants.merge(ext.users) end def source_visible_to_user?(source, user) Ability.allowed?(user, "read_#{source.model_name.element}".to_sym, source) end def filter_by_ability(participants) case self when PersonalSnippet Ability.users_that_can_read_personal_snippet(participants.to_a, self) else Ability.users_that_can_read_project(participants.to_a, project) end end def can_read_participable?(participant) case self when PersonalSnippet participant.can?(:read_snippet, self) else participant.can?(:read_project, project) end end end Participable.prepend_mod_with('Participable')