diff options
Diffstat (limited to 'extensions/PhabBugz/lib/Feed.pm')
-rw-r--r-- | extensions/PhabBugz/lib/Feed.pm | 461 |
1 files changed, 372 insertions, 89 deletions
diff --git a/extensions/PhabBugz/lib/Feed.pm b/extensions/PhabBugz/lib/Feed.pm index 074ecc0f9..c46d36c13 100644 --- a/extensions/PhabBugz/lib/Feed.pm +++ b/extensions/PhabBugz/lib/Feed.pm @@ -9,14 +9,21 @@ package Bugzilla::Extension::PhabBugz::Feed; use 5.10.1; +use IO::Async::Timer::Periodic; +use IO::Async::Loop; use List::Util qw(first); use List::MoreUtils qw(any); use Moo; +use Scalar::Util qw(blessed); +use Try::Tiny; -use Bugzilla::Logging; use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Logging; +use Bugzilla::Mailer; use Bugzilla::Search; -use Bugzilla::Util qw(diff_arrays with_writable_database with_readonly_database); +use Bugzilla::Util qw(diff_arrays format_time with_writable_database with_readonly_database); use Bugzilla::Extension::PhabBugz::Constants; use Bugzilla::Extension::PhabBugz::Policy; @@ -25,13 +32,9 @@ use Bugzilla::Extension::PhabBugz::User; use Bugzilla::Extension::PhabBugz::Util qw( add_security_sync_comments create_revision_attachment - edit_revision_policy get_bug_role_phids - get_phab_bmo_ids - get_project_phid get_security_sync_groups is_attachment_phab_revision - make_revision_public request set_phab_user ); @@ -40,38 +43,85 @@ has 'is_daemon' => ( is => 'rw', default => 0 ); sub start { my ($self) = @_; - while (1) { - my $ok = eval { - if (Bugzilla->params->{phabricator_enabled}) { + + # Query for new revisions or changes + my $feed_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_FEED_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try{ $self->feed_query(); - Bugzilla->_cleanup(); } - 1; - }; - ERROR( $@ // "unknown exception" ) unless $ok; - sleep(PHAB_POLL_SECONDS); - } + catch { + FATAL($_); + }; + Bugzilla->_cleanup(); + }, + ); + + # Query for new users + my $user_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_USER_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try{ + $self->user_query(); + } + catch { + FATAL($_); + }; + Bugzilla->_cleanup(); + }, + ); + + # Update project membership in Phabricator based on Bugzilla groups + my $group_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_GROUP_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try{ + $self->group_query(); + } + catch { + FATAL($_); + }; + Bugzilla->_cleanup(); + }, + ); + + my $loop = IO::Async::Loop->new; + $loop->add($feed_timer); + $loop->add($user_timer); + $loop->add($group_timer); + $feed_timer->start; + $user_timer->start; + $group_timer->start; + $loop->run; } sub feed_query { my ($self) = @_; - my $dbh = Bugzilla->dbh; + + local Bugzilla::Logging->fields->{type} = 'FEED'; # Ensure Phabricator syncing is enabled if (!Bugzilla->params->{phabricator_enabled}) { - INFO("PHABRICATOR SYNC DISABLED"); + WARN("PHABRICATOR SYNC DISABLED"); return; } # PROCESS NEW FEED TRANSACTIONS - INFO("FEED: Fetching new transactions"); + INFO("Fetching new stories"); my $story_last_id = $self->get_last_id('feed'); # Check for new transctions (stories) my $new_stories = $self->new_stories($story_last_id); - INFO("FEED: No new stories") unless @$new_stories; + INFO("No new stories") unless @$new_stories; # Process each story foreach my $story_data (@$new_stories) { @@ -81,25 +131,29 @@ sub feed_query { my $object_phid = $story_data->{objectPHID}; my $story_text = $story_data->{text}; - DEBUG("STORY ID: $story_id"); - DEBUG("STORY PHID: $story_phid"); - DEBUG("AUTHOR PHID: $author_phid"); - DEBUG("OBJECT PHID: $object_phid"); + TRACE("STORY ID: $story_id"); + TRACE("STORY PHID: $story_phid"); + TRACE("AUTHOR PHID: $author_phid"); + TRACE("OBJECT PHID: $object_phid"); INFO("STORY TEXT: $story_text"); # Only interested in changes to revisions for now. if ($object_phid !~ /^PHID-DREV/) { - DEBUG("SKIPPING: Not a revision change"); + INFO("SKIPPING: Not a revision change"); $self->save_last_id($story_id, 'feed'); next; } # Skip changes done by phab-bot user - my $phab_users = get_phab_bmo_ids({ phids => [$author_phid] }); - if (@$phab_users) { - my $user = Bugzilla::User->new({ id => $phab_users->[0]->{id}, cache => 1 }); - if ($user->login eq PHAB_AUTOMATION_USER) { - DEBUG("SKIPPING: Change made by phabricator user"); + my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query( + { + phids => [ $author_phid ] + } + ); + + if ($phab_user && $phab_user->bugzilla_id) { + if ($phab_user->bugzilla_user->login eq PHAB_AUTOMATION_USER) { + INFO("SKIPPING: Change made by phabricator user"); $self->save_last_id($story_id, 'feed'); next; } @@ -111,15 +165,69 @@ sub feed_query { $self->save_last_id($story_id, 'feed'); } + # Process any build targets as well. + my $dbh = Bugzilla->dbh; + + INFO("Checking for revisions in draft mode"); + my $build_targets = $dbh->selectall_arrayref( + "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'", + { Slice => {} } + ); + + my $delete_build_target = $dbh->prepare( + "DELETE FROM phabbugz WHERE name = ? AND VALUE = ?" + ); + + foreach my $target (@$build_targets) { + my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/); + my $build_target = $target->{value}; + + next unless $revision_id && $build_target; + + INFO("Processing revision $revision_id with build target $build_target"); + + my $revision = + Bugzilla::Extension::PhabBugz::Revision->new_from_query( + { + ids => [ int($revision_id) ] + } + ); + + with_writable_database { + $self->process_revision_change($revision, " created D" . $revision->id); + }; + + # Set the build target to a passing status to + # allow the revision to exit draft state + request( 'harbormaster.sendmessage', { + buildTargetPHID => $build_target, + type => 'pass' + } ); + + $delete_build_target->execute($target->{name}, $target->{value}); + } +} + +sub user_query { + my ( $self ) = @_; + + local Bugzilla::Logging->fields->{type} = 'USERS'; + + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } + # PROCESS NEW USERS - INFO("FEED: Fetching new users"); + INFO("Fetching new users"); my $user_last_id = $self->get_last_id('user'); # Check for new users my $new_users = $self->new_users($user_last_id); - INFO("FEED: No new users") unless @$new_users; + INFO("No new users") unless @$new_users; # Process each new user foreach my $user_data (@$new_users) { @@ -128,10 +236,10 @@ sub feed_query { my $user_realname = $user_data->{fields}{realName}; my $object_phid = $user_data->{phid}; - DEBUG("USER ID: $user_id"); - DEBUG("USER LOGIN: $user_login"); - DEBUG("USER REALNAME: $user_realname"); - DEBUG("OBJECT PHID: $object_phid"); + TRACE("ID: $user_id"); + TRACE("LOGIN: $user_login"); + TRACE("REALNAME: $user_realname"); + TRACE("OBJECT PHID: $object_phid"); with_readonly_database { $self->process_new_user($user_data); @@ -140,26 +248,110 @@ sub feed_query { } } +sub group_query { + my ($self) = @_; + + local Bugzilla::Logging->fields->{type} = 'GROUPS'; + + # Ensure Phabricator syncing is enabled + if ( !Bugzilla->params->{phabricator_enabled} ) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } + + # PROCESS SECURITY GROUPS + + INFO("Updating group memberships"); + + # Loop through each group and perform the following: + # + # 1. Load flattened list of group members + # 2. Check to see if Phab project exists for 'bmo-<group_name>' + # 3. Create if does not exist with locked down policy. + # 4. Set project members to exact list including phab-bot user + # 5. Profit + + my $sync_groups = Bugzilla::Group->match( { isactive => 1, isbuggroup => 1 } ); + + # Load phab-bot Phabricator user to add as a member of each project group later + my $phab_bmo_user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER, cache => 1 } ); + my $phab_user = + Bugzilla::Extension::PhabBugz::User->new_from_query( + { + ids => [ $phab_bmo_user->id ] + } + ); + + # secure-revision project that will be used for bmo group projects + my $secure_revision = + Bugzilla::Extension::PhabBugz::Project->new_from_query( + { + name => 'secure-revision' + } + ); + + foreach my $group (@$sync_groups) { + # Create group project if one does not yet exist + my $phab_project_name = 'bmo-' . $group->name; + my $project = + Bugzilla::Extension::PhabBugz::Project->new_from_query( + { + name => $phab_project_name + } + ); + + if ( !$project ) { + INFO("Project $phab_project_name not found. Creating."); + $project = Bugzilla::Extension::PhabBugz::Project->create( + { + name => $phab_project_name, + description => 'BMO Security Group for ' . $group->name, + view_policy => $secure_revision->phid, + edit_policy => $secure_revision->phid, + join_policy => $secure_revision->phid + } + ); + } + else { + # Make sure that the group project permissions are set properly + INFO("Updating permissions on $phab_project_name"); + $project->set_policy( 'view', $secure_revision->phid ); + $project->set_policy( 'edit', $secure_revision->phid ); + $project->set_policy( 'join', $secure_revision->phid ); + } + + # Make sure phab-bot also a member of the new project group so that it can + # make policy changes to the private revisions + INFO("Setting project members for " . $project->name); + my $set_members = $self->get_group_members( $group ); + push @$set_members, $phab_user unless grep $_->phid eq $phab_user->phid, @$set_members; + $project->set_members( $set_members ); + $project->update(); + } +} + sub process_revision_change { my ($self, $revision_phid, $story_text) = @_; # Load the revision from Phabricator - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $revision_phid ] }); + my $revision = + blessed $revision_phid + ? $revision_phid + : Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $revision_phid ] }); # NO BUG ID if (!$revision->bug_id) { if ($story_text =~ /\s+created\s+D\d+/) { # If new revision and bug id was omitted, make revision public - DEBUG("No bug associated with new revision. Marking public."); - $revision->set_policy('view', 'public'); - $revision->set_policy('edit', 'users'); + INFO("No bug associated with new revision. Marking public."); + $revision->make_public(); $revision->update(); INFO("SUCCESS"); return; } else { - DEBUG("SKIPPING: No bug associated with revision change"); + INFO("SKIPPING: No bug associated with revision change"); return; } } @@ -180,11 +372,8 @@ sub process_revision_change { # If bug is public then remove privacy policy if (!@{ $bug->groups_in }) { - DEBUG('Bug is public so setting view/edit public'); - $revision->set_policy('view', 'public'); - $revision->set_policy('edit', 'users'); - my $secure_project_phid = get_project_phid('secure-revision'); - $revision->remove_project($secure_project_phid); + INFO('Bug is public so setting view/edit public'); + $revision->make_public(); } # else bug is private. else { @@ -193,54 +382,51 @@ sub process_revision_change { # If bug privacy groups do not have any matching synchronized groups, # then leave revision private and it will have be dealt with manually. if (!@set_groups) { - DEBUG('No matching groups. Adding comments to bug and revision'); + INFO('No matching groups. Adding comments to bug and revision'); add_security_sync_comments([$revision], $bug); } # Otherwise, we create a new custom policy containing the project # groups that are mapped to bugzilla groups. else { - my @set_projects = map { "bmo-" . $_ } @set_groups; + my $set_project_names = [ map { "bmo-" . $_ } @set_groups ]; # If current policy projects matches what we want to set, then # we leave the current policy alone. my $current_policy; if ($revision->view_policy =~ /^PHID-PLCY/) { - DEBUG("Loading current policy: " . $revision->view_policy); + INFO("Loading current policy: " . $revision->view_policy); $current_policy = Bugzilla::Extension::PhabBugz::Policy->new_from_query({ phids => [ $revision->view_policy ]}); - my $current_projects = $current_policy->rule_projects; - DEBUG("Current policy projects: " . join(", ", @$current_projects)); - my ($added, $removed) = diff_arrays($current_projects, \@set_projects); + my $current_project_names = [ map { $_->name } @{ $current_policy->rule_projects } ]; + INFO("Current policy projects: " . join(", ", @$current_project_names)); + my ($added, $removed) = diff_arrays($current_project_names, $set_project_names); if (@$added || @$removed) { - DEBUG('Project groups do not match. Need new custom policy'); - $current_policy= undef; + INFO('Project groups do not match. Need new custom policy'); + $current_policy = undef; } else { - DEBUG('Project groups match. Leaving current policy as-is'); + INFO('Project groups match. Leaving current policy as-is'); } } if (!$current_policy) { - DEBUG("Creating new custom policy: " . join(", ", @set_projects)); - my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects); - $revision->set_policy('view', $new_policy->phid); - $revision->set_policy('edit', $new_policy->phid); + INFO("Creating new custom policy: " . join(", ", @$set_project_names)); + $revision->make_private($set_project_names); } - my $secure_project_phid = get_project_phid('secure-revision'); - $revision->add_project($secure_project_phid); + # Subscriber list of the private revision should always match + # the bug roles such as assignee, qa contact, and cc members. + my $subscribers = get_bug_role_phids($bug); + $revision->set_subscribers($subscribers); } - - # Subscriber list of the private revision should always match - # the bug roles such as assignee, qa contact, and cc members. - my $subscribers = get_bug_role_phids($bug); - $revision->set_subscribers($subscribers); } my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - my $attachment = create_revision_attachment($bug, $revision, $timestamp); - + INFO('Checking for revision attachment'); + my $attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user); + INFO('Attachment ' . $attachment->id . ' created or already exists.'); + # ATTACHMENT OBSOLETES # fixup attachments on current bug @@ -252,11 +438,11 @@ sub process_revision_change { next if $attach_revision_id != $revision->id; my $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0; - DEBUG('Updating obsolete status on attachmment ' . $attachment->id); + INFO('Updating obsolete status on attachmment ' . $attachment->id); $attachment->set_is_obsolete($make_obsolete); if ($revision->title ne $attachment->description) { - DEBUG('Updating description on attachment ' . $attachment->id); + INFO('Updating description on attachment ' . $attachment->id); $attachment->set_description($revision->title); } @@ -272,8 +458,8 @@ sub process_revision_change { }); foreach my $attachment (@$other_attachments) { $other_bugs{$attachment->bug_id}++; - DEBUG('Updating obsolete status on attachment ' . - $attachment->id . " for bug " . $attachment->bug_id); + INFO('Updating obsolete status on attachment ' . + $attachment->id . " for bug " . $attachment->bug_id); $attachment->set_is_obsolete(1); $attachment->update($timestamp); } @@ -281,17 +467,28 @@ sub process_revision_change { # REVIEWER STATUSES my (@accepted_phids, @denied_phids, @accepted_user_ids, @denied_user_ids); - unless ($revision->status eq 'changes-planned' || $revision->status eq 'needs-review') { - foreach my $reviewer (@{ $revision->reviewers }) { - push(@accepted_phids, $reviewer->phab_phid) if $reviewer->phab_review_status eq 'accepted'; - push(@denied_phids, $reviewer->phab_phid) if $reviewer->phab_review_status eq 'rejected'; - } + foreach my $reviewer (@{ $revision->reviewers }) { + push(@accepted_phids, $reviewer->phid) if $reviewer->{phab_review_status} eq 'accepted'; + push(@denied_phids, $reviewer->phid) if $reviewer->{phab_review_status} eq 'rejected'; } - my $phab_users = get_phab_bmo_ids({ phids => \@accepted_phids }); - @accepted_user_ids = map { $_->{id} } @$phab_users; - $phab_users = get_phab_bmo_ids({ phids => \@denied_phids }); - @denied_user_ids = map { $_->{id} } @$phab_users; + if ( @accepted_phids ) { + my $phab_users = Bugzilla::Extension::PhabBugz::User->match( + { + phids => \@accepted_phids + } + ); + @accepted_user_ids = map { $_->bugzilla_user->id } grep { defined $_->bugzilla_user } @$phab_users; + } + + if ( @denied_phids ) { + my $phab_users = Bugzilla::Extension::PhabBugz::User->match( + { + phids => \@denied_phids + } + ); + @denied_user_ids = map { $_->bugzilla_user->id } grep { defined $_->bugzilla_user } @$phab_users; + } my %reviewers_hash = map { $_->name => 1 } @{ $revision->reviewers }; @@ -299,19 +496,22 @@ sub process_revision_change { my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); next if $revision->id != $attach_revision_id; - # Clear old flags if no longer accepted + # Clear old accepted review flags if no longer accepted my (@denied_flags, @new_flags, @removed_flags, %accepted_done, $flag_type); foreach my $flag (@{ $attachment->flags }) { next if $flag->type->name ne 'review'; $flag_type = $flag->type if $flag->type->is_active; + next if $flag->status ne '+'; if (any { $flag->setter->id == $_ } @denied_user_ids) { + INFO('Denying review flag set by ' . $flag->setter->name); push(@denied_flags, { id => $flag->id, setter => $flag->setter, status => 'X' }); } if (any { $flag->setter->id == $_ } @accepted_user_ids) { + INFO('Skipping as review+ already set by ' . $flag->setter->name); $accepted_done{$flag->setter->id}++; } - if ($flag->status eq '+' - && !any { $flag->setter->id == $_ } (@accepted_user_ids, @denied_user_ids)) { + if (!any { $flag->setter->id == $_ } (@accepted_user_ids, @denied_user_ids)) { + INFO('Clearing review+ flag set by ' . $flag->setter->name); push(@removed_flags, { id => $flag->id, setter => $flag->setter, status => 'X' }); } } @@ -322,6 +522,7 @@ sub process_revision_change { foreach my $user_id (@accepted_user_ids) { next if $accepted_done{$user_id}; my $user = Bugzilla::User->check({ id => $user_id, cache => 1 }); + INFO('Setting new review+ flag for ' . $user->name); push(@new_flags, { type_id => $flag_type->id, setter => $user, status => '+' }); } @@ -344,6 +545,7 @@ sub process_revision_change { if ($comment) { $comment .= "\n" . Bugzilla->params->{phabricator_base_uri} . "D" . $revision->id; + INFO("Flag comment: $comment"); # Add transaction_id as anchor if one present # $comment .= "#" . $params->{transaction_id} if $params->{transaction_id}; $bug->add_comment($comment, { @@ -380,7 +582,7 @@ sub process_new_user { my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data); if (!$phab_user->bugzilla_id) { - DEBUG("SKIPPING: No bugzilla id associated with user"); + WARN("SKIPPING: No bugzilla id associated with user"); return; } @@ -389,6 +591,55 @@ sub process_new_user { # Pre setup before querying DB my $old_user = set_phab_user(); + # CHECK AND WARN FOR POSSIBLE USERNAME SQUATTING + INFO("Checking for username squatters"); + my $dbh = Bugzilla->dbh; + my $regexp = $dbh->quote( ":?:" . quotemeta($phab_user->name) . "[[:>:]]" ); + my $results = $dbh->selectall_arrayref( " + SELECT userid, login_name, realname + FROM profiles + WHERE userid != ? AND " . $dbh->sql_regexp( 'realname', $regexp ), + { Slice => {} }, + $bug_user->id ); + if (@$results) { + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); + + foreach my $row (@$results) { + WARN( + 'Possible username squatter: ', + 'phab user login: ' . $phab_user->name, + ' phab user realname: ' . $phab_user->realname, + ' bugzilla user id: ' . $row->{userid}, + ' bugzilla login: ' . $row->{login_name}, + ' bugzilla realname: ' . $row->{realname} + ); + + my $vars = { + date => $timestamp, + phab_user_login => $phab_user->name, + phab_user_realname => $phab_user->realname, + bugzilla_userid => $bug_user->id, + bugzilla_login => $bug_user->login, + bugzilla_realname => $bug_user->name, + squat_userid => $row->{userid}, + squat_login => $row->{login_name}, + squat_realname => $row->{realname} + }; + + my $message; + my $template = Bugzilla->template; + $template->process("admin/email/squatter-alert.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } + } + + # ADD SUBSCRIBERS TO REVSISIONS FOR CURRENT PRIVATE BUGS + my $params = { f3 => 'OP', j3 => 'OR', @@ -432,8 +683,10 @@ sub process_new_user { # the first value of each row should be the bug id my @bug_ids = map { shift @$_ } @$data; + INFO("Updating subscriber values for old private bugs"); + foreach my $bug_id (@bug_ids) { - DEBUG("Processing bug $bug_id"); + INFO("Processing bug $bug_id"); my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 }); @@ -442,7 +695,8 @@ sub process_new_user { foreach my $attachment (@attachments) { my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); - DEBUG("Processing revision D$revision_id"); + + INFO("Processing revision D$revision_id"); my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( { ids => [ int($revision_id) ] }); @@ -450,7 +704,7 @@ sub process_new_user { $revision->add_subscriber($phab_user->phid); $revision->update(); - DEBUG("Revision $revision_id updated"); + INFO("Revision $revision_id updated"); } } @@ -467,7 +721,9 @@ sub new_stories { my ( $self, $after ) = @_; my $data = { view => 'text' }; $data->{after} = $after if $after; + my $result = request( 'feed.query_id', $data ); + unless ( ref $result->{result}{data} eq 'ARRAY' && @{ $result->{result}{data} } ) { @@ -487,7 +743,9 @@ sub new_users { } }; $data->{before} = $after if $after; + my $result = request( 'user.search', $data ); + unless ( ref $result->{result}{data} eq 'ARRAY' && @{ $result->{result}{data} } ) { @@ -504,7 +762,7 @@ sub get_last_id { my $last_id = Bugzilla->dbh->selectrow_array( " SELECT value FROM phabbugz WHERE name = ?", undef, $type_full ); $last_id ||= 0; - DEBUG( "QUERY " . uc($type_full) . ": $last_id" ); + TRACE(uc($type_full) . ": $last_id" ); return $last_id; } @@ -513,9 +771,34 @@ sub save_last_id { # Store the largest last key so we can start from there in the next session my $type_full = $type . "_last_id"; - DEBUG( "UPDATING " . uc($type_full) . ": $last_id" ); + TRACE("UPDATING " . uc($type_full) . ": $last_id" ); Bugzilla->dbh->do( "REPLACE INTO phabbugz (name, value) VALUES (?, ?)", undef, $type_full, $last_id ); } +sub get_group_members { + my ( $self, $group ) = @_; + + my $group_obj = + ref $group ? $group : Bugzilla::Group->check( { name => $group, cache => 1 } ); + + my $flat_list = join(',', + @{ Bugzilla::Group->flatten_group_membership( $group_obj->id ) } ); + + my $user_query = " + SELECT DISTINCT profiles.userid + FROM profiles, user_group_map AS ugm + WHERE ugm.user_id = profiles.userid + AND ugm.isbless = 0 + AND ugm.group_id IN($flat_list)"; + my $user_ids = Bugzilla->dbh->selectcol_arrayref($user_query); + + # Return matching users in Phabricator + return Bugzilla::Extension::PhabBugz::User->match( + { + ids => $user_ids + } + ); +} + 1; |