diff options
Diffstat (limited to 'extensions/PhabBugz/lib/Feed.pm')
-rw-r--r-- | extensions/PhabBugz/lib/Feed.pm | 1206 |
1 files changed, 596 insertions, 610 deletions
diff --git a/extensions/PhabBugz/lib/Feed.pm b/extensions/PhabBugz/lib/Feed.pm index 71e6aa827..f199a96aa 100644 --- a/extensions/PhabBugz/lib/Feed.pm +++ b/extensions/PhabBugz/lib/Feed.pm @@ -26,7 +26,8 @@ use Bugzilla::Field; use Bugzilla::Logging; use Bugzilla::Mailer; use Bugzilla::Search; -use Bugzilla::Util qw(diff_arrays format_time with_writable_database with_readonly_database); +use Bugzilla::Util + qw(diff_arrays format_time with_writable_database with_readonly_database); use Bugzilla::Types qw(:types); use Bugzilla::Extension::PhabBugz::Types qw(:types); use Bugzilla::Extension::PhabBugz::Constants; @@ -34,663 +35,661 @@ use Bugzilla::Extension::PhabBugz::Policy; use Bugzilla::Extension::PhabBugz::Revision; use Bugzilla::Extension::PhabBugz::User; use Bugzilla::Extension::PhabBugz::Util qw( - create_revision_attachment - get_bug_role_phids - is_attachment_phab_revision - request - set_phab_user + create_revision_attachment + get_bug_role_phids + is_attachment_phab_revision + request + set_phab_user ); -has 'is_daemon' => ( is => 'rw', default => 0 ); +has 'is_daemon' => (is => 'rw', default => 0); -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub start { - my ($self) = @_; - - my $sig_alarm = IO::Async::Signal->new( - name => 'ALRM', - on_receipt => sub { - FATAL("Timeout reached"); - exit; - }, - ); - # 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 { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->feed_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - 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 { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->user_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - 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 { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->group_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - Bugzilla->_cleanup(); - }; - - }, - ); - - my $loop = IO::Async::Loop->new; - $loop->add($feed_timer); - $loop->add($user_timer); - $loop->add($group_timer); - $loop->add($sig_alarm); - $feed_timer->start; - $user_timer->start; - $group_timer->start; - $loop->run; + my ($self) = @_; + + my $sig_alarm = IO::Async::Signal->new( + name => 'ALRM', + on_receipt => sub { + FATAL("Timeout reached"); + exit; + }, + ); + + # 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 { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->feed_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + 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 { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->user_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + 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 { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->group_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + Bugzilla->_cleanup(); + }; + + }, + ); + + my $loop = IO::Async::Loop->new; + $loop->add($feed_timer); + $loop->add($user_timer); + $loop->add($group_timer); + $loop->add($sig_alarm); + $feed_timer->start; + $user_timer->start; + $group_timer->start; + $loop->run; } sub feed_query { - my ($self) = @_; + my ($self) = @_; + + local Bugzilla::Logging->fields->{type} = 'FEED'; + + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } + + # PROCESS NEW FEED 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("No new stories") unless @$new_stories; + + # Process each story + foreach my $story_data (@$new_stories) { + my $story_id = $story_data->{id}; + my $story_phid = $story_data->{phid}; + my $author_phid = $story_data->{authorPHID}; + my $object_phid = $story_data->{objectPHID}; + my $story_text = $story_data->{text}; - local Bugzilla::Logging->fields->{type} = 'FEED'; + 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"); - # Ensure Phabricator syncing is enabled - if (!Bugzilla->params->{phabricator_enabled}) { - WARN("PHABRICATOR SYNC DISABLED"); - return; + # Only interested in changes to revisions for now. + if ($object_phid !~ /^PHID-DREV/) { + INFO("SKIPPING: Not a revision change"); + $self->save_last_id($story_id, 'feed'); + next; } - # PROCESS NEW FEED 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("No new stories") unless @$new_stories; - - # Process each story - foreach my $story_data (@$new_stories) { - my $story_id = $story_data->{id}; - my $story_phid = $story_data->{phid}; - my $author_phid = $story_data->{authorPHID}; - my $object_phid = $story_data->{objectPHID}; - my $story_text = $story_data->{text}; - - 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/) { - INFO("SKIPPING: Not a revision change"); - $self->save_last_id($story_id, 'feed'); - next; - } - - # Skip changes done by phab-bot user - # If changer does not exist in bugzilla database - # we use the phab-bot account as the changer - my $author = Bugzilla::Extension::PhabBugz::User->new_from_query( - { phids => [ $author_phid ] } - ); - - if ($author && $author->bugzilla_id) { - if ($author->bugzilla_user->login eq PHAB_AUTOMATION_USER) { - INFO("SKIPPING: Change made by phabricator user"); - $self->save_last_id($story_id, 'feed'); - next; - } - } - else { - my $phab_user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER } ); - $author = Bugzilla::Extension::PhabBugz::User->new_from_query( - { - ids => [ $phab_user->id ] - } - ); - } - # Load the revision from Phabricator - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $object_phid ] }); - $self->process_revision_change($revision, $author, $story_text); + # Skip changes done by phab-bot user + # If changer does not exist in bugzilla database + # we use the phab-bot account as the changer + my $author = Bugzilla::Extension::PhabBugz::User->new_from_query( + {phids => [$author_phid]}); + + if ($author && $author->bugzilla_id) { + if ($author->bugzilla_user->login eq PHAB_AUTOMATION_USER) { + INFO("SKIPPING: Change made by phabricator user"); $self->save_last_id($story_id, 'feed'); + next; + } + } + else { + my $phab_user = Bugzilla::User->new({name => PHAB_AUTOMATION_USER}); + $author + = Bugzilla::Extension::PhabBugz::User->new_from_query({ids => [$phab_user->id] + }); } - # Process any build targets as well. - my $dbh = Bugzilla->dbh; + # Load the revision from Phabricator + my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( + {phids => [$object_phid]}); + $self->process_revision_change($revision, $author, $story_text); + $self->save_last_id($story_id, 'feed'); + } - INFO("Checking for revisions in draft mode"); - my $build_targets = $dbh->selectall_arrayref( - "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'", - { Slice => {} } - ); + # Process any build targets as well. + my $dbh = Bugzilla->dbh; - my $delete_build_target = $dbh->prepare( - "DELETE FROM phabbugz WHERE name = ? AND VALUE = ?" - ); + INFO("Checking for revisions in draft mode"); + my $build_targets + = $dbh->selectall_arrayref( + "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'", + {Slice => {}}); - foreach my $target (@$build_targets) { - my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/); - my $build_target = $target->{value}; + my $delete_build_target + = $dbh->prepare("DELETE FROM phabbugz WHERE name = ? AND VALUE = ?"); - next unless $revision_id && $build_target; + foreach my $target (@$build_targets) { + my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/); + my $build_target = $target->{value}; - INFO("Processing revision $revision_id with build target $build_target"); + next unless $revision_id && $build_target; - my $revision = - Bugzilla::Extension::PhabBugz::Revision->new_from_query( - { - ids => [ int($revision_id) ] - } - ); + INFO("Processing revision $revision_id with build target $build_target"); - $self->process_revision_change( $revision, $revision->author, " created D" . $revision->id ); + my $revision + = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ + ids => [int($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' - } ); + $self->process_revision_change($revision, $revision->author, + " created D" . $revision->id); - $delete_build_target->execute($target->{name}, $target->{value}); - } + # Set the build target to a passing status to + # allow the revision to exit draft state + request('harbormaster.sendmessage', + {buildTargetPHID => $build_target, type => 'pass'}); - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.feed_query_count'); - } + $delete_build_target->execute($target->{name}, $target->{value}); + } + + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.feed_query_count'); + } } sub user_query { - my ( $self ) = @_; + my ($self) = @_; - local Bugzilla::Logging->fields->{type} = 'USERS'; + local Bugzilla::Logging->fields->{type} = 'USERS'; - # Ensure Phabricator syncing is enabled - if (!Bugzilla->params->{phabricator_enabled}) { - WARN("PHABRICATOR SYNC DISABLED"); - return; - } + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } - # PROCESS NEW USERS + # PROCESS NEW USERS - INFO("Fetching new users"); + INFO("Fetching new users"); - my $user_last_id = $self->get_last_id('user'); + my $user_last_id = $self->get_last_id('user'); - # Check for new users - my $new_users = $self->new_users($user_last_id); - INFO("No new users") unless @$new_users; + # Check for new users + my $new_users = $self->new_users($user_last_id); + INFO("No new users") unless @$new_users; - # Process each new user - foreach my $user_data (@$new_users) { - my $user_id = $user_data->{id}; - my $user_login = $user_data->{fields}{username}; - my $user_realname = $user_data->{fields}{realName}; - my $object_phid = $user_data->{phid}; + # Process each new user + foreach my $user_data (@$new_users) { + my $user_id = $user_data->{id}; + my $user_login = $user_data->{fields}{username}; + my $user_realname = $user_data->{fields}{realName}; + my $object_phid = $user_data->{phid}; - TRACE("ID: $user_id"); - TRACE("LOGIN: $user_login"); - TRACE("REALNAME: $user_realname"); - TRACE("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); - }; - $self->save_last_id($user_id, 'user'); - } + with_readonly_database { + $self->process_new_user($user_data); + }; + $self->save_last_id($user_id, 'user'); + } - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.user_query_count'); - } + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.user_query_count'); + } } sub group_query { - my ($self) = @_; + my ($self) = @_; - local Bugzilla::Logging->fields->{type} = 'GROUPS'; + local Bugzilla::Logging->fields->{type} = 'GROUPS'; - # Ensure Phabricator syncing is enabled - if ( !Bugzilla->params->{phabricator_enabled} ) { - WARN("PHABRICATOR SYNC DISABLED"); - return; - } + # 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( "Checking project members for " . $project->name ); - my $set_members = $self->get_group_members($group); - my @set_member_phids = uniq map { $_->phid } ( @$set_members, $phab_user ); - my @current_member_phids = uniq map { $_->phid } @{ $project->members }; - my ( $removed, $added ) = diff_arrays( \@current_member_phids, \@set_member_phids ); - - if (@$added) { - INFO( 'Adding project members: ' . join( ',', @$added ) ); - $project->add_member($_) foreach @$added; - } - - if (@$removed) { - INFO( 'Removing project members: ' . join( ',', @$removed ) ); - $project->remove_member($_) foreach @$removed; - } - - if (@$added || @$removed) { - my $result = $project->update(); - local Bugzilla::Logging->fields->{api_result} = $result; - INFO( "Project " . $project->name . " updated" ); - } - } + # 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}); - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.group_query_count'); + # 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); } -} -sub process_revision_change { - state $check = compile($Invocant, Revision, LinkedPhabUser, Str); - my ($self, $revision, $changer, $story_text) = $check->(@_); - - # 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 - INFO("No bug associated with new revision. Marking public."); - $revision->make_public(); - $revision->update(); - INFO("SUCCESS"); - return; - } - else { - INFO("SKIPPING: No bug associated with revision change"); - return; - } + # Make sure phab-bot also a member of the new project group so that it can + # make policy changes to the private revisions + INFO("Checking project members for " . $project->name); + my $set_members = $self->get_group_members($group); + my @set_member_phids = uniq map { $_->phid } (@$set_members, $phab_user); + my @current_member_phids = uniq map { $_->phid } @{$project->members}; + my ($removed, $added) = diff_arrays(\@current_member_phids, \@set_member_phids); + + if (@$added) { + INFO('Adding project members: ' . join(',', @$added)); + $project->add_member($_) foreach @$added; } + if (@$removed) { + INFO('Removing project members: ' . join(',', @$removed)); + $project->remove_member($_) foreach @$removed; + } - my $log_message = sprintf( - "REVISION CHANGE FOUND: D%d: %s | bug: %d | %s | %s", - $revision->id, - $revision->title, - $revision->bug_id, - $changer->name, - $story_text); - INFO($log_message); - - # change to the phabricator user, which returns a guard that restores the previous user. - my $restore_prev_user = set_phab_user(); - my $bug = $revision->bug; - - # Check to make sure bug id is valid and author can see it - if ($bug->{error} - ||!$revision->author->bugzilla_user->can_see_bug($revision->bug_id)) - { - if ($story_text =~ /\s+created\s+D\d+/) { - INFO('Invalid bug ID or author does not have access to the bug. ' . - 'Waiting til next revision update to notify author.'); - return; - } - - INFO('Invalid bug ID or author does not have access to the bug'); - my $phab_error_message = ""; - Bugzilla->template->process('revision/comments.html.tmpl', - { message => 'invalid_bug_id' }, - \$phab_error_message); - $revision->add_comment($phab_error_message); - $revision->update(); - return; + if (@$added || @$removed) { + my $result = $project->update(); + local Bugzilla::Logging->fields->{api_result} = $result; + INFO("Project " . $project->name . " updated"); } + } - # REVISION SECURITY POLICY + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.group_query_count'); + } +} - # If bug is public then remove privacy policy - if (!@{ $bug->groups_in }) { - INFO('Bug is public so setting view/edit public'); - $revision->make_public(); +sub process_revision_change { + state $check = compile($Invocant, Revision, LinkedPhabUser, Str); + my ($self, $revision, $changer, $story_text) = $check->(@_); + + # 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 + INFO("No bug associated with new revision. Marking public."); + $revision->make_public(); + $revision->update(); + INFO("SUCCESS"); + return; } - # else bug is private. else { - # Here we create a new custom policy containing the project - # groups that are mapped to bugzilla groups. - my $set_project_names = [ map { "bmo-" . $_->name } @{ $bug->groups_in } ]; - - # 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/) { - INFO("Loading current policy: " . $revision->view_policy); - $current_policy - = Bugzilla::Extension::PhabBugz::Policy->new_from_query({ phids => [ $revision->view_policy ]}); - 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) { - INFO('Project groups do not match. Need new custom policy'); - $current_policy = undef; - } - else { - INFO('Project groups match. Leaving current policy as-is'); - } - } - - if (!$current_policy) { - INFO("Creating new custom policy: " . join(", ", @$set_project_names)); - $revision->make_private($set_project_names); - } - - # 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); + INFO("SKIPPING: No bug associated with revision change"); + return; + } + } + + + my $log_message = sprintf( + "REVISION CHANGE FOUND: D%d: %s | bug: %d | %s | %s", + $revision->id, $revision->title, $revision->bug_id, + $changer->name, $story_text + ); + INFO($log_message); + +# change to the phabricator user, which returns a guard that restores the previous user. + my $restore_prev_user = set_phab_user(); + my $bug = $revision->bug; + + # Check to make sure bug id is valid and author can see it + if ($bug->{error} + || !$revision->author->bugzilla_user->can_see_bug($revision->bug_id)) + { + if ($story_text =~ /\s+created\s+D\d+/) { + INFO( 'Invalid bug ID or author does not have access to the bug. ' + . 'Waiting til next revision update to notify author.'); + return; } - my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - - INFO('Checking for revision attachment'); - my $rev_attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user); - INFO('Attachment ' . $rev_attachment->id . ' created or already exists.'); - - # ATTACHMENT OBSOLETES + INFO('Invalid bug ID or author does not have access to the bug'); + my $phab_error_message = ""; + Bugzilla->template->process('revision/comments.html.tmpl', + {message => 'invalid_bug_id'}, + \$phab_error_message); + $revision->add_comment($phab_error_message); + $revision->update(); + return; + } + + # REVISION SECURITY POLICY + + # If bug is public then remove privacy policy + if (!@{$bug->groups_in}) { + INFO('Bug is public so setting view/edit public'); + $revision->make_public(); + } + + # else bug is private. + else { + # Here we create a new custom policy containing the project + # groups that are mapped to bugzilla groups. + my $set_project_names = [map { "bmo-" . $_->name } @{$bug->groups_in}]; + + # 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/) { + INFO("Loading current policy: " . $revision->view_policy); + $current_policy = Bugzilla::Extension::PhabBugz::Policy->new_from_query( + {phids => [$revision->view_policy]}); + 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) { + INFO('Project groups do not match. Need new custom policy'); + $current_policy = undef; + } + else { + INFO('Project groups match. Leaving current policy as-is'); + } + } - # fixup attachments on current bug - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + if (!$current_policy) { + INFO("Creating new custom policy: " . join(", ", @$set_project_names)); + $revision->make_private($set_project_names); + } - foreach my $attachment (@attachments) { - my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); - next if $attach_revision_id != $revision->id; + # 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 $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0; - INFO('Updating obsolete status on attachmment ' . $attachment->id); - $attachment->set_is_obsolete($make_obsolete); + my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - if ($revision->title ne $attachment->description) { - INFO('Updating description on attachment ' . $attachment->id); - $attachment->set_description($revision->title); - } + INFO('Checking for revision attachment'); + my $rev_attachment = create_revision_attachment($bug, $revision, $timestamp, + $revision->author->bugzilla_user); + INFO('Attachment ' . $rev_attachment->id . ' created or already exists.'); - $attachment->update($timestamp); - } + # ATTACHMENT OBSOLETES - # fixup attachments with same revision id but on different bugs - my %other_bugs; - my $other_attachments = Bugzilla::Attachment->match({ - mimetype => PHAB_CONTENT_TYPE, - filename => 'phabricator-D' . $revision->id . '-url.txt', - WHERE => { 'bug_id != ? AND NOT isobsolete' => $bug->id } - }); - foreach my $attachment (@$other_attachments) { - $other_bugs{$attachment->bug_id}++; - INFO('Updating obsolete status on attachment ' . - $attachment->id . " for bug " . $attachment->bug_id); - $attachment->set_is_obsolete(1); - $attachment->update($timestamp); - } + # fixup attachments on current bug + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - # FINISH UP + foreach my $attachment (@attachments) { + my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + next if $attach_revision_id != $revision->id; - $bug->update($timestamp); - $revision->update(); + my $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0; + INFO('Updating obsolete status on attachmment ' . $attachment->id); + $attachment->set_is_obsolete($make_obsolete); - # Email changes for this revisions bug and also for any other - # bugs that previously had these revision attachments - foreach my $bug_id ($revision->bug_id, keys %other_bugs) { - Bugzilla::BugMail::Send($bug_id, { changer => $changer->bugzilla_user }); + if ($revision->title ne $attachment->description) { + INFO('Updating description on attachment ' . $attachment->id); + $attachment->set_description($revision->title); } - INFO('SUCCESS: Revision D' . $revision->id . ' processed'); + $attachment->update($timestamp); + } + + # fixup attachments with same revision id but on different bugs + my %other_bugs; + my $other_attachments = Bugzilla::Attachment->match({ + mimetype => PHAB_CONTENT_TYPE, + filename => 'phabricator-D' . $revision->id . '-url.txt', + WHERE => {'bug_id != ? AND NOT isobsolete' => $bug->id} + }); + foreach my $attachment (@$other_attachments) { + $other_bugs{$attachment->bug_id}++; + INFO( 'Updating obsolete status on attachment ' + . $attachment->id + . " for bug " + . $attachment->bug_id); + $attachment->set_is_obsolete(1); + $attachment->update($timestamp); + } + + # FINISH UP + + $bug->update($timestamp); + $revision->update(); + + # Email changes for this revisions bug and also for any other + # bugs that previously had these revision attachments + foreach my $bug_id ($revision->bug_id, keys %other_bugs) { + Bugzilla::BugMail::Send($bug_id, {changer => $changer->bugzilla_user}); + } + + INFO('SUCCESS: Revision D' . $revision->id . ' processed'); } sub process_new_user { - state $check = compile($Invocant, HashRef); - my ( $self, $user_data ) = $check->(@_); + state $check = compile($Invocant, HashRef); + my ($self, $user_data) = $check->(@_); - # Load the user data into a proper object - my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data); + # Load the user data into a proper object + my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data); - if (!$phab_user->bugzilla_id) { - WARN("SKIPPING: No bugzilla id associated with user"); - return; - } + if (!$phab_user->bugzilla_id) { + WARN("SKIPPING: No bugzilla id associated with user"); + return; + } - my $bug_user = $phab_user->bugzilla_user; + my $bug_user = $phab_user->bugzilla_user; - # Pre setup before querying DB - my $restore_prev_user = set_phab_user(); + # Pre setup before querying DB + my $restore_prev_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( " + # 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); - } + 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 + # ADD SUBSCRIBERS TO REVSISIONS FOR CURRENT PRIVATE BUGS - my $params = { - f3 => 'OP', - j3 => 'OR', + my $params = { + f3 => 'OP', + j3 => 'OR', - # User must be either reporter, assignee, qa_contact - # or on the cc list of the bug - f4 => 'cc', - o4 => 'equals', - v4 => $bug_user->login, + # User must be either reporter, assignee, qa_contact + # or on the cc list of the bug + f4 => 'cc', + o4 => 'equals', + v4 => $bug_user->login, - f5 => 'assigned_to', - o5 => 'equals', - v5 => $bug_user->login, + f5 => 'assigned_to', + o5 => 'equals', + v5 => $bug_user->login, - f6 => 'qa_contact', - o6 => 'equals', - v6 => $bug_user->login, + f6 => 'qa_contact', + o6 => 'equals', + v6 => $bug_user->login, - f7 => 'reporter', - o7 => 'equals', - v7 => $bug_user->login, + f7 => 'reporter', + o7 => 'equals', + v7 => $bug_user->login, - f9 => 'CP', + f9 => 'CP', - # The bug needs to be private - f10 => 'bug_group', - o10 => 'isnotempty', + # The bug needs to be private + f10 => 'bug_group', + o10 => 'isnotempty', - # And the bug must have one or more attachments - # that are connected to revisions - f11 => 'attachments.filename', - o11 => 'regexp', - v11 => '^phabricator-D[[:digit:]]+-url.txt$', - }; + # And the bug must have one or more attachments + # that are connected to revisions + f11 => 'attachments.filename', + o11 => 'regexp', + v11 => '^phabricator-D[[:digit:]]+-url.txt$', + }; - my $search = Bugzilla::Search->new( fields => [ 'bug_id' ], - params => $params, - order => [ 'bug_id' ] ); - my $data = $search->data; + my $search = Bugzilla::Search->new( + fields => ['bug_id'], + params => $params, + order => ['bug_id'] + ); + my $data = $search->data; - # the first value of each row should be the bug id - my @bug_ids = map { shift @$_ } @$data; + # 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"); + INFO("Updating subscriber values for old private bugs"); - foreach my $bug_id (@bug_ids) { - INFO("Processing bug $bug_id"); + foreach my $bug_id (@bug_ids) { + INFO("Processing bug $bug_id"); - my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 }); + my $bug = Bugzilla::Bug->new({id => $bug_id, cache => 1}); - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - foreach my $attachment (@attachments) { - my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + foreach my $attachment (@attachments) { + my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); - if (!$revision_id) { - WARN("Skipping " . $attachment->filename . " on bug $bug_id. Filename should be fixed."); - next; - } + if (!$revision_id) { + WARN( "Skipping " + . $attachment->filename + . " on bug $bug_id. Filename should be fixed."); + next; + } - INFO("Processing revision D$revision_id"); + INFO("Processing revision D$revision_id"); - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( - { ids => [ int($revision_id) ] }); + my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( + {ids => [int($revision_id)]}); - $revision->add_subscriber($phab_user->phid); - $revision->update(); + $revision->add_subscriber($phab_user->phid); + $revision->update(); - INFO("Revision $revision_id updated"); - } + INFO("Revision $revision_id updated"); } + } - INFO('SUCCESS: User ' . $phab_user->id . ' processed'); + INFO('SUCCESS: User ' . $phab_user->id . ' processed'); } ################## @@ -698,87 +697,74 @@ sub process_new_user { ################## sub new_stories { - my ( $self, $after ) = @_; - my $data = { view => 'text' }; - $data->{after} = $after if $after; + my ($self, $after) = @_; + my $data = {view => 'text'}; + $data->{after} = $after if $after; - my $result = request( 'feed.query_id', $data ); + my $result = request('feed.query_id', $data); - unless ( ref $result->{result}{data} eq 'ARRAY' - && @{ $result->{result}{data} } ) - { - return []; - } + unless (ref $result->{result}{data} eq 'ARRAY' && @{$result->{result}{data}}) { + return []; + } - # Guarantee that the data is in ascending ID order - return [ sort { $a->{id} <=> $b->{id} } @{ $result->{result}{data} } ]; + # Guarantee that the data is in ascending ID order + return [sort { $a->{id} <=> $b->{id} } @{$result->{result}{data}}]; } sub new_users { - my ( $self, $after ) = @_; - my $data = { - order => [ "id" ], - attachments => { - 'external-accounts' => 1 - } - }; - $data->{before} = $after if $after; + my ($self, $after) = @_; + my $data = {order => ["id"], attachments => {'external-accounts' => 1}}; + $data->{before} = $after if $after; - my $result = request( 'user.search', $data ); + my $result = request('user.search', $data); - unless ( ref $result->{result}{data} eq 'ARRAY' - && @{ $result->{result}{data} } ) - { - return []; - } + unless (ref $result->{result}{data} eq 'ARRAY' && @{$result->{result}{data}}) { + return []; + } - # Guarantee that the data is in ascending ID order - return [ sort { $a->{id} <=> $b->{id} } @{ $result->{result}{data} } ]; + # Guarantee that the data is in ascending ID order + return [sort { $a->{id} <=> $b->{id} } @{$result->{result}{data}}]; } sub get_last_id { - my ( $self, $type ) = @_; - my $type_full = $type . "_last_id"; - my $last_id = Bugzilla->dbh->selectrow_array( " - SELECT value FROM phabbugz WHERE name = ?", undef, $type_full ); - $last_id ||= 0; - TRACE(uc($type_full) . ": $last_id" ); - return $last_id; + my ($self, $type) = @_; + my $type_full = $type . "_last_id"; + my $last_id = Bugzilla->dbh->selectrow_array(" + SELECT value FROM phabbugz WHERE name = ?", undef, $type_full); + $last_id ||= 0; + TRACE(uc($type_full) . ": $last_id"); + return $last_id; } sub save_last_id { - my ( $self, $last_id, $type ) = @_; + my ($self, $last_id, $type) = @_; - # Store the largest last key so we can start from there in the next session - my $type_full = $type . "_last_id"; - TRACE("UPDATING " . uc($type_full) . ": $last_id" ); - Bugzilla->dbh->do( "REPLACE INTO phabbugz (name, value) VALUES (?, ?)", - undef, $type_full, $last_id ); + # Store the largest last key so we can start from there in the next session + my $type_full = $type . "_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 { - state $check = compile( $Invocant, Group | Str ); - my ( $self, $group ) = $check->(@_); - my $group_obj = - ref $group ? $group : Bugzilla::Group->check( { name => $group, cache => 1 } ); + state $check = compile($Invocant, Group | Str); + my ($self, $group) = $check->(@_); + 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 $flat_list + = join(',', @{Bugzilla::Group->flatten_group_membership($group_obj->id)}); - my $user_query = " + 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); + my $user_ids = Bugzilla->dbh->selectcol_arrayref($user_query); - # Return matching users in Phabricator - return Bugzilla::Extension::PhabBugz::User->match( - { - ids => $user_ids - } - ); + # Return matching users in Phabricator + return Bugzilla::Extension::PhabBugz::User->match({ids => $user_ids}); } 1; |