From 8ec8da0491ad89604700b3e29a227966f6d84ba1 Mon Sep 17 00:00:00 2001 From: Perl Tidy Date: Wed, 5 Dec 2018 15:38:52 -0500 Subject: no bug - reformat all the code using the new perltidy rules --- extensions/AntiSpam/Config.pm | 9 +- extensions/AntiSpam/Extension.pm | 476 +- extensions/AntiSpam/lib/Config.pm | 116 +- extensions/BMO/Config.pm | 21 +- extensions/BMO/Extension.pm | 4648 ++++++++++---------- extensions/BMO/bin/bug_1022707.pl | 6 +- extensions/BMO/bin/bug_1093952.pl | 52 +- extensions/BMO/bin/bug_1141452.pl | 110 +- extensions/BMO/bin/migrate-github-pull-requests.pl | 67 +- extensions/BMO/lib/Constants.pm | 6 +- extensions/BMO/lib/Data.pm | 403 +- extensions/BMO/lib/FakeBug.pm | 28 +- extensions/BMO/lib/Reports/Groups.pm | 485 +- extensions/BMO/lib/Reports/Internship.pm | 135 +- extensions/BMO/lib/Reports/ProductSecurity.pm | 76 +- extensions/BMO/lib/Reports/Recruiting.pm | 126 +- extensions/BMO/lib/Reports/ReleaseTracking.pm | 779 ++-- extensions/BMO/lib/Reports/Triage.pm | 516 +-- extensions/BMO/lib/Reports/UserActivity.pm | 382 +- extensions/BMO/lib/Util.pm | 111 +- extensions/BMO/lib/WebService.pm | 68 +- extensions/BMO/t/bounty_attachment.t | 93 +- extensions/Bitly/Config.pm | 2 +- extensions/Bitly/Extension.pm | 13 +- extensions/Bitly/lib/WebService.pm | 190 +- extensions/BugModal/Config.pm | 6 +- extensions/BugModal/Extension.pm | 551 ++- extensions/BugModal/lib/ActivityStream.pm | 567 +-- extensions/BugModal/lib/MonkeyPatches.pm | 21 +- extensions/BugModal/lib/Util.pm | 28 +- extensions/BugModal/lib/WebService.pm | 618 +-- extensions/BugmailFilter/Config.pm | 2 +- extensions/BugmailFilter/Extension.pm | 807 ++-- extensions/BugmailFilter/lib/Constants.pm | 120 +- extensions/BugmailFilter/lib/FakeField.pm | 50 +- extensions/BugmailFilter/lib/Filter.pm | 217 +- extensions/BzAPI/Extension.pm | 339 +- extensions/BzAPI/bin/rest.cgi | 16 +- extensions/BzAPI/lib/Constants.pm | 230 +- extensions/BzAPI/lib/Resources/Bug.pm | 1385 +++--- extensions/BzAPI/lib/Resources/Bugzilla.pm | 208 +- extensions/BzAPI/lib/Resources/User.pm | 75 +- extensions/BzAPI/lib/Util.pm | 652 +-- extensions/ComponentWatching/Extension.pm | 891 ++-- extensions/ComponentWatching/lib/WebService.pm | 128 +- extensions/ContributorEngagement/Config.pm | 6 +- extensions/ContributorEngagement/Extension.pm | 147 +- extensions/ContributorEngagement/lib/Constants.pm | 15 +- extensions/EditComments/Config.pm | 2 +- extensions/EditComments/Extension.pm | 355 +- extensions/EditComments/lib/WebService.pm | 89 +- extensions/EditTable/Config.pm | 2 +- extensions/EditTable/Extension.pm | 227 +- extensions/Ember/Extension.pm | 6 +- extensions/Ember/lib/FakeBug.pm | 92 +- extensions/Ember/lib/WebService.pm | 1229 +++--- extensions/Example/Config.pm | 21 +- extensions/Example/Extension.pm | 1344 +++--- extensions/Example/lib/Auth/Login.pm | 2 +- extensions/Example/lib/Auth/Verify.pm | 2 +- extensions/Example/lib/Config.pm | 13 +- extensions/Example/lib/WebService.pm | 4 +- .../template/en/default/setup/strings.txt.pl | 4 +- extensions/FlagDefaultRequestee/Extension.pm | 198 +- extensions/FlagDefaultRequestee/lib/Constants.pm | 8 +- extensions/FlagTypeComment/Extension.pm | 264 +- extensions/FlagTypeComment/lib/Constants.pm | 32 +- extensions/GitHubAuth/Extension.pm | 88 +- extensions/GitHubAuth/lib/Client.pm | 134 +- extensions/GitHubAuth/lib/Client/Error.pm | 42 +- extensions/GitHubAuth/lib/Config.pm | 24 +- extensions/GitHubAuth/lib/Login.pm | 329 +- extensions/GitHubAuth/lib/Verify.pm | 6 +- extensions/GoogleAnalytics/Extension.pm | 6 +- extensions/GoogleAnalytics/lib/Config.pm | 41 +- extensions/Gravatar/Config.pm | 2 +- extensions/Gravatar/Extension.pm | 52 +- extensions/Gravatar/lib/Data.pm | 6 +- extensions/GuidedBugEntry/Config.pm | 6 +- extensions/GuidedBugEntry/Extension.pm | 182 +- extensions/InlineHistory/Extension.pm | 384 +- extensions/LastResolved/Config.pm | 6 +- extensions/LastResolved/Extension.pm | 130 +- extensions/LimitedEmail/Config.pm | 14 +- extensions/LimitedEmail/Extension.pm | 62 +- extensions/MozProjectReview/Config.pm | 6 +- extensions/MozProjectReview/Extension.pm | 254 +- extensions/MyDashboard/Extension.pm | 317 +- extensions/MyDashboard/lib/BugInterest.pm | 54 +- extensions/MyDashboard/lib/Queries.pm | 566 +-- extensions/MyDashboard/lib/Util.pm | 32 +- extensions/MyDashboard/lib/WebService.pm | 220 +- extensions/Needinfo/Config.pm | 6 +- extensions/Needinfo/Extension.pm | 435 +- extensions/OldBugMove/Extension.pm | 248 +- extensions/OldBugMove/lib/Params.pm | 22 +- extensions/OpenGraph/Config.pm | 6 +- extensions/OrangeFactor/Extension.pm | 59 +- extensions/PhabBugz/Extension.pm | 81 +- extensions/PhabBugz/bin/phabbugz_feed.pl | 4 +- extensions/PhabBugz/lib/Config.pm | 76 +- extensions/PhabBugz/lib/Constants.pm | 14 +- extensions/PhabBugz/lib/Daemon.pm | 89 +- extensions/PhabBugz/lib/Feed.pm | 1206 +++-- extensions/PhabBugz/lib/Policy.pm | 133 +- extensions/PhabBugz/lib/Project.pm | 355 +- extensions/PhabBugz/lib/Revision.pm | 544 +-- extensions/PhabBugz/lib/Types.pm | 17 +- extensions/PhabBugz/lib/User.pm | 202 +- extensions/PhabBugz/lib/Util.pm | 251 +- extensions/PhabBugz/lib/WebService.pm | 187 +- extensions/PhabBugz/t/basic.t | 252 +- extensions/PhabBugz/t/feed-daemon-guts.t | 191 +- extensions/PhabBugz/t/review-flags.t | 246 +- extensions/ProdCompSearch/Config.pm | 2 +- extensions/ProdCompSearch/Extension.pm | 6 +- extensions/ProdCompSearch/lib/WebService.pm | 237 +- extensions/Profanivore/Config.pm | 12 +- extensions/Profanivore/Extension.pm | 247 +- extensions/Push/Config.pm | 39 +- extensions/Push/Extension.pm | 876 ++-- extensions/Push/bin/bugzilla-pushd.pl | 4 +- extensions/Push/bin/nagios_push_checker.pl | 33 +- extensions/Push/lib/Admin.pm | 171 +- extensions/Push/lib/BacklogMessage.pm | 135 +- extensions/Push/lib/BacklogQueue.pm | 138 +- extensions/Push/lib/Backoff.pm | 82 +- extensions/Push/lib/Config.pm | 298 +- extensions/Push/lib/Connector.disabled/AMQP.pm | 344 +- .../Push/lib/Connector.disabled/ServiceNow.pm | 690 +-- extensions/Push/lib/Connector/Base.pm | 99 +- extensions/Push/lib/Connector/File.pm | 61 +- extensions/Push/lib/Connector/Phabricator.pm | 153 +- extensions/Push/lib/Connector/Spark.pm | 228 +- extensions/Push/lib/Connectors.pm | 129 +- extensions/Push/lib/Constants.pm | 28 +- extensions/Push/lib/Daemon.pm | 87 +- extensions/Push/lib/Log.pm | 32 +- extensions/Push/lib/LogEntry.pm | 42 +- extensions/Push/lib/Logger.pm | 50 +- extensions/Push/lib/Message.pm | 72 +- extensions/Push/lib/Option.pm | 32 +- extensions/Push/lib/Push.pm | 434 +- extensions/Push/lib/Queue.pm | 71 +- extensions/Push/lib/Serialise.pm | 427 +- extensions/Push/lib/Util.pm | 165 +- .../Push/template/en/default/setup/strings.txt.pl | 4 +- extensions/REMO/Config.pm | 6 +- extensions/REMO/Extension.pm | 547 +-- extensions/RequestNagger/Config.pm | 6 +- extensions/RequestNagger/Extension.pm | 552 ++- extensions/RequestNagger/bin/send-request-nags.pl | 481 +- extensions/RequestNagger/lib/Bug.pm | 34 +- extensions/RequestNagger/lib/Constants.pm | 69 +- extensions/RequestNagger/lib/Settings.pm | 75 +- extensions/RestrictComments/Config.pm | 2 +- extensions/RestrictComments/Extension.pm | 98 +- extensions/RestrictComments/lib/Config.pm | 40 +- extensions/Review/Config.pm | 2 +- extensions/Review/Extension.pm | 1651 ++++--- .../Review/bin/migrate_mentor_from_whiteboard.pl | 268 +- extensions/Review/bin/review_requests_rebuild.pl | 6 +- extensions/Review/lib/FlagStateActivity.pm | 110 +- extensions/Review/lib/Util.pm | 71 +- extensions/Review/lib/WebService.pm | 468 +- extensions/SecureMail/Config.pm | 32 +- extensions/SecureMail/Extension.pm | 1046 ++--- extensions/SecureMail/lib/TCT.pm | 112 +- extensions/ShadowBugs/Config.pm | 2 +- extensions/ShadowBugs/Extension.pm | 114 +- extensions/SiteMapIndex/Config.pm | 10 +- extensions/SiteMapIndex/Extension.pm | 116 +- extensions/SiteMapIndex/lib/Constants.pm | 8 +- extensions/SiteMapIndex/lib/Util.pm | 207 +- extensions/Splinter/Extension.pm | 217 +- extensions/Splinter/lib/Config.pm | 14 +- extensions/Splinter/lib/Util.pm | 208 +- extensions/TagNewUsers/Config.pm | 6 +- extensions/TagNewUsers/Extension.pm | 346 +- extensions/TrackingFlags/Config.pm | 12 +- extensions/TrackingFlags/Extension.pm | 1224 +++--- extensions/TrackingFlags/bin/bug_825946.pl | 25 +- extensions/TrackingFlags/bin/bulk_flag_clear.pl | 80 +- .../TrackingFlags/bin/migrate_tracking_flags.pl | 407 +- extensions/TrackingFlags/lib/Admin.pm | 721 +-- extensions/TrackingFlags/lib/Constants.pm | 44 +- extensions/TrackingFlags/lib/Flag.pm | 632 +-- extensions/TrackingFlags/lib/Flag/Bug.pm | 168 +- extensions/TrackingFlags/lib/Flag/Value.pm | 129 +- extensions/TrackingFlags/lib/Flag/Visibility.pm | 195 +- extensions/TypeSniffer/Config.pm | 14 +- extensions/TypeSniffer/Extension.pm | 103 +- extensions/UserProfile/Config.pm | 6 +- extensions/UserProfile/Extension.pm | 796 ++-- extensions/UserProfile/bin/migrate.pl | 14 +- extensions/UserProfile/bin/update.pl | 62 +- extensions/UserProfile/lib/Util.pm | 365 +- extensions/UserStory/Config.pm | 9 +- extensions/UserStory/Extension.pm | 110 +- extensions/UserStory/lib/Constants.pm | 2 +- extensions/Voting/Config.pm | 6 +- extensions/Voting/Extension.pm | 1367 +++--- extensions/ZPushNotify/Config.pm | 2 +- extensions/ZPushNotify/Extension.pm | 183 +- extensions/create.pl | 38 +- 205 files changed, 22908 insertions(+), 23636 deletions(-) (limited to 'extensions') diff --git a/extensions/AntiSpam/Config.pm b/extensions/AntiSpam/Config.pm index e16add9b7..18cd3efa2 100644 --- a/extensions/AntiSpam/Config.pm +++ b/extensions/AntiSpam/Config.pm @@ -12,13 +12,8 @@ use strict; use warnings; use constant NAME => 'AntiSpam'; -use constant REQUIRED_MODULES => [ - { - package => 'Email-Address', - module => 'Email::Address', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Email-Address', module => 'Email::Address', version => 0,},]; use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/AntiSpam/Extension.pm b/extensions/AntiSpam/Extension.pm index 19eddb4e7..990130c8e 100644 --- a/extensions/AntiSpam/Extension.pm +++ b/extensions/AntiSpam/Extension.pm @@ -26,34 +26,29 @@ our $VERSION = '1'; # sub _project_honeypot_blocking { - my ($self, $api_key, $login) = @_; - my $ip = remote_ip(); - return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; - my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org"; - return unless my $packed = gethostbyname($lookup); - my $honeypot = inet_ntoa($packed); - return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; - my ($status, $days, $threat, $type) = ($1, $2, $3, $4); - - return if $status != 127 - || $threat < Bugzilla->params->{honeypot_threat_threshold}; - - Bugzilla->audit(sprintf("blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot)); - ThrowUserError('account_creation_restricted'); + my ($self, $api_key, $login) = @_; + my $ip = remote_ip(); + return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org"; + return unless my $packed = gethostbyname($lookup); + my $honeypot = inet_ntoa($packed); + return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my ($status, $days, $threat, $type) = ($1, $2, $3, $4); + + return + if $status != 127 || $threat < Bugzilla->params->{honeypot_threat_threshold}; + + Bugzilla->audit( + sprintf("blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot)); + ThrowUserError('account_creation_restricted'); } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{auth}->{params} }, { - name => 'honeypot_api_key', - type => 't', - default => '', - }; - push @{ $args->{panels}->{auth}->{params} }, { - name => 'honeypot_threat_threshold', - type => 't', - default => '32', - }; + my ($self, $args) = @_; + push @{$args->{panels}->{auth}->{params}}, + {name => 'honeypot_api_key', type => 't', default => '',}; + push @{$args->{panels}->{auth}->{params}}, + {name => 'honeypot_threat_threshold', type => 't', default => '32',}; } # @@ -61,20 +56,22 @@ sub config_modify_panels { # sub _comment_blocking { - my ($self, $params) = @_; - my $user = Bugzilla->user; - return if $user->in_group('editbugs'); - - my $blocklist = Bugzilla->dbh->selectcol_arrayref( - 'SELECT word FROM antispam_comment_blocklist' - ); - return unless @$blocklist; - - my $regex = '\b(?:' . join('|', map { quotemeta } @$blocklist) . ')\b'; - if ($params->{thetext} =~ /$regex/i) { - Bugzilla->audit(sprintf("blocked <%s> %s from commenting, blacklisted phrase", remote_ip(), $user->login)); - ThrowUserError('antispam_comment_blocked'); - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + return if $user->in_group('editbugs'); + + my $blocklist = Bugzilla->dbh->selectcol_arrayref( + 'SELECT word FROM antispam_comment_blocklist'); + return unless @$blocklist; + + my $regex = '\b(?:' . join('|', map {quotemeta} @$blocklist) . ')\b'; + if ($params->{thetext} =~ /$regex/i) { + Bugzilla->audit(sprintf( + "blocked <%s> %s from commenting, blacklisted phrase", + remote_ip(), $user->login + )); + ThrowUserError('antispam_comment_blocked'); + } } # @@ -82,17 +79,19 @@ sub _comment_blocking { # sub _domain_blocking { - my ($self, $login) = @_; - my $address = Email::Address->new(undef, $login); - my $blocked = Bugzilla->dbh->selectrow_array( - "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?", - undef, - $address->host - ); - if ($blocked) { - Bugzilla->audit(sprintf("blocked <%s> from creating %s, blacklisted domain", remote_ip(), $login)); - ThrowUserError('account_creation_restricted'); - } + my ($self, $login) = @_; + my $address = Email::Address->new(undef, $login); + my $blocked + = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?", + undef, $address->host); + if ($blocked) { + Bugzilla->audit(sprintf( + "blocked <%s> from creating %s, blacklisted domain", + remote_ip(), $login + )); + ThrowUserError('account_creation_restricted'); + } } # @@ -100,18 +99,18 @@ sub _domain_blocking { # sub _ip_blocking { - my ($self, $login) = @_; - my $ip = remote_ip(); - trick_taint($ip); - my $blocked = Bugzilla->dbh->selectrow_array( - "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?", - undef, - $ip - ); - if ($blocked) { - Bugzilla->audit(sprintf("blocked <%s> from creating %s, blacklisted IP", $ip, $login)); - ThrowUserError('account_creation_restricted'); - } + my ($self, $login) = @_; + my $ip = remote_ip(); + trick_taint($ip); + my $blocked + = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?", + undef, $ip); + if ($blocked) { + Bugzilla->audit( + sprintf("blocked <%s> from creating %s, blacklisted IP", $ip, $login)); + ThrowUserError('account_creation_restricted'); + } } # @@ -119,44 +118,50 @@ sub _ip_blocking { # sub _is_limited_user { - return Bugzilla->user->creation_age < Bugzilla->params->{antispam_multi_user_limit_age}; + return Bugzilla->user->creation_age + < Bugzilla->params->{antispam_multi_user_limit_age}; } sub bug_before_create { - my ($self, $args) = @_; - $self->_cc_limit($args->{params}, 'cc'); + my ($self, $args) = @_; + $self->_cc_limit($args->{params}, 'cc'); } sub bug_start_of_set_all { - my ($self, $args) = @_; - $self->_cc_limit($args->{params}, 'newcc'); + my ($self, $args) = @_; + $self->_cc_limit($args->{params}, 'newcc'); } sub _cc_limit { - my ($self, $params, $cc_field) = @_; - return unless _is_limited_user(); - return unless exists $params->{$cc_field}; - - my $cc_count = ref($params->{$cc_field}) ? scalar(@{ $params->{$cc_field} }) : 1; - if ($cc_count > Bugzilla->params->{antispam_multi_user_limit_count}) { - Bugzilla->audit(sprintf("blocked <%s> from CC'ing %s users", Bugzilla->user->login, $cc_count)); - delete $params->{$cc_field}; - if (exists $params->{cc} && exists $params->{cc}->{add}) { - delete $params->{cc}->{add}; - } + my ($self, $params, $cc_field) = @_; + return unless _is_limited_user(); + return unless exists $params->{$cc_field}; + + my $cc_count = ref($params->{$cc_field}) ? scalar(@{$params->{$cc_field}}) : 1; + if ($cc_count > Bugzilla->params->{antispam_multi_user_limit_count}) { + Bugzilla->audit( + sprintf("blocked <%s> from CC'ing %s users", Bugzilla->user->login, $cc_count)); + delete $params->{$cc_field}; + if (exists $params->{cc} && exists $params->{cc}->{add}) { + delete $params->{cc}->{add}; } + } } sub bug_set_flags { - my ($self, $args) = @_; - return unless _is_limited_user(); - - my $flag_count = @{ $args->{new_flags} }; - if ($flag_count > Bugzilla->params->{antispam_multi_user_limit_count}) { - Bugzilla->audit(sprintf("blocked <%s> from flaging %s users", Bugzilla->user->login, $flag_count)); - # empty the arrayref - $#{ $args->{new_flags} } = -1; - } + my ($self, $args) = @_; + return unless _is_limited_user(); + + my $flag_count = @{$args->{new_flags}}; + if ($flag_count > Bugzilla->params->{antispam_multi_user_limit_count}) { + Bugzilla->audit(sprintf( + "blocked <%s> from flaging %s users", + Bugzilla->user->login, $flag_count + )); + + # empty the arrayref + $#{$args->{new_flags}} = -1; + } } # @@ -164,30 +169,31 @@ sub bug_set_flags { # sub comment_after_add_tag { - my ($self, $args) = @_; - my $tag = lc($args->{tag}); - return unless $tag eq 'spam' or $tag eq 'abusive' or $tag eq 'abuse'; - my $comment = $args->{comment}; - my $author = $comment->author; - - # exclude disabled users - return if !$author->is_enabled; - - # exclude users by group - return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group}); - - # exclude users who are no longer new - return if !$author->is_new; - - # exclude users who haven't made enough comments - my $count = $tag eq 'spam' - ? Bugzilla->params->{antispam_spammer_comment_count} - : Bugzilla->params->{antispam_abusive_comment_count}; - return if $author->comment_count < $count; - - # get user's comments - trick_taint($tag); - my $comments = Bugzilla->dbh->selectall_arrayref(" + my ($self, $args) = @_; + my $tag = lc($args->{tag}); + return unless $tag eq 'spam' or $tag eq 'abusive' or $tag eq 'abuse'; + my $comment = $args->{comment}; + my $author = $comment->author; + + # exclude disabled users + return if !$author->is_enabled; + + # exclude users by group + return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group}); + + # exclude users who are no longer new + return if !$author->is_new; + + # exclude users who haven't made enough comments + my $count + = $tag eq 'spam' + ? Bugzilla->params->{antispam_spammer_comment_count} + : Bugzilla->params->{antispam_abusive_comment_count}; + return if $author->comment_count < $count; + + # get user's comments + trick_taint($tag); + my $comments = Bugzilla->dbh->selectall_arrayref(" SELECT longdescs.comment_id,longdescs_tags.id FROM longdescs LEFT JOIN longdescs_tags @@ -197,41 +203,39 @@ sub comment_after_add_tag { ORDER BY longdescs.bug_when ", undef, $tag, $author->id); - # this comment needs to be counted too - my $comment_id = $comment->id; - foreach my $ra (@$comments) { - if ($ra->[0] == $comment_id) { - $ra->[1] = 1; - last; - } - } - - # throw away comment id and negate bool to make it a list of not-spam/abuse - $comments = [ map { $_->[1] ? 0 : 1 } @$comments ]; - - my $reason; - - # check if the first N comments are spam/abuse - if (!scalar(grep { $_ } @$comments[0..($count - 1)])) { - $reason = "first $count comments are $tag"; - } - - # check if the last N comments are spam/abuse - elsif (!scalar(grep { $_ } @$comments[-$count..-1])) { - $reason = "last $count comments are $tag"; - } - - # disable - if ($reason) { - $author->set_disabledtext( - $tag eq 'spam' - ? Bugzilla->params->{antispam_spammer_disable_text} - : Bugzilla->params->{antispam_abusive_disable_text} - ); - $author->set_disable_mail(1); - $author->update(); - Bugzilla->audit(sprintf("antispam disabled <%s>: %s", $author->login, $reason)); + # this comment needs to be counted too + my $comment_id = $comment->id; + foreach my $ra (@$comments) { + if ($ra->[0] == $comment_id) { + $ra->[1] = 1; + last; } + } + + # throw away comment id and negate bool to make it a list of not-spam/abuse + $comments = [map { $_->[1] ? 0 : 1 } @$comments]; + + my $reason; + + # check if the first N comments are spam/abuse + if (!scalar(grep {$_} @$comments[0 .. ($count - 1)])) { + $reason = "first $count comments are $tag"; + } + + # check if the last N comments are spam/abuse + elsif (!scalar(grep {$_} @$comments[-$count .. -1])) { + $reason = "last $count comments are $tag"; + } + + # disable + if ($reason) { + $author->set_disabledtext($tag eq 'spam' + ? Bugzilla->params->{antispam_spammer_disable_text} + : Bugzilla->params->{antispam_abusive_disable_text}); + $author->set_disable_mail(1); + $author->update(); + Bugzilla->audit(sprintf("antispam disabled <%s>: %s", $author->login, $reason)); + } } # @@ -239,51 +243,54 @@ sub comment_after_add_tag { # sub object_end_of_create_validators { - my ($self, $args) = @_; - if ($args->{class} eq 'Bugzilla::Comment') { - $self->_comment_blocking($args->{params}); - } + my ($self, $args) = @_; + if ($args->{class} eq 'Bugzilla::Comment') { + $self->_comment_blocking($args->{params}); + } } sub user_verify_login { - my ($self, $args) = @_; - if (my $api_key = Bugzilla->params->{honeypot_api_key}) { - $self->_project_honeypot_blocking($api_key, $args->{login}); - } - $self->_ip_blocking($args->{login}); - $self->_domain_blocking($args->{login}); + my ($self, $args) = @_; + if (my $api_key = Bugzilla->params->{honeypot_api_key}) { + $self->_project_honeypot_blocking($api_key, $args->{login}); + } + $self->_ip_blocking($args->{login}); + $self->_domain_blocking($args->{login}); } sub editable_tables { - my ($self, $args) = @_; - my $tables = $args->{tables}; - # allow these tables to be edited with the EditTables extension - $tables->{antispam_domain_blocklist} = { - id_field => 'id', - order_by => 'domain', - blurb => 'List of fully qualified domain names to block at account creation time.', - group => 'can_configure_antispam', - }; - $tables->{antispam_comment_blocklist} = { - id_field => 'id', - order_by => 'word', - blurb => "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" . - "This only applies to comments on bugs which the user didn't report.\n" . - "Users in the editbugs group are exempt from comment blocking.", - group => 'can_configure_antispam', - }; - $tables->{antispam_ip_blocklist} = { - id_field => 'id', - order_by => 'ip_address', - blurb => 'List of IPv4 addresses which are prevented from creating accounts.', - group => 'can_configure_antispam', - }; + my ($self, $args) = @_; + my $tables = $args->{tables}; + + # allow these tables to be edited with the EditTables extension + $tables->{antispam_domain_blocklist} = { + id_field => 'id', + order_by => 'domain', + blurb => + 'List of fully qualified domain names to block at account creation time.', + group => 'can_configure_antispam', + }; + $tables->{antispam_comment_blocklist} = { + id_field => 'id', + order_by => 'word', + blurb => + "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" + . "This only applies to comments on bugs which the user didn't report.\n" + . "Users in the editbugs group are exempt from comment blocking.", + group => 'can_configure_antispam', + }; + $tables->{antispam_ip_blocklist} = { + id_field => 'id', + order_by => 'ip_address', + blurb => 'List of IPv4 addresses which are prevented from creating accounts.', + group => 'can_configure_antispam', + }; } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config"; } # @@ -291,82 +298,43 @@ sub config_add_panels { # sub install_before_final_checks { - if (!Bugzilla::Group->new({ name => 'can_configure_antispam' })) { - Bugzilla::Group->create({ - name => 'can_configure_antispam', - description => 'Can configure Anti-Spam measures', - isbuggroup => 0, - }); - } + if (!Bugzilla::Group->new({name => 'can_configure_antispam'})) { + Bugzilla::Group->create({ + name => 'can_configure_antispam', + description => 'Can configure Anti-Spam measures', + isbuggroup => 0, + }); + } } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'antispam_domain_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - domain => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - comment => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_domain_blocklist_idx => { - FIELDS => [ 'domain' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'antispam_comment_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - word => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_comment_blocklist_idx => { - FIELDS => [ 'word' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'antispam_ip_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - ip_address => { - TYPE => 'VARCHAR(15)', - NOTNULL => 1, - }, - comment => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_ip_blocklist_idx => { - FIELDS => [ 'ip_address' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'antispam_domain_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + domain => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + comment => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_domain_blocklist_idx => {FIELDS => ['domain'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'antispam_comment_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + word => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_comment_blocklist_idx => {FIELDS => ['word'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'antispam_ip_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + ip_address => {TYPE => 'VARCHAR(15)', NOTNULL => 1,}, + comment => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_ip_blocklist_idx => {FIELDS => ['ip_address'], TYPE => 'UNIQUE',},], + }; } __PACKAGE__->NAME; diff --git a/extensions/AntiSpam/lib/Config.pm b/extensions/AntiSpam/lib/Config.pm index 278baea8f..3cddb77ed 100644 --- a/extensions/AntiSpam/lib/Config.pm +++ b/extensions/AntiSpam/lib/Config.pm @@ -17,66 +17,66 @@ use Bugzilla::Group; our $sortkey = 511; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'antispam_spammer_exclude_group', - type => 's', - choices => \&get_all_group_names, - default => 'canconfirm', - checker => \&check_group - }, - { - name => 'antispam_spammer_comment_count', - type => 't', - default => '3', - checker => \&check_numeric - }, - { - name => 'antispam_spammer_disable_text', - type => 'l', - default => - "This account has been automatically disabled as a result of " . - "a high number of spam comments.
\n
\n" . - "Please contact the address at the end of this message if " . - "you believe this to be an error." - }, - { - name => 'antispam_abusive_comment_count', - type => 't', - default => '5', - checker => \&check_numeric - }, - { - name => 'antispam_abusive_disable_text', - type => 'l', - default => - "This account has been automatically disabled as a result of " . - "a high number of comments tagged as abusive.
\n
\n" . - "All interactions on Bugzilla should follow our " . - "localconfig->{'urlbase'} . "page.cgi?id=etiquette.html\">" . - "etiquette guidelines.
\n
\n" . - "Please contact the address at the end of this message if you " . - "believe this to be an error, or if you would like your account " . - "reactivated in order to interact within our etiquette " . - "guidelines." - }, - { - name => 'antispam_multi_user_limit_age', - type => 't', - default => '2', - checker => \&check_numeric, - }, - { - name => 'antispam_multi_user_limit_count', - type => 't', - default => '5', - checker => \&check_numeric, - }, - ); + my @param_list = ( + { + name => 'antispam_spammer_exclude_group', + type => 's', + choices => \&get_all_group_names, + default => 'canconfirm', + checker => \&check_group + }, + { + name => 'antispam_spammer_comment_count', + type => 't', + default => '3', + checker => \&check_numeric + }, + { + name => 'antispam_spammer_disable_text', + type => 'l', + default => "This account has been automatically disabled as a result of " + . "a high number of spam comments.
\n
\n" + . "Please contact the address at the end of this message if " + . "you believe this to be an error." + }, + { + name => 'antispam_abusive_comment_count', + type => 't', + default => '5', + checker => \&check_numeric + }, + { + name => 'antispam_abusive_disable_text', + type => 'l', + default => "This account has been automatically disabled as a result of " + . "a high number of comments tagged as abusive.
\n
\n" + . "All interactions on Bugzilla should follow our " + . "localconfig->{'urlbase'} + . "page.cgi?id=etiquette.html\">" + . "etiquette guidelines.
\n
\n" + . "Please contact the address at the end of this message if you " + . "believe this to be an error, or if you would like your account " + . "reactivated in order to interact within our etiquette " + . "guidelines." + }, + { + name => 'antispam_multi_user_limit_age', + type => 't', + default => '2', + checker => \&check_numeric, + }, + { + name => 'antispam_multi_user_limit_count', + type => 't', + default => '5', + checker => \&check_numeric, + }, + ); - return @param_list; + return @param_list; } 1; diff --git a/extensions/BMO/Config.pm b/extensions/BMO/Config.pm index 153af24cb..e185b0b5d 100644 --- a/extensions/BMO/Config.pm +++ b/extensions/BMO/Config.pm @@ -28,24 +28,11 @@ use warnings; use constant NAME => 'BMO'; use constant REQUIRED_MODULES => [ - { - package => 'Tie-IxHash', - module => 'Tie::IxHash', - version => 0 - }, - { - package => 'Sys-Syslog', - module => 'Sys::Syslog', - version => 0 - }, - { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0' - }, + {package => 'Tie-IxHash', module => 'Tie::IxHash', version => 0}, + {package => 'Sys-Syslog', module => 'Sys::Syslog', version => 0}, + {package => 'File-MimeInfo', module => 'File::MimeInfo::Magic', version => '0'}, ]; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index 7499f0d1c..f4fb6fa32 100644 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -72,296 +72,304 @@ our $VERSION = '0.1'; # BEGIN { - *Bugzilla::Bug::last_closed_date = \&_last_closed_date; - *Bugzilla::Bug::reporters_hw_os = \&_bug_reporters_hw_os; - *Bugzilla::Bug::is_unassigned = \&_bug_is_unassigned; - *Bugzilla::Bug::has_current_patch = \&_bug_has_current_patch; - *Bugzilla::Bug::missing_sec_approval = \&_bug_missing_sec_approval; - *Bugzilla::Product::default_security_group = \&_default_security_group; - *Bugzilla::Product::default_security_group_obj = \&_default_security_group_obj; - *Bugzilla::Product::group_always_settable = \&_group_always_settable; - *Bugzilla::Product::default_platform_id = \&_product_default_platform_id; - *Bugzilla::Product::default_op_sys_id = \&_product_default_op_sys_id; - *Bugzilla::Product::default_platform = \&_product_default_platform; - *Bugzilla::Product::default_op_sys = \&_product_default_op_sys; - *Bugzilla::check_default_product_security_group = \&_check_default_product_security_group; - *Bugzilla::Attachment::is_bounty_attachment = \&_attachment_is_bounty_attachment; - *Bugzilla::Attachment::bounty_details = \&_attachment_bounty_details; - *Bugzilla::Attachment::external_redirect = \&_attachment_external_redirect; - *Bugzilla::Attachment::can_review = \&_attachment_can_review; - *Bugzilla::Attachment::fetch_github_pr_diff = \&_attachment_fetch_github_pr_diff; + *Bugzilla::Bug::last_closed_date = \&_last_closed_date; + *Bugzilla::Bug::reporters_hw_os = \&_bug_reporters_hw_os; + *Bugzilla::Bug::is_unassigned = \&_bug_is_unassigned; + *Bugzilla::Bug::has_current_patch = \&_bug_has_current_patch; + *Bugzilla::Bug::missing_sec_approval = \&_bug_missing_sec_approval; + *Bugzilla::Product::default_security_group = \&_default_security_group; + *Bugzilla::Product::default_security_group_obj = \&_default_security_group_obj; + *Bugzilla::Product::group_always_settable = \&_group_always_settable; + *Bugzilla::Product::default_platform_id = \&_product_default_platform_id; + *Bugzilla::Product::default_op_sys_id = \&_product_default_op_sys_id; + *Bugzilla::Product::default_platform = \&_product_default_platform; + *Bugzilla::Product::default_op_sys = \&_product_default_op_sys; + *Bugzilla::check_default_product_security_group + = \&_check_default_product_security_group; + *Bugzilla::Attachment::is_bounty_attachment + = \&_attachment_is_bounty_attachment; + *Bugzilla::Attachment::bounty_details = \&_attachment_bounty_details; + *Bugzilla::Attachment::external_redirect = \&_attachment_external_redirect; + *Bugzilla::Attachment::can_review = \&_attachment_can_review; + *Bugzilla::Attachment::fetch_github_pr_diff + = \&_attachment_fetch_github_pr_diff; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product; - - if ($file =~ /^list\/list/) { - # Purpose: enable correct sorting of list table - # Matched to changes in list/table.html.tmpl - my %db_order_column_name_map = ( - 'map_components.name' => 'component', - 'map_products.name' => 'product', - 'map_reporter.login_name' => 'reporter', - 'map_assigned_to.login_name' => 'assigned_to', - 'delta_ts' => 'opendate', - 'creation_ts' => 'changeddate', - ); - - my @orderstrings = split(/,\s*/, $vars->{'order'}); - - # contains field names of the columns being used to sort the table. - my @order_columns; - foreach my $o (@orderstrings) { - $o =~ s/bugs.//; - $o = $db_order_column_name_map{$o} if - grep($_ eq $o, keys(%db_order_column_name_map)); - next if (grep($_ eq $o, @order_columns)); - push(@order_columns, $o); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product; + + if ($file =~ /^list\/list/) { + + # Purpose: enable correct sorting of list table + # Matched to changes in list/table.html.tmpl + my %db_order_column_name_map = ( + 'map_components.name' => 'component', + 'map_products.name' => 'product', + 'map_reporter.login_name' => 'reporter', + 'map_assigned_to.login_name' => 'assigned_to', + 'delta_ts' => 'opendate', + 'creation_ts' => 'changeddate', + ); - $vars->{'order_columns'} = \@order_columns; + my @orderstrings = split(/,\s*/, $vars->{'order'}); - # fields that have a custom sortkey. (So they are correctly sorted - # when using js) - my @sortkey_fields = qw(bug_status resolution bug_severity priority - rep_platform op_sys); + # contains field names of the columns being used to sort the table. + my @order_columns; + foreach my $o (@orderstrings) { + $o =~ s/bugs.//; + $o = $db_order_column_name_map{$o} + if grep($_ eq $o, keys(%db_order_column_name_map)); + next if (grep($_ eq $o, @order_columns)); + push(@order_columns, $o); + } - my %columns_sortkey; - foreach my $field (@sortkey_fields) { - $columns_sortkey{$field} = _get_field_values_sort_key($field); - } - $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones'); + $vars->{'order_columns'} = \@order_columns; - $vars->{'columns_sortkey'} = \%columns_sortkey; + # fields that have a custom sortkey. (So they are correctly sorted + # when using js) + my @sortkey_fields = qw(bug_status resolution bug_severity priority + rep_platform op_sys); + + my %columns_sortkey; + foreach my $field (@sortkey_fields) { + $columns_sortkey{$field} = _get_field_values_sort_key($field); } - elsif ($file =~ /^bug\/create\/create[\.-](.*)/) { - my $format = $1; - if (!$vars->{'cloned_bug_id'}) { - # Allow status whiteboard values to be bookmarked - $vars->{'status_whiteboard'} = - Bugzilla->cgi->param('status_whiteboard') || ""; - } + $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones'); - # Purpose: for pretty product chooser - $vars->{'format'} = Bugzilla->cgi->param('format'); + $vars->{'columns_sortkey'} = \%columns_sortkey; + } + elsif ($file =~ /^bug\/create\/create[\.-](.*)/) { + my $format = $1; + if (!$vars->{'cloned_bug_id'}) { - if ($format eq 'doc.html.tmpl') { - my $versions = Bugzilla::Product->new({ name => 'Core' })->versions; - $vars->{'versions'} = [ reverse @$versions ]; - } - } - elsif ($file eq 'bug/edit.html.tmpl' || $file eq 'bug_modal/edit.html.tmpl') { - $vars->{split_cf_crash_signature} = $self->_split_crash_signature($vars); + # Allow status whiteboard values to be bookmarked + $vars->{'status_whiteboard'} = Bugzilla->cgi->param('status_whiteboard') || ""; } + # Purpose: for pretty product chooser + $vars->{'format'} = Bugzilla->cgi->param('format'); - if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) { - # hack to allow the bug entry templates to use check_can_change_field - # to see if various field values should be available to the current user. - $vars->{'default'} = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {}); + if ($format eq 'doc.html.tmpl') { + my $versions = Bugzilla::Product->new({name => 'Core'})->versions; + $vars->{'versions'} = [reverse @$versions]; } + } + elsif ($file eq 'bug/edit.html.tmpl' || $file eq 'bug_modal/edit.html.tmpl') { + $vars->{split_cf_crash_signature} = $self->_split_crash_signature($vars); + } - if ($file =~ /^attachment\/diff-header\./) { - my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid}; - $vars->{attachment} = Bugzilla::Attachment->new({ id => $attachid, cache => 1 }) - if $attachid; - } - if ($file =~ /^admin\/products\/(create|edit)\./) { - my $product = $vars->{product}; - my $security_groups = Bugzilla::Group->match({ isbuggroup => 1, isactive => 1 }); - if ($product) { - # If set group is not active currently, we add it into the list - if (!grep($_->name eq $product->default_security_group, @$security_groups)) { - push(@$security_groups, $product->default_security_group_obj); - @$security_groups = sort { $a->name cmp $b->name } @$security_groups; - } - } - $vars->{security_groups} = $security_groups; - } -} + if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) { -sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + # hack to allow the bug entry templates to use check_can_change_field + # to see if various field values should be available to the current user. + $vars->{'default'} + = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {}); + } - if ($page eq 'user_activity.html') { - require Bugzilla::Extension::BMO::Reports::UserActivity; - Bugzilla::Extension::BMO::Reports::UserActivity::report($vars); + if ($file =~ /^attachment\/diff-header\./) { + my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid}; + $vars->{attachment} = Bugzilla::Attachment->new({id => $attachid, cache => 1}) + if $attachid; + } + if ($file =~ /^admin\/products\/(create|edit)\./) { + my $product = $vars->{product}; + my $security_groups = Bugzilla::Group->match({isbuggroup => 1, isactive => 1}); + if ($product) { + + # If set group is not active currently, we add it into the list + if (!grep($_->name eq $product->default_security_group, @$security_groups)) { + push(@$security_groups, $product->default_security_group_obj); + @$security_groups = sort { $a->name cmp $b->name } @$security_groups; + } } - elsif ($page eq 'triage_reports.html') { - require Bugzilla::Extension::BMO::Reports::Triage; - Bugzilla::Extension::BMO::Reports::Triage::unconfirmed($vars); - } - elsif ($page eq 'triage_owners.html') { - require Bugzilla::Extension::BMO::Reports::Triage; - Bugzilla::Extension::BMO::Reports::Triage::owners($vars); - } - elsif ($page eq 'group_admins.html') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::admins_report($vars); - } - elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::membership_report($page, $vars); - } - elsif ($page eq 'group_members.html' or $page eq 'group_members.json') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::members_report($page, $vars); - } - elsif ($page eq 'recruiting_dashboard.html') { - require Bugzilla::Extension::BMO::Reports::Recruiting; - Bugzilla::Extension::BMO::Reports::Recruiting::report($vars); - } - elsif ($page eq 'internship_dashboard.html') { - require Bugzilla::Extension::BMO::Reports::Internship; - Bugzilla::Extension::BMO::Reports::Internship::report($vars); - } - elsif ($page eq 'email_queue.html') { - print Bugzilla->cgi->redirect('view_job_queue.cgi'); - } - elsif ($page eq 'release_tracking_report.html') { - require Bugzilla::Extension::BMO::Reports::ReleaseTracking; - Bugzilla::Extension::BMO::Reports::ReleaseTracking::report($vars); - } - elsif ($page eq 'product_security_report.html') { - require Bugzilla::Extension::BMO::Reports::ProductSecurity; - Bugzilla::Extension::BMO::Reports::ProductSecurity::report($vars); - } - elsif ($page eq 'fields.html') { - # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl - # were changed for better performance and are now only loaded once. - # I have not found an easy way to allow our hook template to check if - # it is called from pages/fields.html.tmpl. So we set a value in request_cache - # that our hook template can see. - Bugzilla->request_cache->{'bmo_fields_page'} = 1; - } - elsif ($page eq 'query_database.html') { - query_database($vars); - } - elsif ($page eq 'attachment_bounty_form.html') { - bounty_attachment($vars); - } - elsif ($page eq 'triage_request.html') { - triage_request($vars); - } + $vars->{security_groups} = $security_groups; + } +} + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'user_activity.html') { + require Bugzilla::Extension::BMO::Reports::UserActivity; + Bugzilla::Extension::BMO::Reports::UserActivity::report($vars); + + } + elsif ($page eq 'triage_reports.html') { + require Bugzilla::Extension::BMO::Reports::Triage; + Bugzilla::Extension::BMO::Reports::Triage::unconfirmed($vars); + } + elsif ($page eq 'triage_owners.html') { + require Bugzilla::Extension::BMO::Reports::Triage; + Bugzilla::Extension::BMO::Reports::Triage::owners($vars); + } + elsif ($page eq 'group_admins.html') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::admins_report($vars); + } + elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::membership_report($page, $vars); + } + elsif ($page eq 'group_members.html' or $page eq 'group_members.json') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::members_report($page, $vars); + } + elsif ($page eq 'recruiting_dashboard.html') { + require Bugzilla::Extension::BMO::Reports::Recruiting; + Bugzilla::Extension::BMO::Reports::Recruiting::report($vars); + } + elsif ($page eq 'internship_dashboard.html') { + require Bugzilla::Extension::BMO::Reports::Internship; + Bugzilla::Extension::BMO::Reports::Internship::report($vars); + } + elsif ($page eq 'email_queue.html') { + print Bugzilla->cgi->redirect('view_job_queue.cgi'); + } + elsif ($page eq 'release_tracking_report.html') { + require Bugzilla::Extension::BMO::Reports::ReleaseTracking; + Bugzilla::Extension::BMO::Reports::ReleaseTracking::report($vars); + } + elsif ($page eq 'product_security_report.html') { + require Bugzilla::Extension::BMO::Reports::ProductSecurity; + Bugzilla::Extension::BMO::Reports::ProductSecurity::report($vars); + } + elsif ($page eq 'fields.html') { + + # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl + # were changed for better performance and are now only loaded once. + # I have not found an easy way to allow our hook template to check if + # it is called from pages/fields.html.tmpl. So we set a value in request_cache + # that our hook template can see. + Bugzilla->request_cache->{'bmo_fields_page'} = 1; + } + elsif ($page eq 'query_database.html') { + query_database($vars); + } + elsif ($page eq 'attachment_bounty_form.html') { + bounty_attachment($vars); + } + elsif ($page eq 'triage_request.html') { + triage_request($vars); + } } sub bounty_attachment { - my ($vars) = @_; - - my $user = Bugzilla->user; - $user->in_group('bounty-team') - || ThrowUserError("auth_failure", { group => "bounty-team", - action => "add", - object => "bounty_attachments" }); - - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - my $bug = Bugzilla::Bug->check({ id => $input->{bug_id}, cache => 1 }); - my $attachment = first { $_ && _attachment_is_bounty_attachment($_) } @{$bug->attachments}; - $vars->{bug} = $bug; - - if ($input->{submit}) { - ThrowUserError('bounty_attachment_missing_reporter') - unless $input->{reporter_email}; - - check_hash_token($input->{token}, ['bounty', $bug->id]); - - my @fields = qw( reporter_email amount_paid reported_date fixed_date awarded_date publish ); - my %form = map { $_ => $input->{$_} } @fields; - $form{credit} = [ grep { defined } map { $input->{"credit_$_"} } 1..3 ]; - - $dbh->bz_start_transaction(); - if ($attachment) { - $attachment->set( - description => format_bounty_attachment_description(\%form) - ); - $attachment->update; - } - else { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - isprivate => 1, - mimetype => 'text/plain', - data => 'bounty', - filename => 'bugbounty.data', - description => format_bounty_attachment_description(\%form), - }); - } - $dbh->bz_commit_transaction(); + my ($vars) = @_; - Bugzilla::BugMail::Send($bug->id, { changer => $user }); + my $user = Bugzilla->user; + $user->in_group('bounty-team') + || ThrowUserError("auth_failure", + {group => "bounty-team", action => "add", object => "bounty_attachments"}); - print Bugzilla->cgi->redirect('show_bug.cgi?id=' . $bug->id); - exit; - } + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + my $bug = Bugzilla::Bug->check({id => $input->{bug_id}, cache => 1}); + my $attachment + = first { $_ && _attachment_is_bounty_attachment($_) } @{$bug->attachments}; + $vars->{bug} = $bug; + + if ($input->{submit}) { + ThrowUserError('bounty_attachment_missing_reporter') + unless $input->{reporter_email}; + + check_hash_token($input->{token}, ['bounty', $bug->id]); + + my @fields + = qw( reporter_email amount_paid reported_date fixed_date awarded_date publish ); + my %form = map { $_ => $input->{$_} } @fields; + $form{credit} = [grep {defined} map { $input->{"credit_$_"} } 1 .. 3]; + $dbh->bz_start_transaction(); if ($attachment) { - $vars->{form} = $attachment->bounty_details; + $attachment->set(description => format_bounty_attachment_description(\%form)); + $attachment->update; } else { - my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $vars->{form} = { - reporter_email => $bug->reporter->email, - reported_date => format_time($bug->creation_ts, "%Y-%m-%d"), - awarded_date => format_time($now, "%Y-%m-%d"), - publish => 1 - }; - if ($bug->cf_last_resolved) { - $vars->{form}{fixed_date} = format_time($bug->cf_last_resolved, "%Y-%m-%d"), - } + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + isprivate => 1, + mimetype => 'text/plain', + data => 'bounty', + filename => 'bugbounty.data', + description => format_bounty_attachment_description(\%form), + }); + } + $dbh->bz_commit_transaction(); + + Bugzilla::BugMail::Send($bug->id, {changer => $user}); + + print Bugzilla->cgi->redirect('show_bug.cgi?id=' . $bug->id); + exit; + } + + if ($attachment) { + $vars->{form} = $attachment->bounty_details; + } + else { + my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $vars->{form} = { + reporter_email => $bug->reporter->email, + reported_date => format_time($bug->creation_ts, "%Y-%m-%d"), + awarded_date => format_time($now, "%Y-%m-%d"), + publish => 1 + }; + if ($bug->cf_last_resolved) { + $vars->{form}{fixed_date} = format_time($bug->cf_last_resolved, "%Y-%m-%d"),; } - $vars->{form}{token} = issue_hash_token(['bounty', $bug->id]); + } + $vars->{form}{token} = issue_hash_token(['bounty', $bug->id]); } sub _attachment_is_bounty_attachment { - my ($attachment) = @_; + my ($attachment) = @_; - return 0 unless $attachment->filename eq 'bugbounty.data'; - return 0 unless $attachment->contenttype eq 'text/plain'; - return 0 unless $attachment->isprivate; - return 0 unless $attachment->attacher->in_group('bounty-team'); + return 0 unless $attachment->filename eq 'bugbounty.data'; + return 0 unless $attachment->contenttype eq 'text/plain'; + return 0 unless $attachment->isprivate; + return 0 unless $attachment->attacher->in_group('bounty-team'); - return $attachment->description =~ /^(?:[^,]*,)+[^,]*$/; + return $attachment->description =~ /^(?:[^,]*,)+[^,]*$/; } sub _attachment_bounty_details { - my ($attachment) = @_; - if (!exists $attachment->{bounty_details}) { - if ($attachment->is_bounty_attachment) { - $attachment->{bounty_details} = parse_bounty_attachment_description($attachment->description); - } - else { - $attachment->{bounty_details} = undef; - } + my ($attachment) = @_; + if (!exists $attachment->{bounty_details}) { + if ($attachment->is_bounty_attachment) { + $attachment->{bounty_details} + = parse_bounty_attachment_description($attachment->description); + } + else { + $attachment->{bounty_details} = undef; } - return $attachment->{bounty_details}; + } + return $attachment->{bounty_details}; } sub format_bounty_attachment_description { - my ($form) = @_; - my @fields = ( - @$form{qw( reporter_email amount_paid reported_date fixed_date awarded_date )}, - $form->{publish} ? 'true' : 'false', - @{ $form->{credit} // [] } - ); + my ($form) = @_; + my @fields = ( + @$form{qw( reporter_email amount_paid reported_date fixed_date awarded_date )}, + $form->{publish} ? 'true' : 'false', + @{$form->{credit} // []} + ); - return join(',', map { $_ // '' } @fields); + return join(',', map { $_ // '' } @fields); } sub parse_bounty_attachment_description { - my ($desc) = @_; + my ($desc) = @_; - my %map = ( true => 1, false => 0 ); - my $date = qr/\d{4}-\d{2}-\d{2}/; - $desc =~ m! + my %map = (true => 1, false => 0); + my $date = qr/\d{4}-\d{2}-\d{2}/; + $desc =~ m! ^ (? [^,]+) \s*,\s* (? [0-9]+[-+?]?) ? \s*,\s* @@ -373,1799 +381,1830 @@ sub parse_bounty_attachment_description { $ !x; - return { - reporter_email => $+{reporter_email} // '', - amount_paid => $+{amount_paid} // '', - reported_date => $+{reported_date} // '', - fixed_date => $+{fixed_date} // '', - awarded_date => $+{awarded_date} // '', - publish => $map{ $+{publish} // 'false' }, - credit => [grep { $_ } split(/\s*,\s*/, $+{credits}) ] - }; + return { + reporter_email => $+{reporter_email} // '', + amount_paid => $+{amount_paid} // '', + reported_date => $+{reported_date} // '', + fixed_date => $+{fixed_date} // '', + awarded_date => $+{awarded_date} // '', + publish => $map{$+{publish} // 'false'}, + credit => [grep {$_} split(/\s*,\s*/, $+{credits})] + }; } sub triage_request { - my ($vars) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - if (Bugzilla->input_params->{update}) { - Bugzilla->set_user(Bugzilla::User->super_user); - $user->set_groups({ add => [ 'canconfirm' ] }); - Bugzilla->set_user($user); - $user->update(); - $vars->{updated} = 1; - } + my ($vars) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + if (Bugzilla->input_params->{update}) { + Bugzilla->set_user(Bugzilla::User->super_user); + $user->set_groups({add => ['canconfirm']}); + Bugzilla->set_user($user); + $user->update(); + $vars->{updated} = 1; + } } sub _get_field_values_sort_key { - my ($field) = @_; - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectall_arrayref( - "SELECT value, sortkey FROM $field - ORDER BY sortkey, value"); - - my %field_values; - foreach my $field (@$fields) { - my ($value, $sortkey) = @$field; - $field_values{$value} = $sortkey; - } - return \%field_values; + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + "SELECT value, sortkey FROM $field + ORDER BY sortkey, value" + ); + + my %field_values; + foreach my $field (@$fields) { + my ($value, $sortkey) = @$field; + $field_values{$value} = $sortkey; + } + return \%field_values; } sub active_custom_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my $params = $args->{'params'}; - my $product = $params->{'product'}; - my $component = $params->{'component'}; + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my $params = $args->{'params'}; + my $product = $params->{'product'}; + my $component = $params->{'component'}; - return if !$product; + return if !$product; - my $product_name = blessed $product ? $product->name : $product; - my $component_name = blessed $component ? $component->name : $component; + my $product_name = blessed $product ? $product->name : $product; + my $component_name = blessed $component ? $component->name : $component; - my @tmp_fields; - foreach my $field (@$$fields) { - next if cf_hidden_in_product($field->name, $product_name, $component_name); - push(@tmp_fields, $field); - } - $$fields = \@tmp_fields; + my @tmp_fields; + foreach my $field (@$$fields) { + next if cf_hidden_in_product($field->name, $product_name, $component_name); + push(@tmp_fields, $field); + } + $$fields = \@tmp_fields; } sub cf_hidden_in_product { - my ($field_name, $product_name, $component_name, $bug) = @_; - - # check bugzilla's built-in visibility controls first - if ($bug) { - my $field = Bugzilla::Field->new({ name => $field_name, cache => 1 }); - return 1 if $field && !$field->is_visible_on_bug($bug); - } - - # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product - # elsewhere, we just pass the name of the product. - $product_name = blessed($product_name) - ? $product_name->name - : $product_name; - - # Also in buglist.cgi, we pass in a list of components instead - # of a single component name everywhere else. - my $component_list = []; - if ($component_name) { - $component_list = ref $component_name - ? $component_name - : [ $component_name ]; - } - - foreach my $field_re (keys %$cf_visible_in_products) { - if ($field_name =~ $field_re) { - # If no product given, for example more than one product - # in buglist.cgi, then hide field by default - return 1 if !$product_name; - - my $products = $cf_visible_in_products->{$field_re}; - foreach my $product (keys %$products) { - my $components = $products->{$product}; - - my $found_component = 0; - if (@$components) { - foreach my $component (@$components) { - if (ref($component) eq 'Regexp') { - if (grep($_ =~ $component, @$component_list)) { - $found_component = 1; - last; - } - } else { - if (grep($_ eq $component, @$component_list)) { - $found_component = 1; - last; - } - } - } - } - - # If product matches and at at least one component matches - # from component_list (if a matching component was required), - # we allow the field to be seen - if ($product eq $product_name && (!@$components || $found_component)) { - return 0; - } + my ($field_name, $product_name, $component_name, $bug) = @_; + + # check bugzilla's built-in visibility controls first + if ($bug) { + my $field = Bugzilla::Field->new({name => $field_name, cache => 1}); + return 1 if $field && !$field->is_visible_on_bug($bug); + } + + # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product + # elsewhere, we just pass the name of the product. + $product_name = blessed($product_name) ? $product_name->name : $product_name; + + # Also in buglist.cgi, we pass in a list of components instead + # of a single component name everywhere else. + my $component_list = []; + if ($component_name) { + $component_list = ref $component_name ? $component_name : [$component_name]; + } + + foreach my $field_re (keys %$cf_visible_in_products) { + if ($field_name =~ $field_re) { + + # If no product given, for example more than one product + # in buglist.cgi, then hide field by default + return 1 if !$product_name; + + my $products = $cf_visible_in_products->{$field_re}; + foreach my $product (keys %$products) { + my $components = $products->{$product}; + + my $found_component = 0; + if (@$components) { + foreach my $component (@$components) { + if (ref($component) eq 'Regexp') { + if (grep($_ =~ $component, @$component_list)) { + $found_component = 1; + last; + } } - return 1; + else { + if (grep($_ eq $component, @$component_list)) { + $found_component = 1; + last; + } + } + } } + + # If product matches and at at least one component matches + # from component_list (if a matching component was required), + # we allow the field to be seen + if ($product eq $product_name && (!@$components || $found_component)) { + return 0; + } + } + return 1; } + } - return 0; + return 0; } # Purpose: CC certain email addresses on bugmail when a bug is added or # removed from a particular group. sub bugmail_recipients { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $recipients = $args->{'recipients'}; - my $diffs = $args->{'diffs'}; - - if (@$diffs) { - # Changed bug - foreach my $ref (@$diffs) { - my $old = $ref->{old}; - my $new = $ref->{new}; - my $fieldname = $ref->{field_name}; - - if ($fieldname eq "bug_group") { - _cc_if_special_group($old, $recipients); - _cc_if_special_group($new, $recipients); - } - } - } else { - # Determine if it's a new bug, or a comment without a field change - my $comment_count = scalar @{$bug->comments}; - if ($comment_count == 1) { - # New bug - foreach my $group (@{ $bug->groups_in }) { - _cc_if_special_group($group->{'name'}, $recipients); - } - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + if (@$diffs) { + + # Changed bug + foreach my $ref (@$diffs) { + my $old = $ref->{old}; + my $new = $ref->{new}; + my $fieldname = $ref->{field_name}; + + if ($fieldname eq "bug_group") { + _cc_if_special_group($old, $recipients); + _cc_if_special_group($new, $recipients); + } + } + } + else { + # Determine if it's a new bug, or a comment without a field change + my $comment_count = scalar @{$bug->comments}; + if ($comment_count == 1) { + + # New bug + foreach my $group (@{$bug->groups_in}) { + _cc_if_special_group($group->{'name'}, $recipients); + } } + } } sub _cc_if_special_group { - my ($group, $recipients) = @_; + my ($group, $recipients) = @_; - return if !$group; + return if !$group; - if (exists $group_change_notification{$group}) { - foreach my $login (@{ $group_change_notification{$group} }) { - my $id = login_to_id($login); - $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT(); - } + if (exists $group_change_notification{$group}) { + foreach my $login (@{$group_change_notification{$group}}) { + my $id = login_to_id($login); + $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT(); } + } } sub _check_trusted { - my ($field, $trusted, $priv_results) = @_; + my ($field, $trusted, $priv_results) = @_; - my $needed_group = $trusted->{'_default'} || ""; - foreach my $dfield (keys %$trusted) { - if ($field =~ $dfield) { - $needed_group = $trusted->{$dfield}; - } - } - if ($needed_group && !Bugzilla->user->in_group($needed_group)) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + my $needed_group = $trusted->{'_default'} || ""; + foreach my $dfield (keys %$trusted) { + if ($field =~ $dfield) { + $needed_group = $trusted->{$dfield}; } + } + if ($needed_group && !Bugzilla->user->in_group($needed_group)) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } sub _is_field_set { - my $value = shift; - return $value ne '---' && $value !~ /\?$/; + my $value = shift; + return $value ne '---' && $value !~ /\?$/; } sub bug_check_can_change_field { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $field = $args->{'field'}; - my $new_value = $args->{'new_value'}; - my $old_value = $args->{'old_value'}; - my $priv_results = $args->{'priv_results'}; - my $user = Bugzilla->user; - - if ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') { - # Cannot use the standard %cf_setter mapping as we want anyone - # to be able to set ?, just not the other values. - if ($field eq 'cf_cab_review') { - if ($new_value ne '1' - && $new_value ne '?' - && !$user->in_group('infra', $bug->product_id)) - { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } - } - # "other" custom field setters restrictions - elsif (exists $cf_setters->{$field}) { - my $in_group = 0; - foreach my $group (@{$cf_setters->{$field}}) { - if ($user->in_group($group, $bug->product_id)) { - $in_group = 1; - last; - } - } - if (!$in_group) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $field = $args->{'field'}; + my $new_value = $args->{'new_value'}; + my $old_value = $args->{'old_value'}; + my $priv_results = $args->{'priv_results'}; + my $user = Bugzilla->user; + + if ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') { + + # Cannot use the standard %cf_setter mapping as we want anyone + # to be able to set ?, just not the other values. + if ($field eq 'cf_cab_review') { + if ( $new_value ne '1' + && $new_value ne '?' + && !$user->in_group('infra', $bug->product_id)) + { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + + # "other" custom field setters restrictions + elsif (exists $cf_setters->{$field}) { + my $in_group = 0; + foreach my $group (@{$cf_setters->{$field}}) { + if ($user->in_group($group, $bug->product_id)) { + $in_group = 1; + last; } + } + if (!$in_group) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } - elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') { - # The EXPIRED resolution should only be settable by gerv. - if ($user->login ne 'gerv@mozilla.org') { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + } + elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') { - } elsif ($field eq 'resolution' && $new_value eq 'FIXED') { - # You need at least canconfirm to mark a bug as FIXED - if (!$user->in_group('canconfirm', $bug->{'product_id'})) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + # The EXPIRED resolution should only be settable by gerv. + if ($user->login ne 'gerv@mozilla.org') { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } - } elsif ( - ($field eq 'bug_status' && $old_value eq 'VERIFIED') - || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED') - || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED') - ) { - # You need at least editbugs to reopen a resolved/verified bug - if (!$user->in_group('editbugs', $bug->{'product_id'})) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + } + elsif ($field eq 'resolution' && $new_value eq 'FIXED') { - } elsif ($user->in_group('canconfirm', $bug->{'product_id'})) { - # Canconfirm is really "cantriage"; users with canconfirm can also mark - # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE. - if ($field eq 'bug_status' - && is_open_state($old_value) - && !is_open_state($new_value)) - { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } - elsif ($field eq 'resolution' && - ($new_value eq 'DUPLICATE' || - $new_value eq 'WORKSFORME' || - $new_value eq 'INCOMPLETE' || - ($old_value eq '' && $new_value eq '1'))) - { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } - elsif ($field eq 'dup_id') { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } + # You need at least canconfirm to mark a bug as FIXED + if (!$user->in_group('canconfirm', $bug->{'product_id'})) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } - } elsif ($field eq 'bug_status') { - # Disallow reopening of bugs which have been resolved for > 1 year - if (is_open_state($new_value) - && !is_open_state($old_value) - && $bug->resolution eq 'FIXED') - { - my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone); - $days_ago->subtract(days => 365); - my $last_closed = datetime_from($bug->last_closed_date); - if ($last_closed lt $days_ago) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } - } + } + elsif (($field eq 'bug_status' && $old_value eq 'VERIFIED') + || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED') + || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED')) + { + # You need at least editbugs to reopen a resolved/verified bug + if (!$user->in_group('editbugs', $bug->{'product_id'})) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); } + + } + elsif ($user->in_group('canconfirm', $bug->{'product_id'})) { + + # Canconfirm is really "cantriage"; users with canconfirm can also mark + # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE. + if ( $field eq 'bug_status' + && is_open_state($old_value) + && !is_open_state($new_value)) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + elsif ( + $field eq 'resolution' + && ( $new_value eq 'DUPLICATE' + || $new_value eq 'WORKSFORME' + || $new_value eq 'INCOMPLETE' + || ($old_value eq '' && $new_value eq '1')) + ) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + elsif ($field eq 'dup_id') { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + + } + elsif ($field eq 'bug_status') { + + # Disallow reopening of bugs which have been resolved for > 1 year + if ( is_open_state($new_value) + && !is_open_state($old_value) + && $bug->resolution eq 'FIXED') + { + my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $days_ago->subtract(days => 365); + my $last_closed = datetime_from($bug->last_closed_date); + if ($last_closed lt $days_ago) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + } } # link up various Mozilla-specific strings sub bug_format_comment { - my ($self, $args) = @_; - my $regexes = $args->{'regexes'}; + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; - # link to crash-stats - # Only match if not already in an URL using the negative lookbehind (? qr/(? qr/(? sub { - my $args = shift; - my $match = html_quote($args->{matches}->[0]); - return qq{bp-$match}; - } - }); - - # link to CVE/CAN security releases - push (@$regexes, { - match => qr/(? sub { - my $args = shift; - my $match = html_quote($args->{matches}->[0]); - return qq{$match}; - } - }); - - # link to svn.m.o - push (@$regexes, { - match => qr/(^|\s)r(\d{4,})\b/, - replace => sub { - my $args = shift; - my $match = html_quote($args->{matches}->[1]); - return - $args->{matches}->[0] . - qq{r$match}; - } - }); - - # link old git.mozilla.org commit messages to github - push (@$regexes, { - match => qr#^(To\s(?:ssh://)?(?:[^\@]+\@)?git\.mozilla\.org[:/](.+?\.git)\n + replace => sub { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + return + qq{bp-$match}; + } + } + ); + + # link to CVE/CAN security releases + push( + @$regexes, + { + match => qr/(? sub { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + return + qq{$match}; + } + } + ); + + # link to svn.m.o + push( + @$regexes, + { + match => qr/(^|\s)r(\d{4,})\b/, + replace => sub { + my $args = shift; + my $match = html_quote($args->{matches}->[1]); + return $args->{matches}->[0] + . qq{r$match}; + } + } + ); + + # link old git.mozilla.org commit messages to github + push( + @$regexes, + { + match => qr#^(To\s(?:ssh://)?(?:[^\@]+\@)?git\.mozilla\.org[:/](.+?\.git)\n \s+)([0-9a-z]+\.\.([0-9a-z]+)\s+\S+\s->\s\S+)#mx, - replace => sub { - my $args = shift; - my $preamble = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $text = html_quote($args->{matches}->[2]); - my $revision = html_quote($args->{matches}->[3]); - $repo = 'mozilla/webtools-bmo-bugzilla' if $repo =~ /^webtools\/bmo\/bugzilla/; - $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git/; - $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org/; - return qq#$preamble$text#; - } - }); - - # link github commit messages - push (@$regexes, { - match => qr#^(To\s(?:https://|git@)?github\.com[:/](.+?)\.git\n + replace => sub { + my $args = shift; + my $preamble = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $text = html_quote($args->{matches}->[2]); + my $revision = html_quote($args->{matches}->[3]); + $repo = 'mozilla/webtools-bmo-bugzilla' if $repo =~ /^webtools\/bmo\/bugzilla/; + $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git/; + $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org/; + return + qq#$preamble$text#; + } + } + ); + + # link github commit messages + push( + @$regexes, + { + match => qr#^(To\s(?:https://|git@)?github\.com[:/](.+?)\.git\n \s+)([0-9a-z]+\.\.([0-9a-z]+)\s+\S+\s->\s\S+)#mx, - replace => sub { - my $args = shift; - my $preamble = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $text = html_quote($args->{matches}->[2]); - my $revision = html_quote($args->{matches}->[3]); - return qq#$preamble$text#; - } - }); - - # link github pull requests and issues - push (@$regexes, { - match => qr/(\s)([A-Za-z0-9_\.-]+)\/([A-Za-z0-9_\.-]+)\#([0-9]+)\b/, - replace => sub { - my $args = shift; - my $owner = html_quote($args->{matches}->[1]); - my $repo = html_quote($args->{matches}->[2]); - my $number = html_quote($args->{matches}->[3]); - return qq# $owner/$repo\#$number#; + replace => sub { + my $args = shift; + my $preamble = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $text = html_quote($args->{matches}->[2]); + my $revision = html_quote($args->{matches}->[3]); + return + qq#$preamble$text#; + } + } + ); + + # link github pull requests and issues + push( + @$regexes, + { + match => qr/(\s)([A-Za-z0-9_\.-]+)\/([A-Za-z0-9_\.-]+)\#([0-9]+)\b/, + replace => sub { + my $args = shift; + my $owner = html_quote($args->{matches}->[1]); + my $repo = html_quote($args->{matches}->[2]); + my $number = html_quote($args->{matches}->[3]); + return + qq# $owner/$repo\#$number#; + } + } + ); + +# Update certain links to git.mozilla.org to go to github.com instead +# https://git.mozilla.org/?p=webtools/bmo/bugzilla.git;a=blob;f=Bugzilla/WebService/Bug.pm;h=d7a1d8f9bb5fdee524f2bb342a4573a63d890f2e;hb=HEAD#l657 + push( + @$regexes, + { + match => qr#\b(https?://git\.mozilla\.org\S+)\b#mx, + replace => sub { + my $args = shift; + my $match = $args->{matches}->[0]; + my $uri = URI->new($match); + my $text = html_quote($match); + + # Only work on BMO and Bugzilla repos + my $repo = html_quote($uri->query_param_delete("p")) || ''; + if ($repo !~ /(webtools\/bmo|bugzilla)\//) { + return qq#$text#; } - }); - - # Update certain links to git.mozilla.org to go to github.com instead - # https://git.mozilla.org/?p=webtools/bmo/bugzilla.git;a=blob;f=Bugzilla/WebService/Bug.pm;h=d7a1d8f9bb5fdee524f2bb342a4573a63d890f2e;hb=HEAD#l657 - push(@$regexes, { - match => qr#\b(https?://git\.mozilla\.org\S+)\b#mx, - replace => sub { - my $args = shift; - my $match = $args->{matches}->[0]; - my $uri = URI->new($match); - my $text = html_quote($match); - - # Only work on BMO and Bugzilla repos - my $repo = html_quote($uri->query_param_delete("p")) || ''; - if ($repo !~ /(webtools\/bmo|bugzilla)\//) { - return qq#$text#; - } - my $action = html_quote($uri->query_param_delete("a")) || ''; - my $file = html_quote($uri->query_param_delete("f")) || ''; - my $frag = html_quote($uri->fragment) || ''; - my $from_rev = html_quote($uri->query_param_delete("h")) || ''; - my $to_rev = html_quote($uri->query_param_delete("hb")) || ''; + my $action = html_quote($uri->query_param_delete("a")) || ''; + my $file = html_quote($uri->query_param_delete("f")) || ''; + my $frag = html_quote($uri->fragment) || ''; + my $from_rev = html_quote($uri->query_param_delete("h")) || ''; + my $to_rev = html_quote($uri->query_param_delete("hb")) || ''; - if ($frag) { - $frag =~ tr/l/L/; - $frag = "#$frag"; - } + if ($frag) { + $frag =~ tr/l/L/; + $frag = "#$frag"; + } - $to_rev = $from_rev if !$to_rev; - $to_rev = 'master' if $to_rev eq 'HEAD'; - $to_rev =~ s#refs/heads/(.*)$#$1#; + $to_rev = $from_rev if !$to_rev; + $to_rev = 'master' if $to_rev eq 'HEAD'; + $to_rev =~ s#refs/heads/(.*)$#$1#; - $repo = 'mozilla-bteam/bmo' if $repo =~ /^webtools\/bmo\/bugzilla\.git$/; - $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git$/; - $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org\.git$/; + $repo = 'mozilla-bteam/bmo' if $repo =~ /^webtools\/bmo\/bugzilla\.git$/; + $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git$/; + $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org\.git$/; - if ($action eq 'tree') { - return $to_rev eq 'HEAD' - ? qq#$text [github]# - : qq#$text [github]#; - } - if ($action eq 'blob') { - return qq#$text [github]#; - } - if ($action eq 'shortlog' || $action eq 'log') { - return qq#$text [github]#; - } - if ($action eq 'commit' || $action eq 'commitdiff') { - return qq#$text [github]#; - } - return qq#$text#; + if ($action eq 'tree') { + return $to_rev eq 'HEAD' + ? qq#$text [github]# + : qq#$text [github]#; } - }); - - # link to hg.m.o - # Note: for grouping in this regexp, always use non-capturing parentheses. - my $hgrepos = join('|', qw!(?:releases/)?comm-[\w.]+ - (?:releases/)?mozilla-[\w.]+ - (?:releases/)?mobile-[\w.]+ - tracemonkey - tamarin-[\w.]+ - camino!); - - push (@$regexes, { - match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/, - replace => sub { - my $args = shift; - my $text = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $id = html_quote($args->{matches}->[2]); - $repo = 'integration/mozilla-inbound' if $repo eq 'mozilla-inbound'; - return qq{$text}; + if ($action eq 'blob') { + return + qq#$text [github]#; } - }); + if ($action eq 'shortlog' || $action eq 'log') { + return + qq#$text [github]#; + } + if ($action eq 'commit' || $action eq 'commitdiff') { + return qq#$text [github]#; + } + return qq#$text#; + } + } + ); + + # link to hg.m.o + # Note: for grouping in this regexp, always use non-capturing parentheses. + my $hgrepos = join( + '|', qw!(?:releases/)?comm-[\w.]+ + (?:releases/)?mozilla-[\w.]+ + (?:releases/)?mobile-[\w.]+ + tracemonkey + tamarin-[\w.]+ + camino! + ); + + push( + @$regexes, + { + match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/, + replace => sub { + my $args = shift; + my $text = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $id = html_quote($args->{matches}->[2]); + $repo = 'integration/mozilla-inbound' if $repo eq 'mozilla-inbound'; + return qq{$text}; + } + } + ); } sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; + my ($self, $args) = @_; + my $map = $args->{'map'}; - foreach my $name (keys %$map) { - if ($name =~ /cf_crash_signature$/) { - $map->{'sig'} = $name; - } + foreach my $name (keys %$map) { + if ($name =~ /cf_crash_signature$/) { + $map->{'sig'} = $name; } + } } sub object_columns { - my ($self, $args) = @_; - return unless $args->{class}->isa('Bugzilla::Product'); - push @{ $args->{columns} }, qw( - default_platform_id - default_op_sys_id - security_group_id - ); + my ($self, $args) = @_; + return unless $args->{class}->isa('Bugzilla::Product'); + push @{$args->{columns}}, qw( + default_platform_id + default_op_sys_id + security_group_id + ); } sub object_update_columns { - my ($self, $args) = @_; - return unless $args->{object}->isa('Bugzilla::Product'); - push @{ $args->{columns} }, qw( - default_platform_id - default_op_sys_id - security_group_id - ); + my ($self, $args) = @_; + return unless $args->{object}->isa('Bugzilla::Product'); + push @{$args->{columns}}, qw( + default_platform_id + default_op_sys_id + security_group_id + ); } sub object_before_create { - my ($self, $args) = @_; - return unless $args->{class}->isa('Bugzilla::Product'); + my ($self, $args) = @_; + return unless $args->{class}->isa('Bugzilla::Product'); - my $cgi = Bugzilla->cgi; - my $params = $args->{params}; - foreach my $field (qw( default_platform_id default_op_sys_id security_group_id )) { - $params->{$field} = $cgi->param($field); - } + my $cgi = Bugzilla->cgi; + my $params = $args->{params}; + foreach + my $field (qw( default_platform_id default_op_sys_id security_group_id )) + { + $params->{$field} = $cgi->param($field); + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Product'); - - my $cgi = Bugzilla->cgi; - my $params = $args->{params}; - foreach my $field (qw( default_platform_id default_op_sys_id security_group_id )) { - my $value = $cgi->param($field); - detaint_natural($value); - $object->set($field, $value); - } + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::Product'); + + my $cgi = Bugzilla->cgi; + my $params = $args->{params}; + foreach + my $field (qw( default_platform_id default_op_sys_id security_group_id )) + { + my $value = $cgi->param($field); + detaint_natural($value); + $object->set($field, $value); + } } sub object_end_of_create { - my ($self, $args) = @_; - my $class = $args->{class}; - - if ($class eq 'Bugzilla::User') { - my $user = $args->{object}; - - # Log real IP addresses for auditing - Bugzilla->audit(sprintf('<%s> created user %s', remote_ip(), $user->login)); - - # Add default searches to new user's footer - my $dbh = Bugzilla->dbh; - - my $sharer = Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'} }) - or return; - my $group = Bugzilla::Group->new({ name => 'everyone' }) - or return; - - foreach my $definition (@default_named_queries) { - my ($namedquery_id) = _get_named_query($sharer->id, $group->id, $definition); - $dbh->do( - "INSERT INTO namedqueries_link_in_footer(namedquery_id,user_id) VALUES (?,?)", - undef, - $namedquery_id, $user->id - ); - } + my ($self, $args) = @_; + my $class = $args->{class}; + + if ($class eq 'Bugzilla::User') { + my $user = $args->{object}; + + # Log real IP addresses for auditing + Bugzilla->audit(sprintf('<%s> created user %s', remote_ip(), $user->login)); + + # Add default searches to new user's footer + my $dbh = Bugzilla->dbh; - } elsif ($class eq 'Bugzilla::Bug') { - # Log real IP addresses for auditing - Bugzilla->audit(sprintf('%s <%s> created bug %s', Bugzilla->user->login, remote_ip(), $args->{object}->id)); + my $sharer = Bugzilla::User->new({name => Bugzilla->params->{'nobody_user'}}) + or return; + my $group = Bugzilla::Group->new({name => 'everyone'}) or return; + + foreach my $definition (@default_named_queries) { + my ($namedquery_id) = _get_named_query($sharer->id, $group->id, $definition); + $dbh->do( + "INSERT INTO namedqueries_link_in_footer(namedquery_id,user_id) VALUES (?,?)", + undef, $namedquery_id, $user->id); } + + } + elsif ($class eq 'Bugzilla::Bug') { + + # Log real IP addresses for auditing + Bugzilla->audit(sprintf( + '%s <%s> created bug %s', + Bugzilla->user->login, remote_ip(), $args->{object}->id + )); + } } sub _bug_reporters_hw_os { - my ($self) = @_; - return $self->{ua_hw_os} if exists $self->{ua_hw_os}; - my $memcached = Bugzilla->memcached; - my $hw_os = $memcached->get({ key => 'bug.ua.' . $self->id }); - if (!$hw_os) { - (my $ua) = Bugzilla->dbh->selectrow_array( - "SELECT user_agent FROM bug_user_agent WHERE bug_id = ?", - undef, - $self->id); - $hw_os = $ua - ? [ detect_platform($ua), detect_op_sys($ua) ] - : []; - $memcached->set({ key => 'bug.ua.' . $self->id, value => $hw_os }); - } - return $self->{ua_hw_os} = $hw_os; + my ($self) = @_; + return $self->{ua_hw_os} if exists $self->{ua_hw_os}; + my $memcached = Bugzilla->memcached; + my $hw_os = $memcached->get({key => 'bug.ua.' . $self->id}); + if (!$hw_os) { + (my $ua) + = Bugzilla->dbh->selectrow_array( + "SELECT user_agent FROM bug_user_agent WHERE bug_id = ?", + undef, $self->id); + $hw_os = $ua ? [detect_platform($ua), detect_op_sys($ua)] : []; + $memcached->set({key => 'bug.ua.' . $self->id, value => $hw_os}); + } + return $self->{ua_hw_os} = $hw_os; } sub _bug_is_unassigned { - my ($self) = @_; - my $assignee = $self->assigned_to->login; - return $assignee eq Bugzilla->params->{'nobody_user'} || $assignee =~ /\.bugs$/; + my ($self) = @_; + my $assignee = $self->assigned_to->login; + return $assignee eq Bugzilla->params->{'nobody_user'} || $assignee =~ /\.bugs$/; } sub _bug_has_current_patch { - my ($self) = @_; - foreach my $attachment (@{ $self->attachments }) { - next if $attachment->isobsolete; - return 1 if $attachment->can_review; - } - return 0; + my ($self) = @_; + foreach my $attachment (@{$self->attachments}) { + next if $attachment->isobsolete; + return 1 if $attachment->can_review; + } + return 0; } sub _bug_missing_sec_approval { - my ($self) = @_; - # see https://wiki.mozilla.org/Security/Bug_Approval_Process for the rules + my ($self) = @_; - # no need to alert once a bug is closed - return 0 if $self->resolution; + # see https://wiki.mozilla.org/Security/Bug_Approval_Process for the rules - # only bugs with sec-high or sec-critical keywords need sec-approval - return 0 unless $self->has_keyword('sec-high') || $self->has_keyword('sec-critical'); + # no need to alert once a bug is closed + return 0 if $self->resolution; - # look for patches with sec-approval set to any value - foreach my $attachment (@{ $self->attachments }) { - next if $attachment->isobsolete || !$attachment->ispatch; - foreach my $flag (@{ $attachment->flags }) { - # only one patch needs sec-approval - return 0 if $flag->name eq 'sec-approval'; - } - } + # only bugs with sec-high or sec-critical keywords need sec-approval + return 0 + unless $self->has_keyword('sec-high') || $self->has_keyword('sec-critical'); - # tracking flags - require Bugzilla::Extension::TrackingFlags::Flag; - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $self->product, - component => $self->component, - bug_id => $self->id, - is_active => 1, - WHERE => { - 'name like ?' => 'cf_status_firefox%', - }, - }); - # set flags are added after the sql query, filter those out - $flags = [ grep { $_->name =~ /^cf_status_firefox/ } @$flags ]; - return 0 unless @$flags; - - my $nightly = last_value { $_->name !~ /_esr\d+$/ } @$flags; - my $set = 0; - foreach my $flag (@$flags) { - my $value = $flag->bug_flag($self->id)->value; - next if $value eq '---'; - $set++; - # sec-approval is required if any of the current status-firefox - # tracking flags that aren't the latest are set to 'affected' - return 1 if $flag->name ne $nightly->name && $value eq 'affected'; + # look for patches with sec-approval set to any value + foreach my $attachment (@{$self->attachments}) { + next if $attachment->isobsolete || !$attachment->ispatch; + foreach my $flag (@{$attachment->flags}) { + + # only one patch needs sec-approval + return 0 if $flag->name eq 'sec-approval'; } - # sec-approval is required if no tracking flags are set - return $set == 0; + } + + # tracking flags + require Bugzilla::Extension::TrackingFlags::Flag; + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $self->product, + component => $self->component, + bug_id => $self->id, + is_active => 1, + WHERE => {'name like ?' => 'cf_status_firefox%',}, + }); + + # set flags are added after the sql query, filter those out + $flags = [grep { $_->name =~ /^cf_status_firefox/ } @$flags]; + return 0 unless @$flags; + + my $nightly = last_value { $_->name !~ /_esr\d+$/ } @$flags; + my $set = 0; + foreach my $flag (@$flags) { + my $value = $flag->bug_flag($self->id)->value; + next if $value eq '---'; + $set++; + + # sec-approval is required if any of the current status-firefox + # tracking flags that aren't the latest are set to 'affected' + return 1 if $flag->name ne $nightly->name && $value eq 'affected'; + } + + # sec-approval is required if no tracking flags are set + return $set == 0; } sub _product_default_platform_id { $_[0]->{default_platform_id} } -sub _product_default_op_sys_id { $_[0]->{default_op_sys_id} } +sub _product_default_op_sys_id { $_[0]->{default_op_sys_id} } sub _product_default_platform { - my ($self) = @_; - if (!exists $self->{default_platform}) { - $self->{default_platform} = $self->default_platform_id - ? Bugzilla::Field::Choice - ->type('rep_platform') - ->new($_[0]->{default_platform_id}) - ->name - : undef; - } - return $self->{default_platform}; + my ($self) = @_; + if (!exists $self->{default_platform}) { + $self->{default_platform} + = $self->default_platform_id + ? Bugzilla::Field::Choice->type('rep_platform') + ->new($_[0]->{default_platform_id})->name + : undef; + } + return $self->{default_platform}; } + sub _product_default_op_sys { - my ($self) = @_; - if (!exists $self->{default_op_sys}) { - $self->{default_op_sys} = $self->default_op_sys_id - ? Bugzilla::Field::Choice - ->type('op_sys') - ->new($_[0]->{default_op_sys_id}) - ->name - : undef; - } - return $self->{default_op_sys}; + my ($self) = @_; + if (!exists $self->{default_op_sys}) { + $self->{default_op_sys} + = $self->default_op_sys_id + ? Bugzilla::Field::Choice->type('op_sys')->new($_[0]->{default_op_sys_id}) + ->name + : undef; + } + return $self->{default_op_sys}; } sub _get_named_query { - my ($sharer_id, $group_id, $definition) = @_; - my $dbh = Bugzilla->dbh; - # find existing namedquery - my ($namedquery_id) = $dbh->selectrow_array( - "SELECT id FROM namedqueries WHERE userid=? AND name=?", - undef, - $sharer_id, $definition->{name} - ); - return $namedquery_id if $namedquery_id; - # create namedquery - $dbh->do( - "INSERT INTO namedqueries(userid,name,query) VALUES (?,?,?)", - undef, - $sharer_id, $definition->{name}, $definition->{query} - ); - $namedquery_id = $dbh->bz_last_key(); - # and share it - $dbh->do( - "INSERT INTO namedquery_group_map(namedquery_id,group_id) VALUES (?,?)", - undef, - $namedquery_id, $group_id, - ); - return $namedquery_id; + my ($sharer_id, $group_id, $definition) = @_; + my $dbh = Bugzilla->dbh; + + # find existing namedquery + my ($namedquery_id) + = $dbh->selectrow_array( + "SELECT id FROM namedqueries WHERE userid=? AND name=?", + undef, $sharer_id, $definition->{name}); + return $namedquery_id if $namedquery_id; + + # create namedquery + $dbh->do("INSERT INTO namedqueries(userid,name,query) VALUES (?,?,?)", + undef, $sharer_id, $definition->{name}, $definition->{query}); + $namedquery_id = $dbh->bz_last_key(); + + # and share it + $dbh->do( + "INSERT INTO namedquery_group_map(namedquery_id,group_id) VALUES (?,?)", + undef, $namedquery_id, $group_id,); + return $namedquery_id; } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - - # automatically CC users to bugs based on group & product - foreach my $group_name (keys %group_auto_cc) { - my $group_obj = Bugzilla::Group->new({ name => $group_name }); - if ($group_obj && $bug->in_group($group_obj)) { - my $ra_logins = exists $group_auto_cc{$group_name}->{$bug->product} - ? $group_auto_cc{$group_name}->{$bug->product} - : $group_auto_cc{$group_name}->{'_default'}; - foreach my $login (@$ra_logins) { - $bug->add_cc($login); - } - } - } - - # store user-agent - if (my $ua = Bugzilla->cgi->user_agent) { - trick_taint($ua); - Bugzilla->dbh->do( - "INSERT INTO bug_user_agent (bug_id, user_agent) VALUES (?, ?)", - undef, - $bug->id, $ua - ); - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + + # automatically CC users to bugs based on group & product + foreach my $group_name (keys %group_auto_cc) { + my $group_obj = Bugzilla::Group->new({name => $group_name}); + if ($group_obj && $bug->in_group($group_obj)) { + my $ra_logins + = exists $group_auto_cc{$group_name}->{$bug->product} + ? $group_auto_cc{$group_name}->{$bug->product} + : $group_auto_cc{$group_name}->{'_default'}; + foreach my $login (@$ra_logins) { + $bug->add_cc($login); + } + } + } + + # store user-agent + if (my $ua = Bugzilla->cgi->user_agent) { + trick_taint($ua); + Bugzilla->dbh->do( + "INSERT INTO bug_user_agent (bug_id, user_agent) VALUES (?, ?)", + undef, $bug->id, $ua); + } } sub sanitycheck_check { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $status = $args->{'status'}; - $status->('bmo_check_cf_visible_in_products'); - - my $products = $dbh->selectcol_arrayref('SELECT name FROM products'); - my %product = map { $_ => 1 } @$products; - my @cf_products = map { keys %$_ } values %$cf_visible_in_products; - foreach my $cf_product (@cf_products) { - $status->('bmo_check_cf_visible_in_products_missing', - { cf_product => $cf_product }, 'alert') unless $product{$cf_product}; - } + my $dbh = Bugzilla->dbh; + my $status = $args->{'status'}; + $status->('bmo_check_cf_visible_in_products'); + + my $products = $dbh->selectcol_arrayref('SELECT name FROM products'); + my %product = map { $_ => 1 } @$products; + my @cf_products = map { keys %$_ } values %$cf_visible_in_products; + foreach my $cf_product (@cf_products) { + $status->( + 'bmo_check_cf_visible_in_products_missing', + {cf_product => $cf_product}, 'alert' + ) unless $product{$cf_product}; + } } sub db_sanitize { - print "deleting reporter's user-agents...\n"; - Bugzilla->dbh->do("TRUNCATE TABLE bug_user_agent"); + print "deleting reporter's user-agents...\n"; + Bugzilla->dbh->do("TRUNCATE TABLE bug_user_agent"); } # bugs in an ASSIGNED state must be assigned to a real person # reset bugs to NEW if the assignee is nobody/.bugs$ sub object_start_of_update { - my ($self, $args) = @_; - my ($new_bug, $old_bug) = @$args{qw( object old_object )}; - return unless $new_bug->isa('Bugzilla::Bug'); - - # if either the assignee or status has changed - return unless - $old_bug->assigned_to->id != $new_bug->assigned_to->id - || $old_bug->bug_status ne $new_bug->bug_status; - - # and the bug is now ASSIGNED - return unless - $new_bug->bug_status eq 'ASSIGNED'; - - # and the assignee isn't a real person - return unless - $new_bug->assigned_to->login eq Bugzilla->params->{'nobody_user'} - || $new_bug->assigned_to->login =~ /\.bugs$/; - - # and the user can set the status to NEW - return unless - $old_bug->check_can_change_field('bug_status', $old_bug->bug_status, 'NEW'); - - # if the user is changing the assignee, silently change the bug's status to new - if ($old_bug->assigned_to->id != $new_bug->assigned_to->id) { - $new_bug->set_bug_status('NEW'); - } + my ($self, $args) = @_; + my ($new_bug, $old_bug) = @$args{qw( object old_object )}; + return unless $new_bug->isa('Bugzilla::Bug'); - # otherwise the user is trying to set the bug's status to ASSIGNED without - # assigning a real person. throw an error. - else { - ThrowUserError('bug_status_unassigned'); - } + # if either the assignee or status has changed + return + unless $old_bug->assigned_to->id != $new_bug->assigned_to->id + || $old_bug->bug_status ne $new_bug->bug_status; + + # and the bug is now ASSIGNED + return unless $new_bug->bug_status eq 'ASSIGNED'; + + # and the assignee isn't a real person + return + unless $new_bug->assigned_to->login eq Bugzilla->params->{'nobody_user'} + || $new_bug->assigned_to->login =~ /\.bugs$/; + + # and the user can set the status to NEW + return + unless $old_bug->check_can_change_field('bug_status', $old_bug->bug_status, + 'NEW'); + + # if the user is changing the assignee, silently change the bug's status to new + if ($old_bug->assigned_to->id != $new_bug->assigned_to->id) { + $new_bug->set_bug_status('NEW'); + } + + # otherwise the user is trying to set the bug's status to ASSIGNED without + # assigning a real person. throw an error. + else { + ThrowUserError('bug_status_unassigned'); + } } # detect github pull requests and reviewboard reviews, set the content-type sub attachment_process_data { - my ($self, $args) = @_; - my $attributes = $args->{attributes}; - - # must be a text attachment - return unless $attributes->{mimetype} eq 'text/plain'; - - # check the attachment size, and get attachment content if it isn't too large - my $data = $attributes->{data}; - my $url; - if (blessed($data) && blessed($data) eq 'Fh') { - # filehandle - my $size = -s $data; - return if $size > 256; - sysread($data, $url, $size); - seek($data, 0, 0); - } else { - # string - $url = $data; - } - - if (my $detected = _detect_attached_url($url)) { - $attributes->{mimetype} = $detected->{content_type}; - $attributes->{ispatch} = 0; - } + my ($self, $args) = @_; + my $attributes = $args->{attributes}; + + # must be a text attachment + return unless $attributes->{mimetype} eq 'text/plain'; + + # check the attachment size, and get attachment content if it isn't too large + my $data = $attributes->{data}; + my $url; + if (blessed($data) && blessed($data) eq 'Fh') { + + # filehandle + my $size = -s $data; + return if $size > 256; + sysread($data, $url, $size); + seek($data, 0, 0); + } + else { + # string + $url = $data; + } + + if (my $detected = _detect_attached_url($url)) { + $attributes->{mimetype} = $detected->{content_type}; + $attributes->{ispatch} = 0; + } } sub _detect_attached_url { - my ($url) = @_; - - # trim and check for the pull request url - return unless defined $url; - return if length($url) > 256; - $url = trim($url); - # ignore urls that contain unescaped characters outside of the range mentioned in RFC 3986 section 2 - return if $url =~ m<[^A-Za-z0-9._~:/?#\[\]@!\$&'()*+,;=`.%-]>; - - foreach my $key (keys %autodetect_attach_urls) { - my $regex = $autodetect_attach_urls{$key}->{regex}; - if (ref($regex) eq 'CODE') { - $regex = $regex->(); - } - if ($url =~ $regex) { - return $autodetect_attach_urls{$key}; - } + my ($url) = @_; + + # trim and check for the pull request url + return unless defined $url; + return if length($url) > 256; + $url = trim($url); + +# ignore urls that contain unescaped characters outside of the range mentioned in RFC 3986 section 2 + return if $url =~ m<[^A-Za-z0-9._~:/?#\[\]@!\$&'()*+,;=`.%-]>; + + foreach my $key (keys %autodetect_attach_urls) { + my $regex = $autodetect_attach_urls{$key}->{regex}; + if (ref($regex) eq 'CODE') { + $regex = $regex->(); + } + if ($url =~ $regex) { + return $autodetect_attach_urls{$key}; } + } - return undef; + return undef; } sub _attachment_external_redirect { - my ($self) = @_; + my ($self) = @_; - # must be our supported content-type - return undef unless - any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } - keys %autodetect_attach_urls; + # must be our supported content-type + return undef + unless + any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } + keys %autodetect_attach_urls; - # must still be a valid url - return _detect_attached_url($self->data) + # must still be a valid url + return _detect_attached_url($self->data); } sub _attachment_can_review { - my ($self) = @_; + my ($self) = @_; - return 1 if $self->ispatch; - my $external = $self->external_redirect // return; - return $external->{can_review}; + return 1 if $self->ispatch; + my $external = $self->external_redirect // return; + return $external->{can_review}; } sub _attachment_fetch_github_pr_diff { - my ($self) = @_; + my ($self) = @_; - # must be our supported content-type - return undef unless - any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } - keys %autodetect_attach_urls; + # must be our supported content-type + return undef + unless + any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } + keys %autodetect_attach_urls; - # must still be a valid url - return undef unless _detect_attached_url($self->data); + # must still be a valid url + return undef unless _detect_attached_url($self->data); - my $ua = LWP::UserAgent->new( timeout => 10 ); - if (Bugzilla->params->{proxy_url}) { - $ua->proxy('https', Bugzilla->params->{proxy_url}); - } + my $ua = LWP::UserAgent->new(timeout => 10); + if (Bugzilla->params->{proxy_url}) { + $ua->proxy('https', Bugzilla->params->{proxy_url}); + } - my $pr_diff = $self->data . ".diff"; - my $response = $ua->get($pr_diff); - if ($response->is_error) { - warn "Github fetch error: $pr_diff, " . $response->status_line; - return "Error retrieving Github pull request diff for " . $self->data; - } - return $response->decoded_content; + my $pr_diff = $self->data . ".diff"; + my $response = $ua->get($pr_diff); + if ($response->is_error) { + warn "Github fetch error: $pr_diff, " . $response->status_line; + return "Error retrieving Github pull request diff for " . $self->data; + } + return $response->decoded_content; } # redirect automatically to github urls sub attachment_view { - my ($self, $args) = @_; - my $attachment = $args->{attachment}; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $attachment = $args->{attachment}; + my $cgi = Bugzilla->cgi; - # don't redirect if the content-type is specified explicitly - return if defined $cgi->param('content_type'); + # don't redirect if the content-type is specified explicitly + return if defined $cgi->param('content_type'); - # must be a valid redirection url - return unless defined $attachment->external_redirect; + # must be a valid redirection url + return unless defined $attachment->external_redirect; - # redirect - print $cgi->redirect(trim($attachment->data)); - exit; + # redirect + print $cgi->redirect(trim($attachment->data)); + exit; } sub install_before_final_checks { - my ($self, $args) = @_; - - # Add product chooser setting - add_setting({ - name => 'product_chooser', - options => ['pretty_product_chooser', 'full_product_chooser'], - default => 'pretty_product_chooser', - category => 'User Interface' - }); - - # Add option to inject x-bugzilla headers into the message body to work - # around gmail filtering limitations - add_setting({ - name => 'headers_in_body', - options => ['on', 'off'], - default => 'off', - category => 'Email Notifications' - }); - - # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix' - my $dbh = Bugzilla->dbh; - if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) { - $dbh->bz_start_transaction(); - $dbh->do("UPDATE profile_setting + my ($self, $args) = @_; + + # Add product chooser setting + add_setting({ + name => 'product_chooser', + options => ['pretty_product_chooser', 'full_product_chooser'], + default => 'pretty_product_chooser', + category => 'User Interface' + }); + + # Add option to inject x-bugzilla headers into the message body to work + # around gmail filtering limitations + add_setting({ + name => 'headers_in_body', + options => ['on', 'off'], + default => 'off', + category => 'Email Notifications' + }); + + # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix' + my $dbh = Bugzilla->dbh; + if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) + { + $dbh->bz_start_transaction(); + $dbh->do( + "UPDATE profile_setting SET setting_value='on-temp' - WHERE setting_name='gmail_threading' AND setting_value='Off'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='Off'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_value='off' - WHERE setting_name='gmail_threading' AND setting_value='On'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='On'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_value='on' - WHERE setting_name='gmail_threading' AND setting_value='on-temp'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='on-temp'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_name='bugmail_new_prefix' - WHERE setting_name='gmail_threading'"); - $dbh->do("DELETE FROM setting WHERE name='gmail_threading'"); - $dbh->bz_commit_transaction(); - } + WHERE setting_name='gmail_threading'" + ); + $dbh->do("DELETE FROM setting WHERE name='gmail_threading'"); + $dbh->bz_commit_transaction(); + } } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{schema}->{bug_user_agent} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_agent => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1, - }, - ], - INDEXES => [ - bug_user_agent_idx => { - FIELDS => [ 'bug_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{schema}->{job_last_run} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - name => { - TYPE => 'VARCHAR(100)', - NOTNULL => 1, - }, - last_run => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - job_last_run_name_idx => { - FIELDS => [ 'name' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{schema}->{secbugs_BugHistory} = { - FIELDS => [ - bugid => { TYPE => 'BIGINT', NOTNULL => 1 }, - changetime => { TYPE => 'NATIVE_DATETIME' }, - fieldname => { TYPE => 'VARCHAR(32)', NOTNULL => 1 }, - new => { TYPE => 'VARCHAR(255)' }, - old => { TYPE => 'VARCHAR(255)' }, - ], - }; - - $args->{schema}->{secbugs_Bugs} = { - FIELDS => [ - bugid => { TYPE => 'BIGINT', NOTNULL => 1, PRIMARYKEY => 1 }, - opendate => { TYPE => 'NATIVE_DATETIME' }, - closedate => { TYPE => 'NATIVE_DATETIME', NOTNULL => 1 }, - severity => { TYPE => 'VARCHAR(16)' }, - summary => { TYPE => 'VARCHAR(255)' }, - updated => { TYPE => 'NATIVE_DATETIME' }, - ], - }; - - $args->{schema}->{secbugs_Details} = { - FIELDS => [ - did => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - sid => { - TYPE => 'INT4', - }, - product => { - TYPE => 'VARCHAR(255)', - }, - component => { - TYPE => 'VARCHAR(255)', - }, - count => { TYPE => 'INT4' }, - bug_list => { TYPE => 'TEXT' }, - date => { TYPE => 'NATIVE_DATETIME' }, - avg_age_days => { TYPE => 'INT4' }, - med_age_days => { TYPE => 'INT4' }, - ] - }; - - $args->{schema}->{secbugs_Stats} = { - FIELDS => [ - sid => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - category => { TYPE => 'VARCHAR(32)' }, - count => { TYPE => 'INT4' }, - date => { TYPE => 'NATIVE_DATETIME' }, - ] - }; + my ($self, $args) = @_; + $args->{schema}->{bug_user_agent} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_agent => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,}, + ], + INDEXES => [bug_user_agent_idx => {FIELDS => ['bug_id'], TYPE => 'UNIQUE',},], + }; + $args->{schema}->{job_last_run} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + name => {TYPE => 'VARCHAR(100)', NOTNULL => 1,}, + last_run => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [job_last_run_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; + $args->{schema}->{secbugs_BugHistory} = { + FIELDS => [ + bugid => {TYPE => 'BIGINT', NOTNULL => 1}, + changetime => {TYPE => 'NATIVE_DATETIME'}, + fieldname => {TYPE => 'VARCHAR(32)', NOTNULL => 1}, + new => {TYPE => 'VARCHAR(255)'}, + old => {TYPE => 'VARCHAR(255)'}, + ], + }; + + $args->{schema}->{secbugs_Bugs} = { + FIELDS => [ + bugid => {TYPE => 'BIGINT', NOTNULL => 1, PRIMARYKEY => 1}, + opendate => {TYPE => 'NATIVE_DATETIME'}, + closedate => {TYPE => 'NATIVE_DATETIME', NOTNULL => 1}, + severity => {TYPE => 'VARCHAR(16)'}, + summary => {TYPE => 'VARCHAR(255)'}, + updated => {TYPE => 'NATIVE_DATETIME'}, + ], + }; + + $args->{schema}->{secbugs_Details} = { + FIELDS => [ + did => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + sid => {TYPE => 'INT4',}, + product => {TYPE => 'VARCHAR(255)',}, + component => {TYPE => 'VARCHAR(255)',}, + count => {TYPE => 'INT4'}, + bug_list => {TYPE => 'TEXT'}, + date => {TYPE => 'NATIVE_DATETIME'}, + avg_age_days => {TYPE => 'INT4'}, + med_age_days => {TYPE => 'INT4'}, + ] + }; + + $args->{schema}->{secbugs_Stats} = { + FIELDS => [ + sid => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + category => {TYPE => 'VARCHAR(32)'}, + count => {TYPE => 'INT4'}, + date => {TYPE => 'NATIVE_DATETIME'}, + ] + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # per-product hw/os defaults + my $op_sys_default = _field_value('op_sys', 'Unspecified', 50); + $dbh->bz_add_column( + 'products', + 'default_op_sys_id' => { + TYPE => 'INT2', + DEFAULT => $op_sys_default->id, + REFERENCES => {TABLE => 'op_sys', COLUMN => 'id', DELETE => 'SET NULL',}, + } + ); + my $platform_default = _field_value('rep_platform', 'Unspecified', 50); + $dbh->bz_add_column( + 'products', + 'default_platform_id' => { + TYPE => 'INT2', + DEFAULT => $platform_default->id, + REFERENCES => {TABLE => 'rep_platform', COLUMN => 'id', DELETE => 'SET NULL',}, + } + ); + + # Migrate old is_active stuff to new patch (is in core in 4.2), The old + # column name was 'is_active', the new one is 'isactive' (no underscore). + if ($dbh->bz_column_info('milestones', 'is_active')) { + $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;"); + $dbh->bz_drop_column('milestones', 'is_active'); + $dbh->bz_drop_column('milestones', 'is_searchable'); + } + + # remove tables from the old TryAutoLand extension + $dbh->bz_drop_table('autoland_branches'); + $dbh->bz_drop_table('autoland_attachments'); + + unless (Bugzilla::Field->new({name => 'cf_rank'})) { + Bugzilla::Field->create({ + name => 'cf_rank', + description => 'Rank', + type => FIELD_TYPE_INTEGER, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 1, + }); + } + unless (Bugzilla::Field->new({name => 'cf_crash_signature'})) { + Bugzilla::Field->create({ + name => 'cf_crash_signature', + description => 'Crash Signature', + type => FIELD_TYPE_TEXTAREA, + mailhead => 0, + enter_bug => 1, + obsolete => 0, + custom => 1, + buglist => 0, + }); + } - # per-product hw/os defaults - my $op_sys_default = _field_value('op_sys', 'Unspecified', 50); - $dbh->bz_add_column( - 'products', - 'default_op_sys_id' => { - TYPE => 'INT2', - DEFAULT => $op_sys_default->id, - REFERENCES => { - TABLE => 'op_sys', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } - ); - my $platform_default = _field_value('rep_platform', 'Unspecified', 50); + # Add default security group id column + if (!$dbh->bz_column_info('products', 'security_group_id')) { $dbh->bz_add_column( - 'products', - 'default_platform_id' => { - TYPE => 'INT2', - DEFAULT => $platform_default->id, - REFERENCES => { - TABLE => 'rep_platform', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } + 'products', + 'security_group_id' => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL',}, + } ); - # Migrate old is_active stuff to new patch (is in core in 4.2), The old - # column name was 'is_active', the new one is 'isactive' (no underscore). - if ($dbh->bz_column_info('milestones', 'is_active')) { - $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;"); - $dbh->bz_drop_column('milestones', 'is_active'); - $dbh->bz_drop_column('milestones', 'is_searchable'); - } - - # remove tables from the old TryAutoLand extension - $dbh->bz_drop_table('autoland_branches'); - $dbh->bz_drop_table('autoland_attachments'); - - unless (Bugzilla::Field->new({ name => 'cf_rank' })) { - Bugzilla::Field->create({ - name => 'cf_rank', - description => 'Rank', - type => FIELD_TYPE_INTEGER, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 1, - }); - } - unless (Bugzilla::Field->new({ name => 'cf_crash_signature' })) { - Bugzilla::Field->create({ - name => 'cf_crash_signature', - description => 'Crash Signature', - type => FIELD_TYPE_TEXTAREA, - mailhead => 0, - enter_bug => 1, - obsolete => 0, - custom => 1, - buglist => 0, - }); - } - - # Add default security group id column - if (!$dbh->bz_column_info('products', 'security_group_id')) { - $dbh->bz_add_column( - 'products', - 'security_group_id' => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } - ); - - # if there are no groups, then we're creating a database from scratch - # and there's nothing to migrate - my ($group_count) = $dbh->selectrow_array("SELECT COUNT(*) FROM groups"); - if ($group_count) { - # Migrate old product_sec_group mappings from the time this change was made - my %product_sec_groups = ( - "addons.mozilla.org" => 'client-services-security', - "Air Mozilla" => 'mozilla-employee-confidential', - "Android Background Services" => 'cloud-services-security', - "Audio/Visual Infrastructure" => 'mozilla-employee-confidential', - "AUS" => 'client-services-security', - "Bugzilla" => 'bugzilla-security', - "bugzilla.mozilla.org" => 'bugzilla-security', - "Cloud Services" => 'cloud-services-security', - "Community Tools" => 'websites-security', - "Data & BI Services Team" => 'metrics-private', - "Developer Documentation" => 'websites-security', - "Developer Ecosystem" => 'client-services-security', - "Finance" => 'finance', - "Firefox Friends" => 'mozilla-employee-confidential', - "Firefox Health Report" => 'cloud-services-security', - "Infrastructure & Operations" => 'mozilla-employee-confidential', - "Input" => 'websites-security', - "Intellego" => 'intellego-team', - "Internet Public Policy" => 'mozilla-employee-confidential', - "L20n" => 'l20n-security', - "Legal" => 'legal', - "Marketing" => 'marketing-private', - "Marketplace" => 'client-services-security', - "Mozilla Communities" => 'mozilla-communities-security', - "Mozilla Corporation" => 'mozilla-employee-confidential', - "Mozilla Developer Network" => 'websites-security', - "Mozilla Foundation" => 'mozilla-employee-confidential', - "Mozilla Foundation Operations" => 'mozilla-foundation-operations', - "Mozilla Grants" => 'grants', - "mozillaignite" => 'websites-security', - "Mozilla Messaging" => 'mozilla-messaging-confidential', - "Mozilla Metrics" => 'metrics-private', - "mozilla.org" => 'mozilla-employee-confidential', - "Mozilla PR" => 'pr-private', - "Mozilla QA" => 'mozilla-employee-confidential', - "Mozilla Reps" => 'mozilla-reps', - "Popcorn" => 'websites-security', - "Privacy" => 'privacy', - "quality.mozilla.org" => 'websites-security', - "Recruiting" => 'hr', - "Release Engineering" => 'mozilla-employee-confidential', - "Snippets" => 'websites-security', - "Socorro" => 'client-services-security', - "support.mozillamessaging.com" => 'websites-security', - "support.mozilla.org" => 'websites-security', - "Talkback" => 'talkback-private', - "Tamarin" => 'tamarin-security', - "Taskcluster" => 'taskcluster-security', - "Testopia" => 'bugzilla-security', - "Tree Management" => 'mozilla-employee-confidential', - "Web Apps" => 'client-services-security', - "Webmaker" => 'websites-security', - "Websites" => 'websites-security', - "Webtools" => 'webtools-security', - "www.mozilla.org" => 'websites-security', - ); - # 1. Set all to core-security by default - my $core_sec_group = Bugzilla::Group->new({ name => Bugzilla->params->{insidergroup} }); - $dbh->do("UPDATE products SET security_group_id = ?", undef, $core_sec_group->id); - # 2. Update the ones that have explicit security groups - foreach my $prod_name (keys %product_sec_groups) { - my $group_name = $product_sec_groups{$prod_name}; - next if $group_name eq Bugzilla->params->{insidergroup}; # already done - my $group = Bugzilla::Group->new({ name => $group_name, cache => 1 }); - if (!$group) { - warn "Security group $group_name not found. Using insider group instead.\n"; - next; - } - $dbh->do("UPDATE products SET security_group_id = ? WHERE name = ?", undef, $group->id, $prod_name); - } + # if there are no groups, then we're creating a database from scratch + # and there's nothing to migrate + my ($group_count) = $dbh->selectrow_array("SELECT COUNT(*) FROM groups"); + if ($group_count) { + + # Migrate old product_sec_group mappings from the time this change was made + my %product_sec_groups = ( + "addons.mozilla.org" => 'client-services-security', + "Air Mozilla" => 'mozilla-employee-confidential', + "Android Background Services" => 'cloud-services-security', + "Audio/Visual Infrastructure" => 'mozilla-employee-confidential', + "AUS" => 'client-services-security', + "Bugzilla" => 'bugzilla-security', + "bugzilla.mozilla.org" => 'bugzilla-security', + "Cloud Services" => 'cloud-services-security', + "Community Tools" => 'websites-security', + "Data & BI Services Team" => 'metrics-private', + "Developer Documentation" => 'websites-security', + "Developer Ecosystem" => 'client-services-security', + "Finance" => 'finance', + "Firefox Friends" => 'mozilla-employee-confidential', + "Firefox Health Report" => 'cloud-services-security', + "Infrastructure & Operations" => 'mozilla-employee-confidential', + "Input" => 'websites-security', + "Intellego" => 'intellego-team', + "Internet Public Policy" => 'mozilla-employee-confidential', + "L20n" => 'l20n-security', + "Legal" => 'legal', + "Marketing" => 'marketing-private', + "Marketplace" => 'client-services-security', + "Mozilla Communities" => 'mozilla-communities-security', + "Mozilla Corporation" => 'mozilla-employee-confidential', + "Mozilla Developer Network" => 'websites-security', + "Mozilla Foundation" => 'mozilla-employee-confidential', + "Mozilla Foundation Operations" => 'mozilla-foundation-operations', + "Mozilla Grants" => 'grants', + "mozillaignite" => 'websites-security', + "Mozilla Messaging" => 'mozilla-messaging-confidential', + "Mozilla Metrics" => 'metrics-private', + "mozilla.org" => 'mozilla-employee-confidential', + "Mozilla PR" => 'pr-private', + "Mozilla QA" => 'mozilla-employee-confidential', + "Mozilla Reps" => 'mozilla-reps', + "Popcorn" => 'websites-security', + "Privacy" => 'privacy', + "quality.mozilla.org" => 'websites-security', + "Recruiting" => 'hr', + "Release Engineering" => 'mozilla-employee-confidential', + "Snippets" => 'websites-security', + "Socorro" => 'client-services-security', + "support.mozillamessaging.com" => 'websites-security', + "support.mozilla.org" => 'websites-security', + "Talkback" => 'talkback-private', + "Tamarin" => 'tamarin-security', + "Taskcluster" => 'taskcluster-security', + "Testopia" => 'bugzilla-security', + "Tree Management" => 'mozilla-employee-confidential', + "Web Apps" => 'client-services-security', + "Webmaker" => 'websites-security', + "Websites" => 'websites-security', + "Webtools" => 'webtools-security', + "www.mozilla.org" => 'websites-security', + ); + + # 1. Set all to core-security by default + my $core_sec_group + = Bugzilla::Group->new({name => Bugzilla->params->{insidergroup}}); + $dbh->do("UPDATE products SET security_group_id = ?", + undef, $core_sec_group->id); + + # 2. Update the ones that have explicit security groups + foreach my $prod_name (keys %product_sec_groups) { + my $group_name = $product_sec_groups{$prod_name}; + next if $group_name eq Bugzilla->params->{insidergroup}; # already done + my $group = Bugzilla::Group->new({name => $group_name, cache => 1}); + if (!$group) { + warn "Security group $group_name not found. Using insider group instead.\n"; + next; } + $dbh->do("UPDATE products SET security_group_id = ? WHERE name = ?", + undef, $group->id, $prod_name); + } } + } } # return the Bugzilla::Field::Choice object for the specified field and value. # if the value doesn't exist it will be created. sub _field_value { - my ($field_name, $value_name, $sort_key) = @_; - my $field = Bugzilla::Field->check({ name => $field_name }); - my $existing = Bugzilla::Field::Choice->type($field)->match({ value => $value_name }); - return $existing->[0] if $existing && @$existing; - return Bugzilla::Field::Choice->type($field)->create({ - value => $value_name, - sortkey => $sort_key, - isactive => 1, - }); + my ($field_name, $value_name, $sort_key) = @_; + my $field = Bugzilla::Field->check({name => $field_name}); + my $existing + = Bugzilla::Field::Choice->type($field)->match({value => $value_name}); + return $existing->[0] if $existing && @$existing; + return Bugzilla::Field::Choice->type($field) + ->create({value => $value_name, sortkey => $sort_key, isactive => 1,}); } sub _last_closed_date { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - return $self->{'last_closed_date'} if defined $self->{'last_closed_date'}; + return $self->{'last_closed_date'} if defined $self->{'last_closed_date'}; - my $closed_statuses = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'"; - my $status_field_id = get_field_id('bug_status'); + my $closed_statuses + = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'"; + my $status_field_id = get_field_id('bug_status'); - $self->{'last_closed_date'} = $dbh->selectrow_array(" + $self->{'last_closed_date'} = $dbh->selectrow_array(" SELECT bugs_activity.bug_when FROM bugs_activity WHERE bugs_activity.fieldid = ? AND bugs_activity.added IN ($closed_statuses) AND bugs_activity.bug_id = ? - ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1), - undef, $status_field_id, $self->id - ); + ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1), undef, + $status_field_id, $self->id); - return $self->{'last_closed_date'}; + return $self->{'last_closed_date'}; } sub field_end_of_create { - my ($self, $args) = @_; - my $field = $args->{'field'}; - - # Create an IT bug so Mozilla's DBAs so they can update the grants for metrics - - if (Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.mozilla.org/' - && Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.allizom.org/') - { - return; - } - - my $name = $field->name; - - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - Bugzilla->set_user(Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} })); - print "Creating IT permission grant bug for new field '$name'..."; - } - - my $bug_data = { - short_desc => "Custom field '$name' added to bugzilla.mozilla.org", - product => 'Data & BI Services Team', - component => 'Database Operations', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'other', - }; - - my $comment = <{'field'}; + + # Create an IT bug so Mozilla's DBAs so they can update the grants for metrics + + if ( Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.mozilla.org/' + && Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.allizom.org/') + { + return; + } + + my $name = $field->name; + + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + Bugzilla->set_user(Bugzilla::User->check( + {name => Bugzilla->params->{'nobody_user'}})); + print "Creating IT permission grant bug for new field '$name'..."; + } + + my $bug_data = { + short_desc => "Custom field '$name' added to bugzilla.mozilla.org", + product => 'Data & BI Services Team', + component => 'Database Operations', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'other', + }; + + my $comment = <type == FIELD_TYPE_SINGLE_SELECT - || $field->type == FIELD_TYPE_MULTI_SELECT) { - $comment .= <type == FIELD_TYPE_SINGLE_SELECT + || $field->type == FIELD_TYPE_MULTI_SELECT) + { + $comment .= <type == FIELD_TYPE_MULTI_SELECT) { - $comment .= <type == FIELD_TYPE_MULTI_SELECT) { + $comment .= <type != FIELD_TYPE_MULTI_SELECT) { - $comment .= <type != FIELD_TYPE_MULTI_SELECT) { + $comment .= <{'comment'} = $comment; + $bug_data->{'comment'} = $comment; - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); - my $new_bug = eval { Bugzilla::Bug->create($bug_data) }; + my $new_bug = eval { Bugzilla::Bug->create($bug_data) }; - my $error = $@; - undef $@; - Bugzilla->error_mode($old_error_mode); + my $error = $@; + undef $@; + Bugzilla->error_mode($old_error_mode); - if ($error || !($new_bug && $new_bug->{'bug_id'})) { - warn "Error creating IT bug for new field $name: $error"; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\nError: $error\n"; - } + if ($error || !($new_bug && $new_bug->{'bug_id'})) { + warn "Error creating IT bug for new field $name: $error"; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\nError: $error\n"; } - else { - Bugzilla::BugMail::Send($new_bug->id, { changer => Bugzilla->user }); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "bug " . $new_bug->id . " created.\n"; - } + } + else { + Bugzilla::BugMail::Send($new_bug->id, {changer => Bugzilla->user}); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "bug " . $new_bug->id . " created.\n"; } + } } sub webservice { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService"; + my $dispatch = $args->{dispatch}; + $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService"; } sub psgi_builder { - my ($self, $args) = @_; - my $mount = $args->{mount}; - - my $ses_index = Plack::Builder::builder(sub { - my $auth_user = Bugzilla->localconfig->{ses_username}; - my $auth_pass = Bugzilla->localconfig->{ses_password}; - Plack::Builder::enable("Auth::Basic", authenticator => sub { - my ($username, $password, $env) = @_; - return ( $auth_user - && $auth_pass - && $username - && $password - && $username eq $auth_user - && $password eq $auth_pass ); - }); - compile_cgi("ses/index.cgi"); - }); + my ($self, $args) = @_; + my $mount = $args->{mount}; + + my $ses_index = Plack::Builder::builder(sub { + my $auth_user = Bugzilla->localconfig->{ses_username}; + my $auth_pass = Bugzilla->localconfig->{ses_password}; + Plack::Builder::enable( + "Auth::Basic", + authenticator => sub { + my ($username, $password, $env) = @_; + return ($auth_user + && $auth_pass + && $username + && $password + && $username eq $auth_user + && $password eq $auth_pass); + } + ); + compile_cgi("ses/index.cgi"); + }); - $mount->{'ses/index.cgi'} = $ses_index; + $mount->{'ses/index.cgi'} = $ses_index; } our $search_content_matches; + BEGIN { - $search_content_matches = \&Bugzilla::Search::_content_matches; + $search_content_matches = \&Bugzilla::Search::_content_matches; } sub search_operator_field_override { - my ($self, $args) = @_; - my $search = $args->{'search'}; - my $operators = $args->{'operators'}; - - my $cgi = Bugzilla->cgi; - my @comments = $cgi->param('comments'); - my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments; - - if ($cgi->param('query_format') - && $cgi->param('query_format') eq 'specific' - && $exclude_comments - ) { - # use the non-comment operator - $operators->{'content'}->{matches} = \&_short_desc_matches; - $operators->{'content'}->{notmatches} = \&_short_desc_matches; - - } else { - # restore default content operator - $operators->{'content'}->{matches} = $search_content_matches; - $operators->{'content'}->{notmatches} = $search_content_matches; - } + my ($self, $args) = @_; + my $search = $args->{'search'}; + my $operators = $args->{'operators'}; + + my $cgi = Bugzilla->cgi; + my @comments = $cgi->param('comments'); + my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments; + + if ( $cgi->param('query_format') + && $cgi->param('query_format') eq 'specific' + && $exclude_comments) + { + # use the non-comment operator + $operators->{'content'}->{matches} = \&_short_desc_matches; + $operators->{'content'}->{notmatches} = \&_short_desc_matches; + + } + else { + # restore default content operator + $operators->{'content'}->{matches} = $search_content_matches; + $operators->{'content'}->{notmatches} = $search_content_matches; + } } sub _short_desc_matches { - # copy of Bugzilla::Search::_content_matches with comment searching removed - my ($self, $args) = @_; - my ($chart_id, $joins, $fields, $operator, $value) = - @$args{qw(chart_id joins fields operator value)}; - my $dbh = Bugzilla->dbh; + # copy of Bugzilla::Search::_content_matches with comment searching removed - # Add the fulltext table to the query so we can search on it. - my $table = "bugs_fulltext_$chart_id"; - push(@$joins, { table => 'bugs_fulltext', as => $table }); + my ($self, $args) = @_; + my ($chart_id, $joins, $fields, $operator, $value) + = @$args{qw(chart_id joins fields operator value)}; + my $dbh = Bugzilla->dbh; - # Create search terms to add to the SELECT and WHERE clauses. - my ($term, $rterm) = - $dbh->sql_fulltext_search("$table.short_desc", $value, 2); - $rterm = $term if !$rterm; + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$chart_id"; + push(@$joins, {table => 'bugs_fulltext', as => $table}); - # The term to use in the WHERE clause. - if ($operator =~ /not/i) { - $term = "NOT($term)"; - } - $args->{term} = $term; + # Create search terms to add to the SELECT and WHERE clauses. + my ($term, $rterm) = $dbh->sql_fulltext_search("$table.short_desc", $value, 2); + $rterm = $term if !$rterm; + + # The term to use in the WHERE clause. + if ($operator =~ /not/i) { + $term = "NOT($term)"; + } + $args->{term} = $term; + + my $current = $self->COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; - my $current = $self->COLUMNS->{'relevance'}->{name}; - $current = $current ? "$current + " : ''; - # For NOT searches, we just add 0 to the relevance. - my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)"; - $self->COLUMNS->{'relevance'}->{name} = $select_term; + # For NOT searches, we just add 0 to the relevance. + my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)"; + $self->COLUMNS->{'relevance'}->{name} = $select_term; } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; + my ($self, $args) = @_; + my $email = $args->{email}; - _log_sent_email($email); + _log_sent_email($email); - # $bug->mentors is added by the Review extension - if (Bugzilla::Bug->can('mentors')) { - _add_mentors_header($email); - } + # $bug->mentors is added by the Review extension + if (Bugzilla::Bug->can('mentors')) { + _add_mentors_header($email); + } - # insert x-bugzilla headers into the body - _inject_headers_into_body($email); + # insert x-bugzilla headers into the body + _inject_headers_into_body($email); } # Log a summary of bugmail sent to the syslog, for auditing and monitoring sub _log_sent_email { - my $email = shift; + my $email = shift; - my $recipient = $email->header('to'); - return unless $recipient; + my $recipient = $email->header('to'); + return unless $recipient; - my $subject = $email->header('Subject'); + my $subject = $email->header('Subject'); - my $bug_id = $email->header('X-Bugzilla-ID'); - if (!$bug_id && $subject =~ /[\[\(]Bug (\d+)/i) { - $bug_id = $1; - } - $bug_id = $bug_id ? "bug-$bug_id" : '-'; - - my $message_type; - my $type = $email->header('X-Bugzilla-Type'); - my $reason = $email->header('X-Bugzilla-Reason'); - if ($type eq 'whine' || $type eq 'request' || $type eq 'admin') { - $message_type = $type; - } elsif ($reason && $reason ne 'None') { - $message_type = $reason; - } else { - $message_type = $email->header('X-Bugzilla-Watch-Reason'); - } - $message_type ||= $type || '?'; + my $bug_id = $email->header('X-Bugzilla-ID'); + if (!$bug_id && $subject =~ /[\[\(]Bug (\d+)/i) { + $bug_id = $1; + } + $bug_id = $bug_id ? "bug-$bug_id" : '-'; - $subject =~ s/[\[\(]Bug \d+[\]\)]\s*//; + my $message_type; + my $type = $email->header('X-Bugzilla-Type'); + my $reason = $email->header('X-Bugzilla-Reason'); + if ($type eq 'whine' || $type eq 'request' || $type eq 'admin') { + $message_type = $type; + } + elsif ($reason && $reason ne 'None') { + $message_type = $reason; + } + else { + $message_type = $email->header('X-Bugzilla-Watch-Reason'); + } + $message_type ||= $type || '?'; - _syslog("[bugmail] $recipient ($message_type) $bug_id $subject"); + $subject =~ s/[\[\(]Bug \d+[\]\)]\s*//; + + _syslog("[bugmail] $recipient ($message_type) $bug_id $subject"); } # Add X-Bugzilla-Mentors field to bugmail sub _add_mentors_header { - my $email = shift; - return unless my $bug_id = $email->header('X-Bugzilla-ID'); - return unless my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 }); - return unless my $mentors = $bug->mentors; - return unless @$mentors; - $email->header_set('X-Bugzilla-Mentors', join(', ', map { $_->login } @$mentors)); + my $email = shift; + return unless my $bug_id = $email->header('X-Bugzilla-ID'); + return unless my $bug = Bugzilla::Bug->new({id => $bug_id, cache => 1}); + return unless my $mentors = $bug->mentors; + return unless @$mentors; + $email->header_set('X-Bugzilla-Mentors', + join(', ', map { $_->login } @$mentors)); } sub _inject_headers_into_body { - my $email = shift; - my $replacement = ''; - - my $recipient = Bugzilla::User->new({ name => $email->header('To'), cache => 1 }); - if ($recipient - && $recipient->settings->{headers_in_body}->{value} eq 'on') - { - my @headers; - my $it = natatime(2, $email->header_pairs); - while (my ($name, $value) = $it->()) { - next unless $name =~ /^X-Bugzilla-(.+)/; - if ($name eq 'X-Bugzilla-Flags' || $name eq 'X-Bugzilla-Changed-Field-Names') { - # these are multi-value fields, split on space - foreach my $v (split(/\s+/, $value)) { - push @headers, "$name: $v"; - } - } - elsif ($name eq 'X-Bugzilla-Changed-Fields') { - # cannot split on space for this field, because field names contain - # spaces. instead work from a list of field names. - my @fields = - map { $_->description } - @{ Bugzilla->fields }; - # these aren't real fields, but exist in the headers - push @fields, ('Comment Created', 'Attachment Created'); - @fields = - sort { length($b) <=> length($a) } - @fields; - while ($value ne '') { - foreach my $field (@fields) { - if ($value eq $field) { - push @headers, "$name: $field"; - $value = ''; - last; - } - if (substr($value, 0, length($field) + 1) eq $field . ' ') { - push @headers, "$name: $field"; - $value = substr($value, length($field) + 1); - last; - } - } - } + my $email = shift; + my $replacement = ''; + + my $recipient = Bugzilla::User->new({name => $email->header('To'), cache => 1}); + if ($recipient && $recipient->settings->{headers_in_body}->{value} eq 'on') { + my @headers; + my $it = natatime(2, $email->header_pairs); + while (my ($name, $value) = $it->()) { + next unless $name =~ /^X-Bugzilla-(.+)/; + if ($name eq 'X-Bugzilla-Flags' || $name eq 'X-Bugzilla-Changed-Field-Names') { + + # these are multi-value fields, split on space + foreach my $v (split(/\s+/, $value)) { + push @headers, "$name: $v"; + } + } + elsif ($name eq 'X-Bugzilla-Changed-Fields') { + + # cannot split on space for this field, because field names contain + # spaces. instead work from a list of field names. + my @fields = map { $_->description } @{Bugzilla->fields}; + + # these aren't real fields, but exist in the headers + push @fields, ('Comment Created', 'Attachment Created'); + @fields = sort { length($b) <=> length($a) } @fields; + while ($value ne '') { + foreach my $field (@fields) { + if ($value eq $field) { + push @headers, "$name: $field"; + $value = ''; + last; } - else { - push @headers, "$name: $value"; + if (substr($value, 0, length($field) + 1) eq $field . ' ') { + push @headers, "$name: $field"; + $value = substr($value, length($field) + 1); + last; } + } } - $replacement = join("\n", @headers); - } - - # update the message body - if (scalar($email->parts) > 1) { - $email->walk_parts(sub { - my ($part) = @_; - - # skip top-level - return if $part->parts > 1; - - # do not filter attachments such as patches, etc. - return if - $part->header('Content-Disposition') - && $part->header('Content-Disposition') =~ /attachment/; - - # text/plain|html only - return unless $part->content_type =~ /^text\/(?:html|plain)/; - - # hide in html content - if ($replacement && $part->content_type =~ /^text\/html/) { - $replacement = '
' . $replacement . '
'; - } - - # and inject - _replace_placeholder_in_part($part, $replacement); - }); + } + else { + push @headers, "$name: $value"; + } + } + $replacement = join("\n", @headers); + } + + # update the message body + if (scalar($email->parts) > 1) { + $email->walk_parts(sub { + my ($part) = @_; + + # skip top-level + return if $part->parts > 1; + + # do not filter attachments such as patches, etc. + return + if $part->header('Content-Disposition') + && $part->header('Content-Disposition') =~ /attachment/; + + # text/plain|html only + return unless $part->content_type =~ /^text\/(?:html|plain)/; + + # hide in html content + if ($replacement && $part->content_type =~ /^text\/html/) { + $replacement + = '
' . $replacement . '
'; + } + + # and inject + _replace_placeholder_in_part($part, $replacement); + }); - # force Email::MIME to re-create all the parts. without this - # as_string() doesn't return the updated body for multi-part sub-parts. - $email->parts_set([ $email->subparts ]); - } - elsif (!$email->content_type - || $email->content_type =~ /^text\/(?:html|plain)/) - { - # text-only email - _replace_placeholder_in_part($email, $replacement); - } + # force Email::MIME to re-create all the parts. without this + # as_string() doesn't return the updated body for multi-part sub-parts. + $email->parts_set([$email->subparts]); + } + elsif (!$email->content_type || $email->content_type =~ /^text\/(?:html|plain)/) + { + # text-only email + _replace_placeholder_in_part($email, $replacement); + } } sub _replace_placeholder_in_part { - my ($part, $replacement) = @_; + my ($part, $replacement) = @_; - _fix_encoding($part); + _fix_encoding($part); - # replace - my $placeholder = quotemeta('@@body-headers@@'); - my $body = $part->body_str; - $body =~ s/$placeholder/$replacement/; - $part->body_str_set($body); + # replace + my $placeholder = quotemeta('@@body-headers@@'); + my $body = $part->body_str; + $body =~ s/$placeholder/$replacement/; + $part->body_str_set($body); } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _syslog { - my $message = shift; - openlog('apache', 'cons,pid', 'local4'); - syslog('notice', encode_utf8($message)); - closelog(); + my $message = shift; + openlog('apache', 'cons,pid', 'local4'); + syslog('notice', encode_utf8($message)); + closelog(); } sub post_bug_after_creation { - my ($self, $args) = @_; - return unless my $format = Bugzilla->input_params->{format}; - my $bug = $args->{vars}->{bug}; - - if ($format eq 'employee-incident' - && $bug->component eq 'Server Operations: Desktop Issues') - { - $self->_post_employee_incident_bug($args); - } - elsif ($format eq 'swag') { - $self->_post_gear_bug($args); - } - elsif ($format eq 'mozpr') { - $self->_post_mozpr_bug($args); - } - elsif ($format eq 'dev-engagement-event') { - $self->_post_dev_engagement($args); - } - elsif ($format eq 'shield-studies') { - $self->_post_shield_studies($args); - } + my ($self, $args) = @_; + return unless my $format = Bugzilla->input_params->{format}; + my $bug = $args->{vars}->{bug}; + + if ( $format eq 'employee-incident' + && $bug->component eq 'Server Operations: Desktop Issues') + { + $self->_post_employee_incident_bug($args); + } + elsif ($format eq 'swag') { + $self->_post_gear_bug($args); + } + elsif ($format eq 'mozpr') { + $self->_post_mozpr_bug($args); + } + elsif ($format eq 'dev-engagement-event') { + $self->_post_dev_engagement($args); + } + elsif ($format eq 'shield-studies') { + $self->_post_shield_studies($args); + } } sub _post_employee_incident_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + + my ($investigate_bug, $ssh_key_bug); + my $old_user = Bugzilla->user; + eval { + Bugzilla->set_user(Bugzilla::User->new( + {name => Bugzilla->params->{'nobody_user'}})); + my $new_user = Bugzilla->user; + + # HACK: User needs to be in the editbugs and primary bug's group to allow + # setting of dependencies. + $new_user->{'groups'} = [ + Bugzilla::Group->new({name => 'editbugs'}), + Bugzilla::Group->new({name => 'infra'}), + Bugzilla::Group->new({name => 'infrasec'}) + ]; - my ($investigate_bug, $ssh_key_bug); - my $old_user = Bugzilla->user; - eval { - Bugzilla->set_user(Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'} })); - my $new_user = Bugzilla->user; - - # HACK: User needs to be in the editbugs and primary bug's group to allow - # setting of dependencies. - $new_user->{'groups'} = [ Bugzilla::Group->new({ name => 'editbugs' }), - Bugzilla::Group->new({ name => 'infra' }), - Bugzilla::Group->new({ name => 'infrasec' }) ]; - - my $recipients = { changer => $new_user }; - $vars->{original_reporter} = $old_user; - - my $comment; - $cgi->param('display_action', ''); - $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment) - || ThrowTemplateError($template->error()); - - $investigate_bug = Bugzilla::Bug->create({ - short_desc => 'Investigate Lost Device', - product => 'mozilla.org', - component => 'Security Assurance: Incident', - status_whiteboard => '[infrasec:incident]', - bug_severity => 'critical', - cc => [ 'jstevensen@mozilla.com' ], - groups => [ 'infrasec' ], - comment => $comment, - op_sys => 'All', - rep_platform => 'All', - version => 'other', - dependson => $bug->bug_id, - }); - $bug->set_all({ blocked => { add => [ $investigate_bug->bug_id ] }}); - Bugzilla::BugMail::Send($investigate_bug->id, $recipients); - - Bugzilla->set_user($old_user); - $vars->{original_reporter} = ''; - $comment = ''; - $cgi->param('display_action', 'ssh'); - $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment) - || ThrowTemplateError($template->error()); - - $ssh_key_bug = Bugzilla::Bug->create({ - short_desc => 'Disable/Regenerate SSH Key', - product => $bug->product, - component => $bug->component, - bug_severity => 'critical', - cc => $bug->cc, - groups => [ map { $_->{name} } @{ $bug->groups } ], - comment => $comment, - op_sys => 'All', - rep_platform => 'All', - version => 'other', - dependson => $bug->bug_id, - }); - $bug->set_all({ blocked => { add => [ $ssh_key_bug->bug_id ] }}); - Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients); - }; - my $error = $@; + my $recipients = {changer => $new_user}; + $vars->{original_reporter} = $old_user; + + my $comment; + $cgi->param('display_action', ''); + $template->process('bug/create/comment-employee-incident.txt.tmpl', + $vars, \$comment) + || ThrowTemplateError($template->error()); + + $investigate_bug = Bugzilla::Bug->create({ + short_desc => 'Investigate Lost Device', + product => 'mozilla.org', + component => 'Security Assurance: Incident', + status_whiteboard => '[infrasec:incident]', + bug_severity => 'critical', + cc => ['jstevensen@mozilla.com'], + groups => ['infrasec'], + comment => $comment, + op_sys => 'All', + rep_platform => 'All', + version => 'other', + dependson => $bug->bug_id, + }); + $bug->set_all({blocked => {add => [$investigate_bug->bug_id]}}); + Bugzilla::BugMail::Send($investigate_bug->id, $recipients); Bugzilla->set_user($old_user); - Bugzilla->error_mode($error_mode_cache); + $vars->{original_reporter} = ''; + $comment = ''; + $cgi->param('display_action', 'ssh'); + $template->process('bug/create/comment-employee-incident.txt.tmpl', + $vars, \$comment) + || ThrowTemplateError($template->error()); + + $ssh_key_bug = Bugzilla::Bug->create({ + short_desc => 'Disable/Regenerate SSH Key', + product => $bug->product, + component => $bug->component, + bug_severity => 'critical', + cc => $bug->cc, + groups => [map { $_->{name} } @{$bug->groups}], + comment => $comment, + op_sys => 'All', + rep_platform => 'All', + version => 'other', + dependson => $bug->bug_id, + }); + $bug->set_all({blocked => {add => [$ssh_key_bug->bug_id]}}); + Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients); + }; + my $error = $@; - if ($error || !$investigate_bug || !$ssh_key_bug) { - warn "Failed to create additional employee-incident bug: $error" if $error; - $vars->{'message'} = 'employee_incident_creation_failed'; - } + Bugzilla->set_user($old_user); + Bugzilla->error_mode($error_mode_cache); + + if ($error || !$investigate_bug || !$ssh_key_bug) { + warn "Failed to create additional employee-incident bug: $error" if $error; + $vars->{'message'} = 'employee_incident_creation_failed'; + } } sub _post_gear_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $input = Bugzilla->input_params; - - my ($team, $code) = $input->{teamcode} =~ /^(.+?) \((\d+)\)$/; - my @request = ( - "Date Required: $input->{date_required}", - "$input->{firstname} $input->{lastname}", - $input->{email}, - $input->{mozspace}, - $team, - $code, - $input->{purpose}, - ); - my @recipient = ( - "$input->{shiptofirstname} $input->{shiptolastname}", - $input->{shiptoemail}, - $input->{shiptoaddress1}, - $input->{shiptoaddress2}, - $input->{shiptocity}, - $input->{shiptostate}, - $input->{shiptopostcode}, - $input->{shiptocountry}, - "Phone: $input->{shiptophone}", - $input->{shiptoidrut}, - ); - - # the csv has 14 item fields - my @items = map { trim($_) } split(/\n/, $input->{items}); - my @csv; - while (@items) { - my @batch; - if (scalar(@items) > 14) { - @batch = splice(@items, 0, 14); - } - else { - @batch = @items; - push @batch, '' for scalar(@items)..13; - @items = (); - } - push @csv, [ @request, @batch, @recipient ]; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $input = Bugzilla->input_params; + + my ($team, $code) = $input->{teamcode} =~ /^(.+?) \((\d+)\)$/; + my @request = ( + "Date Required: $input->{date_required}", + "$input->{firstname} $input->{lastname}", + $input->{email}, $input->{mozspace}, $team, $code, $input->{purpose}, + ); + my @recipient = ( + "$input->{shiptofirstname} $input->{shiptolastname}", + $input->{shiptoemail}, + $input->{shiptoaddress1}, + $input->{shiptoaddress2}, + $input->{shiptocity}, + $input->{shiptostate}, + $input->{shiptopostcode}, + $input->{shiptocountry}, + "Phone: $input->{shiptophone}", + $input->{shiptoidrut}, + ); + + # the csv has 14 item fields + my @items = map { trim($_) } split(/\n/, $input->{items}); + my @csv; + while (@items) { + my @batch; + if (scalar(@items) > 14) { + @batch = splice(@items, 0, 14); + } + else { + @batch = @items; + push @batch, '' for scalar(@items) .. 13; + @items = (); } + push @csv, [@request, @batch, @recipient]; + } - # csv quoting and concat - foreach my $line (@csv) { - foreach my $field (@$line) { - if ($field =~ s/"/""/g || $field =~ /,/) { - $field = qq#"$field"#; - } - } - $line = join(',', @$line); + # csv quoting and concat + foreach my $line (@csv) { + foreach my $field (@$line) { + if ($field =~ s/"/""/g || $field =~ /,/) { + $field = qq#"$field"#; + } } + $line = join(',', @$line); + } - $self->_add_attachment($args, { - data => join("\n", @csv), - description => "Items (CSV)", - filename => "gear_" . $bug->id . ".csv", - mimetype => "text/csv", - }); - $bug->update($bug->creation_ts); + $self->_add_attachment( + $args, + { + data => join("\n", @csv), + description => "Items (CSV)", + filename => "gear_" . $bug->id . ".csv", + mimetype => "text/csv", + } + ); + $bug->update($bug->creation_ts); } sub _post_mozpr_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $input = Bugzilla->input_params; - - if ($input->{proj_mat_file}) { - $self->_add_attachment($args, { - data => $input->{proj_mat_file_attach}, - description => $input->{proj_mat_file_desc}, - filename => scalar $input->{proj_mat_file_attach}, - }); - } - if ($input->{pr_mat_file}) { - $self->_add_attachment($args, { - data => $input->{pr_mat_file_attach}, - description => $input->{pr_mat_file_desc}, - filename => scalar $input->{pr_mat_file_attach}, - }); - } - $bug->update($bug->creation_ts); + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $input = Bugzilla->input_params; + + if ($input->{proj_mat_file}) { + $self->_add_attachment( + $args, + { + data => $input->{proj_mat_file_attach}, + description => $input->{proj_mat_file_desc}, + filename => scalar $input->{proj_mat_file_attach}, + } + ); + } + if ($input->{pr_mat_file}) { + $self->_add_attachment( + $args, + { + data => $input->{pr_mat_file_attach}, + description => $input->{pr_mat_file_desc}, + filename => scalar $input->{pr_mat_file_attach}, + } + ); + } + $bug->update($bug->creation_ts); } sub _post_dev_engagement { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $parent_bug = $vars->{bug}; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $params = Bugzilla->input_params; - my $old_user = Bugzilla->user; - - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - eval { - # Add attachment containing tab delimited field values for - # spreadsheet import. - my @columns = qw(event start_date end_date location attendees - audience desc mozilla_attending_list); - my @attach_values; - foreach my $column(@columns) { - my $value = $params->{$column} || ""; - $value =~ s/"/""/g; - push(@attach_values, qq{"$value"}); - } - - my @requested; - foreach my $param (grep(/^request_/, keys %$params)) { - next if !$params->{$param} || $param eq 'request_other_text'; - $param =~ s/^request_//; - push(@requested, ucfirst($param)); - } - push(@attach_values, '"' . join(",", @requested) . '"'); - - # we wrap the data inside a textarea to allow for the delimited data to - # be pasted directly into google docs. - - my $values = html_quote(join("\t", @attach_values)); - my $data = <{vars}; + my $parent_bug = $vars->{bug}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $params = Bugzilla->input_params; + my $old_user = Bugzilla->user; + + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + eval { + # Add attachment containing tab delimited field values for + # spreadsheet import. + my @columns = qw(event start_date end_date location attendees + audience desc mozilla_attending_list); + my @attach_values; + foreach my $column (@columns) { + my $value = $params->{$column} || ""; + $value =~ s/"/""/g; + push(@attach_values, qq{"$value"}); + } + + my @requested; + foreach my $param (grep(/^request_/, keys %$params)) { + next if !$params->{$param} || $param eq 'request_other_text'; + $param =~ s/^request_//; + push(@requested, ucfirst($param)); + } + push(@attach_values, '"' . join(",", @requested) . '"'); + + # we wrap the data inside a textarea to allow for the delimited data to + # be pasted directly into google docs. + + my $values = html_quote(join("\t", @attach_values)); + my $data = < @@ -2196,651 +2235,728 @@ sub _post_dev_engagement { EOF - $self->_add_attachment($args, { - data => $data, - description => 'Spreadsheet Data', - filename => 'dev_engagement_submission.html', - mimetype => 'text/html', - }); - }; + $self->_add_attachment( + $args, + { + data => $data, + description => 'Spreadsheet Data', + filename => 'dev_engagement_submission.html', + mimetype => 'text/html', + } + ); + }; - $parent_bug->update($parent_bug->creation_ts); + $parent_bug->update($parent_bug->creation_ts); } sub _post_shield_studies { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $parent_bug = $vars->{bug}; - my $params = Bugzilla->input_params; - my (@dep_comment, @dep_errors, @send_mail); - - # Common parameters always passed to _file_child_bug - # bug_data and template_suffix will be different for each bug - my $child_params = { - parent_bug => $parent_bug, - template_vars => $vars, - dep_comment => \@dep_comment, - dep_errors => \@dep_errors, - send_mail => \@send_mail, - }; - - # Study Validation Review - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Study Validation Review for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'validation-review'; - _file_child_bug($child_params); - - # Shipping Status - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Shipping Status for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'shipping-status'; - - # Data Review - _file_child_bug($child_params); - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Data Review for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'data-review'; - _file_child_bug($child_params); - - # Legal Review - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Legal Review for ' . $params->{hypothesis}, - product => 'Legal', - component => 'Firefox', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - groups => [ 'mozilla-employee-confidential' ], - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'legal'; - _file_child_bug($child_params); - + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $parent_bug = $vars->{bug}; + my $params = Bugzilla->input_params; + my (@dep_comment, @dep_errors, @send_mail); + + # Common parameters always passed to _file_child_bug + # bug_data and template_suffix will be different for each bug + my $child_params = { + parent_bug => $parent_bug, + template_vars => $vars, + dep_comment => \@dep_comment, + dep_errors => \@dep_errors, + send_mail => \@send_mail, + }; + + # Study Validation Review + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Study Validation Review for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'validation-review'; + _file_child_bug($child_params); + + # Shipping Status + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Shipping Status for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'shipping-status'; + + # Data Review + _file_child_bug($child_params); + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Data Review for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'data-review'; + _file_child_bug($child_params); + + # Legal Review + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Legal Review for ' . $params->{hypothesis}, + product => 'Legal', + component => 'Firefox', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + groups => ['mozilla-employee-confidential'], + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'legal'; + _file_child_bug($child_params); + + if (scalar @dep_errors) { + warn "[Bug " + . $parent_bug->id + . "] Failed to create additional moz-project-review bugs:\n" + . join("\n", @dep_errors); + $vars->{'message'} = 'moz_project_review_creation_failed'; + } + + if (scalar @dep_comment) { + my $comment = join("\n", @dep_comment); if (scalar @dep_errors) { - warn "[Bug " . $parent_bug->id . "] Failed to create additional moz-project-review bugs:\n" . - join("\n", @dep_errors); - $vars->{'message'} = 'moz_project_review_creation_failed'; - } - - if (scalar @dep_comment) { - my $comment = join("\n", @dep_comment); - if (scalar @dep_errors) { - $comment .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; - } - $parent_bug->add_comment($comment); - $parent_bug->update($parent_bug->creation_ts); + $comment + .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; } + $parent_bug->add_comment($comment); + $parent_bug->update($parent_bug->creation_ts); + } - foreach my $bug_id (@send_mail) { - Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user }); - } + foreach my $bug_id (@send_mail) { + Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + } } sub _file_child_bug { - my ($params) = @_; - my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, $dep_errors, $send_mail) - = @$params{qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail)}; - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $new_bug; - eval { - my $comment; - my $full_template = "bug/create/comment-shield-studies-$template_suffix.txt.tmpl"; - Bugzilla->template->process($full_template, $template_vars, \$comment) - || ThrowTemplateError(Bugzilla->template->error()); - $bug_data->{'comment'} = $comment; - if ($new_bug = Bugzilla::Bug->create($bug_data)) { - my $set_all = { - dependson => { add => [ $new_bug->bug_id ] } - }; - $parent_bug->set_all($set_all); - $parent_bug->update($parent_bug->creation_ts); - } + my ($params) = @_; + my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, + $dep_errors, $send_mail) + = @$params{ + qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail) }; - - if ($@ || !($new_bug && $new_bug->{'bug_id'})) { - push(@$dep_comment, "Error creating $template_suffix review bug"); - push(@$dep_errors, "$template_suffix : $@") if $@; - # Since we performed Bugzilla::Bug::create in an eval block, we - # need to manually rollback the commit as this is not done - # in Bugzilla::Error automatically for eval'ed code. - Bugzilla->dbh->bz_rollback_transaction(); - } - else { - push(@$send_mail, $new_bug->id); - push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $new_bug; + eval { + my $comment; + my $full_template + = "bug/create/comment-shield-studies-$template_suffix.txt.tmpl"; + Bugzilla->template->process($full_template, $template_vars, \$comment) + || ThrowTemplateError(Bugzilla->template->error()); + $bug_data->{'comment'} = $comment; + if ($new_bug = Bugzilla::Bug->create($bug_data)) { + my $set_all = {dependson => {add => [$new_bug->bug_id]}}; + $parent_bug->set_all($set_all); + $parent_bug->update($parent_bug->creation_ts); } + }; + + if ($@ || !($new_bug && $new_bug->{'bug_id'})) { + push(@$dep_comment, "Error creating $template_suffix review bug"); + push(@$dep_errors, "$template_suffix : $@") if $@; + + # Since we performed Bugzilla::Bug::create in an eval block, we + # need to manually rollback the commit as this is not done + # in Bugzilla::Error automatically for eval'ed code. + Bugzilla->dbh->bz_rollback_transaction(); + } + else { + push(@$send_mail, $new_bug->id); + push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + } - undef $@; - Bugzilla->error_mode($old_error_mode); + undef $@; + Bugzilla->error_mode($old_error_mode); } sub _pre_fxos_feature { - my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $params = $args->{params}; + my ($self, $args) = @_; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $params = $args->{params}; - $params->{keywords} = 'foxfood'; - $params->{keywords} .= ',feature' if ($cgi->param('feature_type') // '') eq 'new'; - $params->{bug_status} = $user->in_group('canconfirm') ? 'NEW' : 'UNCONFIRMED'; + $params->{keywords} = 'foxfood'; + $params->{keywords} .= ',feature' + if ($cgi->param('feature_type') // '') eq 'new'; + $params->{bug_status} = $user->in_group('canconfirm') ? 'NEW' : 'UNCONFIRMED'; } sub _add_attachment { - my ($self, $args, $attachment_args) = @_; - - my $bug = $args->{vars}->{bug}; - $attachment_args->{bug} = $bug; - $attachment_args->{creation_ts} = $bug->creation_ts; - $attachment_args->{ispatch} = 0 unless exists $attachment_args->{ispatch}; - $attachment_args->{isprivate} = 0 unless exists $attachment_args->{isprivate}; - $attachment_args->{mimetype} ||= $self->_detect_content_type($attachment_args->{data}); - - # If the attachment cannot be successfully added to the bug, - # we notify the user, but we don't interrupt the bug creation process. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $attachment; - eval { - $attachment = Bugzilla::Attachment->create($attachment_args); - }; - warn "$@" if $@; - Bugzilla->error_mode($old_error_mode); - - if ($attachment) { - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - delete $bug->{attachments}; - } - else { - $args->{vars}->{'message'} = 'attachment_creation_failed'; - } + my ($self, $args, $attachment_args) = @_; + + my $bug = $args->{vars}->{bug}; + $attachment_args->{bug} = $bug; + $attachment_args->{creation_ts} = $bug->creation_ts; + $attachment_args->{ispatch} = 0 unless exists $attachment_args->{ispatch}; + $attachment_args->{isprivate} = 0 unless exists $attachment_args->{isprivate}; + $attachment_args->{mimetype} + ||= $self->_detect_content_type($attachment_args->{data}); + + # If the attachment cannot be successfully added to the bug, + # we notify the user, but we don't interrupt the bug creation process. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $attachment; + eval { $attachment = Bugzilla::Attachment->create($attachment_args); }; + warn "$@" if $@; + Bugzilla->error_mode($old_error_mode); + + if ($attachment) { + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + delete $bug->{attachments}; + } + else { + $args->{vars}->{'message'} = 'attachment_creation_failed'; + } - # Note: you must call $bug->update($bug->creation_ts) after adding all attachments +# Note: you must call $bug->update($bug->creation_ts) after adding all attachments } # bugzilla's content_type detection makes assumptions about form fields, which # means we can't use it here. this code is lifted from # Bugzilla::Attachment::get_content_type and the TypeSniffer extension. sub _detect_content_type { - my ($self, $data) = @_; - my $cgi = Bugzilla->cgi; - - # browser provided content-type - my $content_type = $cgi->uploadInfo($data)->{'Content-Type'}; - $content_type = 'image/png' if $content_type eq 'image/x-png'; - - if ($content_type eq 'application/octet-stream') { - # detect from filename - my $filename = scalar($data); - if (my $from_filename = mimetype($filename)) { - return $from_filename; - } + my ($self, $data) = @_; + my $cgi = Bugzilla->cgi; + + # browser provided content-type + my $content_type = $cgi->uploadInfo($data)->{'Content-Type'}; + $content_type = 'image/png' if $content_type eq 'image/x-png'; + + if ($content_type eq 'application/octet-stream') { + + # detect from filename + my $filename = scalar($data); + if (my $from_filename = mimetype($filename)) { + return $from_filename; } + } - return $content_type || 'application/octet-stream'; + return $content_type || 'application/octet-stream'; } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - $columns->{'cc_count'} = { - name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)', - title => 'CC Count', - }; - $columns->{'dupe_count'} = { - name => '(SELECT COUNT(*) FROM duplicates WHERE duplicates.dupe_of = bugs.bug_id)', - title => 'Duplicate Count', - }; + my ($self, $args) = @_; + my $columns = $args->{columns}; + $columns->{'cc_count'} = { + name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)', + title => 'CC Count', + }; + $columns->{'dupe_count'} = { + name => + '(SELECT COUNT(*) FROM duplicates WHERE duplicates.dupe_of = bugs.bug_id)', + title => 'Duplicate Count', + }; } sub enter_bug_start { - my ($self, $args) = @_; - # if configured with create_bug_formats, force users into a custom bug - # format (can be overridden with a __standard__ format) - my $cgi = Bugzilla->cgi; - if ($cgi->param('format')) { - if ($cgi->param('format') eq '__standard__') { - $cgi->delete('format'); - $cgi->param('format_forced', 1); - } - } elsif (my $format = forced_format($cgi->param('product'))) { - $cgi->param('format', $format); - } - - # If product eq 'mozilla.org' and format eq 'itrequest', then - # switch to the new 'Infrastructure & Operations' product. - if ($cgi->param('product') && $cgi->param('product') eq 'mozilla.org' - && $cgi->param('format') && $cgi->param('format') eq 'itrequest') - { - $cgi->param('product', 'Infrastructure & Operations'); - } - - # map renamed groups - $cgi->param('groups', _map_groups($cgi->param('groups'))); + my ($self, $args) = @_; + + # if configured with create_bug_formats, force users into a custom bug + # format (can be overridden with a __standard__ format) + my $cgi = Bugzilla->cgi; + if ($cgi->param('format')) { + if ($cgi->param('format') eq '__standard__') { + $cgi->delete('format'); + $cgi->param('format_forced', 1); + } + } + elsif (my $format = forced_format($cgi->param('product'))) { + $cgi->param('format', $format); + } + + # If product eq 'mozilla.org' and format eq 'itrequest', then + # switch to the new 'Infrastructure & Operations' product. + if ( $cgi->param('product') + && $cgi->param('product') eq 'mozilla.org' + && $cgi->param('format') + && $cgi->param('format') eq 'itrequest') + { + $cgi->param('product', 'Infrastructure & Operations'); + } + + # map renamed groups + $cgi->param('groups', _map_groups($cgi->param('groups'))); } sub bug_before_create { - my ($self, $args) = @_; - my $params = $args->{params}; - if (exists $params->{groups}) { - # map renamed groups - $params->{groups} = [ _map_groups($params->{groups}) ]; - } - if ((Bugzilla->cgi->param('format') // '') eq 'fxos-feature') { - $self->_pre_fxos_feature($args); - } + my ($self, $args) = @_; + my $params = $args->{params}; + if (exists $params->{groups}) { + + # map renamed groups + $params->{groups} = [_map_groups($params->{groups})]; + } + if ((Bugzilla->cgi->param('format') // '') eq 'fxos-feature') { + $self->_pre_fxos_feature($args); + } } sub _map_groups { - my (@groups) = @_; - return unless @groups; - @groups = @{ $groups[0] } if ref($groups[0]); - return map { - # map mozilla-corporation-confidential => mozilla-employee-confidential - $_ eq 'mozilla-corporation-confidential' - ? 'mozilla-employee-confidential' - : $_ - } @groups; + my (@groups) = @_; + return unless @groups; + @groups = @{$groups[0]} if ref($groups[0]); + return map { + + # map mozilla-corporation-confidential => mozilla-employee-confidential + $_ eq 'mozilla-corporation-confidential' ? 'mozilla-employee-confidential' : $_ + } @groups; } sub forced_format { - # note: this is also called from the guided bug entry extension - my ($product) = @_; - return undef unless defined $product; - - # always work on the correct product name - $product = Bugzilla::Product->new({ name => $product, cache => 1 }) - unless blessed($product); - return undef unless $product; - - # check for a forced-format entry - my $forced = $create_bug_formats{$product->name} - || return; - - # should this user be included? - my $user = Bugzilla->user; - my $include = ref($forced->{include}) ? $forced->{include} : [ $forced->{include} ]; - foreach my $inc (@$include) { - return $forced->{format} if $user->in_group($inc); - } - return undef; + # note: this is also called from the guided bug entry extension + my ($product) = @_; + return undef unless defined $product; + + # always work on the correct product name + $product = Bugzilla::Product->new({name => $product, cache => 1}) + unless blessed($product); + return undef unless $product; + + # check for a forced-format entry + my $forced = $create_bug_formats{$product->name} || return; + + # should this user be included? + my $user = Bugzilla->user; + my $include + = ref($forced->{include}) ? $forced->{include} : [$forced->{include}]; + foreach my $inc (@$include) { + return $forced->{format} if $user->in_group($inc); + } + + return undef; } sub query_database { - my ($vars) = @_; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - - # validate group membership - $user->in_group('query_database') - || ThrowUserError('auth_failure', { group => 'query_database', - action => 'access', - object => 'query_database' }); - - # read query - my $input = Bugzilla->input_params; - my $query = $input->{query}; - $vars->{query} = $query; - - if ($query) { - # Only allow POST requests - if ($cgi->request_method ne 'POST') { - ThrowCodeError('illegal_request_method', - { method => $cgi->request_method, accepted => ['POST'] }); - } + my ($vars) = @_; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $template = Bugzilla->template; - check_hash_token($input->{token}, ['query_database']); - trick_taint($query); - $vars->{executed} = 1; + # validate group membership + $user->in_group('query_database') + || ThrowUserError('auth_failure', + {group => 'query_database', action => 'access', object => 'query_database'}); - # add limit if missing - if ($query !~ /\sLIMIT\s+\d+\s*$/si) { - $query .= ' LIMIT 1000'; - $vars->{query} = $query; - } + # read query + my $input = Bugzilla->input_params; + my $query = $input->{query}; + $vars->{query} = $query; - # log query - _syslog(sprintf("[db_query] %s %s", $user->login, $query)); - - # connect to database and execute - # switching to the shadow db gives us a read-only connection - my $dbh = Bugzilla->switch_to_shadow_db(); - my $sth; - eval { - $sth = $dbh->prepare($query); - $sth->execute(); - }; - if ($@) { - $vars->{sql_error} = $@; - return; - } + if ($query) { - # build result - my $columns = $sth->{NAME}; - my $rows; - while (my @row = $sth->fetchrow_array) { - push @$rows, \@row; - } + # Only allow POST requests + if ($cgi->request_method ne 'POST') { + ThrowCodeError('illegal_request_method', + {method => $cgi->request_method, accepted => ['POST']}); + } - # return results - $vars->{columns} = $columns; - $vars->{rows} = $rows; + check_hash_token($input->{token}, ['query_database']); + trick_taint($query); + $vars->{executed} = 1; - if ($input->{csv}) { - print $cgi->header(-type=> 'text/csv', - -content_disposition=> "attachment; filename=\"query_database.csv\""); - $template->process("pages/query_database.csv.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + # add limit if missing + if ($query !~ /\sLIMIT\s+\d+\s*$/si) { + $query .= ' LIMIT 1000'; + $vars->{query} = $query; } + + # log query + _syslog(sprintf("[db_query] %s %s", $user->login, $query)); + + # connect to database and execute + # switching to the shadow db gives us a read-only connection + my $dbh = Bugzilla->switch_to_shadow_db(); + my $sth; + eval { + $sth = $dbh->prepare($query); + $sth->execute(); + }; + if ($@) { + $vars->{sql_error} = $@; + return; + } + + # build result + my $columns = $sth->{NAME}; + my $rows; + while (my @row = $sth->fetchrow_array) { + push @$rows, \@row; + } + + # return results + $vars->{columns} = $columns; + $vars->{rows} = $rows; + + if ($input->{csv}) { + print $cgi->header( + -type => 'text/csv', + -content_disposition => "attachment; filename=\"query_database.csv\"" + ); + $template->process("pages/query_database.csv.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + } } # you can always file bugs into a product's default security group, as well as # into any of the groups in @always_fileable_groups sub _group_always_settable { - my ($self, $group) = @_; - return - $group->name eq $self->default_security_group - || ((grep { $_ eq $group->name } @always_fileable_groups) ? 1 : 0); + my ($self, $group) = @_; + return $group->name eq $self->default_security_group + || ((grep { $_ eq $group->name } @always_fileable_groups) ? 1 : 0); } sub _default_security_group { - return $_[0]->default_security_group_obj->name; + return $_[0]->default_security_group_obj->name; } sub _default_security_group_obj { - my $group_id = $_[0]->{security_group_id}; - if (!$group_id) { - return Bugzilla::Group->new({ name => Bugzilla->params->{insidergroup}, cache => 1 }); - } - return Bugzilla::Group->new({ id => $group_id, cache => 1 }); + my $group_id = $_[0]->{security_group_id}; + if (!$group_id) { + return Bugzilla::Group->new( + {name => Bugzilla->params->{insidergroup}, cache => 1}); + } + return Bugzilla::Group->new({id => $group_id, cache => 1}); } # called from the verify version, component, and group page. # if we're making a group invalid, stuff the default group into the cgi param # to make it checked by default. sub _check_default_product_security_group { - my ($self, $product, $invalid_groups, $optional_group_controls) = @_; - return unless my $group = $product->default_security_group_obj; - if (@$invalid_groups) { - my $cgi = Bugzilla->cgi; - my @groups = $cgi->param('groups'); - push @groups, $group->name unless grep { $_ eq $group->name } @groups; - $cgi->param('groups', @groups); - } + my ($self, $product, $invalid_groups, $optional_group_controls) = @_; + return unless my $group = $product->default_security_group_obj; + if (@$invalid_groups) { + my $cgi = Bugzilla->cgi; + my @groups = $cgi->param('groups'); + push @groups, $group->name unless grep { $_ eq $group->name } @groups; + $cgi->param('groups', @groups); + } } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $create_files = $args->{create_files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $create_files->{__lbheartbeat__} = { - perms => Bugzilla::Install::Filesystem::WS_SERVE, - overwrite => 1, # the original value for this was wrong, overwrite it - contents => 'httpd OK', - }; - - - # version.json needs to have a source attribute pointing to - # our repository. We already have this information in the (static) - # contribute.json file, so parse that in - my $json = JSON::XS->new->pretty->utf8->canonical(); - my $contribute = eval { - $json->decode(scalar read_file(bz_locations()->{cgi_path} . "/contribute.json")); - }; - - if (!$contribute) { - die "Missing or invalid contribute.json file"; - } - - my $version_obj = { - source => $contribute->{repository}{url}, - version => BUGZILLA_VERSION, - commit => $ENV{CIRCLE_SHA1} // 'unknown', - build => $ENV{CIRCLE_BUILD_URL} // 'unknown', - }; - - $create_files->{'version.json'} = { - overwrite => 1, - perms => Bugzilla::Install::Filesystem::WS_SERVE, - contents => $json->encode($version_obj), - }; - - $files->{"$extensions_dir/BMO/bin/migrate-github-pull-requests.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $create_files = $args->{create_files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $create_files->{__lbheartbeat__} = { + perms => Bugzilla::Install::Filesystem::WS_SERVE, + overwrite => 1, # the original value for this was wrong, overwrite it + contents => 'httpd OK', + }; + + + # version.json needs to have a source attribute pointing to + # our repository. We already have this information in the (static) + # contribute.json file, so parse that in + my $json = JSON::XS->new->pretty->utf8->canonical(); + my $contribute = eval { + $json->decode( + scalar read_file(bz_locations()->{cgi_path} . "/contribute.json")); + }; + + if (!$contribute) { + die "Missing or invalid contribute.json file"; + } + + my $version_obj = { + source => $contribute->{repository}{url}, + version => BUGZILLA_VERSION, + commit => $ENV{CIRCLE_SHA1} // 'unknown', + build => $ENV{CIRCLE_BUILD_URL} // 'unknown', + }; + + $create_files->{'version.json'} = { + overwrite => 1, + perms => Bugzilla::Install::Filesystem::WS_SERVE, + contents => $json->encode($version_obj), + }; + + $files->{"$extensions_dir/BMO/bin/migrate-github-pull-requests.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } # "deleted" comment tag sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{groupsecurity}->{params} }, { - name => 'delete_comments_group', - type => 's', - choices => \&get_all_group_names, - default => 'admin', - checker => \&check_group + my ($self, $args) = @_; + push @{$args->{panels}->{groupsecurity}->{params}}, + { + name => 'delete_comments_group', + type => 's', + choices => \&get_all_group_names, + default => 'admin', + checker => \&check_group }; } sub comment_after_add_tag { - my ($self, $args) = @_; - my $tag = $args->{tag}; - return unless lc($tag) eq 'deleted'; - - my $group_name = Bugzilla->params->{delete_comments_group}; - if (!$group_name || !Bugzilla->user->in_group($group_name)) { - ThrowUserError('auth_failure', { group => $group_name, - action => 'delete', - object => 'comments' }); - } + my ($self, $args) = @_; + my $tag = $args->{tag}; + return unless lc($tag) eq 'deleted'; + + my $group_name = Bugzilla->params->{delete_comments_group}; + if (!$group_name || !Bugzilla->user->in_group($group_name)) { + ThrowUserError('auth_failure', + {group => $group_name, action => 'delete', object => 'comments'}); + } } sub comment_after_remove_tag { - my ($self, $args) = @_; - my $tag = $args->{tag}; - return unless lc($tag) eq 'deleted'; - - my $group_name = Bugzilla->params->{delete_comments_group}; - if (!$group_name || !Bugzilla->user->in_group($group_name)) { - ThrowUserError('auth_failure', { group => $group_name, - action => 'delete', - object => 'comments' }); - } + my ($self, $args) = @_; + my $tag = $args->{tag}; + return unless lc($tag) eq 'deleted'; + + my $group_name = Bugzilla->params->{delete_comments_group}; + if (!$group_name || !Bugzilla->user->in_group($group_name)) { + ThrowUserError('auth_failure', + {group => $group_name, action => 'delete', object => 'comments'}); + } } BEGIN { - *Bugzilla::Comment::has_tag = \&_comment_has_tag; + *Bugzilla::Comment::has_tag = \&_comment_has_tag; } sub _comment_has_tag { - my ($self, $test_tag) = @_; - $test_tag = lc($test_tag); - foreach my $tag (@{ $self->tags }) { - return 1 if lc($tag) eq $test_tag; - } - return 0; + my ($self, $test_tag) = @_; + $test_tag = lc($test_tag); + foreach my $tag (@{$self->tags}) { + return 1 if lc($tag) eq $test_tag; + } + return 0; } sub bug_comments { - my ($self, $args) = @_; - my $can_delete = Bugzilla->user->in_group(Bugzilla->params->{delete_comments_group}); - my $comments = $args->{comments}; - my @deleted = grep { $_->has_tag('deleted') } @$comments; - while (my $comment = pop @deleted) { - for (my $i = scalar(@$comments) - 1; $i >= 0; $i--) { - if ($comment == $comments->[$i]) { - if ($can_delete) { - # don't remove comment from users who can "delete" them - # just collapse it instead - $comment->{collapsed} = 1; - } - else { - # otherwise, remove it from the array - splice(@$comments, $i, 1); - } - last; - } + my ($self, $args) = @_; + my $can_delete + = Bugzilla->user->in_group(Bugzilla->params->{delete_comments_group}); + my $comments = $args->{comments}; + my @deleted = grep { $_->has_tag('deleted') } @$comments; + while (my $comment = pop @deleted) { + for (my $i = scalar(@$comments) - 1; $i >= 0; $i--) { + if ($comment == $comments->[$i]) { + if ($can_delete) { + + # don't remove comment from users who can "delete" them + # just collapse it instead + $comment->{collapsed} = 1; } + else { + # otherwise, remove it from the array + splice(@$comments, $i, 1); + } + last; + } } + } } sub _split_crash_signature { - my ($self, $vars) = @_; - my $bug = $vars->{bug} // return; - my $crash_signature = $bug->cf_crash_signature // return; - return [ - grep { /\S/ } - extract_multiple($crash_signature, [ sub { extract_bracketed($_[0], '[]') } ]) - ]; + my ($self, $vars) = @_; + my $bug = $vars->{bug} // return; + my $crash_signature = $bug->cf_crash_signature // return; + return [grep {/\S/} + extract_multiple($crash_signature, [sub { extract_bracketed($_[0], '[]') }])]; } sub enter_bug_entrydefaultvars { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $cgi = Bugzilla->cgi; - return unless my $format = $cgi->param('format'); - - if ($format eq 'fxos-feature') { - $vars->{feature_type} = $cgi->param('feature_type'); - $vars->{description} = $cgi->param('description'); - $vars->{discussion} = $cgi->param('discussion'); - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $cgi = Bugzilla->cgi; + return unless my $format = $cgi->param('format'); + + if ($format eq 'fxos-feature') { + $vars->{feature_type} = $cgi->param('feature_type'); + $vars->{description} = $cgi->param('description'); + $vars->{discussion} = $cgi->param('discussion'); + } } sub app_startup { - my ($self, $args) = @_; - my $app = $args->{app}; - my $r = $app->routes; - - $r->get( - '/favicon.ico' => sub { - my $c = shift; - $c->reply->file( - $c->app->home->child('extensions/BMO/web/images/favicon.ico') - ); - } + my ($self, $args) = @_; + my $app = $args->{app}; + my $r = $app->routes; + + $r->get( + '/favicon.ico' => sub { + my $c = shift; + $c->reply->file($c->app->home->child('extensions/BMO/web/images/favicon.ico')); + } + ); + + $r->any('/:REWRITE_itrequest' => [REWRITE_itrequest => qr{form[\.:]itrequest}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Infrastructure & Operations', 'format' => 'itrequest'}); + $r->any('/:REWRITE_mozlist' => [REWRITE_mozlist => qr{form[\.:]mozlist}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'mozlist'}); + $r->any('/:REWRITE_poweredby' => [REWRITE_poweredby => qr{form[\.:]poweredby}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'poweredby'}); + $r->any( + '/:REWRITE_presentation' => [REWRITE_presentation => qr{form[\.:]presentation}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'presentation'} ); - - $r->any( '/:REWRITE_itrequest' => [ REWRITE_itrequest => qr{form[\.:]itrequest} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'itrequest' } ); - $r->any( '/:REWRITE_mozlist' => [ REWRITE_mozlist => qr{form[\.:]mozlist} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'mozlist' } ); - $r->any( '/:REWRITE_poweredby' => [ REWRITE_poweredby => qr{form[\.:]poweredby} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'poweredby' } ); - $r->any( '/:REWRITE_presentation' => [ REWRITE_presentation => qr{form[\.:]presentation} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'presentation' } ); - $r->any( '/:REWRITE_trademark' => [ REWRITE_trademark => qr{form[\.:]trademark} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'trademark' } ); - $r->any( '/:REWRITE_recoverykey' => [ REWRITE_recoverykey => qr{form[\.:]recoverykey} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'recoverykey' } ); - $r->any( '/:REWRITE_legal' => [ REWRITE_legal => qr{form[\.:]legal} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'legal' }, ); - $r->any( '/:REWRITE_recruiting' => [ REWRITE_recruiting => qr{form[\.:]recruiting} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'recruiting' } ); - $r->any( '/:REWRITE_intern' => [ REWRITE_intern => qr{form[\.:]intern} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'intern' } ); - $r->any( '/:REWRITE_mozpr' => [ REWRITE_mozpr => qr{form[\.:]mozpr} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla PR', 'format' => 'mozpr' }, ); - $r->any( '/:REWRITE_reps_mentorship' => [ REWRITE_reps_mentorship => qr{form[\.:]reps[\.:]mentorship} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'mozreps' }, ); - $r->any( '/:REWRITE_reps_budget' => [ REWRITE_reps_budget => qr{form[\.:]reps[\.:]budget} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-budget' } ); - $r->any( '/:REWRITE_reps_swag' => [ REWRITE_reps_swag => qr{form[\.:]reps[\.:]swag} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-swag' } ); - $r->any( '/:REWRITE_reps_it' => [ REWRITE_reps_it => qr{form[\.:]reps[\.:]it} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-it' } ); - $r->any( '/:REWRITE_reps_payment' => [ REWRITE_reps_payment => qr{form[\.:]reps[\.:]payment} ] ) - ->to( 'CGI#page_cgi' => { 'id' => 'remo-form-payment.html' } ); - $r->any( '/:REWRITE_csa_discourse' => [ REWRITE_csa_discourse => qr{form[\.:]csa[\.:]discourse} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'csa-discourse' } ); - $r->any( '/:REWRITE_employee_incident' => [ REWRITE_employee_incident => qr{form[\.:]employee[\.\-:]incident} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'employee-incident' } ); - $r->any( '/:REWRITE_brownbag' => [ REWRITE_brownbag => qr{form[\.:]brownbag} ] ) - ->to( 'CGI#https_air_mozilla_org_requests' => {} ); - $r->any( '/:REWRITE_finance' => [ REWRITE_finance => qr{form[\.:]finance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Finance', 'format' => 'finance' } ); - $r->any( - '/:REWRITE_moz_project_review' => [ REWRITE_moz_project_review => qr{form[\.:]moz[\.\-:]project[\.\-:]review} ] - )->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'moz-project-review' } ); - $r->any( '/:REWRITE_docs' => [ REWRITE_docs => qr{form[\.:]docs?} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Documentation', 'format' => 'doc' } ); - $r->any( '/:REWRITE_mdn' => [ REWRITE_mdn => qr{form[\.:]mdn?} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'mdn', 'product' => 'developer.mozilla.org' } ); - $r->any( '/:REWRITE_swag_gear' => [ REWRITE_swag_gear => qr{form[\.:](swag|gear)} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'swag', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_costume' => [ REWRITE_costume => qr{form[\.:]costume} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Marketing', 'format' => 'costume' } ); - $r->any( '/:REWRITE_ipp' => [ REWRITE_ipp => qr{form[\.:]ipp} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Internet Public Policy', 'format' => 'ipp' } ); - $r->any( '/:REWRITE_creative' => [ REWRITE_creative => qr{form[\.:]creative} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'creative', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_user_engagement' => [ REWRITE_user_engagement => qr{form[\.:]user[\.\-:]engagement} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'user-engagement', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_dev_engagement_event' => - [ REWRITE_dev_engagement_event => qr{form[\.:]dev[\.\-:]engagement[\.\-\:]event} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Engagement', 'format' => 'dev-engagement-event' } ); - $r->any( '/:REWRITE_mobile_compat' => [ REWRITE_mobile_compat => qr{form[\.:]mobile[\.\-:]compat} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Tech Evangelism', 'format' => 'mobile-compat' } ); - $r->any( '/:REWRITE_web_bounty' => [ REWRITE_web_bounty => qr{form[\.:]web[\.:]bounty} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'web-bounty', 'product' => 'mozilla.org' } ); - $r->any( '/:REWRITE_automative' => [ REWRITE_automative => qr{form[\.:]automative} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Testing', 'format' => 'automative' } ); - $r->any( '/:REWRITE_comm_newsletter' => [ REWRITE_comm_newsletter => qr{form[\.:]comm[\.:]newsletter} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'comm-newsletter', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_screen_share_whitelist' => - [ REWRITE_screen_share_whitelist => qr{form[\.:]screen[\.:]share[\.:]whitelist} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'screen-share-whitelist', 'product' => 'Firefox' } ); - $r->any( '/:REWRITE_data_compliance' => [ REWRITE_data_compliance => qr{form[\.:]data[\.\-:]compliance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Data Compliance', 'format' => 'data-compliance' } ); - $r->any( '/:REWRITE_fsa_budget' => [ REWRITE_fsa_budget => qr{form[\.:]fsa[\.:]budget} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'FSA', 'format' => 'fsa-budget' } ); - $r->any( '/:REWRITE_triage_request' => [ REWRITE_triage_request => qr{form[\.:]triage[\.\-]request} ] ) - ->to( 'CGI#page_cgi' => { 'id' => 'triage_request.html' } ); - $r->any( '/:REWRITE_crm_CRM' => [ REWRITE_crm_CRM => qr{form[\.:](crm|CRM)} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'crm', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_nda' => [ REWRITE_nda => qr{form[\.:]nda} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'nda' } ); - $r->any( '/:REWRITE_name_clearance' => [ REWRITE_name_clearance => qr{form[\.:]name[\.:]clearance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'name-clearance', 'product' => 'Legal' } ); - $r->any( '/:REWRITE_shield_studies' => [ REWRITE_shield_studies => qr{form[\.:]shield[\.:]studies} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Shield', 'format' => 'shield-studies' } ); - $r->any( '/:REWRITE_client_bounty' => [ REWRITE_client_bounty => qr{form[\.:]client[\.:]bounty} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Firefox', 'format' => 'client-bounty' } ); + $r->any('/:REWRITE_trademark' => [REWRITE_trademark => qr{form[\.:]trademark}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'trademark'}); + $r->any( + '/:REWRITE_recoverykey' => [REWRITE_recoverykey => qr{form[\.:]recoverykey}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'recoverykey'}); + $r->any('/:REWRITE_legal' => [REWRITE_legal => qr{form[\.:]legal}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Legal', 'format' => 'legal'},); + $r->any( + '/:REWRITE_recruiting' => [REWRITE_recruiting => qr{form[\.:]recruiting}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Recruiting', 'format' => 'recruiting'}); + $r->any('/:REWRITE_intern' => [REWRITE_intern => qr{form[\.:]intern}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Recruiting', 'format' => 'intern'}); + $r->any('/:REWRITE_mozpr' => [REWRITE_mozpr => qr{form[\.:]mozpr}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Mozilla PR', 'format' => 'mozpr'}, + ); + $r->any('/:REWRITE_reps_mentorship' => + [REWRITE_reps_mentorship => qr{form[\.:]reps[\.:]mentorship}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'mozreps'},); + $r->any('/:REWRITE_reps_budget' => + [REWRITE_reps_budget => qr{form[\.:]reps[\.:]budget}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-budget'} + ); + $r->any( + '/:REWRITE_reps_swag' => [REWRITE_reps_swag => qr{form[\.:]reps[\.:]swag}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-swag'}); + $r->any('/:REWRITE_reps_it' => [REWRITE_reps_it => qr{form[\.:]reps[\.:]it}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-it'}); + $r->any('/:REWRITE_reps_payment' => + [REWRITE_reps_payment => qr{form[\.:]reps[\.:]payment}]) + ->to('CGI#page_cgi' => {'id' => 'remo-form-payment.html'}); + $r->any('/:REWRITE_csa_discourse' => + [REWRITE_csa_discourse => qr{form[\.:]csa[\.:]discourse}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Infrastructure & Operations', 'format' => 'csa-discourse'}); + $r->any('/:REWRITE_employee_incident' => + [REWRITE_employee_incident => qr{form[\.:]employee[\.\-:]incident}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'mozilla.org', 'format' => 'employee-incident'}); + $r->any('/:REWRITE_brownbag' => [REWRITE_brownbag => qr{form[\.:]brownbag}]) + ->to('CGI#https_air_mozilla_org_requests' => {}); + $r->any('/:REWRITE_finance' => [REWRITE_finance => qr{form[\.:]finance}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Finance', 'format' => 'finance'}); + $r->any('/:REWRITE_moz_project_review' => + [REWRITE_moz_project_review => qr{form[\.:]moz[\.\-:]project[\.\-:]review}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'mozilla.org', 'format' => 'moz-project-review'}); + $r->any('/:REWRITE_docs' => [REWRITE_docs => qr{form[\.:]docs?}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Developer Documentation', 'format' => 'doc'}); + $r->any('/:REWRITE_mdn' => [REWRITE_mdn => qr{form[\.:]mdn?}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'mdn', 'product' => 'developer.mozilla.org'}); + $r->any( + '/:REWRITE_swag_gear' => [REWRITE_swag_gear => qr{form[\.:](swag|gear)}]) + ->to('CGI#enter_bug_cgi' => {'format' => 'swag', 'product' => 'Marketing'}); + $r->any('/:REWRITE_costume' => [REWRITE_costume => qr{form[\.:]costume}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Marketing', 'format' => 'costume'}); + $r->any('/:REWRITE_ipp' => [REWRITE_ipp => qr{form[\.:]ipp}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Internet Public Policy', 'format' => 'ipp'}); + $r->any('/:REWRITE_creative' => [REWRITE_creative => qr{form[\.:]creative}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'creative', 'product' => 'Marketing'}); + $r->any('/:REWRITE_user_engagement' => + [REWRITE_user_engagement => qr{form[\.:]user[\.\-:]engagement}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'user-engagement', 'product' => 'Marketing'}); + $r->any( + '/:REWRITE_dev_engagement_event' => [ + REWRITE_dev_engagement_event => qr{form[\.:]dev[\.\-:]engagement[\.\-\:]event} + ] + ) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Developer Engagement', 'format' => 'dev-engagement-event'}); + $r->any('/:REWRITE_mobile_compat' => + [REWRITE_mobile_compat => qr{form[\.:]mobile[\.\-:]compat}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Tech Evangelism', 'format' => 'mobile-compat'}); + $r->any( + '/:REWRITE_web_bounty' => [REWRITE_web_bounty => qr{form[\.:]web[\.:]bounty}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'web-bounty', 'product' => 'mozilla.org'}); + $r->any( + '/:REWRITE_automative' => [REWRITE_automative => qr{form[\.:]automative}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Testing', 'format' => 'automative'}); + $r->any('/:REWRITE_comm_newsletter' => + [REWRITE_comm_newsletter => qr{form[\.:]comm[\.:]newsletter}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'comm-newsletter', 'product' => 'Marketing'}); + $r->any( + '/:REWRITE_screen_share_whitelist' => [ + REWRITE_screen_share_whitelist => qr{form[\.:]screen[\.:]share[\.:]whitelist} + ] + ) + ->to('CGI#enter_bug_cgi' => + {'format' => 'screen-share-whitelist', 'product' => 'Firefox'}); + $r->any('/:REWRITE_data_compliance' => + [REWRITE_data_compliance => qr{form[\.:]data[\.\-:]compliance}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Data Compliance', 'format' => 'data-compliance'}); + $r->any( + '/:REWRITE_fsa_budget' => [REWRITE_fsa_budget => qr{form[\.:]fsa[\.:]budget}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'FSA', 'format' => 'fsa-budget'}); + $r->any('/:REWRITE_triage_request' => + [REWRITE_triage_request => qr{form[\.:]triage[\.\-]request}]) + ->to('CGI#page_cgi' => {'id' => 'triage_request.html'}); + $r->any('/:REWRITE_crm_CRM' => [REWRITE_crm_CRM => qr{form[\.:](crm|CRM)}]) + ->to('CGI#enter_bug_cgi' => {'format' => 'crm', 'product' => 'Marketing'}); + $r->any('/:REWRITE_nda' => [REWRITE_nda => qr{form[\.:]nda}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Legal', 'format' => 'nda'}); + $r->any('/:REWRITE_name_clearance' => + [REWRITE_name_clearance => qr{form[\.:]name[\.:]clearance}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'name-clearance', 'product' => 'Legal'}); + $r->any('/:REWRITE_shield_studies' => + [REWRITE_shield_studies => qr{form[\.:]shield[\.:]studies}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Shield', 'format' => 'shield-studies'}); + $r->any('/:REWRITE_client_bounty' => + [REWRITE_client_bounty => qr{form[\.:]client[\.:]bounty}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Firefox', 'format' => 'client-bounty'}); } __PACKAGE__->NAME; diff --git a/extensions/BMO/bin/bug_1022707.pl b/extensions/BMO/bin/bug_1022707.pl index 4d48db01d..31afa7d05 100755 --- a/extensions/BMO/bin/bug_1022707.pl +++ b/extensions/BMO/bin/bug_1022707.pl @@ -37,8 +37,10 @@ print "About to fix $total flags\n"; print "Press to start, or ^C to cancel...\n"; readline; -my $update_fsa_sql= "UPDATE flag_state_activity SET type_id = 4 WHERE " . $dbh->sql_in('flag_id', $flag_ids); -my $update_flags_sql = "UPDATE flags SET type_id = 4 WHERE " . $dbh->sql_in('id', $flag_ids); +my $update_fsa_sql = "UPDATE flag_state_activity SET type_id = 4 WHERE " + . $dbh->sql_in('flag_id', $flag_ids); +my $update_flags_sql + = "UPDATE flags SET type_id = 4 WHERE " . $dbh->sql_in('id', $flag_ids); $dbh->bz_start_transaction(); $dbh->do($update_fsa_sql); diff --git a/extensions/BMO/bin/bug_1093952.pl b/extensions/BMO/bin/bug_1093952.pl index fd891f4ae..f52427284 100755 --- a/extensions/BMO/bin/bug_1093952.pl +++ b/extensions/BMO/bin/bug_1093952.pl @@ -23,14 +23,17 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; -my $infra = Bugzilla::Product->check({ name => 'Infrastructure & Operations' }); -my $relops_id = Bugzilla::Component->check({ product => $infra, name => 'RelOps' })->id; -my $puppet_id = Bugzilla::Component->check({ product => $infra, name => 'RelOps: Puppet' })->id; -my $infra_id = $infra->id; -my $components = $dbh->sql_in('component_id', [ $relops_id, $puppet_id ]); +my $infra = Bugzilla::Product->check({name => 'Infrastructure & Operations'}); +my $relops_id + = Bugzilla::Component->check({product => $infra, name => 'RelOps'})->id; +my $puppet_id + = Bugzilla::Component->check({product => $infra, name => 'RelOps: Puppet'}) + ->id; +my $infra_id = $infra->id; +my $components = $dbh->sql_in('component_id', [$relops_id, $puppet_id]); print "Searching for bugs..\n"; -my $bugs = $dbh->selectall_arrayref(< {} }); +my $bugs = $dbh->selectall_arrayref(< {}}); SELECT bug_id, product_id, @@ -52,9 +55,9 @@ printf "About to fix %s bugs\n", scalar(@$bugs); print "Press to stop or to continue...\n"; getc(); -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $field = Bugzilla::Field->check({ name => 'status_whiteboard' }); -my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $field = Bugzilla::Field->check({name => 'status_whiteboard'}); +my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); my $sth_bugs = $dbh->prepare(" UPDATE bugs @@ -70,22 +73,25 @@ my $sth_activity = $dbh->prepare(" $dbh->bz_start_transaction(); foreach my $bug (@$bugs) { - my $bug_id = $bug->{bug_id}; - my $whiteboard = $bug->{status_whiteboard}; - print "bug $bug_id\n $whiteboard\n"; + my $bug_id = $bug->{bug_id}; + my $whiteboard = $bug->{status_whiteboard}; + print "bug $bug_id\n $whiteboard\n"; - my $updated = $whiteboard; - $updated =~ s#\[kanban:engops:https://kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; - if ($bug->{product_id} == $infra->id - && $bug->{component_id} != $relops_id - && $bug->{component_id} != $puppet_id - ) { - $updated =~ s#\[kanban:engops:https://mozilla\.kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; - } - print " $updated\n"; + my $updated = $whiteboard; + $updated + =~ s#\[kanban:engops:https://kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; + if ( $bug->{product_id} == $infra->id + && $bug->{component_id} != $relops_id + && $bug->{component_id} != $puppet_id) + { + $updated + =~ s#\[kanban:engops:https://mozilla\.kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; + } + print " $updated\n"; - $sth_bugs->execute($updated, $when, $when, $bug_id); - $sth_activity->execute($bug_id, $nobody->id, $when, $field->id, $whiteboard, $updated); + $sth_bugs->execute($updated, $when, $when, $bug_id); + $sth_activity->execute($bug_id, $nobody->id, $when, $field->id, $whiteboard, + $updated); } $dbh->bz_commit_transaction(); diff --git a/extensions/BMO/bin/bug_1141452.pl b/extensions/BMO/bin/bug_1141452.pl index 155c4704c..56b63db91 100755 --- a/extensions/BMO/bin/bug_1141452.pl +++ b/extensions/BMO/bin/bug_1141452.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants qw( USAGE_MODE_CMDLINE ); @@ -24,14 +24,17 @@ use Bugzilla::User; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; -my $blocking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check({ name => 'cf_blocking_b2g' }); -my $tracking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check({ name => 'cf_tracking_b2g' }); +my $blocking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check( + {name => 'cf_blocking_b2g'}); +my $tracking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check( + {name => 'cf_tracking_b2g'}); die "tracking-b2g does not have a 'backlog' value\n" - unless grep { $_->value eq 'backlog' } @{ $tracking_b2g->values }; + unless grep { $_->value eq 'backlog' } @{$tracking_b2g->values}; print "Searching for bugs..\n"; -my $flags = $dbh->selectall_arrayref(< {} }, $blocking_b2g->flag_id, $tracking_b2g->flag_id); +my $flags = $dbh->selectall_arrayref( + < {}}, $blocking_b2g->flag_id, $tracking_b2g->flag_id); SELECT bugs.bug_id, blocking_b2g.id id, @@ -50,54 +53,59 @@ printf "About to fix %s bugs\n", scalar(@$flags); print "Press to stop or to continue...\n"; getc(); -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); $dbh->bz_start_transaction(); foreach my $flag (@$flags) { - if (!$flag->{value}) { - print $flag->{bug_id}, ": changing blocking_b2g:backlog -> tracking_b2g:backlog\n"; - # no tracking_b2g value, change blocking_b2g:backlog -> tracking_b2g:backlog - $dbh->do( - "UPDATE tracking_flags_bugs SET tracking_flag_id = ? WHERE id = ?", - undef, - $tracking_b2g->flag_id, $flag->{id}, - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $when, $when, $flag->{bug_id}, - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $blocking_b2g->id, 'backlog', '---', - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $tracking_b2g->id, '---', 'backlog', - ); - } - elsif ($flag->{value}) { - print $flag->{bug_id}, ": deleting blocking_b2g:backlog\n"; - # tracking_b2g already has a value, just delete blocking_b2g:backlog - $dbh->do( - "DELETE FROM tracking_flags_bugs WHERE id = ?", - undef, - $flag->{id}, - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $when, $when, $flag->{bug_id}, - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $blocking_b2g->id, 'backlog', '---', - ); - } + if (!$flag->{value}) { + print $flag->{bug_id}, + ": changing blocking_b2g:backlog -> tracking_b2g:backlog\n"; + + # no tracking_b2g value, change blocking_b2g:backlog -> tracking_b2g:backlog + $dbh->do("UPDATE tracking_flags_bugs SET tracking_flag_id = ? WHERE id = ?", + undef, $tracking_b2g->flag_id, $flag->{id},); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $when, $when, $flag->{bug_id},); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $blocking_b2g->id, + 'backlog', + '---', + ); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $tracking_b2g->id, + '---', + 'backlog', + ); + } + elsif ($flag->{value}) { + print $flag->{bug_id}, ": deleting blocking_b2g:backlog\n"; + + # tracking_b2g already has a value, just delete blocking_b2g:backlog + $dbh->do("DELETE FROM tracking_flags_bugs WHERE id = ?", undef, $flag->{id},); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $when, $when, $flag->{bug_id},); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $blocking_b2g->id, + 'backlog', + '---', + ); + } } $dbh->bz_commit_transaction(); diff --git a/extensions/BMO/bin/migrate-github-pull-requests.pl b/extensions/BMO/bin/migrate-github-pull-requests.pl index c39778a4a..c8afdedfb 100755 --- a/extensions/BMO/bin/migrate-github-pull-requests.pl +++ b/extensions/BMO/bin/migrate-github-pull-requests.pl @@ -22,9 +22,9 @@ use Bugzilla::Install::Util qw(indicate_progress); use Bugzilla::User; use Bugzilla::Util qw(trim); -my $dbh = Bugzilla->dbh; -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $field = Bugzilla::Field->check({ name => 'attachments.mimetype' }); +my $dbh = Bugzilla->dbh; +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $field = Bugzilla::Field->check({name => 'attachments.mimetype'}); # grab list of suitable attachments @@ -42,7 +42,7 @@ SELECT attachments.attach_id, AND LENGTH(thedata) <= 256 EOF print "Searching for suitable attachments..\n"; -my $attachments = $dbh->selectall_arrayref($sql, { Slice => {} }); +my $attachments = $dbh->selectall_arrayref($sql, {Slice => {}}); my ($current, $total, $updated) = (1, scalar(@$attachments), 0); die "No suitable attachments found\n" unless $total; @@ -52,39 +52,32 @@ print "Press to start, or ^C to cancel...\n"; <>; foreach my $attachment (@$attachments) { - indicate_progress({ current => $current++, total => $total, every => 25 }); - - # check payload - my $url = trim($attachment->{thedata}); - next if $url =~ /\s/; - next unless $url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+\/?$#i; - - $dbh->bz_start_transaction; - - # set content-type - $dbh->do( - "UPDATE attachments SET mimetype = ? WHERE attach_id = ?", - undef, - 'text/x-github-pull-request', $attachment->{attach_id} - ); - - # insert into bugs_activity - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) - VALUES (?, ?, ?, ?, ?, ?)", - undef, - $attachment->{bug_id}, $nobody->id, $timestamp, $field->id, - $attachment->{mimetype}, 'text/x-github-pull-request' - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $timestamp, $timestamp, $attachment->{bug_id} - ); - - $dbh->bz_commit_transaction; - $updated++; + indicate_progress({current => $current++, total => $total, every => 25}); + + # check payload + my $url = trim($attachment->{thedata}); + next if $url =~ /\s/; + next unless $url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+\/?$#i; + + $dbh->bz_start_transaction; + + # set content-type + $dbh->do("UPDATE attachments SET mimetype = ? WHERE attach_id = ?", + undef, 'text/x-github-pull-request', $attachment->{attach_id}); + + # insert into bugs_activity + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) + VALUES (?, ?, ?, ?, ?, ?)", undef, $attachment->{bug_id}, + $nobody->id, $timestamp, $field->id, $attachment->{mimetype}, + 'text/x-github-pull-request' + ); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $timestamp, $timestamp, $attachment->{bug_id}); + + $dbh->bz_commit_transaction; + $updated++; } print "Attachments updated: $updated\n"; diff --git a/extensions/BMO/lib/Constants.pm b/extensions/BMO/lib/Constants.pm index 8227208c8..7ec92befb 100644 --- a/extensions/BMO/lib/Constants.pm +++ b/extensions/BMO/lib/Constants.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - REQUEST_MAX_ATTACH_LINES - DEV_ENGAGE_DISCUSS_NEEDINFO + REQUEST_MAX_ATTACH_LINES + DEV_ENGAGE_DISCUSS_NEEDINFO ); # Maximum attachment size in lines that will be sent with a @@ -24,7 +24,7 @@ use constant REQUEST_MAX_ATTACH_LINES => 1000; # Requestees who need a needinfo flag set for the dev engagement # discussion bug use constant DEV_ENGAGE_DISCUSS_NEEDINFO => qw( - spersing@mozilla.com + spersing@mozilla.com ); 1; diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm index 349f88093..a1e010346 100644 --- a/extensions/BMO/lib/Data.pm +++ b/extensions/BMO/lib/Data.pm @@ -15,13 +15,13 @@ use base qw(Exporter); use Tie::IxHash; our @EXPORT = qw( $cf_visible_in_products - %group_change_notification - $cf_setters - @always_fileable_groups - %group_auto_cc - %create_bug_formats - @default_named_queries - %autodetect_attach_urls ); + %group_change_notification + $cf_setters + @always_fileable_groups + %group_auto_cc + %create_bug_formats + @default_named_queries + %autodetect_attach_urls ); # Creating an attachment whose contents is a URL matching one of these regexes # will result in the user being redirected to that URL when viewing the @@ -43,35 +43,37 @@ my $mozreview_url_re = qr{ }ix; sub phabricator_url_re { - my $phab_uri = Bugzilla->params->{phabricator_base_uri} || 'https://example.com'; - return qr/^\Q${phab_uri}\ED\d+$/i; + my $phab_uri + = Bugzilla->params->{phabricator_base_uri} || 'https://example.com'; + return qr/^\Q${phab_uri}\ED\d+$/i; } our %autodetect_attach_urls = ( - github_pr => { - title => 'GitHub Pull Request', - regex => qr#^https://github\.com/[^/]+/[^/]+/pull/\d+/?$#i, - content_type => 'text/x-github-pull-request', - can_review => 1, - }, - reviewboard => { - title => 'MozReview', - regex => $mozreview_url_re, - content_type => 'text/x-review-board-request', - can_review => 0, - }, - Phabricator => { - title => 'Phabricator', - regex => \&phabricator_url_re, - content_type => 'text/x-phabricator-request', - can_review => 1, - }, - google_docs => { - title => 'Google Doc', - regex => qr#^https://docs\.google\.com/(?:document|spreadsheets|presentation)/d/#i, - content_type => 'text/x-google-doc', - can_review => 0, - }, + github_pr => { + title => 'GitHub Pull Request', + regex => qr#^https://github\.com/[^/]+/[^/]+/pull/\d+/?$#i, + content_type => 'text/x-github-pull-request', + can_review => 1, + }, + reviewboard => { + title => 'MozReview', + regex => $mozreview_url_re, + content_type => 'text/x-review-board-request', + can_review => 0, + }, + Phabricator => { + title => 'Phabricator', + regex => \&phabricator_url_re, + content_type => 'text/x-phabricator-request', + can_review => 1, + }, + google_docs => { + title => 'Google Doc', + regex => + qr#^https://docs\.google\.com/(?:document|spreadsheets|presentation)/d/#i, + content_type => 'text/x-google-doc', + can_review => 0, + }, ); # Which custom fields are visible in which products and components. @@ -83,209 +85,184 @@ our %autodetect_attach_urls = ( # # IxHash keeps them in insertion order, and so we get regexp priorities right. our $cf_visible_in_products; -tie(%$cf_visible_in_products, "Tie::IxHash", - qr/^cf_colo_site$/ => { - "mozilla.org" => [ - "Server Operations", - "Server Operations: DCOps", - "Server Operations: Projects", - "Server Operations: RelEng", - "Server Operations: Security", - ], - "Infrastructure & Operations" => [ - "RelOps", - "RelOps: Puppet", - "DCOps", - ], - }, - qr/^cf_office$/ => { - "mozilla.org" => ["Server Operations: Desktop Issues"], - }, - qr/^cf_crash_signature$/ => { - "Add-on SDK" => [], - "addons.mozilla.org" => [], - "Android Background Services" => [], - "B2GDroid" => [], - "Calendar" => [], - "Composer" => [], - "Core" => [], - "DevTools" => [], - "Directory" => [], - "External Software Affecting Firefox" => [], - "Firefox" => [], - "Firefox for Android" => [], - "GeckoView" => [], - "JSS" => [], - "MailNews Core" => [], - "Mozilla Labs" => [], - "Mozilla Localizations" => [], - "mozilla.org" => [], - "Cloud Services" => [], - "NSPR" => [], - "NSS" => [], - "Other Applications" => [], - "Penelope" => [], - "Release Engineering" => [], - "Rhino" => [], - "SeaMonkey" => [], - "Tamarin" => [], - "Tech Evangelism" => [], - "Testing" => [], - "Thunderbird" => [], - "Toolkit" => [], - "WebExtensions" => [], - }, - qr/^cf_due_date$/ => { - "bugzilla.mozilla.org" => [], - "Community Building" => [], - "Data & BI Services Team" => [], - "Data Compliance" => [], - "Developer Engagement" => [], - "Firefox" => ["Security: Review Requests"], - "Infrastructure & Operations" => [], - "Marketing" => [], - "mozilla.org" => ["Security Assurance: Review Request"], - "Mozilla Metrics" => [], - "Mozilla PR" => [], - "Mozilla Reps" => [], - }, - qr/^cf_locale$/ => { - "Mozilla Localizations" => ['Other'], - "www.mozilla.org" => [], - }, - qr/^cf_mozilla_project$/ => { - "Data & BI Services Team" => [], - }, - qr/^cf_machine_state$/ => { - "Release Engineering" => ["Buildduty"], - }, - qr/^cf_rank$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Hello (Loop)" => [], - "Cloud Services" => [], - "Tech Evangelism" => [], - "Toolkit" => [], - }, - qr/^cf_has_regression_range$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Toolkit" => [], - }, - qr/^cf_has_str$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Toolkit" => [], - }, - qr/^cf_cab_review$/ => { - "Infrastructure & Operations Graveyard" => [], - "Infrastructure & Operations" => [], - "Data & BI Services Team" => [], - } +tie( + %$cf_visible_in_products, + "Tie::IxHash", + qr/^cf_colo_site$/ => { + "mozilla.org" => [ + "Server Operations", + "Server Operations: DCOps", + "Server Operations: Projects", + "Server Operations: RelEng", + "Server Operations: Security", + ], + "Infrastructure & Operations" => ["RelOps", "RelOps: Puppet", "DCOps",], + }, + qr/^cf_office$/ => {"mozilla.org" => ["Server Operations: Desktop Issues"],}, + qr/^cf_crash_signature$/ => { + "Add-on SDK" => [], + "addons.mozilla.org" => [], + "Android Background Services" => [], + "B2GDroid" => [], + "Calendar" => [], + "Composer" => [], + "Core" => [], + "DevTools" => [], + "Directory" => [], + "External Software Affecting Firefox" => [], + "Firefox" => [], + "Firefox for Android" => [], + "GeckoView" => [], + "JSS" => [], + "MailNews Core" => [], + "Mozilla Labs" => [], + "Mozilla Localizations" => [], + "mozilla.org" => [], + "Cloud Services" => [], + "NSPR" => [], + "NSS" => [], + "Other Applications" => [], + "Penelope" => [], + "Release Engineering" => [], + "Rhino" => [], + "SeaMonkey" => [], + "Tamarin" => [], + "Tech Evangelism" => [], + "Testing" => [], + "Thunderbird" => [], + "Toolkit" => [], + "WebExtensions" => [], + }, + qr/^cf_due_date$/ => { + "bugzilla.mozilla.org" => [], + "Community Building" => [], + "Data & BI Services Team" => [], + "Data Compliance" => [], + "Developer Engagement" => [], + "Firefox" => ["Security: Review Requests"], + "Infrastructure & Operations" => [], + "Marketing" => [], + "mozilla.org" => ["Security Assurance: Review Request"], + "Mozilla Metrics" => [], + "Mozilla PR" => [], + "Mozilla Reps" => [], + }, + qr/^cf_locale$/ => + {"Mozilla Localizations" => ['Other'], "www.mozilla.org" => [],}, + qr/^cf_mozilla_project$/ => {"Data & BI Services Team" => [],}, + qr/^cf_machine_state$/ => {"Release Engineering" => ["Buildduty"],}, + qr/^cf_rank$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Hello (Loop)" => [], + "Cloud Services" => [], + "Tech Evangelism" => [], + "Toolkit" => [], + }, + qr/^cf_has_regression_range$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Toolkit" => [], + }, + qr/^cf_has_str$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Toolkit" => [], + }, + qr/^cf_cab_review$/ => { + "Infrastructure & Operations Graveyard" => [], + "Infrastructure & Operations" => [], + "Data & BI Services Team" => [], + } ); # Who to CC on particular bugmails when certain groups are added or removed. our %group_change_notification = ( - 'addons-security' => ['amo-editors@mozilla.org'], - 'b2g-core-security' => ['security@mozilla.org'], - 'bugzilla-security' => ['security@bugzilla.org'], - 'client-services-security' => ['amo-admins@mozilla.org', 'web-security@mozilla.org'], - 'cloud-services-security' => ['web-security@mozilla.org'], - 'core-security' => ['security@mozilla.org'], - 'crypto-core-security' => ['security@mozilla.org'], - 'dom-core-security' => ['security@mozilla.org'], - 'firefox-core-security' => ['security@mozilla.org'], - 'gfx-core-security' => ['security@mozilla.org'], - 'javascript-core-security' => ['security@mozilla.org'], - 'layout-core-security' => ['security@mozilla.org'], - 'mail-core-security' => ['security@mozilla.org'], - 'media-core-security' => ['security@mozilla.org'], - 'network-core-security' => ['security@mozilla.org'], - 'core-security-release' => ['security@mozilla.org'], - 'tamarin-security' => ['tamarinsecurity@adobe.com'], - 'toolkit-core-security' => ['security@mozilla.org'], - 'websites-security' => ['web-security@mozilla.org'], - 'webtools-security' => ['web-security@mozilla.org'], + 'addons-security' => ['amo-editors@mozilla.org'], + 'b2g-core-security' => ['security@mozilla.org'], + 'bugzilla-security' => ['security@bugzilla.org'], + 'client-services-security' => + ['amo-admins@mozilla.org', 'web-security@mozilla.org'], + 'cloud-services-security' => ['web-security@mozilla.org'], + 'core-security' => ['security@mozilla.org'], + 'crypto-core-security' => ['security@mozilla.org'], + 'dom-core-security' => ['security@mozilla.org'], + 'firefox-core-security' => ['security@mozilla.org'], + 'gfx-core-security' => ['security@mozilla.org'], + 'javascript-core-security' => ['security@mozilla.org'], + 'layout-core-security' => ['security@mozilla.org'], + 'mail-core-security' => ['security@mozilla.org'], + 'media-core-security' => ['security@mozilla.org'], + 'network-core-security' => ['security@mozilla.org'], + 'core-security-release' => ['security@mozilla.org'], + 'tamarin-security' => ['tamarinsecurity@adobe.com'], + 'toolkit-core-security' => ['security@mozilla.org'], + 'websites-security' => ['web-security@mozilla.org'], + 'webtools-security' => ['web-security@mozilla.org'], ); # Who can set custom flags (use full field names only, not regex's) -our $cf_setters = { - 'cf_colo_site' => [ 'infra', 'build' ], - 'cf_rank' => [ 'rank-setters' ], -}; +our $cf_setters + = {'cf_colo_site' => ['infra', 'build'], 'cf_rank' => ['rank-setters'],}; # Groups in which you can always file a bug, regardless of product or user. our @always_fileable_groups = qw( - addons-security - bugzilla-security - client-services-security - consulting - core-security - finance - infra - infrasec - l20n-security - marketing-private - mozilla-confidential - mozilla-employee-confidential - mozilla-foundation-confidential - mozilla-engagement - mozilla-messaging-confidential - partner-confidential - payments-confidential - tamarin-security - websites-security - webtools-security + addons-security + bugzilla-security + client-services-security + consulting + core-security + finance + infra + infrasec + l20n-security + marketing-private + mozilla-confidential + mozilla-employee-confidential + mozilla-foundation-confidential + mozilla-engagement + mozilla-messaging-confidential + partner-confidential + payments-confidential + tamarin-security + websites-security + webtools-security ); # Automatically CC users to bugs filed into configured groups and products our %group_auto_cc = ( - 'partner-confidential' => { - 'Marketing' => ['jbalaco@mozilla.com'], - '_default' => ['mbest@mozilla.com'], - }, + 'partner-confidential' => { + 'Marketing' => ['jbalaco@mozilla.com'], + '_default' => ['mbest@mozilla.com'], + }, ); # Force create-bug template by product # Users in 'include' group will be forced into using the form. our %create_bug_formats = ( - 'Data Compliance' => { - 'format' => 'data-compliance', - 'include' => 'everyone', - }, - 'developer.mozilla.org' => { - 'format' => 'mdn', - 'include' => 'everyone', - }, - 'Legal' => { - 'format' => 'legal', - 'include' => 'everyone', - }, - 'Recruiting' => { - 'format' => 'recruiting', - 'include' => 'everyone', - }, - 'Internet Public Policy' => { - 'format' => 'ipp', - 'include' => 'everyone', - }, + 'Data Compliance' => {'format' => 'data-compliance', 'include' => 'everyone',}, + 'developer.mozilla.org' => {'format' => 'mdn', 'include' => 'everyone',}, + 'Legal' => {'format' => 'legal', 'include' => 'everyone',}, + 'Recruiting' => {'format' => 'recruiting', 'include' => 'everyone',}, + 'Internet Public Policy' => {'format' => 'ipp', 'include' => 'everyone',}, ); # List of named queries which will be added to new users' footer our @default_named_queries = ( - { - name => 'Bugs Filed Today', - query => 'query_format=advanced&chfieldto=Now&chfield=[Bug creation]&chfieldfrom=-24h&order=bug_id', - }, + { + name => 'Bugs Filed Today', + query => + 'query_format=advanced&chfieldto=Now&chfield=[Bug creation]&chfieldfrom=-24h&order=bug_id', + }, ); 1; diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm index f84835ddd..5b5395619 100644 --- a/extensions/BMO/lib/FakeBug.pm +++ b/extensions/BMO/lib/FakeBug.pm @@ -12,32 +12,32 @@ use Bugzilla::Bug; our $AUTOLOAD; sub new { - my $class = shift; - my $self = shift; - bless $self, $class; - return $self; + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; } sub AUTOLOAD { - my $self = shift; - my $name = $AUTOLOAD; - $name =~ s/.*://; - return exists $self->{$name} ? $self->{$name} : undef; + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; } sub check_can_change_field { - my $self = shift; - return Bugzilla::Bug::check_can_change_field($self, @_) + my $self = shift; + return Bugzilla::Bug::check_can_change_field($self, @_); } sub _changes_everconfirmed { - my $self = shift; - return Bugzilla::Bug::_changes_everconfirmed($self, @_) + my $self = shift; + return Bugzilla::Bug::_changes_everconfirmed($self, @_); } sub everconfirmed { - my $self = shift; - return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1; + my $self = shift; + return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1; } 1; diff --git a/extensions/BMO/lib/Reports/Groups.pm b/extensions/BMO/lib/Reports/Groups.pm index ce7df767c..7b395aca9 100644 --- a/extensions/BMO/lib/Reports/Groups.pm +++ b/extensions/BMO/lib/Reports/Groups.pm @@ -19,25 +19,24 @@ use Bugzilla::Util qw(trim datetime_from); use JSON qw(encode_json); sub admins_report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - ($user->in_group('editbugs')) - || ThrowUserError('auth_failure', { group => 'editbugs', - action => 'run', - object => 'group_admins' }); + ($user->in_group('editbugs')) + || ThrowUserError('auth_failure', + {group => 'editbugs', action => 'run', object => 'group_admins'}); - my @grouplist = - ($user->in_group('editusers') || $user->in_group('infrasec')) - ? map { lc($_->name) } Bugzilla::Group->get_all - : _get_permitted_membership_groups(); + my @grouplist + = ($user->in_group('editusers') || $user->in_group('infrasec')) + ? map { lc($_->name) } Bugzilla::Group->get_all + : _get_permitted_membership_groups(); - my $groups = join(',', map { $dbh->quote($_) } @grouplist); + my $groups = join(',', map { $dbh->quote($_) } @grouplist); - my $query = " - SELECT groups.id, " . - $dbh->sql_group_concat('profiles.userid', "','", 1) . " + my $query = " + SELECT groups.id, " + . $dbh->sql_group_concat('profiles.userid', "','", 1) . " FROM groups LEFT JOIN user_group_map ON user_group_map.group_id = groups.id @@ -49,271 +48,275 @@ sub admins_report { AND groups.name IN ($groups) GROUP BY groups.name"; - my @groups; - foreach my $row (@{ $dbh->selectall_arrayref($query) }) { - my $group = Bugzilla::Group->new({ id => shift @$row, cache => 1}); - my @admins; - if (my $admin_ids = shift @$row) { - foreach my $uid (split(/,/, $admin_ids)) { - push(@admins, Bugzilla::User->new({ id => $uid, cache => 1 })); - } - } - push(@groups, { name => $group->name, - description => $group->description, - owner => $group->owner, - admins => \@admins }); + my @groups; + foreach my $row (@{$dbh->selectall_arrayref($query)}) { + my $group = Bugzilla::Group->new({id => shift @$row, cache => 1}); + my @admins; + if (my $admin_ids = shift @$row) { + foreach my $uid (split(/,/, $admin_ids)) { + push(@admins, Bugzilla::User->new({id => $uid, cache => 1})); + } } + push( + @groups, + { + name => $group->name, + description => $group->description, + owner => $group->owner, + admins => \@admins + } + ); + } - $vars->{'groups'} = \@groups; + $vars->{'groups'} = \@groups; } sub membership_report { - my ($page, $vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - - ($user->in_group('editusers') || $user->in_group('infrasec')) - || ThrowUserError('auth_failure', { group => 'editusers', - action => 'run', - object => 'group_admins' }); - - my $who = $cgi->param('who'); - if (!defined($who) || $who eq '') { - if ($page eq 'group_membership.txt') { - print $cgi->redirect("page.cgi?id=group_membership.html&output=txt"); - exit; - } - $vars->{'output'} = $cgi->param('output'); - return; + my ($page, $vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + + ($user->in_group('editusers') || $user->in_group('infrasec')) + || ThrowUserError('auth_failure', + {group => 'editusers', action => 'run', object => 'group_admins'}); + + my $who = $cgi->param('who'); + if (!defined($who) || $who eq '') { + if ($page eq 'group_membership.txt') { + print $cgi->redirect("page.cgi?id=group_membership.html&output=txt"); + exit; } + $vars->{'output'} = $cgi->param('output'); + return; + } - Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); - $who = Bugzilla->input_params->{'who'}; - $who = ref($who) ? $who : [ $who ]; + Bugzilla::User::match_field({'who' => {'type' => 'multi'}}); + $who = Bugzilla->input_params->{'who'}; + $who = ref($who) ? $who : [$who]; - my @users; - foreach my $login (@$who) { - my $u = Bugzilla::User->new(login_to_id($login, 1)); + my @users; + foreach my $login (@$who) { + my $u = Bugzilla::User->new(login_to_id($login, 1)); - # this is lifted from $user->groups() - # we need to show which groups are direct and which are inherited + # this is lifted from $user->groups() + # we need to show which groups are direct and which are inherited - my $groups_to_check = $dbh->selectcol_arrayref( - q{SELECT DISTINCT group_id + my $groups_to_check = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = 0}, undef, $u->id); + WHERE user_id = ? AND isbless = 0}, undef, $u->id + ); - my $rows = $dbh->selectall_arrayref( - "SELECT DISTINCT grantor_id, member_id + my $rows = $dbh->selectall_arrayref( + "SELECT DISTINCT grantor_id, member_id FROM group_group_map - WHERE grant_type = " . GROUP_MEMBERSHIP); + WHERE grant_type = " . GROUP_MEMBERSHIP + ); - my %group_membership; - foreach my $row (@$rows) { - my ($grantor_id, $member_id) = @$row; - push (@{ $group_membership{$member_id} }, $grantor_id); - } + my %group_membership; + foreach my $row (@$rows) { + my ($grantor_id, $member_id) = @$row; + push(@{$group_membership{$member_id}}, $grantor_id); + } - my %checked_groups; - my %direct_groups; - my %indirect_groups; - my %groups; + my %checked_groups; + my %direct_groups; + my %indirect_groups; + my %groups; - foreach my $member_id (@$groups_to_check) { - $direct_groups{$member_id} = 1; - } + foreach my $member_id (@$groups_to_check) { + $direct_groups{$member_id} = 1; + } - while (scalar(@$groups_to_check) > 0) { - my $member_id = shift @$groups_to_check; - if (!$checked_groups{$member_id}) { - $checked_groups{$member_id} = 1; - my $members = $group_membership{$member_id}; - my @new_to_check = grep(!$checked_groups{$_}, @$members); - push(@$groups_to_check, @new_to_check); - foreach my $id (@new_to_check) { - $indirect_groups{$id} = $member_id; - } - $groups{$member_id} = 1; - } + while (scalar(@$groups_to_check) > 0) { + my $member_id = shift @$groups_to_check; + if (!$checked_groups{$member_id}) { + $checked_groups{$member_id} = 1; + my $members = $group_membership{$member_id}; + my @new_to_check = grep(!$checked_groups{$_}, @$members); + push(@$groups_to_check, @new_to_check); + foreach my $id (@new_to_check) { + $indirect_groups{$id} = $member_id; } + $groups{$member_id} = 1; + } + } - my @groups; - my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]); - foreach my $group (@$ra_groups) { - my $via; - if ($direct_groups{$group->id}) { - $via = ''; - } else { - foreach my $g (@$ra_groups) { - if ($g->id == $indirect_groups{$group->id}) { - $via = $g->name; - last; - } - } - } - push @groups, { - name => $group->name, - desc => $group->description, - via => $via, - }; + my @groups; + my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]); + foreach my $group (@$ra_groups) { + my $via; + if ($direct_groups{$group->id}) { + $via = ''; + } + else { + foreach my $g (@$ra_groups) { + if ($g->id == $indirect_groups{$group->id}) { + $via = $g->name; + last; + } } - - push @users, { - user => $u, - groups => \@groups, - }; + } + push @groups, {name => $group->name, desc => $group->description, via => $via,}; } - $vars->{'who'} = $who; - $vars->{'users'} = \@users; + push @users, {user => $u, groups => \@groups,}; + } + + $vars->{'who'} = $who; + $vars->{'users'} = \@users; } sub members_report { - my ($page, $vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - - ($user->in_group('editbugs')) - || ThrowUserError('auth_failure', { group => 'editbugs', - action => 'run', - object => 'group_admins' }); - - my $privileged = $user->in_group('editusers') || $user->in_group('infrasec'); - $vars->{privileged} = $privileged; - - my @grouplist = $privileged - ? map { lc($_->name) } Bugzilla::Group->get_all - : _get_permitted_membership_groups(); - - my $include_disabled = $cgi->param('include_disabled') ? 1 : 0; - $vars->{'include_disabled'} = $include_disabled; - - # don't allow all groups, to avoid putting pain on the servers - my @group_names = - sort - grep { !/^(?:bz_.+|canconfirm|editbugs|editbugs-team|everyone)$/ } - @grouplist; - unshift(@group_names, ''); - $vars->{'groups'} = \@group_names; - - # load selected group - my $group = lc(trim($cgi->param('group') || '')); - $group = '' unless grep { $_ eq $group } @group_names; - return if $group eq ''; - my $group_obj = Bugzilla::Group->new({ name => $group }); - $vars->{'group'} = $group_obj; - - $vars->{'privileged'} = 1 if ($group_obj->owner && $group_obj->owner->id == $user->id); - - my @types; - my $members = $group_obj->members_complete(); - foreach my $name (sort keys %$members) { - push @types, { - name => ($name eq '_direct' ? 'direct' : $name), - members => _filter_userlist($members->{$name}), + my ($page, $vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + + ($user->in_group('editbugs')) + || ThrowUserError('auth_failure', + {group => 'editbugs', action => 'run', object => 'group_admins'}); + + my $privileged = $user->in_group('editusers') || $user->in_group('infrasec'); + $vars->{privileged} = $privileged; + + my @grouplist + = $privileged + ? map { lc($_->name) } Bugzilla::Group->get_all + : _get_permitted_membership_groups(); + + my $include_disabled = $cgi->param('include_disabled') ? 1 : 0; + $vars->{'include_disabled'} = $include_disabled; + + # don't allow all groups, to avoid putting pain on the servers + my @group_names + = sort grep { !/^(?:bz_.+|canconfirm|editbugs|editbugs-team|everyone)$/ } + @grouplist; + unshift(@group_names, ''); + $vars->{'groups'} = \@group_names; + + # load selected group + my $group = lc(trim($cgi->param('group') || '')); + $group = '' unless grep { $_ eq $group } @group_names; + return if $group eq ''; + my $group_obj = Bugzilla::Group->new({name => $group}); + $vars->{'group'} = $group_obj; + + $vars->{'privileged'} = 1 + if ($group_obj->owner && $group_obj->owner->id == $user->id); + + my @types; + my $members = $group_obj->members_complete(); + foreach my $name (sort keys %$members) { + push @types, + { + name => ($name eq '_direct' ? 'direct' : $name), + members => _filter_userlist($members->{$name}), + }; + } + + # make it easy for the template to detect an empty group + my $has_members = 0; + foreach my $type (@types) { + $has_members += scalar(@{$type->{members}}); + last if $has_members; + } + @types = () unless $has_members; + + if ($page eq 'group_members.json') { + my %users; + foreach my $rh (@types) { + foreach my $member (@{$rh->{members}}) { + my $login = $member->login; + if (exists $users{$login}) { + push @{$users{$login}->{groups}}, $rh->{name} if $privileged; } - } - - # make it easy for the template to detect an empty group - my $has_members = 0; - foreach my $type (@types) { - $has_members += scalar(@{ $type->{members} }); - last if $has_members; - } - @types = () unless $has_members; - - if ($page eq 'group_members.json') { - my %users; - foreach my $rh (@types) { - foreach my $member (@{ $rh->{members} }) { - my $login = $member->login; - if (exists $users{$login}) { - push @{ $users{$login}->{groups} }, $rh->{name} if $privileged; - } - else { - my $rh_user = { - login => $login, - membership => $rh->{name} eq 'direct' ? 'direct' : 'indirect', - rh_name => $rh->{name}, - }; - if ($privileged) { - $rh_user->{group} = $rh->{name}; - $rh_user->{groups} = [ $rh->{name} ]; - $rh_user->{lastseeon} = $member->last_seen_date; - $rh_user->{mfa} = $member->mfa; - $rh_user->{api_key_only} = $member->settings->{api_key_only}->{value} eq 'on' - ? JSON::true : JSON::false; - } - $users{$login} = $rh_user; - } - } + else { + my $rh_user = { + login => $login, + membership => $rh->{name} eq 'direct' ? 'direct' : 'indirect', + rh_name => $rh->{name}, + }; + if ($privileged) { + $rh_user->{group} = $rh->{name}; + $rh_user->{groups} = [$rh->{name}]; + $rh_user->{lastseeon} = $member->last_seen_date; + $rh_user->{mfa} = $member->mfa; + $rh_user->{api_key_only} + = $member->settings->{api_key_only}->{value} eq 'on' + ? JSON::true + : JSON::false; + } + $users{$login} = $rh_user; } - $vars->{types_json} = JSON->new->pretty->canonical->utf8->encode([ values %users ]); + } } - else { - my %users; - foreach my $rh (@types) { - foreach my $member (@{ $rh->{members} }) { - $users{$member->login} = 1 unless exists $users{$member->login}; - } - } - $vars->{types} = \@types; - $vars->{count} = scalar(keys %users); + $vars->{types_json} + = JSON->new->pretty->canonical->utf8->encode([values %users]); + } + else { + my %users; + foreach my $rh (@types) { + foreach my $member (@{$rh->{members}}) { + $users{$member->login} = 1 unless exists $users{$member->login}; + } } + $vars->{types} = \@types; + $vars->{count} = scalar(keys %users); + } } sub _filter_userlist { - my ($list, $include_disabled) = @_; - $list = [ grep { $_->is_enabled } @$list ] unless $include_disabled; - my $now = DateTime->now(); - my $never = DateTime->from_epoch( epoch => 0 ); - foreach my $user (@$list) { - my $last_seen = $user->last_seen_date ? datetime_from($user->last_seen_date) : $never; - $user->{last_seen_days} = sprintf( - '%.0f', - $now->subtract_datetime_absolute($last_seen)->delta_seconds / (28 * 60 * 60)); - } - return [ sort { lc($a->identity) cmp lc($b->identity) } @$list ]; + my ($list, $include_disabled) = @_; + $list = [grep { $_->is_enabled } @$list] unless $include_disabled; + my $now = DateTime->now(); + my $never = DateTime->from_epoch(epoch => 0); + foreach my $user (@$list) { + my $last_seen + = $user->last_seen_date ? datetime_from($user->last_seen_date) : $never; + $user->{last_seen_days} = sprintf('%.0f', + $now->subtract_datetime_absolute($last_seen)->delta_seconds / (28 * 60 * 60)); + } + return [sort { lc($a->identity) cmp lc($b->identity) } @$list]; } # Groups that any user with editbugs can see the membership or admin lists for. # Transparency FTW. sub _get_permitted_membership_groups { - my $user = Bugzilla->user; - - # Default publicly viewable groups - my %default_public_groups = map { $_ => 1 } qw( - bugzilla-approvers - bugzilla-reviewers - can_restrict_comments - community-it-team - mozilla-employee-confidential - mozilla-foundation-confidential - mozilla-reps - qa-approvers - ); - - # We add the group to the permitted list if: - # 1. it is a drivers group - this gives us a little - # future-proofing - # 2. it is a one of the default public groups - # 3. the user is the group's owner - # 4. or the user can bless others into the group - my @permitted_groups; - foreach my $group (Bugzilla::Group->get_all) { - my $name = $group->name; - if ($name =~ /-drivers$/ - || exists $default_public_groups{$name} - || ($group->owner && $group->owner->id == $user->id) - || $user->can_bless($group->id)) - { - push(@permitted_groups, $name); - } + my $user = Bugzilla->user; + + # Default publicly viewable groups + my %default_public_groups = map { $_ => 1 } qw( + bugzilla-approvers + bugzilla-reviewers + can_restrict_comments + community-it-team + mozilla-employee-confidential + mozilla-foundation-confidential + mozilla-reps + qa-approvers + ); + + # We add the group to the permitted list if: + # 1. it is a drivers group - this gives us a little + # future-proofing + # 2. it is a one of the default public groups + # 3. the user is the group's owner + # 4. or the user can bless others into the group + my @permitted_groups; + foreach my $group (Bugzilla::Group->get_all) { + my $name = $group->name; + if ( $name =~ /-drivers$/ + || exists $default_public_groups{$name} + || ($group->owner && $group->owner->id == $user->id) + || $user->can_bless($group->id)) + { + push(@permitted_groups, $name); } + } - return @permitted_groups; + return @permitted_groups; } 1; diff --git a/extensions/BMO/lib/Reports/Internship.pm b/extensions/BMO/lib/Reports/Internship.pm index 2dfa583a6..f9ad1a578 100644 --- a/extensions/BMO/lib/Reports/Internship.pm +++ b/extensions/BMO/lib/Reports/Internship.pm @@ -17,33 +17,30 @@ use Bugzilla::Product; use Bugzilla::Component; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; - - $user->in_group('hr') - || ThrowUserError('auth_failure', { group => 'hr', - action => 'run', - object => 'internship_dashboard' }); - - my $product = Bugzilla::Product->check({ name => 'Recruiting', cache => 1 }); - my $component = Bugzilla::Component->new({ product => $product, name => 'Intern', cache => 1 }); - - # find all open internship bugs - my $bugs = Bugzilla::Bug->match({ - product_id => $product->id, - component_id => $component->id, - resolution => '', - }); - - # filter bugs based on visibility and re-bless - $user->visible_bugs($bugs); - $bugs = [ - map { bless($_, 'InternshipBug') } - grep { $user->can_see_bug($_->id) } - @$bugs - ]; - - $vars->{bugs} = $bugs; + my ($vars) = @_; + my $user = Bugzilla->user; + + $user->in_group('hr') + || ThrowUserError('auth_failure', + {group => 'hr', action => 'run', object => 'internship_dashboard'}); + + my $product = Bugzilla::Product->check({name => 'Recruiting', cache => 1}); + my $component = Bugzilla::Component->new( + {product => $product, name => 'Intern', cache => 1}); + + # find all open internship bugs + my $bugs = Bugzilla::Bug->match({ + product_id => $product->id, + component_id => $component->id, + resolution => '', + }); + + # filter bugs based on visibility and re-bless + $user->visible_bugs($bugs); + $bugs = [map { bless($_, 'InternshipBug') } + grep { $user->can_see_bug($_->id) } @$bugs]; + + $vars->{bugs} = $bugs; } 1; @@ -58,64 +55,62 @@ use Bugzilla::Comment; use Bugzilla::Util qw(trim); sub _extract { - my ($self) = @_; - return if exists $self->{internship_data}; - $self->{internship_data} = {}; - - # we only need the first comment - my $comment = Bugzilla::Comment->match({ - bug_id => $self->id, - LIMIT => 1, - })->[0]->body; - - # extract just what we need - # changing the comment will break this - - if ($comment =~ /Hiring Manager:\s+(.+)\nTeam:\n/s) { - $self->{internship_data}->{hiring_manager} = trim($1); - } - if ($comment =~ /\nVP Authority:\s+(.+)\nProduct Line:\n/s) { - $self->{internship_data}->{scvp} = trim($1); - } - if ($comment =~ /\nProduct Line:\s+(.+)\nLevel 1/s) { - $self->{internship_data}->{product_line} = trim($1); - } - if ($comment =~ /\nBusiness Need:\s+(.+)\nPotential Project:\n/s) { - $self->{internship_data}->{business_need} = trim($1); - } - if ($comment =~ /\nName:\s+(.+)$/s) { - $self->{internship_data}->{intern_name} = trim($1); - } + my ($self) = @_; + return if exists $self->{internship_data}; + $self->{internship_data} = {}; + + # we only need the first comment + my $comment + = Bugzilla::Comment->match({bug_id => $self->id, LIMIT => 1,})->[0]->body; + + # extract just what we need + # changing the comment will break this + + if ($comment =~ /Hiring Manager:\s+(.+)\nTeam:\n/s) { + $self->{internship_data}->{hiring_manager} = trim($1); + } + if ($comment =~ /\nVP Authority:\s+(.+)\nProduct Line:\n/s) { + $self->{internship_data}->{scvp} = trim($1); + } + if ($comment =~ /\nProduct Line:\s+(.+)\nLevel 1/s) { + $self->{internship_data}->{product_line} = trim($1); + } + if ($comment =~ /\nBusiness Need:\s+(.+)\nPotential Project:\n/s) { + $self->{internship_data}->{business_need} = trim($1); + } + if ($comment =~ /\nName:\s+(.+)$/s) { + $self->{internship_data}->{intern_name} = trim($1); + } } sub hiring_manager { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{hiring_manager}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{hiring_manager}; } sub scvp { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{scvp}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{scvp}; } sub business_need { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{business_need}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{business_need}; } sub product_line { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{product_line}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{product_line}; } sub intern_name { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{intern_name}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{intern_name}; } 1; diff --git a/extensions/BMO/lib/Reports/ProductSecurity.pm b/extensions/BMO/lib/Reports/ProductSecurity.pm index e7ccda171..fb773cd93 100644 --- a/extensions/BMO/lib/Reports/ProductSecurity.pm +++ b/extensions/BMO/lib/Reports/ProductSecurity.pm @@ -16,54 +16,54 @@ use Bugzilla::Error; use Bugzilla::Product; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; + my ($vars) = @_; + my $user = Bugzilla->user; - ($user->in_group('admin') || $user->in_group('infrasec')) - || ThrowUserError('auth_failure', { group => 'admin', - action => 'run', - object => 'product_security' }); + ($user->in_group('admin') || $user->in_group('infrasec')) + || ThrowUserError('auth_failure', + {group => 'admin', action => 'run', object => 'product_security'}); - my $moco = Bugzilla::Group->new({ name => 'mozilla-employee-confidential' }) - or return; + my $moco = Bugzilla::Group->new({name => 'mozilla-employee-confidential'}) + or return; - my $products = []; - foreach my $product (@{ Bugzilla::Product->match({}) }) { - my $default_group = $product->default_security_group_obj; - my $group_controls = $product->group_controls(); + my $products = []; + foreach my $product (@{Bugzilla::Product->match({})}) { + my $default_group = $product->default_security_group_obj; + my $group_controls = $product->group_controls(); - my $item = { - name => $product->name, - default_security_group => $product->default_security_group, - group_visibility => 'None/None', - moco => exists $group_controls->{$moco->id}, - }; + my $item = { + name => $product->name, + default_security_group => $product->default_security_group, + group_visibility => 'None/None', + moco => exists $group_controls->{$moco->id}, + }; - if ($default_group) { - if (my $control = $group_controls->{$default_group->id}) { - $item->{group_visibility} = control_to_string($control->{membercontrol}) . - '/' . control_to_string($control->{othercontrol}); - } - } + if ($default_group) { + if (my $control = $group_controls->{$default_group->id}) { + $item->{group_visibility} = control_to_string($control->{membercontrol}) . '/' + . control_to_string($control->{othercontrol}); + } + } - $item->{group_problem} = $default_group ? '' : "Invalid group " . $product->default_security_group; - $item->{visibility_problem} = 'Default security group should be Shown/Shown' - if ($item->{group_visibility} ne 'Shown/Shown') - && ($item->{group_visibility} ne 'Mandatory/Mandatory') - && ($item->{group_visibility} ne 'Default/Default'); + $item->{group_problem} + = $default_group ? '' : "Invalid group " . $product->default_security_group; + $item->{visibility_problem} = 'Default security group should be Shown/Shown' + if ($item->{group_visibility} ne 'Shown/Shown') + && ($item->{group_visibility} ne 'Mandatory/Mandatory') + && ($item->{group_visibility} ne 'Default/Default'); - push @$products, $item; - } - $vars->{products} = $products; + push @$products, $item; + } + $vars->{products} = $products; } sub control_to_string { - my ($control) = @_; - return 'NA' if $control == CONTROLMAPNA; - return 'Shown' if $control == CONTROLMAPSHOWN; - return 'Default' if $control == CONTROLMAPDEFAULT; - return 'Mandatory' if $control == CONTROLMAPMANDATORY; - return ''; + my ($control) = @_; + return 'NA' if $control == CONTROLMAPNA; + return 'Shown' if $control == CONTROLMAPSHOWN; + return 'Default' if $control == CONTROLMAPDEFAULT; + return 'Mandatory' if $control == CONTROLMAPMANDATORY; + return ''; } 1; diff --git a/extensions/BMO/lib/Reports/Recruiting.pm b/extensions/BMO/lib/Reports/Recruiting.pm index 39eb8327d..c35b0cbff 100644 --- a/extensions/BMO/lib/Reports/Recruiting.pm +++ b/extensions/BMO/lib/Reports/Recruiting.pm @@ -17,33 +17,30 @@ use Bugzilla::Product; use Bugzilla::Component; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; - - $user->in_group('hr') - || ThrowUserError('auth_failure', { group => 'hr', - action => 'run', - object => 'recruiting_dashboard' }); - - my $product = Bugzilla::Product->check({ name => 'Recruiting', cache => 1 }); - my $component = Bugzilla::Component->new({ product => $product, name => 'General', cache => 1 }); - - # find all open recruiting bugs - my $bugs = Bugzilla::Bug->match({ - product_id => $product->id, - component_id => $component->id, - resolution => '', - }); - - # filter bugs based on visibility and re-bless - $user->visible_bugs($bugs); - $bugs = [ - map { bless($_, 'RecruitingBug') } - grep { $user->can_see_bug($_->id) } - @$bugs - ]; - - $vars->{bugs} = $bugs; + my ($vars) = @_; + my $user = Bugzilla->user; + + $user->in_group('hr') + || ThrowUserError('auth_failure', + {group => 'hr', action => 'run', object => 'recruiting_dashboard'}); + + my $product = Bugzilla::Product->check({name => 'Recruiting', cache => 1}); + my $component = Bugzilla::Component->new( + {product => $product, name => 'General', cache => 1}); + + # find all open recruiting bugs + my $bugs = Bugzilla::Bug->match({ + product_id => $product->id, + component_id => $component->id, + resolution => '', + }); + + # filter bugs based on visibility and re-bless + $user->visible_bugs($bugs); + $bugs = [map { bless($_, 'RecruitingBug') } + grep { $user->can_see_bug($_->id) } @$bugs]; + + $vars->{bugs} = $bugs; } 1; @@ -58,55 +55,56 @@ use Bugzilla::Comment; use Bugzilla::Util qw(trim); sub _extract { - my ($self) = @_; - return if exists $self->{recruitment_data}; - $self->{recruitment_data} = {}; - - # we only need the first comment - my $comment = Bugzilla::Comment->match({ - bug_id => $self->id, - LIMIT => 1, - })->[0]->body; - - # extract just what we need - # changing the comment will break this - - if ($comment =~ /\nHiring Manager:\s+(.+)VP Authority:\n/s) { - $self->{recruitment_data}->{hiring_manager} = trim($1); - } - if ($comment =~ /\nVP Authority:\s+(.+)HRBP:\n/s) { - $self->{recruitment_data}->{scvp} = trim($1); - } - if ($comment =~ /\nWhat part of your strategic plan does this role impact\?\s+(.+)Why is this critical for success\?\n/s) { - $self->{recruitment_data}->{strategic_plan} = trim($1); - } - if ($comment =~ /\nWhy is this critical for success\?\s+(.+)$/s) { - $self->{recruitment_data}->{why_critical} = trim($1); - } + my ($self) = @_; + return if exists $self->{recruitment_data}; + $self->{recruitment_data} = {}; + + # we only need the first comment + my $comment + = Bugzilla::Comment->match({bug_id => $self->id, LIMIT => 1,})->[0]->body; + + # extract just what we need + # changing the comment will break this + + if ($comment =~ /\nHiring Manager:\s+(.+)VP Authority:\n/s) { + $self->{recruitment_data}->{hiring_manager} = trim($1); + } + if ($comment =~ /\nVP Authority:\s+(.+)HRBP:\n/s) { + $self->{recruitment_data}->{scvp} = trim($1); + } + if ($comment + =~ /\nWhat part of your strategic plan does this role impact\?\s+(.+)Why is this critical for success\?\n/s + ) + { + $self->{recruitment_data}->{strategic_plan} = trim($1); + } + if ($comment =~ /\nWhy is this critical for success\?\s+(.+)$/s) { + $self->{recruitment_data}->{why_critical} = trim($1); + } } sub hiring_manager { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{hiring_manager}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{hiring_manager}; } sub scvp { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{scvp}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{scvp}; } sub strategic_plan { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{strategic_plan}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{strategic_plan}; } sub why_critical { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{why_critical}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{why_critical}; } 1; diff --git a/extensions/BMO/lib/Reports/ReleaseTracking.pm b/extensions/BMO/lib/Reports/ReleaseTracking.pm index 9fba1e14b..38a07aee7 100644 --- a/extensions/BMO/lib/Reports/ReleaseTracking.pm +++ b/extensions/BMO/lib/Reports/ReleaseTracking.pm @@ -21,496 +21,381 @@ use JSON qw(-convert_blessed_universally); use List::MoreUtils qw(uniq); use constant DATE_RANGES => [ - { - value => '20160126-20160307', - label => '2016-01-26 and 2016-03-07' - }, - { - value => '20151215-20160125', - label => '2015-12-15 and 2016-01-25' - }, - { - value => '20151103-20151214', - label => '2015-11-03 and 2015-12-14' - }, - { - value => '20150922-20151102', - label => '2015-09-22 and 2015-11-02' - }, - { - value => '20150811-20150921', - label => '2015-08-11 and 2015-09-21' - }, - { - value => '20150630-20150810', - label => '2015-06-30 and 2015-08-10' - }, - { - value => '20150512-20150629', - label => '2015-05-12 and 2015-06-29' - }, - { - value => '20150331-20150511', - label => '2015-03-31 and 2015-05-11' - }, - { - value => '20150224-20150330', - label => '2015-02-24 and 2015-03-30' - }, - { - value => '20150113-20150223', - label => '2015-01-13 and 2015-02-23' - }, - { - value => '20141111-20141222', - label => '2014-11-11 and 2014-12-22' - }, - { - value => '20140930-20141110', - label => '2014-09-30 and 2014-11-10' - }, - { - value => '20140819-20140929', - label => '2014-08-19 and 2014-09-29' - }, - { - value => '20140708-20140818', - label => '2014-07-08 and 2014-08-18' - }, - { - value => '20140527-20140707', - label => '2014-05-27 and 2014-07-07' - }, - { - value => '20140415-20140526', - label => '2014-04-15 and 2014-05-26' - }, - { - value => '20140304-20140414', - label => '2014-03-04 and 2014-04-14' - }, - { - value => '20140121-20140303', - label => '2014-01-21 and 2014-03-03' - }, - { - value => '20131210-20140120', - label => '2013-12-10 and 2014-01-20' - }, - { - value => '20131029-20131209', - label => '2013-10-29 and 2013-12-09' - }, - { - value => '20130917-20131028', - label => '2013-09-17 and 2013-10-28' - }, - { - value => '20130806-20130916', - label => '2013-08-06 and 2013-09-16' - }, - { - value => '20130625-20130805', - label => '2013-06-25 and 2013-08-05' - }, - { - value => '20130514-20130624', - label => '2013-05-14 and 2013-06-24' - }, - { - value => '20130402-20130513', - label => '2013-04-02 and 2013-05-13' - }, - { - value => '20130219-20130401', - label => '2013-02-19 and 2013-04-01' - }, - { - value => '20130108-20130218', - label => '2013-01-08 and 2013-02-18' - }, - { - value => '20121120-20130107', - label => '2012-11-20 and 2013-01-07' - }, - { - value => '20121009-20121119', - label => '2012-10-09 and 2012-11-19' - }, - { - value => '20120828-20121008', - label => '2012-08-28 and 2012-10-08' - }, - { - value => '20120717-20120827', - label => '2012-07-17 and 2012-08-27' - }, - { - value => '20120605-20120716', - label => '2012-06-05 and 2012-07-16' - }, - { - value => '20120424-20120604', - label => '2012-04-24 and 2012-06-04' - }, - { - value => '20120313-20120423', - label => '2012-03-13 and 2012-04-23' - }, - { - value => '20120131-20120312', - label => '2012-01-31 and 2012-03-12' - }, - { - value => '20111220-20120130', - label => '2011-12-20 and 2012-01-30' - }, - { - value => '20111108-20111219', - label => '2011-11-08 and 2011-12-19' - }, - { - value => '20110927-20111107', - label => '2011-09-27 and 2011-11-07' - }, - { - value => '20110816-20110926', - label => '2011-08-16 and 2011-09-26' - }, - { - value => '*', - label => 'Anytime' - } + {value => '20160126-20160307', label => '2016-01-26 and 2016-03-07'}, + {value => '20151215-20160125', label => '2015-12-15 and 2016-01-25'}, + {value => '20151103-20151214', label => '2015-11-03 and 2015-12-14'}, + {value => '20150922-20151102', label => '2015-09-22 and 2015-11-02'}, + {value => '20150811-20150921', label => '2015-08-11 and 2015-09-21'}, + {value => '20150630-20150810', label => '2015-06-30 and 2015-08-10'}, + {value => '20150512-20150629', label => '2015-05-12 and 2015-06-29'}, + {value => '20150331-20150511', label => '2015-03-31 and 2015-05-11'}, + {value => '20150224-20150330', label => '2015-02-24 and 2015-03-30'}, + {value => '20150113-20150223', label => '2015-01-13 and 2015-02-23'}, + {value => '20141111-20141222', label => '2014-11-11 and 2014-12-22'}, + {value => '20140930-20141110', label => '2014-09-30 and 2014-11-10'}, + {value => '20140819-20140929', label => '2014-08-19 and 2014-09-29'}, + {value => '20140708-20140818', label => '2014-07-08 and 2014-08-18'}, + {value => '20140527-20140707', label => '2014-05-27 and 2014-07-07'}, + {value => '20140415-20140526', label => '2014-04-15 and 2014-05-26'}, + {value => '20140304-20140414', label => '2014-03-04 and 2014-04-14'}, + {value => '20140121-20140303', label => '2014-01-21 and 2014-03-03'}, + {value => '20131210-20140120', label => '2013-12-10 and 2014-01-20'}, + {value => '20131029-20131209', label => '2013-10-29 and 2013-12-09'}, + {value => '20130917-20131028', label => '2013-09-17 and 2013-10-28'}, + {value => '20130806-20130916', label => '2013-08-06 and 2013-09-16'}, + {value => '20130625-20130805', label => '2013-06-25 and 2013-08-05'}, + {value => '20130514-20130624', label => '2013-05-14 and 2013-06-24'}, + {value => '20130402-20130513', label => '2013-04-02 and 2013-05-13'}, + {value => '20130219-20130401', label => '2013-02-19 and 2013-04-01'}, + {value => '20130108-20130218', label => '2013-01-08 and 2013-02-18'}, + {value => '20121120-20130107', label => '2012-11-20 and 2013-01-07'}, + {value => '20121009-20121119', label => '2012-10-09 and 2012-11-19'}, + {value => '20120828-20121008', label => '2012-08-28 and 2012-10-08'}, + {value => '20120717-20120827', label => '2012-07-17 and 2012-08-27'}, + {value => '20120605-20120716', label => '2012-06-05 and 2012-07-16'}, + {value => '20120424-20120604', label => '2012-04-24 and 2012-06-04'}, + {value => '20120313-20120423', label => '2012-03-13 and 2012-04-23'}, + {value => '20120131-20120312', label => '2012-01-31 and 2012-03-12'}, + {value => '20111220-20120130', label => '2011-12-20 and 2012-01-30'}, + {value => '20111108-20111219', label => '2011-11-08 and 2011-12-19'}, + {value => '20110927-20111107', label => '2011-09-27 and 2011-11-07'}, + {value => '20110816-20110926', label => '2011-08-16 and 2011-09-26'}, + {value => '*', label => 'Anytime'} ]; sub report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; - - my @flag_names = qw( - approval-mozilla-release - approval-mozilla-beta - approval-mozilla-aurora - approval-mozilla-central - approval-comm-release - approval-comm-beta - approval-comm-aurora - approval-calendar-release - approval-calendar-beta - approval-calendar-aurora - approval-mozilla-esr10 - ); - - my @flags_json; - my @fields_json; - my @products_json; - - # - # tracking flags - # - - my $all_products = $user->get_selectable_products; - my @usable_products; - - # build list of flags and their matching products - - my @invalid_flag_names; - foreach my $flag_name (@flag_names) { - # grab all matching flag_types - my @flag_types = @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })}; - - # remove invalid flags - if (!@flag_types) { - push @invalid_flag_names, $flag_name; - next; - } + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + my @flag_names = qw( + approval-mozilla-release + approval-mozilla-beta + approval-mozilla-aurora + approval-mozilla-central + approval-comm-release + approval-comm-beta + approval-comm-aurora + approval-calendar-release + approval-calendar-beta + approval-calendar-aurora + approval-mozilla-esr10 + ); + + my @flags_json; + my @fields_json; + my @products_json; + + # + # tracking flags + # + + my $all_products = $user->get_selectable_products; + my @usable_products; + + # build list of flags and their matching products + + my @invalid_flag_names; + foreach my $flag_name (@flag_names) { + + # grab all matching flag_types + my @flag_types + = @{Bugzilla::FlagType::match({name => $flag_name, is_active => 1})}; + + # remove invalid flags + if (!@flag_types) { + push @invalid_flag_names, $flag_name; + next; + } - # we need a list of products, based on inclusions/exclusions - my @products; - my %flag_types; - foreach my $flag_type (@flag_types) { - $flag_types{$flag_type->name} = $flag_type->id; - my $has_all = 0; - my @exclusion_ids; - my @inclusion_ids; - foreach my $flag_type (@flag_types) { - if (scalar keys %{$flag_type->inclusions}) { - my $inclusions = $flag_type->inclusions; - foreach my $key (keys %$inclusions) { - push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/); - } - } elsif (scalar keys %{$flag_type->exclusions}) { - my $exclusions = $flag_type->exclusions; - foreach my $key (keys %$exclusions) { - push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/); - } - } else { - $has_all = 1; - last; - } - } - - if ($has_all) { - push @products, @$all_products; - } elsif (scalar @exclusion_ids) { - push @products, @$all_products; - foreach my $exclude_id (uniq @exclusion_ids) { - @products = grep { $_->id != $exclude_id } @products; - } - } else { - foreach my $include_id (uniq @inclusion_ids) { - push @products, grep { $_->id == $include_id } @$all_products; - } - } + # we need a list of products, based on inclusions/exclusions + my @products; + my %flag_types; + foreach my $flag_type (@flag_types) { + $flag_types{$flag_type->name} = $flag_type->id; + my $has_all = 0; + my @exclusion_ids; + my @inclusion_ids; + foreach my $flag_type (@flag_types) { + if (scalar keys %{$flag_type->inclusions}) { + my $inclusions = $flag_type->inclusions; + foreach my $key (keys %$inclusions) { + push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/); + } } - @products = uniq @products; - push @usable_products, @products; - my @product_ids = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products; - - push @flags_json, { - name => $flag_name, - id => $flag_types{$flag_name} || 0, - products => \@product_ids, - fields => [], - }; - } - foreach my $flag_name (@invalid_flag_names) { - @flag_names = grep { $_ ne $flag_name } @flag_names; - } - @usable_products = uniq @usable_products; - - # build a list of tracking flags for each product - # also build the list of all fields - - my @unlink_products; - foreach my $product (@usable_products) { - my @fields = - sort { $a->sortkey <=> $b->sortkey } - grep { is_active_status_field($_) } - Bugzilla->active_custom_fields({ product => $product }); - my @field_ids = map { $_->id } @fields; - if (!scalar @fields) { - push @unlink_products, $product; - next; + elsif (scalar keys %{$flag_type->exclusions}) { + my $exclusions = $flag_type->exclusions; + foreach my $key (keys %$exclusions) { + push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/); + } } - - # product - push @products_json, { - name => $product->name, - id => $product->id, - fields => \@field_ids, - }; - - # add fields to flags - foreach my $rh (@flags_json) { - if (grep { $_ eq $product->id } @{$rh->{products}}) { - push @{$rh->{fields}}, @field_ids; - } + else { + $has_all = 1; + last; } - - # add fields to fields_json - foreach my $field (@fields) { - my $existing = 0; - foreach my $rh (@fields_json) { - if ($rh->{id} == $field->id) { - $existing = 1; - last; - } - } - if (!$existing) { - push @fields_json, { - name => $field->name, - desc => $field->description, - id => $field->id, - }; - } + } + + if ($has_all) { + push @products, @$all_products; + } + elsif (scalar @exclusion_ids) { + push @products, @$all_products; + foreach my $exclude_id (uniq @exclusion_ids) { + @products = grep { $_->id != $exclude_id } @products; } + } + else { + foreach my $include_id (uniq @inclusion_ids) { + push @products, grep { $_->id == $include_id } @$all_products; + } + } } - foreach my $rh (@flags_json) { - my @fields = uniq @{$rh->{fields}}; - $rh->{fields} = \@fields; + @products = uniq @products; + push @usable_products, @products; + my @product_ids + = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products; + + push @flags_json, + { + name => $flag_name, + id => $flag_types{$flag_name} || 0, + products => \@product_ids, + fields => [], + }; + } + foreach my $flag_name (@invalid_flag_names) { + @flag_names = grep { $_ ne $flag_name } @flag_names; + } + @usable_products = uniq @usable_products; + + # build a list of tracking flags for each product + # also build the list of all fields + + my @unlink_products; + foreach my $product (@usable_products) { + my @fields + = sort { $a->sortkey <=> $b->sortkey } + grep { is_active_status_field($_) } + Bugzilla->active_custom_fields({product => $product}); + my @field_ids = map { $_->id } @fields; + if (!scalar @fields) { + push @unlink_products, $product; + next; } - # remove products which aren't linked with status fields + # product + push @products_json, + {name => $product->name, id => $product->id, fields => \@field_ids,}; + # add fields to flags foreach my $rh (@flags_json) { - my @product_ids; - foreach my $id (@{$rh->{products}}) { - unless (grep { $_->id == $id } @unlink_products) { - push @product_ids, $id; - } - $rh->{products} = \@product_ids; + if (grep { $_ eq $product->id } @{$rh->{products}}) { + push @{$rh->{fields}}, @field_ids; + } + } + + # add fields to fields_json + foreach my $field (@fields) { + my $existing = 0; + foreach my $rh (@fields_json) { + if ($rh->{id} == $field->id) { + $existing = 1; + last; } + } + if (!$existing) { + push @fields_json, + {name => $field->name, desc => $field->description, id => $field->id,}; + } } + } + foreach my $rh (@flags_json) { + my @fields = uniq @{$rh->{fields}}; + $rh->{fields} = \@fields; + } + + # remove products which aren't linked with status fields + + foreach my $rh (@flags_json) { + my @product_ids; + foreach my $id (@{$rh->{products}}) { + unless (grep { $_->id == $id } @unlink_products) { + push @product_ids, $id; + } + $rh->{products} = \@product_ids; + } + } - # - # run report - # + # + # run report + # - if ($input->{q} && !$input->{edit}) { - my $q = _parse_query($input->{q}); + if ($input->{q} && !$input->{edit}) { + my $q = _parse_query($input->{q}); - my @where; - my @params; - my $query = " + my @where; + my @params; + my $query = " SELECT DISTINCT b.bug_id FROM bugs b INNER JOIN flags f ON f.bug_id = b.bug_id\n"; - if ($q->{start_date}) { - $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id\n"; - } + if ($q->{start_date}) { + $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id\n"; + } - $query .= "WHERE "; + $query .= "WHERE "; - if ($q->{start_date}) { - push @where, "(a.fieldid = ?)"; - push @params, $q->{field_id}; + if ($q->{start_date}) { + push @where, "(a.fieldid = ?)"; + push @params, $q->{field_id}; - push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') >= ?)"; - push @params, $q->{start_date} . ' 00:00:00'; - push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') <= ?)"; - push @params, $q->{end_date} . ' 23:59:59'; + push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') >= ?)"; + push @params, $q->{start_date} . ' 00:00:00'; + push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') <= ?)"; + push @params, $q->{end_date} . ' 23:59:59'; - push @where, "(a.added LIKE ?)"; - push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%'; - } + push @where, "(a.added LIKE ?)"; + push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%'; + } - my ($type_id) = $dbh->selectrow_array( - "SELECT id FROM flagtypes WHERE name = ?", - undef, - $q->{flag_name} - ); - push @where, "(f.type_id = ?)"; - push @params, $type_id; + my ($type_id) = $dbh->selectrow_array("SELECT id FROM flagtypes WHERE name = ?", + undef, $q->{flag_name}); + push @where, "(f.type_id = ?)"; + push @params, $type_id; - push @where, "(f.status = ?)"; - push @params, $q->{flag_status}; + push @where, "(f.status = ?)"; + push @params, $q->{flag_status}; - if ($q->{product_id}) { - push @where, "(b.product_id = ?)"; - push @params, $q->{product_id}; - } + if ($q->{product_id}) { + push @where, "(b.product_id = ?)"; + push @params, $q->{product_id}; + } - if (scalar @{$q->{fields}}) { - my @fields; - foreach my $field (@{$q->{fields}}) { - my $field_sql = "("; - if ($field->{type} == FIELD_TYPE_EXTENSION) { - $field_sql .= " + if (scalar @{$q->{fields}}) { + my @fields; + foreach my $field (@{$q->{fields}}) { + my $field_sql = "("; + if ($field->{type} == FIELD_TYPE_EXTENSION) { + $field_sql .= " COALESCE( (SELECT tracking_flags_bugs.value FROM tracking_flags_bugs LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id WHERE tracking_flags_bugs.bug_id = b.bug_id - AND tracking_flags.name = " . $dbh->quote($field->{name}) . ") + AND tracking_flags.name = " + . $dbh->quote($field->{name}) . ") , '') "; - } - else { - $field_sql .= "b." . $field->{name}; - } - $field_sql .= " " . ($field->{value} eq '+' ? '' : 'NOT ') . "IN ('fixed','verified'))"; - push(@fields, $field_sql); - } - my $join = uc $q->{join}; - push @where, '(' . join(" $join ", @fields) . ')'; } - - $query .= join("\nAND ", @where); - - my $bugs = $dbh->selectcol_arrayref($query, undef, @params); - push @$bugs, 0 unless @$bugs; - - my $urlbase = Bugzilla->localconfig->{urlbase}; - my $cgi = Bugzilla->cgi; - print $cgi->redirect( - -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs) - ); - exit; + else { + $field_sql .= "b." . $field->{name}; + } + $field_sql + .= " " . ($field->{value} eq '+' ? '' : 'NOT ') . "IN ('fixed','verified'))"; + push(@fields, $field_sql); + } + my $join = uc $q->{join}; + push @where, '(' . join(" $join ", @fields) . ')'; } - # - # set template vars - # - - my $json = JSON->new()->shrink(1); - $vars->{flags_json} = $json->encode(\@flags_json); - $vars->{products_json} = $json->encode(\@products_json); - $vars->{fields_json} = $json->encode(\@fields_json); - $vars->{flag_names} = \@flag_names; - $vars->{ranges} = DATE_RANGES; - $vars->{default_query} = $input->{q}; - $vars->{is_custom} = $input->{is_custom}; - foreach my $field (qw(product flags range)) { - $vars->{$field} = $input->{$field}; - } + $query .= join("\nAND ", @where); + + my $bugs = $dbh->selectcol_arrayref($query, undef, @params); + push @$bugs, 0 unless @$bugs; + + my $urlbase = Bugzilla->localconfig->{urlbase}; + my $cgi = Bugzilla->cgi; + print $cgi->redirect( + -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs)); + exit; + } + + # + # set template vars + # + + my $json = JSON->new()->shrink(1); + $vars->{flags_json} = $json->encode(\@flags_json); + $vars->{products_json} = $json->encode(\@products_json); + $vars->{fields_json} = $json->encode(\@fields_json); + $vars->{flag_names} = \@flag_names; + $vars->{ranges} = DATE_RANGES; + $vars->{default_query} = $input->{q}; + $vars->{is_custom} = $input->{is_custom}; + foreach my $field (qw(product flags range)) { + $vars->{$field} = $input->{$field}; + } } sub _parse_query { - my $q = shift; - my @query = split(/:/, $q); - my $query; - - # field_id for flag changes - $query->{field_id} = get_field_id('flagtypes.name'); - - # flag_name - my $flag_name = shift @query; - @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })} - or ThrowUserError('report_invalid_parameter', { name => 'flag_name' }); - trick_taint($flag_name); - $query->{flag_name} = $flag_name; - - # flag_status - my $flag_status = shift @query; - $flag_status =~ /^([\?\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'flag_status' }); - $query->{flag_status} = $1; - - # date_range -> from_ymd to_ymd - my $date_range = shift @query; - if ($date_range ne '*') { - $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/ - or ThrowUserError('report_invalid_parameter', { name => 'date_range' }); - $query->{start_date} = "$1-$2-$3"; - $query->{end_date} = "$4-$5-$6"; - validate_date($query->{start_date}) - || ThrowUserError('illegal_date', { date => $query->{start_date}, - format => 'YYYY-MM-DD' }); - validate_date($query->{end_date}) - || ThrowUserError('illegal_date', { date => $query->{end_date}, - format => 'YYYY-MM-DD' }); - } - - # product_id - my $product_id = shift @query; - $product_id =~ /^(\d+)$/ - or ThrowUserError('report_invalid_parameter', { name => 'product_id' }); - $query->{product_id} = $1; - - # join - my $join = shift @query; - $join =~ /^(and|or)$/ - or ThrowUserError('report_invalid_parameter', { name => 'join' }); - $query->{join} = $1; - - # fields - my @fields; - foreach my $field (@query) { - $field =~ /^(\d+)([\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'fields' }); - my ($id, $value) = ($1, $2); - my $field_obj = Bugzilla::Field->new($id) - or ThrowUserError('report_invalid_parameter', { name => 'field_id' }); - push @fields, { id => $id, value => $value, - name => $field_obj->name, type => $field_obj->type }; - } - $query->{fields} = \@fields; - - return $query; + my $q = shift; + my @query = split(/:/, $q); + my $query; + + # field_id for flag changes + $query->{field_id} = get_field_id('flagtypes.name'); + + # flag_name + my $flag_name = shift @query; + @{Bugzilla::FlagType::match({name => $flag_name, is_active => 1})} + or ThrowUserError('report_invalid_parameter', {name => 'flag_name'}); + trick_taint($flag_name); + $query->{flag_name} = $flag_name; + + # flag_status + my $flag_status = shift @query; + $flag_status =~ /^([\?\-\+])$/ + or ThrowUserError('report_invalid_parameter', {name => 'flag_status'}); + $query->{flag_status} = $1; + + # date_range -> from_ymd to_ymd + my $date_range = shift @query; + if ($date_range ne '*') { + $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/ + or ThrowUserError('report_invalid_parameter', {name => 'date_range'}); + $query->{start_date} = "$1-$2-$3"; + $query->{end_date} = "$4-$5-$6"; + validate_date($query->{start_date}) + || ThrowUserError('illegal_date', + {date => $query->{start_date}, format => 'YYYY-MM-DD'}); + validate_date($query->{end_date}) + || ThrowUserError('illegal_date', + {date => $query->{end_date}, format => 'YYYY-MM-DD'}); + } + + # product_id + my $product_id = shift @query; + $product_id =~ /^(\d+)$/ + or ThrowUserError('report_invalid_parameter', {name => 'product_id'}); + $query->{product_id} = $1; + + # join + my $join = shift @query; + $join =~ /^(and|or)$/ + or ThrowUserError('report_invalid_parameter', {name => 'join'}); + $query->{join} = $1; + + # fields + my @fields; + foreach my $field (@query) { + $field =~ /^(\d+)([\-\+])$/ + or ThrowUserError('report_invalid_parameter', {name => 'fields'}); + my ($id, $value) = ($1, $2); + my $field_obj = Bugzilla::Field->new($id) + or ThrowUserError('report_invalid_parameter', {name => 'field_id'}); + push @fields, + { + id => $id, + value => $value, + name => $field_obj->name, + type => $field_obj->type + }; + } + $query->{fields} = \@fields; + + return $query; } 1; diff --git a/extensions/BMO/lib/Reports/Triage.pm b/extensions/BMO/lib/Reports/Triage.pm index 55eeb17eb..0ccbbee6e 100644 --- a/extensions/BMO/lib/Reports/Triage.pm +++ b/extensions/BMO/lib/Reports/Triage.pm @@ -25,261 +25,279 @@ use List::MoreUtils qw(any); # set an upper limit on the *unfiltered* number of bugs to process use constant MAX_NUMBER_BUGS => 4000; -use constant DEFAULT_OWNER_PRODUCTS => ( - 'Core', - 'Firefox', - 'Firefox for Android', - 'Firefox for iOS', - 'Toolkit', -); +use constant DEFAULT_OWNER_PRODUCTS => + ('Core', 'Firefox', 'Firefox for Android', 'Firefox for iOS', 'Toolkit',); sub unconfirmed { - my ($vars, $filter) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; - - if (exists $input->{'action'} && $input->{'action'} eq 'run' && $input->{'product'}) { - - # load product and components from input - - my $product = Bugzilla::Product->new({ name => $input->{'product'} }) - || ThrowUserError('invalid_object', { object => 'Product', value => $input->{'product'} }); - - my @component_ids; - if ($input->{'component'} ne '') { - my $ra_components = ref($input->{'component'}) - ? $input->{'component'} : [ $input->{'component'} ]; - foreach my $component_name (@$ra_components) { - my $component = Bugzilla::Component->new({ name => $component_name, product => $product }) - || ThrowUserError('invalid_object', { object => 'Component', value => $component_name }); - push @component_ids, $component->id; - } - } + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; - # determine which comment filters to run + if ( exists $input->{'action'} + && $input->{'action'} eq 'run' + && $input->{'product'}) + { - my $filter_commenter = $input->{'filter_commenter'}; - my $filter_commenter_on = $input->{'commenter'}; - my $filter_last = $input->{'filter_last'}; - my $filter_last_period = $input->{'last'}; + # load product and components from input - if (!$filter_commenter || $filter_last) { - $filter_commenter = '1'; - $filter_commenter_on = 'reporter'; - } + my $product + = Bugzilla::Product->new({name => $input->{'product'}}) + || ThrowUserError('invalid_object', + {object => 'Product', value => $input->{'product'}}); - my $filter_commenter_id; - if ($filter_commenter && $filter_commenter_on eq 'is') { - Bugzilla::User::match_field({ 'commenter_is' => {'type' => 'single'} }); - my $user = Bugzilla::User->new({ name => $input->{'commenter_is'} }) - || ThrowUserError('invalid_object', { object => 'User', value => $input->{'commenter_is'} }); - $filter_commenter_id = $user ? $user->id : 0; - } + my @component_ids; + if ($input->{'component'} ne '') { + my $ra_components + = ref($input->{'component'}) + ? $input->{'component'} + : [$input->{'component'}]; + foreach my $component_name (@$ra_components) { + my $component + = Bugzilla::Component->new({name => $component_name, product => $product}) + || ThrowUserError('invalid_object', + {object => 'Component', value => $component_name}); + push @component_ids, $component->id; + } + } - my $filter_last_time; - if ($filter_last) { - if ($filter_last_period eq 'is') { - $filter_last_period = -1; - $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0; - } else { - detaint_natural($filter_last_period); - $filter_last_period = 14 if $filter_last_period < 14; - } - } + # determine which comment filters to run + + my $filter_commenter = $input->{'filter_commenter'}; + my $filter_commenter_on = $input->{'commenter'}; + my $filter_last = $input->{'filter_last'}; + my $filter_last_period = $input->{'last'}; + + if (!$filter_commenter || $filter_last) { + $filter_commenter = '1'; + $filter_commenter_on = 'reporter'; + } + + my $filter_commenter_id; + if ($filter_commenter && $filter_commenter_on eq 'is') { + Bugzilla::User::match_field({'commenter_is' => {'type' => 'single'}}); + my $user + = Bugzilla::User->new({name => $input->{'commenter_is'}}) + || ThrowUserError('invalid_object', + {object => 'User', value => $input->{'commenter_is'}}); + $filter_commenter_id = $user ? $user->id : 0; + } - # form sql queries + my $filter_last_time; + if ($filter_last) { + if ($filter_last_period eq 'is') { + $filter_last_period = -1; + $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0; + } + else { + detaint_natural($filter_last_period); + $filter_last_period = 14 if $filter_last_period < 14; + } + } + + # form sql queries - my $now = (time); - my $bugs_sql = " + my $now = (time); + my $bugs_sql = " SELECT bug_id, short_desc, reporter, creation_ts FROM bugs WHERE product_id = ? AND bug_status = 'UNCONFIRMED'"; - if (@component_ids) { - $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")"; - } - $bugs_sql .= " + if (@component_ids) { + $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")"; + } + $bugs_sql .= " ORDER BY creation_ts "; - my $comment_count_sql = " + my $comment_count_sql = " SELECT COUNT(*) FROM longdescs WHERE bug_id = ? "; - my $comment_sql = " + my $comment_sql = " SELECT who, bug_when, type, thetext, extra_data FROM longdescs WHERE bug_id = ? "; - if (!Bugzilla->user->is_insider) { - $comment_sql .= " AND isprivate = 0 "; - } - $comment_sql .= " + if (!Bugzilla->user->is_insider) { + $comment_sql .= " AND isprivate = 0 "; + } + $comment_sql .= " ORDER BY bug_when DESC LIMIT 1 "; - my $attach_sql = " + my $attach_sql = " SELECT description, isprivate FROM attachments WHERE attach_id = ? "; - # work on an initial list of bugs + # work on an initial list of bugs - my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id); - my @bugs; + my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id); + my @bugs; - # this can be slow to process, resulting in 'service unavailable' errors from zeus - # so if too many bugs are returned, throw an error + # this can be slow to process, resulting in 'service unavailable' errors from zeus + # so if too many bugs are returned, throw an error - if (scalar(@$list) > MAX_NUMBER_BUGS) { - ThrowUserError('report_too_many_bugs'); - } + if (scalar(@$list) > MAX_NUMBER_BUGS) { + ThrowUserError('report_too_many_bugs'); + } - foreach my $entry (@$list) { - my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry; - - next unless $user->can_see_bug($bug_id); - - # get last comment information - - my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id); - my ($commenter_id, $comment_ts, $type, $comment, $extra) - = $dbh->selectrow_array($comment_sql, undef, $bug_id); - my $commenter = 0; - - # apply selected filters - - if ($filter_commenter) { - next if $comment_count <= 1; - - if ($filter_commenter_on eq 'reporter') { - next if $commenter_id != $reporter_id; - - } elsif ($filter_commenter_on eq 'noconfirm') { - $commenter = Bugzilla::User->new({ id => $commenter_id, cache => 1 }); - next if $commenter_id != $reporter_id - || $commenter->in_group('canconfirm'); - - } elsif ($filter_commenter_on eq 'is') { - next if $commenter_id != $filter_commenter_id; - } - } else { - $input->{'commenter'} = ''; - $input->{'commenter_is'} = ''; - } - - if ($filter_last) { - my $comment_time = str2time($comment_ts) - or next; - if ($filter_last_period == -1) { - next if $comment_time >= $filter_last_time; - } else { - next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period; - } - } else { - $input->{'last'} = ''; - $input->{'last_is'} = ''; - } - - # get data for attachment comments - - if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) { - my ($description, $is_private) = $dbh->selectrow_array($attach_sql, undef, $extra); - next if $is_private && !Bugzilla->user->is_insider; - $comment = "(Attachment) " . $description; - } - - # truncate long comments - - if (length($comment) > 80) { - $comment = substr($comment, 0, 80) . '...'; - } - - # build bug hash for template - - my $bug = {}; - $bug->{id} = $bug_id; - $bug->{summary} = $summary; - $bug->{reporter} = Bugzilla::User->new({ id => $reporter_id, cache => 1 }); - $bug->{creation_ts} = $creation_ts; - $bug->{commenter} = $commenter || Bugzilla::User->new({ id => $commenter_id, cache => 1 }); - $bug->{comment_ts} = $comment_ts; - $bug->{comment} = $comment; - $bug->{comment_count} = $comment_count; - push @bugs, $bug; - } + foreach my $entry (@$list) { + my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry; - @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs; + next unless $user->can_see_bug($bug_id); - $vars->{bugs} = \@bugs; - } else { - $input->{action} = ''; - } + # get last comment information - if (!$input->{filter_commenter} && !$input->{filter_last}) { - $input->{filter_commenter} = 1; - } + my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id); + my ($commenter_id, $comment_ts, $type, $comment, $extra) + = $dbh->selectrow_array($comment_sql, undef, $bug_id); + my $commenter = 0; - $vars->{'input'} = $input; -} + # apply selected filters -sub owners { - my ($vars, $filter) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; + if ($filter_commenter) { + next if $comment_count <= 1; - Bugzilla::User::match_field({ 'owner' => {'type' => 'multi'} }); + if ($filter_commenter_on eq 'reporter') { + next if $commenter_id != $reporter_id; - my @products; - if (!$input->{product} && $input->{owner}) { - @products = @{ $user->get_selectable_products }; - } - else { - my @product_names = $input->{product} ? ($input->{product}) : DEFAULT_OWNER_PRODUCTS; - foreach my $name (@product_names) { - push(@products, Bugzilla::Product->check({ name => $name })); } - } + elsif ($filter_commenter_on eq 'noconfirm') { + $commenter = Bugzilla::User->new({id => $commenter_id, cache => 1}); + next if $commenter_id != $reporter_id || $commenter->in_group('canconfirm'); - my @component_ids; - if (@products == 1 && $input->{'component'}) { - my $ra_components = ref($input->{'component'}) - ? $input->{'component'} - : [ $input->{'component'} ]; - foreach my $component_name (@$ra_components) { - my $component = Bugzilla::Component->check({ name => $component_name, product => $products[0] }); - push @component_ids, $component->id; } + elsif ($filter_commenter_on eq 'is') { + next if $commenter_id != $filter_commenter_id; + } + } + else { + $input->{'commenter'} = ''; + $input->{'commenter_is'} = ''; + } + + if ($filter_last) { + my $comment_time = str2time($comment_ts) or next; + if ($filter_last_period == -1) { + next if $comment_time >= $filter_last_time; + } + else { + next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period; + } + } + else { + $input->{'last'} = ''; + $input->{'last_is'} = ''; + } + + # get data for attachment comments + + if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) { + my ($description, $is_private) + = $dbh->selectrow_array($attach_sql, undef, $extra); + next if $is_private && !Bugzilla->user->is_insider; + $comment = "(Attachment) " . $description; + } + + # truncate long comments + + if (length($comment) > 80) { + $comment = substr($comment, 0, 80) . '...'; + } + + # build bug hash for template + + my $bug = {}; + $bug->{id} = $bug_id; + $bug->{summary} = $summary; + $bug->{reporter} = Bugzilla::User->new({id => $reporter_id, cache => 1}); + $bug->{creation_ts} = $creation_ts; + $bug->{commenter} + = $commenter || Bugzilla::User->new({id => $commenter_id, cache => 1}); + $bug->{comment_ts} = $comment_ts; + $bug->{comment} = $comment; + $bug->{comment_count} = $comment_count; + push @bugs, $bug; } - my @owner_names = split(/[,;]+/, $input->{owner}) if $input->{owner}; - my @owner_ids; - foreach my $name (@owner_names) { - $name = trim($name); - next unless $name; - push(@owner_ids, login_to_id($name, THROW_ERROR)); - } + @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs; - my $sql = "SELECT products.name, components.name, components.id, components.triage_owner_id - FROM components JOIN products ON components.product_id = products.id - WHERE products.id IN (" . join(',', map { $_->id } @products) . ")"; - if (@component_ids) { - $sql .= " AND components.id IN (" . join(',', @component_ids) . ")"; - } - if (@owner_ids) { - $sql .= " AND components.triage_owner_id IN (" . join(',', @owner_ids) . ")"; - } - $sql .= " ORDER BY products.name, components.name"; + $vars->{bugs} = \@bugs; + } + else { + $input->{action} = ''; + } - my $rows = $dbh->selectall_arrayref($sql); + if (!$input->{filter_commenter} && !$input->{filter_last}) { + $input->{filter_commenter} = 1; + } - my $bug_count_sth = $dbh->prepare(" + $vars->{'input'} = $input; +} + +sub owners { + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + Bugzilla::User::match_field({'owner' => {'type' => 'multi'}}); + + my @products; + if (!$input->{product} && $input->{owner}) { + @products = @{$user->get_selectable_products}; + } + else { + my @product_names + = $input->{product} ? ($input->{product}) : DEFAULT_OWNER_PRODUCTS; + foreach my $name (@product_names) { + push(@products, Bugzilla::Product->check({name => $name})); + } + } + + my @component_ids; + if (@products == 1 && $input->{'component'}) { + my $ra_components + = ref($input->{'component'}) + ? $input->{'component'} + : [$input->{'component'}]; + foreach my $component_name (@$ra_components) { + my $component = Bugzilla::Component->check( + {name => $component_name, product => $products[0]}); + push @component_ids, $component->id; + } + } + + my @owner_names = split(/[,;]+/, $input->{owner}) if $input->{owner}; + my @owner_ids; + foreach my $name (@owner_names) { + $name = trim($name); + next unless $name; + push(@owner_ids, login_to_id($name, THROW_ERROR)); + } + + my $sql + = "SELECT products.name, components.name, components.id, components.triage_owner_id + FROM components JOIN products ON components.product_id = products.id + WHERE products.id IN (" + . join(',', map { $_->id } @products) . ")"; + if (@component_ids) { + $sql .= " AND components.id IN (" . join(',', @component_ids) . ")"; + } + if (@owner_ids) { + $sql .= " AND components.triage_owner_id IN (" . join(',', @owner_ids) . ")"; + } + $sql .= " ORDER BY products.name, components.name"; + + my $rows = $dbh->selectall_arrayref($sql); + + my $bug_count_sth = $dbh->prepare(" SELECT COUNT(bugs.bug_id) FROM bugs INNER JOIN components AS map_component ON bugs.component_id = map_component.id INNER JOIN bug_status AS map_bug_status ON bugs.bug_status = map_bug_status.value @@ -296,55 +314,57 @@ sub owners { WHERE bugs_1.bug_id = bugs.bug_id AND CONCAT(flagtypes_1.name, flags_1.status) = 'needinfo?'))) AND bugs.component_id = ?"); - my @results; - foreach my $row (@$rows) { - my ($product_name, $component_name, $component_id, $triage_owner_id) = @$row; - my $triage_owner = $triage_owner_id - ? Bugzilla::User->new({ id => $triage_owner_id, cache => 1 }) - : ""; - my $data = { - product => $product_name, - component => $component_name, - owner => $triage_owner, - }; - $data->{buglist_url} = 'priority=--&resolution=---&f1=creation_ts&o1=greaterthaneq&v1=2016-06-01'. - '&f2=flagtypes.name&o2=notequals&v2=needinfo%3F'; - if ($triage_owner) { - $data->{buglist_url} .= '&f3=triage_owner&o3=equals&v3=' . url_quote($triage_owner->login); - } - $bug_count_sth->execute($component_id); - ($data->{bug_count}) = $bug_count_sth->fetchrow_array(); - push @results, $data; + my @results; + foreach my $row (@$rows) { + my ($product_name, $component_name, $component_id, $triage_owner_id) = @$row; + my $triage_owner + = $triage_owner_id + ? Bugzilla::User->new({id => $triage_owner_id, cache => 1}) + : ""; + my $data = { + product => $product_name, + component => $component_name, + owner => $triage_owner, + }; + $data->{buglist_url} + = 'priority=--&resolution=---&f1=creation_ts&o1=greaterthaneq&v1=2016-06-01' + . '&f2=flagtypes.name&o2=notequals&v2=needinfo%3F'; + if ($triage_owner) { + $data->{buglist_url} + .= '&f3=triage_owner&o3=equals&v3=' . url_quote($triage_owner->login); } - $vars->{results} = \@results; - - my $json_data = { products => [] }; - foreach my $product (@{ $user->get_selectable_products }) { - my $prod_data = { - name => $product->name, - components => [], - }; - foreach my $component (@{ $product->components }) { - my $selected = 0; - if ($input->{product} - && $input->{product} eq $product->name - && $input->{component}) - { - $selected = 1 if (ref $input->{component} && any { $_ eq $component->name } @{ $input->{component} }); - $selected = 1 if (!ref $input->{componet} && $input->{component} eq $component->name); - } - my $comp_data = { - name => $component->name, - selected => $selected - }; - push(@{ $prod_data->{components} }, $comp_data); - } - push(@{ $json_data->{products} }, $prod_data); + $bug_count_sth->execute($component_id); + ($data->{bug_count}) = $bug_count_sth->fetchrow_array(); + push @results, $data; + } + $vars->{results} = \@results; + + my $json_data = {products => []}; + foreach my $product (@{$user->get_selectable_products}) { + my $prod_data = {name => $product->name, components => [],}; + foreach my $component (@{$product->components}) { + my $selected = 0; + if ( $input->{product} + && $input->{product} eq $product->name + && $input->{component}) + { + $selected = 1 + if ( + ref $input->{component} && any { $_ eq $component->name } + @{$input->{component}} + ); + $selected = 1 + if (!ref $input->{componet} && $input->{component} eq $component->name); + } + my $comp_data = {name => $component->name, selected => $selected}; + push(@{$prod_data->{components}}, $comp_data); } + push(@{$json_data->{products}}, $prod_data); + } - $vars->{product} = $input->{product}; - $vars->{owner} = $input->{owner}; - $vars->{json_data} = encode_json($json_data); + $vars->{product} = $input->{product}; + $vars->{owner} = $input->{owner}; + $vars->{json_data} = encode_json($json_data); } 1; diff --git a/extensions/BMO/lib/Reports/UserActivity.pm b/extensions/BMO/lib/Reports/UserActivity.pm index 8dfe0c5cd..3be6f74c9 100644 --- a/extensions/BMO/lib/Reports/UserActivity.pm +++ b/extensions/BMO/lib/Reports/UserActivity.pm @@ -18,104 +18,104 @@ use Bugzilla::Util qw(trim); use DateTime; sub report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - - my @who = (); - my $from = trim($input->{'from'} || ''); - my $to = trim($input->{'to'} || ''); - my $action = $input->{'action'} || ''; - - # fix non-breaking hyphens - $from =~ s/\N{U+2011}/-/g; - $to =~ s/\N{U+2011}/-/g; - - if ($from eq '') { - my $dt = DateTime->now()->subtract('weeks' => 1); - $from = $dt->ymd('-'); + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + + my @who = (); + my $from = trim($input->{'from'} || ''); + my $to = trim($input->{'to'} || ''); + my $action = $input->{'action'} || ''; + + # fix non-breaking hyphens + $from =~ s/\N{U+2011}/-/g; + $to =~ s/\N{U+2011}/-/g; + + if ($from eq '') { + my $dt = DateTime->now()->subtract('weeks' => 1); + $from = $dt->ymd('-'); + } + if ($to eq '') { + my $dt = DateTime->now(); + $to = $dt->ymd('-'); + } + + if ($action eq 'run') { + if (!exists $input->{'who'} || $input->{'who'} eq '') { + ThrowUserError('user_activity_missing_username'); } - if ($to eq '') { - my $dt = DateTime->now(); - $to = $dt->ymd('-'); - } - - if ($action eq 'run') { - if (!exists $input->{'who'} || $input->{'who'} eq '') { - ThrowUserError('user_activity_missing_username'); - } - Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); + Bugzilla::User::match_field({'who' => {'type' => 'multi'}}); - my $from_dt = string_to_datetime($from); - $from = $from_dt->ymd(); + my $from_dt = string_to_datetime($from); + $from = $from_dt->ymd(); - my $to_dt = string_to_datetime($to); - $to = $to_dt->ymd(); + my $to_dt = string_to_datetime($to); + $to = $to_dt->ymd(); - my ($activity_joins, $activity_where) = ('', ''); - my ($attachments_joins, $attachments_where) = ('', ''); - my ($tags_activity_joins, $tags_activity_where) = ('', ''); - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) - { - $activity_joins = "LEFT JOIN attachments + my ($activity_joins, $activity_where) = ('', ''); + my ($attachments_joins, $attachments_where) = ('', ''); + my ($tags_activity_joins, $tags_activity_where) = ('', ''); + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) + { + $activity_joins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0"; - $attachments_where = $activity_where; + $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0"; + $attachments_where = $activity_where; - $tags_activity_joins = 'LEFT JOIN longdescs + $tags_activity_joins = 'LEFT JOIN longdescs ON longdescs_tags_activity.comment_id = longdescs.comment_id'; - $tags_activity_where = 'AND COALESCE(longdescs.isprivate, 0) = 0'; - } + $tags_activity_where = 'AND COALESCE(longdescs.isprivate, 0) = 0'; + } - my @who_bits; - foreach my $who ( - ref $input->{'who'} - ? @{$input->{'who'}} - : $input->{'who'} - ) { - push @who, $who; - push @who_bits, '?'; - } - my $who_bits = join(',', @who_bits); - - if (!@who) { - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $vars = {}; - $vars->{'script'} = $cgi->url(-relative => 1); - $vars->{'fields'} = {}; - $vars->{'matches'} = []; - $vars->{'matchsuccess'} = 0; - $vars->{'matchmultiple'} = 1; - print $cgi->header(); - $template->process("global/confirm-user-match.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + my @who_bits; + foreach my $who (ref $input->{'who'} ? @{$input->{'who'}} : $input->{'who'}) { + push @who, $who; + push @who_bits, '?'; + } + my $who_bits = join(',', @who_bits); + + if (!@who) { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $vars = {}; + $vars->{'script'} = $cgi->url(-relative => 1); + $vars->{'fields'} = {}; + $vars->{'matches'} = []; + $vars->{'matchsuccess'} = 0; + $vars->{'matchmultiple'} = 1; + print $cgi->header(); + $template->process("global/confirm-user-match.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } - $from_dt = $from_dt->ymd() . ' 00:00:00'; - $to_dt = $to_dt->ymd() . ' 23:59:59'; - my @params; - for (1..5) { - push @params, @who; - push @params, ($from_dt, $to_dt); - } + $from_dt = $from_dt->ymd() . ' 00:00:00'; + $to_dt = $to_dt->ymd() . ' 23:59:59'; + my @params; + for (1 .. 5) { + push @params, @who; + push @params, ($from_dt, $to_dt); + } - my $order = ($input->{'group'} && $input->{'group'} eq 'bug') - ? 'bug_id, bug_when' : 'bug_when'; + my $order + = ($input->{'group'} && $input->{'group'} eq 'bug') + ? 'bug_id, bug_when' + : 'bug_when'; - my $comment_filter = ''; - if (!Bugzilla->user->is_insider) { - $comment_filter = 'AND longdescs.isprivate = 0'; - } + my $comment_filter = ''; + if (!Bugzilla->user->is_insider) { + $comment_filter = 'AND longdescs.isprivate = 0'; + } - my $query = " + my $query = " SELECT fielddefs.name, bugs_activity.bug_id, bugs_activity.attach_id, - ".$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS ts, bugs_activity.removed, bugs_activity.added, profiles.login_name, @@ -138,8 +138,10 @@ sub report { 'comment_tag' AS name, longdescs_tags_activity.bug_id, NULL as attach_id, - ".$dbh->sql_date_format('longdescs_tags_activity.bug_when', - '%Y.%m.%d %H:%i:%s') . " AS bug_when, + " + . $dbh->sql_date_format('longdescs_tags_activity.bug_when', + '%Y.%m.%d %H:%i:%s') + . " AS bug_when, longdescs_tags_activity.removed, longdescs_tags_activity.added, profiles.login_name, @@ -160,7 +162,8 @@ sub report { 'bug_id' AS name, bugs.bug_id, NULL AS attach_id, - ".$dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s') . " AS ts, '(new bug)' AS removed, bugs.short_desc AS added, profiles.login_name, @@ -199,7 +202,9 @@ sub report { 'attachments.description' AS name, attachments.bug_id, attachments.attach_id, - ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s') + . " AS ts, '(new attachment)' AS removed, attachments.description AS added, profiles.login_name, @@ -215,119 +220,118 @@ sub report { ORDER BY $order "; - my $list = $dbh->selectall_arrayref($query, undef, @params); + my $list = $dbh->selectall_arrayref($query, undef, @params); - if ($input->{debug}) { - while (my $param = shift @params) { - $query =~ s/\?/$dbh->quote($param)/e; - } - $vars->{debug_sql} = $query; + if ($input->{debug}) { + while (my $param = shift @params) { + $query =~ s/\?/$dbh->quote($param)/e; + } + $vars->{debug_sql} = $query; + } + + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; + my %bug_ids; + + foreach my $entry (@$list) { + my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who, $comment_id) + = @$entry; + my %change; + my $activity_visible = 1; + + next unless Bugzilla->user->can_see_bug($bugid); + + # check if the user should see this field's activity + if ( $fieldname eq 'remaining_time' + || $fieldname eq 'estimated_time' + || $fieldname eq 'work_time' + || $fieldname eq 'deadline') + { + $activity_visible = Bugzilla->user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' + && !Bugzilla->user->is_insider + && $added) + { + $activity_visible = 0; + } + else { + $activity_visible = 1; + } + + if ($activity_visible) { + + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; } - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; - my %bug_ids; - - foreach my $entry (@$list) { - my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who, - $comment_id) = @$entry; - my %change; - my $activity_visible = 1; - - next unless Bugzilla->user->can_see_bug($bugid); - - # check if the user should see this field's activity - if ($fieldname eq 'remaining_time' - || $fieldname eq 'estimated_time' - || $fieldname eq 'work_time' - || $fieldname eq 'deadline') - { - $activity_visible = Bugzilla->user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !Bugzilla->user->is_insider - && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } - - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # Start a new changeset if required (depends on the grouping type) - my $is_new_changeset; - if ($order eq 'bug_when') { - $is_new_changeset = - $operation->{'who'} && - ( - $who ne $operation->{'who'} - || $when ne $operation->{'when'} - || $bugid != $operation->{'bug'} - ); - } else { - $is_new_changeset = - $operation->{'bug'} && - $bugid != $operation->{'bug'}; - } - if ($is_new_changeset) { - $operation->{'changes'} = $changes; - push (@operations, $operation); - $operation = {}; - $changes = []; - } - - $bug_ids{$bugid} = 1; - - $operation->{'bug'} = $bugid; - $operation->{'who'} = $who; - $operation->{'when'} = $when; - - $change{'fieldname'} = $fieldname; - $change{'attachid'} = $attachid; - $change{'removed'} = $removed; - $change{'added'} = $added; - $change{'when'} = $when; - - if ($comment_id) { - $change{'comment'} = Bugzilla::Comment->new($comment_id); - next if $change{'comment'}->count == 0; - } - - if ($attachid) { - $change{'attach'} = Bugzilla::Attachment->new($attachid); - } - - push (@$changes, \%change); - } + # Start a new changeset if required (depends on the grouping type) + my $is_new_changeset; + if ($order eq 'bug_when') { + $is_new_changeset + = $operation->{'who'} + && ($who ne $operation->{'who'} + || $when ne $operation->{'when'} + || $bugid != $operation->{'bug'}); + } + else { + $is_new_changeset = $operation->{'bug'} && $bugid != $operation->{'bug'}; } + if ($is_new_changeset) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + $operation = {}; + $changes = []; + } + + $bug_ids{$bugid} = 1; + + $operation->{'bug'} = $bugid; + $operation->{'who'} = $who; + $operation->{'when'} = $when; - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + $change{'fieldname'} = $fieldname; + $change{'attachid'} = $attachid; + $change{'removed'} = $removed; + $change{'added'} = $added; + $change{'when'} = $when; + + if ($comment_id) { + $change{'comment'} = Bugzilla::Comment->new($comment_id); + next if $change{'comment'}->count == 0; + } + + if ($attachid) { + $change{'attach'} = Bugzilla::Attachment->new($attachid); } - $vars->{'incomplete_data'} = $incomplete_data; - $vars->{'operations'} = \@operations; + push(@$changes, \%change); + } + } - my @bug_ids = sort { $a <=> $b } keys %bug_ids; - $vars->{'bug_ids'} = \@bug_ids; + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); } - $vars->{'action'} = $action; - $vars->{'who'} = join(',', @who); - $vars->{'who_count'} = scalar @who; - $vars->{'from'} = $from; - $vars->{'to'} = $to; - $vars->{'group'} = $input->{'group'}; + $vars->{'incomplete_data'} = $incomplete_data; + $vars->{'operations'} = \@operations; + + my @bug_ids = sort { $a <=> $b } keys %bug_ids; + $vars->{'bug_ids'} = \@bug_ids; + } + + $vars->{'action'} = $action; + $vars->{'who'} = join(',', @who); + $vars->{'who_count'} = scalar @who; + $vars->{'from'} = $from; + $vars->{'to'} = $to; + $vars->{'group'} = $input->{'group'}; } 1; diff --git a/extensions/BMO/lib/Util.pm b/extensions/BMO/lib/Util.pm index dc9c904f9..4d376aecb 100644 --- a/extensions/BMO/lib/Util.pm +++ b/extensions/BMO/lib/Util.pm @@ -19,74 +19,77 @@ use DateTime; use base qw(Exporter); our @EXPORT = qw( string_to_datetime - time_to_datetime - parse_date - is_active_status_field ); + time_to_datetime + parse_date + is_active_status_field ); sub string_to_datetime { - my $input = shift; - my $time = parse_date($input) - or ThrowUserError('report_invalid_date', { date => $input }); - return time_to_datetime($time); + my $input = shift; + my $time = parse_date($input) + or ThrowUserError('report_invalid_date', {date => $input}); + return time_to_datetime($time); } sub time_to_datetime { - my $time = shift; - return DateTime->from_epoch(epoch => $time) - ->set_time_zone('local') - ->truncate(to => 'day'); + my $time = shift; + return DateTime->from_epoch(epoch => $time)->set_time_zone('local') + ->truncate(to => 'day'); } sub parse_date { - my ($str) = @_; - if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { - # relative date - my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time); - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); - $amount = -$amount if $sign && $sign eq '+'; - if ($unit eq 'w') { - # convert weeks to days - $amount = 7 * $amount + $wday; - $unit = 'd'; - } - if ($unit eq 'd') { - $date -= $sec + 60 * $min + 3600 * $hour + 24 * 3600 * $amount; - return $date; - } - elsif ($unit eq 'y') { - return str2time(sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount)); - } - elsif ($unit eq 'm') { - $month -= $amount; - while ($month < 0) { $year--; $month += 12; } - return str2time(sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1)); - } - elsif ($unit eq 'h') { - # Special case 0h for 'beginning of this hour' - if ($amount == 0) { - $date -= $sec + 60 * $min; - } else { - $date -= 3600 * $amount; - } - return $date; - } - return undef; + my ($str) = @_; + if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { + + # relative date + my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time); + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); + $amount = -$amount if $sign && $sign eq '+'; + if ($unit eq 'w') { + + # convert weeks to days + $amount = 7 * $amount + $wday; + $unit = 'd'; + } + if ($unit eq 'd') { + $date -= $sec + 60 * $min + 3600 * $hour + 24 * 3600 * $amount; + return $date; + } + elsif ($unit eq 'y') { + return str2time(sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount)); } - return str2time($str); + elsif ($unit eq 'm') { + $month -= $amount; + while ($month < 0) { $year--; $month += 12; } + return str2time(sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1)); + } + elsif ($unit eq 'h') { + + # Special case 0h for 'beginning of this hour' + if ($amount == 0) { + $date -= $sec + 60 * $min; + } + else { + $date -= 3600 * $amount; + } + return $date; + } + return undef; + } + return str2time($str); } sub is_active_status_field { - my ($field) = @_; + my ($field) = @_; - if ($field->type == FIELD_TYPE_EXTENSION - && $field->isa('Bugzilla::Extension::TrackingFlags::Flag') - && $field->flag_type eq 'tracking' - && $field->name =~ /_status_/ - ) { - return $field->is_active; - } + if ( $field->type == FIELD_TYPE_EXTENSION + && $field->isa('Bugzilla::Extension::TrackingFlags::Flag') + && $field->flag_type eq 'tracking' + && $field->name =~ /_status_/) + { + return $field->is_active; + } - return 0; + return 0; } 1; diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm index 327c8563f..4c9187254 100644 --- a/extensions/BMO/lib/WebService.pm +++ b/extensions/BMO/lib/WebService.pm @@ -32,26 +32,26 @@ use Bugzilla::WebService::Util qw(validate); use Bugzilla::Field; use constant PUBLIC_METHODS => qw( - getBugsConfirmer - getBugsVerifier + getBugsConfirmer + getBugsVerifier ); sub getBugsConfirmer { - my ($self, $params) = validate(@_, 'names'); - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; - defined($params->{names}) - || ThrowCodeError('params_required', - { function => 'BMO.getBugsConfirmer', params => ['names'] }); + defined($params->{names}) + || ThrowCodeError('params_required', + {function => 'BMO.getBugsConfirmer', params => ['names']}); - my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + my @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}; - # start filtering to remove duplicate user ids - @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + # start filtering to remove duplicate user ids + @user_objects = values %{{map { $_->id => $_ } @user_objects}}; - my $fieldid = get_field_id('bug_status'); + my $fieldid = get_field_id('bug_status'); - my $query = "SELECT DISTINCT bugs_activity.bug_id + my $query = "SELECT DISTINCT bugs_activity.bug_id FROM bugs_activity LEFT JOIN bug_group_map ON bugs_activity.bug_id = bug_group_map.bug_id @@ -62,31 +62,31 @@ sub getBugsConfirmer { AND bug_group_map.bug_id IS NULL ORDER BY bugs_activity.bug_id"; - my %users; - foreach my $user (@user_objects) { - my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); - $users{$user->login} = $bugs; - } + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } - return \%users; + return \%users; } sub getBugsVerifier { - my ($self, $params) = validate(@_, 'names'); - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; - defined($params->{names}) - || ThrowCodeError('params_required', - { function => 'BMO.getBugsVerifier', params => ['names'] }); + defined($params->{names}) + || ThrowCodeError('params_required', + {function => 'BMO.getBugsVerifier', params => ['names']}); - my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + my @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}; - # start filtering to remove duplicate user ids - @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + # start filtering to remove duplicate user ids + @user_objects = values %{{map { $_->id => $_ } @user_objects}}; - my $fieldid = get_field_id('bug_status'); + my $fieldid = get_field_id('bug_status'); - my $query = "SELECT DISTINCT bugs_activity.bug_id + my $query = "SELECT DISTINCT bugs_activity.bug_id FROM bugs_activity LEFT JOIN bug_group_map ON bugs_activity.bug_id = bug_group_map.bug_id @@ -97,13 +97,13 @@ sub getBugsVerifier { AND bug_group_map.bug_id IS NULL ORDER BY bugs_activity.bug_id"; - my %users; - foreach my $user (@user_objects) { - my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); - $users{$user->login} = $bugs; - } + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } - return \%users; + return \%users; } 1; diff --git a/extensions/BMO/t/bounty_attachment.t b/extensions/BMO/t/bounty_attachment.t index 6e596eeba..61552c074 100644 --- a/extensions/BMO/t/bounty_attachment.t +++ b/extensions/BMO/t/bounty_attachment.t @@ -13,46 +13,69 @@ use Test::More; use Bugzilla; BEGIN { Bugzilla->extensions } -my $class = 'Bugzilla::Extension::BMO'; +my $class = 'Bugzilla::Extension::BMO'; my $parse = $class->can('parse_bounty_attachment_description'); my $format = $class->can('format_bounty_attachment_description'); ok($parse, "got the function"); my $bughunter = $parse->('bughunter@hacker.org, , 2014-06-25, , ,false'); -is_deeply({ reporter_email => 'bughunter@hacker.org', - amount_paid => '', - reported_date => '2014-06-25', - fixed_date => '', - awarded_date => '', - publish => 0, - credit => []}, $bughunter); - -my $hfli = $parse->('hfli@fortinet.com, 1000, 2010-07-16, 2010-08-04, 2011-06-15, true, Fortiguard Labs'); -is_deeply({ reporter_email => 'hfli@fortinet.com', - amount_paid => '1000', - reported_date => '2010-07-16', - fixed_date => '2010-08-04', - awarded_date => '2011-06-15', - publish => 1, - credit => ['Fortiguard Labs']}, $hfli); - -is('batman@justiceleague.america,1000,2015-01-01,2015-02-02,2015-03-03,true,JLA,Wayne Industries,Test', - $format->({ reporter_email => 'batman@justiceleague.america', - amount_paid => 1000, - reported_date => '2015-01-01', - fixed_date => '2015-02-02', - awarded_date => '2015-03-03', - publish => 1, - credit => ['JLA', 'Wayne Industries', 'Test'] })); - -my $dylan = $parse->('dylan@hardison.net,2,2014-09-23,2014-09-24,2014-09-25,true,Foo bar,Bork,'); -is_deeply({ reporter_email => 'dylan@hardison.net', - amount_paid => 2, - reported_date => '2014-09-23', - fixed_date => '2014-09-24', - awarded_date => '2014-09-25', - publish => 1, - credit => ['Foo bar', 'Bork']}, $dylan); +is_deeply( + { + reporter_email => 'bughunter@hacker.org', + amount_paid => '', + reported_date => '2014-06-25', + fixed_date => '', + awarded_date => '', + publish => 0, + credit => [] + }, + $bughunter +); + +my $hfli + = $parse->( + 'hfli@fortinet.com, 1000, 2010-07-16, 2010-08-04, 2011-06-15, true, Fortiguard Labs' + ); +is_deeply( + { + reporter_email => 'hfli@fortinet.com', + amount_paid => '1000', + reported_date => '2010-07-16', + fixed_date => '2010-08-04', + awarded_date => '2011-06-15', + publish => 1, + credit => ['Fortiguard Labs'] + }, + $hfli +); + +is( + 'batman@justiceleague.america,1000,2015-01-01,2015-02-02,2015-03-03,true,JLA,Wayne Industries,Test', + $format->({ + reporter_email => 'batman@justiceleague.america', + amount_paid => 1000, + reported_date => '2015-01-01', + fixed_date => '2015-02-02', + awarded_date => '2015-03-03', + publish => 1, + credit => ['JLA', 'Wayne Industries', 'Test'] + }) +); + +my $dylan = $parse->( + 'dylan@hardison.net,2,2014-09-23,2014-09-24,2014-09-25,true,Foo bar,Bork,'); +is_deeply( + { + reporter_email => 'dylan@hardison.net', + amount_paid => 2, + reported_date => '2014-09-23', + fixed_date => '2014-09-24', + awarded_date => '2014-09-25', + publish => 1, + credit => ['Foo bar', 'Bork'] + }, + $dylan +); done_testing; diff --git a/extensions/Bitly/Config.pm b/extensions/Bitly/Config.pm index 7e46a6ad8..ce27508c7 100644 --- a/extensions/Bitly/Config.pm +++ b/extensions/Bitly/Config.pm @@ -16,6 +16,6 @@ use constant NAME => 'Bitly'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; -use constant API_VERSION_MAP => { '1_0' => '1_0' }; +use constant API_VERSION_MAP => {'1_0' => '1_0'}; __PACKAGE__->NAME; diff --git a/extensions/Bitly/Extension.pm b/extensions/Bitly/Extension.pm index 82f17bc2a..31f4b7438 100644 --- a/extensions/Bitly/Extension.pm +++ b/extensions/Bitly/Extension.pm @@ -17,17 +17,14 @@ our $VERSION = '1'; use Bugzilla; sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{Bitly} = "Bugzilla::Extension::Bitly::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{Bitly} = "Bugzilla::Extension::Bitly::WebService"; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'bitly_token', - type => 't', - default => '', - }; + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + {name => 'bitly_token', type => 't', default => '',}; } __PACKAGE__->NAME; diff --git a/extensions/Bitly/lib/WebService.pm b/extensions/Bitly/lib/WebService.pm index 4b44faa0e..b900e17ee 100644 --- a/extensions/Bitly/lib/WebService.pm +++ b/extensions/Bitly/lib/WebService.pm @@ -27,121 +27,115 @@ use URI::Escape; use URI::QueryParam; use constant PUBLIC_METHODS => qw( - list - shorten + list + shorten ); sub _validate_uri { - my ($self, $params) = @_; - - # extract url from params - if (!defined $params->{url}) { - ThrowCodeError( - 'param_required', - { function => 'Bitly.shorten', param => 'url' } - ); - } - my $url = ref($params->{url}) ? $params->{url}->[0] : $params->{url}; - - # only allow buglist queries for this bugzilla install - my $uri = URI->new($url); - $uri->query(undef); - $uri->fragment(undef); - if ($uri->as_string ne Bugzilla->localconfig->{urlbase} . 'buglist.cgi') { - ThrowUserError('bitly_unsupported'); - } - - return URI->new($url); + my ($self, $params) = @_; + + # extract url from params + if (!defined $params->{url}) { + ThrowCodeError('param_required', {function => 'Bitly.shorten', param => 'url'}); + } + my $url = ref($params->{url}) ? $params->{url}->[0] : $params->{url}; + + # only allow buglist queries for this bugzilla install + my $uri = URI->new($url); + $uri->query(undef); + $uri->fragment(undef); + if ($uri->as_string ne Bugzilla->localconfig->{urlbase} . 'buglist.cgi') { + ThrowUserError('bitly_unsupported'); + } + + return URI->new($url); } sub shorten { - my ($self) = shift; - my $uri = $self->_validate_uri(@_); + my ($self) = shift; + my $uri = $self->_validate_uri(@_); - # the list_id is user-specific, remove it - $uri->query_param_delete('list_id'); + # the list_id is user-specific, remove it + $uri->query_param_delete('list_id'); - return $self->_bitly($uri); + return $self->_bitly($uri); } sub list { - my ($self) = shift; - my $uri = $self->_validate_uri(@_); - - # map params to cgi vars, converting quicksearch if required - my $params = $uri->query_param('quicksearch') - ? Bugzilla::CGI->new(quicksearch($uri->query_param('quicksearch')))->Vars - : Bugzilla::CGI->new($uri->query)->Vars; - - # execute the search - my $search = Bugzilla::Search->new( - params => $params, - fields => ['bug_id'], - limit => Bugzilla->params->{max_search_results}, - ); - my $data = $search->data; - - # form a bug_id only url, sanity check the length - $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'buglist.cgi?bug_id=' . join(',', map { $_->[0] } @$data)); - if (length($uri->as_string) > CGI_URI_LIMIT) { - ThrowUserError('bitly_failure', { message => "Too many bugs returned by search" }); - } - - # shorten - return $self->_bitly($uri); + my ($self) = shift; + my $uri = $self->_validate_uri(@_); + + # map params to cgi vars, converting quicksearch if required + my $params + = $uri->query_param('quicksearch') + ? Bugzilla::CGI->new(quicksearch($uri->query_param('quicksearch')))->Vars + : Bugzilla::CGI->new($uri->query)->Vars; + + # execute the search + my $search = Bugzilla::Search->new( + params => $params, + fields => ['bug_id'], + limit => Bugzilla->params->{max_search_results}, + ); + my $data = $search->data; + + # form a bug_id only url, sanity check the length + $uri + = URI->new(Bugzilla->localconfig->{urlbase} + . 'buglist.cgi?bug_id=' + . join(',', map { $_->[0] } @$data)); + if (length($uri->as_string) > CGI_URI_LIMIT) { + ThrowUserError('bitly_failure', + {message => "Too many bugs returned by search"}); + } + + # shorten + return $self->_bitly($uri); } sub _bitly { - my ($self, $uri) = @_; - - # form request url - # http://dev.bitly.com/links.html#v3_shorten - my $bitly_url = sprintf( - 'https://api-ssl.bitly.com/v3/shorten?access_token=%s&longUrl=%s', - Bugzilla->params->{bitly_token}, - uri_escape($uri->as_string) - ); - - # is Mozilla::CA isn't installed, skip certificate verification - eval { require Mozilla::CA }; - $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = $@ ? 0 : 1; - - # request - my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); - $ua->timeout(10); - $ua->protocols_allowed(['http', 'https']); - if (my $proxy_url = Bugzilla->params->{proxy_url}) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy(); - } - my $response = $ua->get($bitly_url); - if ($response->is_error) { - ThrowUserError('bitly_failure', { message => $response->message }); - } - my $result = decode_json($response->decoded_content); - if ($result->{status_code} != 200) { - ThrowUserError('bitly_failure', { message => $result->{status_txt} }); - } - - # return just the short url - return { url => $result->{data}->{url} }; + my ($self, $uri) = @_; + + # form request url + # http://dev.bitly.com/links.html#v3_shorten + my $bitly_url = sprintf( + 'https://api-ssl.bitly.com/v3/shorten?access_token=%s&longUrl=%s', + Bugzilla->params->{bitly_token}, + uri_escape($uri->as_string) + ); + + # is Mozilla::CA isn't installed, skip certificate verification + eval { require Mozilla::CA }; + $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = $@ ? 0 : 1; + + # request + my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + if (my $proxy_url = Bugzilla->params->{proxy_url}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy(); + } + my $response = $ua->get($bitly_url); + if ($response->is_error) { + ThrowUserError('bitly_failure', {message => $response->message}); + } + my $result = decode_json($response->decoded_content); + if ($result->{status_code} != 200) { + ThrowUserError('bitly_failure', {message => $result->{status_txt}}); + } + + # return just the short url + return {url => $result->{data}->{url}}; } sub rest_resources { - return [ - qr{^/bitly/shorten$}, { - GET => { - method => 'shorten', - }, - }, - qr{^/bitly/list$}, { - GET => { - method => 'list', - }, - }, - ] + return [ + qr{^/bitly/shorten$}, {GET => {method => 'shorten',},}, + qr{^/bitly/list$}, {GET => {method => 'list',},}, + ]; } 1; diff --git a/extensions/BugModal/Config.pm b/extensions/BugModal/Config.pm index 25a864e6e..bd15c075c 100644 --- a/extensions/BugModal/Config.pm +++ b/extensions/BugModal/Config.pm @@ -10,8 +10,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'BugModal'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'BugModal'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/BugModal/Extension.pm b/extensions/BugModal/Extension.pm index ef9c93a37..8b72bb757 100644 --- a/extensions/BugModal/Extension.pm +++ b/extensions/BugModal/Extension.pm @@ -26,322 +26,321 @@ use JSON::XS qw(encode_json); our $VERSION = '1'; use constant READABLE_BUG_STATUS_PRODUCTS => ( - 'Core', - 'Toolkit', - 'Firefox', - 'Firefox for Android', - 'Firefox for iOS', - 'Bugzilla', - 'bugzilla.mozilla.org' + 'Core', 'Toolkit', + 'Firefox', 'Firefox for Android', + 'Firefox for iOS', 'Bugzilla', + 'bugzilla.mozilla.org' ); sub show_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub edit_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub _alternative_show_bug_format { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - if (my $ctype = $cgi->param('ctype')) { - return '' if $ctype ne 'html'; - } - if (my $format = $cgi->param('format')) { - return ($format eq '__default__' || $format eq 'default') ? '' : $format; - } - return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + if (my $ctype = $cgi->param('ctype')) { + return '' if $ctype ne 'html'; + } + if (my $format = $cgi->param('format')) { + return ($format eq '__default__' || $format eq 'default') ? '' : $format; + } + return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; } sub template_after_create { - my ($self, $args) = @_; - my $context = $args->{template}->context; - - # wrapper around time_ago() - $context->define_filter( - time_duration => sub { - my ($context) = @_; - return sub { - my ($timestamp) = @_; - my $datetime = datetime_from($timestamp) - // return $timestamp; - return time_ago($datetime); - }; - }, 1 - ); - - # morph a string into one which is suitable to use as an element's id - $context->define_filter( - id => sub { - my ($context) = @_; - return sub { - my ($id) = @_; - $id //= ''; - $id = lc($id); - while ($id ne '' && $id !~ /^[a-z]/) { - $id = substr($id, 1); - } - $id =~ tr/ /-/; - $id =~ s/[^a-z\d\-_:\.]/_/g; - return $id; - }; - }, 1 - ); - - # parse date string and output epoch - $context->define_filter( - epoch => sub { - my ($context) = @_; - return sub { - my ($date_str) = @_; - return date_str_to_time($date_str); - }; - }, 1 - ); - - # flatten a list of hashrefs to a list of values - # eg. logins = users.pluck("login") - $context->define_vmethod( - list => pluck => sub { - my ($list, $field) = @_; - return [ map { $_->$field } @$list ]; - } - ); - - # returns array where the value in $field does not equal $value - # opposite of "only" - # eg. not_byron = users.skip("name", "Byron") - $context->define_vmethod( - list => skip => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field ne $value } @$list ]; - } - ); - - # returns array where the value in $field equals $value - # opposite of "skip" - # eg. byrons_only = users.only("name", "Byron") - $context->define_vmethod( - list => only => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field eq $value } @$list ]; - } - ); - - # returns boolean indicating if the value exists in the list - # eg. has_byron = user_names.exists("byron") - $context->define_vmethod( - list => exists => sub { - my ($list, $value) = @_; - return any { $_ eq $value } @$list; - } - ); - - # ucfirst is only available in new template::toolkit versions - $context->define_vmethod( - item => ucfirst => sub { - my ($text) = @_; - return ucfirst($text); - } - ); -} - -sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{file}; - my $vars = $args->{vars}; - - if ($file eq 'bug/process/header.html.tmpl' - || $file eq 'bug/create/created.html.tmpl' - || $file eq 'attachment/created.html.tmpl' - || $file eq 'attachment/updated.html.tmpl') - { - if (_alternative_show_bug_format() eq 'modal') { - $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; - $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; - $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; + my ($self, $args) = @_; + my $context = $args->{template}->context; + + # wrapper around time_ago() + $context->define_filter( + time_duration => sub { + my ($context) = @_; + return sub { + my ($timestamp) = @_; + my $datetime = datetime_from($timestamp) // return $timestamp; + return time_ago($datetime); + }; + }, + 1 + ); + + # morph a string into one which is suitable to use as an element's id + $context->define_filter( + id => sub { + my ($context) = @_; + return sub { + my ($id) = @_; + $id //= ''; + $id = lc($id); + while ($id ne '' && $id !~ /^[a-z]/) { + $id = substr($id, 1); } - return; + $id =~ tr/ /-/; + $id =~ s/[^a-z\d\-_:\.]/_/g; + return $id; + }; + }, + 1 + ); + + # parse date string and output epoch + $context->define_filter( + epoch => sub { + my ($context) = @_; + return sub { + my ($date_str) = @_; + return date_str_to_time($date_str); + }; + }, + 1 + ); + + # flatten a list of hashrefs to a list of values + # eg. logins = users.pluck("login") + $context->define_vmethod( + list => pluck => sub { + my ($list, $field) = @_; + return [map { $_->$field } @$list]; } - - if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { - my $format = $1; - return unless _alternative_show_bug_format() eq $format; + ); + + # returns array where the value in $field does not equal $value + # opposite of "only" + # eg. not_byron = users.skip("name", "Byron") + $context->define_vmethod( + list => skip => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field ne $value } @$list]; } - elsif ($file ne 'bug_modal/edit.html.tmpl') { - return; + ); + + # returns array where the value in $field equals $value + # opposite of "skip" + # eg. byrons_only = users.only("name", "Byron") + $context->define_vmethod( + list => only => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field eq $value } @$list]; } - - if ($vars->{bug} && !$vars->{bugs}) { - $vars->{bugs} = [$vars->{bug}]; + ); + + # returns boolean indicating if the value exists in the list + # eg. has_byron = user_names.exists("byron") + $context->define_vmethod( + list => exists => sub { + my ($list, $value) = @_; + return any { $_ eq $value } @$list; } + ); - return unless - $vars->{bugs} - && ref($vars->{bugs}) eq 'ARRAY' - && scalar(@{ $vars->{bugs} }) == 1; - my $bug = $vars->{bugs}->[0]; - return if exists $bug->{error}; - - # trigger loading of tracking flags - if (Bugzilla->has_extension('TrackingFlags')) { - Bugzilla::Extension::TrackingFlags->template_before_process({ - file => 'bug/edit.html.tmpl', - vars => $vars, - }); + # ucfirst is only available in new template::toolkit versions + $context->define_vmethod( + item => ucfirst => sub { + my ($text) = @_; + return ucfirst($text); } + ); +} - if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { - my @flags = map { { name => $_->name, status => $_->status } } @{$bug->flags}; - $vars->{readable_bug_status_json} = encode_json({ - dupe_of => $bug->dup_id, - id => $bug->id, - keywords => [ map { $_->name } @{$bug->keyword_objects} ], - priority => $bug->priority, - resolution => $bug->resolution, - status => $bug->bug_status, - flags => \@flags, - target_milestone => $bug->target_milestone, - map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, - }); - # HTML4 attributes cannot be longer than this, so just skip it in this case. - if (length($vars->{readable_bug_status_json}) > 65536) { - delete $vars->{readable_bug_status_json}; - } +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{file}; + my $vars = $args->{vars}; + + if ( $file eq 'bug/process/header.html.tmpl' + || $file eq 'bug/create/created.html.tmpl' + || $file eq 'attachment/created.html.tmpl' + || $file eq 'attachment/updated.html.tmpl') + { + if (_alternative_show_bug_format() eq 'modal') { + $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; + $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; + $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; } + return; + } + + if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { + my $format = $1; + return unless _alternative_show_bug_format() eq $format; + } + elsif ($file ne 'bug_modal/edit.html.tmpl') { + return; + } + + if ($vars->{bug} && !$vars->{bugs}) { + $vars->{bugs} = [$vars->{bug}]; + } + + return + unless $vars->{bugs} + && ref($vars->{bugs}) eq 'ARRAY' + && scalar(@{$vars->{bugs}}) == 1; + my $bug = $vars->{bugs}->[0]; + return if exists $bug->{error}; + + # trigger loading of tracking flags + if (Bugzilla->has_extension('TrackingFlags')) { + Bugzilla::Extension::TrackingFlags->template_before_process({ + file => 'bug/edit.html.tmpl', vars => $vars, + }); + } + + if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { + my @flags = map { {name => $_->name, status => $_->status} } @{$bug->flags}; + $vars->{readable_bug_status_json} = encode_json({ + dupe_of => $bug->dup_id, + id => $bug->id, + keywords => [map { $_->name } @{$bug->keyword_objects}], + priority => $bug->priority, + resolution => $bug->resolution, + status => $bug->bug_status, + flags => \@flags, + target_milestone => $bug->target_milestone, + map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, + }); - # bug->choices loads a lot of data that we want to lazy-load - # just load the status and resolutions and perform extra checks here - # upstream does these checks in the bug/fields template - my $perms = $bug->user; - my @resolutions; - foreach my $r (@{ Bugzilla::Field->new({ name => 'resolution', cache => 1 })->legal_values }) { - my $resolution = $r->name; - next unless $resolution; - - # always allow the current value - if ($resolution eq $bug->resolution) { - push @resolutions, $r; - next; - } - - # never allow inactive values - next unless $r->is_active; + # HTML4 attributes cannot be longer than this, so just skip it in this case. + if (length($vars->{readable_bug_status_json}) > 65536) { + delete $vars->{readable_bug_status_json}; + } + } + + # bug->choices loads a lot of data that we want to lazy-load + # just load the status and resolutions and perform extra checks here + # upstream does these checks in the bug/fields template + my $perms = $bug->user; + my @resolutions; + foreach my $r ( + @{Bugzilla::Field->new({name => 'resolution', cache => 1})->legal_values}) + { + my $resolution = $r->name; + next unless $resolution; + + # always allow the current value + if ($resolution eq $bug->resolution) { + push @resolutions, $r; + next; + } - # ensure the user has basic rights to change this field - next unless $bug->check_can_change_field('resolution', '---', $resolution); + # never allow inactive values + next unless $r->is_active; - # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE - if ($perms->{canconfirm} - && !($perms->{canedit} || $perms->{isreporter})) - { - next if - $resolution ne 'WORKSFORME' - && $resolution ne 'INCOMPLETE' - && $resolution ne 'DUPLICATE'; - } + # ensure the user has basic rights to change this field + next unless $bug->check_can_change_field('resolution', '---', $resolution); - # reporters can set it to anything, except INCOMPLETE - if ($perms->{isreporter} - && !($perms->{canconfirm} || $perms->{canedit})) - { - next if $resolution eq 'INCOMPLETE'; - } + # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE + if ($perms->{canconfirm} && !($perms->{canedit} || $perms->{isreporter})) { + next + if $resolution ne 'WORKSFORME' + && $resolution ne 'INCOMPLETE' + && $resolution ne 'DUPLICATE'; + } - # expired has, uh, expired - next if $resolution eq 'EXPIRED'; + # reporters can set it to anything, except INCOMPLETE + if ($perms->{isreporter} && !($perms->{canconfirm} || $perms->{canedit})) { + next if $resolution eq 'INCOMPLETE'; + } - push @resolutions, $r; + # expired has, uh, expired + next if $resolution eq 'EXPIRED'; + + push @resolutions, $r; + } + $bug->{choices} = { + bug_status => [ + grep { $_->is_active || $_->name eq $bug->bug_status } + @{$bug->statuses_available} + ], + resolution => \@resolutions, + }; + + # group tracking flags by version to allow for a better tabular output + my @tracking_table; + my $tracking_flags = $vars->{tracking_flags}; + foreach my $flag (@$tracking_flags) { + my $flag_type = $flag->flag_type; + my $type = 'status'; + my $name = $flag->description; + if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { + ($type, $name) = ($1, $2); } - $bug->{choices} = { - bug_status => [ - grep { $_->is_active || $_->name eq $bug->bug_status } - @{ $bug->statuses_available } - ], - resolution => \@resolutions, - }; - - # group tracking flags by version to allow for a better tabular output - my @tracking_table; - my $tracking_flags = $vars->{tracking_flags}; - foreach my $flag (@$tracking_flags) { - my $flag_type = $flag->flag_type; - my $type = 'status'; - my $name = $flag->description; - if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { - ($type, $name) = ($1, $2); - } - my ($existing) = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; - if ($existing) { - $existing->{$type} = $flag; - } - else { - push @tracking_table, { - $type => $flag, - name => $name, - type => $flag_type, - }; - } + my ($existing) + = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; + if ($existing) { + $existing->{$type} = $flag; } - $vars->{tracking_flags_table} = \@tracking_table; - - # for the "view -> hide treeherder comments" menu item - my $treeherder_id = Bugzilla->treeherder_user->id; - foreach my $change_set (@{ $bug->activity_stream }) { - if ($change_set->{comment} && $change_set->{comment}->author->id == $treeherder_id) { - $vars->{treeherder} = Bugzilla->treeherder_user; - last; - } + else { + push @tracking_table, {$type => $flag, name => $name, type => $flag_type,}; + } + } + $vars->{tracking_flags_table} = \@tracking_table; + + # for the "view -> hide treeherder comments" menu item + my $treeherder_id = Bugzilla->treeherder_user->id; + foreach my $change_set (@{$bug->activity_stream}) { + if ( $change_set->{comment} + && $change_set->{comment}->author->id == $treeherder_id) + { + $vars->{treeherder} = Bugzilla->treeherder_user; + last; } + } } sub bug_start_of_set_all { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $params = $args->{params}; - - # reset to the component defaults if not supplied - if (exists $params->{assigned_to} && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) { - $params->{assigned_to} = $bug->component_obj->default_assignee->login; - } - if (exists $params->{qa_contact} && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') - && $bug->component_obj->default_qa_contact->id) - { - $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; - } + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $params = $args->{params}; + + # reset to the component defaults if not supplied + if (exists $params->{assigned_to} + && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) + { + $params->{assigned_to} = $bug->component_obj->default_assignee->login; + } + if ( exists $params->{qa_contact} + && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') + && $bug->component_obj->default_qa_contact->id) + { + $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; + } } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'ui_experiments', - options => ['on', 'off'], - default => 'on', - category => 'User Interface' - }); - add_setting({ - name => 'ui_remember_collapsed', - options => ['on', 'off'], - default => 'off', - category => 'User Interface' - }); - add_setting({ - name => 'ui_use_absolute_time', - options => ['on', 'off'], - default => 'off', - category => 'User Interface', - }); + my ($self, $args) = @_; + add_setting({ + name => 'ui_experiments', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' + }); + add_setting({ + name => 'ui_remember_collapsed', + options => ['on', 'off'], + default => 'off', + category => 'User Interface' + }); + add_setting({ + name => 'ui_use_absolute_time', + options => ['on', 'off'], + default => 'off', + category => 'User Interface', + }); } __PACKAGE__->NAME; diff --git a/extensions/BugModal/lib/ActivityStream.pm b/extensions/BugModal/lib/ActivityStream.pm index 098c5df33..a7983e85c 100644 --- a/extensions/BugModal/lib/ActivityStream.pm +++ b/extensions/BugModal/lib/ActivityStream.pm @@ -49,310 +49,324 @@ use Bugzilla::Constants; # ] sub activity_stream { - my ($self) = @_; - if (!$self->{activity_stream}) { - my $stream = []; - _add_comments_to_stream($self, $stream); - _add_activities_to_stream($self, $stream); - _add_duplicates_to_stream($self, $stream); - - my $base_time = date_str_to_time($self->creation_ts); - foreach my $change_set (@$stream) { - $change_set->{id} = $change_set->{comment} - ? 'c' . $change_set->{comment}->count - : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; - foreach my $activity (@{ $change_set->{activity} }) { - $activity->{changes} = [ - sort { $a->{fieldname} cmp $b->{fieldname} } - @{ $activity->{changes} } - ]; - } - } - my $order = Bugzilla->user->setting('comment_sort_order'); - if ($order eq 'oldest_to_newest') { - $self->{activity_stream} = [ sort { $a->{time} <=> $b->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest') { - $self->{activity_stream} = [ sort { $b->{time} <=> $a->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest_desc_first') { - my $desc = shift @$stream; - $self->{activity_stream} = [ $desc, sort { $b->{time} <=> $a->{time} } @$stream ]; - } + my ($self) = @_; + if (!$self->{activity_stream}) { + my $stream = []; + _add_comments_to_stream($self, $stream); + _add_activities_to_stream($self, $stream); + _add_duplicates_to_stream($self, $stream); + + my $base_time = date_str_to_time($self->creation_ts); + foreach my $change_set (@$stream) { + $change_set->{id} + = $change_set->{comment} + ? 'c' . $change_set->{comment}->count + : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; + foreach my $activity (@{$change_set->{activity}}) { + $activity->{changes} + = [sort { $a->{fieldname} cmp $b->{fieldname} } @{$activity->{changes}}]; + } + } + my $order = Bugzilla->user->setting('comment_sort_order'); + if ($order eq 'oldest_to_newest') { + $self->{activity_stream} = [sort { $a->{time} <=> $b->{time} } @$stream]; } - return $self->{activity_stream}; + elsif ($order eq 'newest_to_oldest') { + $self->{activity_stream} = [sort { $b->{time} <=> $a->{time} } @$stream]; + } + elsif ($order eq 'newest_to_oldest_desc_first') { + my $desc = shift @$stream; + $self->{activity_stream} = [$desc, sort { $b->{time} <=> $a->{time} } @$stream]; + } + } + return $self->{activity_stream}; } sub find_activity_id_for_attachment { - my ($self, $attachment) = @_; - my $attach_id = $attachment->id; - my $stream = $self->activity_stream; - foreach my $change_set (@$stream) { - next unless exists $change_set->{attach_id}; - return $change_set->{id} if $change_set->{attach_id} == $attach_id; - } - return undef; + my ($self, $attachment) = @_; + my $attach_id = $attachment->id; + my $stream = $self->activity_stream; + foreach my $change_set (@$stream) { + next unless exists $change_set->{attach_id}; + return $change_set->{id} if $change_set->{attach_id} == $attach_id; + } + return undef; } sub find_activity_id_for_flag { - my ($self, $flag) = @_; - my $flagtype_name = $flag->type->name; - my $date = $flag->modification_date; - my $setter_id = $flag->setter->id; - my $stream = $self->activity_stream; - - # unfortunately bugs_activity treats all flag changes as the same field, so - # we don't have an object_id to match on - - if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { - foreach my $change_set (reverse @$stream) { - foreach my $activity (@{ $change_set->{activity} }) { - # match by user, timestamp, and flag-type name - next unless - $activity->{who}->id == $setter_id - && $activity->{when} eq $date; - foreach my $change (@{ $activity->{changes} }) { - next unless - $change->{fieldname} eq 'flagtypes.name' - && $change->{flagtype_name} eq $flagtype_name; - $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; - return $change_set->{id}; - } - } + my ($self, $flag) = @_; + my $flagtype_name = $flag->type->name; + my $date = $flag->modification_date; + my $setter_id = $flag->setter->id; + my $stream = $self->activity_stream; + + # unfortunately bugs_activity treats all flag changes as the same field, so + # we don't have an object_id to match on + + if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { + foreach my $change_set (reverse @$stream) { + foreach my $activity (@{$change_set->{activity}}) { + + # match by user, timestamp, and flag-type name + next unless $activity->{who}->id == $setter_id && $activity->{when} eq $date; + foreach my $change (@{$activity->{changes}}) { + next + unless $change->{fieldname} eq 'flagtypes.name' + && $change->{flagtype_name} eq $flagtype_name; + $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; + return $change_set->{id}; } - # if we couldn't find the flag in bugs_activity it means it was set - # during bug creation - $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } } - return $self->{activity_cache}->{flag}->{$flag->id}; + + # if we couldn't find the flag in bugs_activity it means it was set + # during bug creation + $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } + return $self->{activity_cache}->{flag}->{$flag->id}; } # comments are processed first, so there's no need to merge into existing entries sub _add_comment_to_stream { - my ($stream, $time, $user_id, $comment) = @_; - my $rh = { - time => $time, - user_id => $user_id, - comment => $comment, - activity => [], - }; - if ($comment->type == CMT_ATTACHMENT_CREATED || $comment->type == CMT_ATTACHMENT_UPDATED) { - $rh->{attach_id} = $comment->extra_data; - } - push @$stream, $rh; + my ($stream, $time, $user_id, $comment) = @_; + my $rh + = {time => $time, user_id => $user_id, comment => $comment, activity => [],}; + if ( $comment->type == CMT_ATTACHMENT_CREATED + || $comment->type == CMT_ATTACHMENT_UPDATED) + { + $rh->{attach_id} = $comment->extra_data; + } + push @$stream, $rh; } sub _add_activity_to_stream { - my ($stream, $time, $user_id, $data) = @_; - foreach my $entry (@$stream) { - next unless $entry->{time} == $time && $entry->{user_id} == $user_id; - $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; - push @{ $entry->{activity} }, $data; - return; - } - push @$stream, { - time => $time, - user_id => $user_id, - comment => undef, - cc_only => $data->{cc_only}, - activity => [ $data ], + my ($stream, $time, $user_id, $data) = @_; + foreach my $entry (@$stream) { + next unless $entry->{time} == $time && $entry->{user_id} == $user_id; + $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; + push @{$entry->{activity}}, $data; + return; + } + push @$stream, + { + time => $time, + user_id => $user_id, + comment => undef, + cc_only => $data->{cc_only}, + activity => [$data], }; } sub _add_comments_to_stream { - my ($bug, $stream) = @_; - my $user = Bugzilla->user; - my $treeherder_id = Bugzilla->treeherder_user->id; - - my $raw_comments = $bug->comments(); - foreach my $comment (@$raw_comments) { - next if $comment->type == CMT_HAS_DUPE; - my $author_id = $comment->author->id; - next if $comment->is_private && !($user->is_insider || $user->id == $author_id); - next if $comment->body eq '' && ($comment->work_time - 0) != 0 && $user->is_timetracker; - - # treeherder is so spammy we hide its comments by default - if ($author_id == $treeherder_id) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = $comment->author->name; - } - if ($comment->type != CMT_ATTACHMENT_CREATED && $comment->count == 0 && length($comment->body) == 0) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = 'empty'; - } - # If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body - if ($comment->type == CMT_DUPE_OF) { - $comment->set_type(0); - # Skip if user did not supply comment also - next if $comment->body eq ''; - } - - _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), $comment->author->id, $comment); + my ($bug, $stream) = @_; + my $user = Bugzilla->user; + my $treeherder_id = Bugzilla->treeherder_user->id; + + my $raw_comments = $bug->comments(); + foreach my $comment (@$raw_comments) { + next if $comment->type == CMT_HAS_DUPE; + my $author_id = $comment->author->id; + next if $comment->is_private && !($user->is_insider || $user->id == $author_id); + next + if $comment->body eq '' + && ($comment->work_time - 0) != 0 + && $user->is_timetracker; + + # treeherder is so spammy we hide its comments by default + if ($author_id == $treeherder_id) { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = $comment->author->name; } -} - -sub _add_activities_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # build bug activity - my ($raw_activity) = $bug->can('get_activity') - ? $bug->get_activity() - : Bugzilla::Bug::GetBugActivity($bug->id); - - # allow other extensions to alter history - Bugzilla::Hook::process('inline_history_activitiy', { activity => $raw_activity }); - - my %attachment_cache; - foreach my $attachment (@{$bug->attachments}) { - $attachment_cache{$attachment->id} = $attachment; + if ( $comment->type != CMT_ATTACHMENT_CREATED + && $comment->count == 0 + && length($comment->body) == 0) + { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = 'empty'; } - # build a list of bugs we need to check visibility of, so we can check with a single query - my %visible_bug_ids; +# If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body + if ($comment->type == CMT_DUPE_OF) { + $comment->set_type(0); - # envelope, augment and tweak - foreach my $operation (@$raw_activity) { - - # make operation.who an object - $operation->{who} = Bugzilla::User->new({ name => $operation->{who}, cache => 1 }); - - # we need to track operations which are just cc changes - $operation->{cc_only} = 1; - - for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { - my $change = $operation->{changes}->[$i]; - - # make an attachment object - if ($change->{attachid}) { - $change->{attach} = $attachment_cache{$change->{attachid}}; - } - - # empty resolutions are displayed as --- by default - # make it explicit here to enable correct display of the change - if ($change->{fieldname} eq 'resolution') { - $change->{removed} = '---' if $change->{removed} eq ''; - $change->{added} = '---' if $change->{added} eq ''; - } - - # make boolean fields true/false instead of 1/0 - my ($table, $field) = ('bugs', $change->{fieldname}); - if ($field =~ /^([^\.]+)\.(.+)$/) { - ($table, $field) = ($1, $2); - } - my $column = $dbh->bz_column_info($table, $field); - if ($column && $column->{TYPE} eq 'BOOLEAN') { - $change->{removed} = ''; - $change->{added} = $change->{added} ? 'true' : 'false'; - } - - # load field object (only required for custom fields), and set the - # field type for custom fields - my $field_obj; - if ($change->{fieldname} =~ /^cf_/) { - $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 }); - $change->{fieldtype} = $field_obj->type; - } + # Skip if user did not supply comment also + next if $comment->body eq ''; + } - # identify buglist changes - if ($change->{fieldname} eq 'blocked' || - $change->{fieldname} eq 'dependson' || - $change->{fieldname} eq 'dupe' || - ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID) - ) { - $change->{buglist} = 1; - foreach my $what (qw(removed added)) { - my @buglist = split(/[\s,]+/, $change->{$what}); - foreach my $id (@buglist) { - if ($id && $id =~ /^\d+$/) { - $visible_bug_ids{$id} = 1; - } - } - } - } + _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), + $comment->author->id, $comment); + } +} - # split see-also - if ($change->{fieldname} eq 'see_also') { - my $url_base = Bugzilla->localconfig->{urlbase}; - foreach my $f (qw( added removed )) { - my @values; - foreach my $value (split(/, /, $change->{$f})) { - my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base - ? $value =~ /id=(\d+)$/ - : undef; - push @values, { - url => $value, - bug_id => $bug_id, - }; - } - $change->{$f} = \@values; - } +sub _add_activities_to_stream { + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # build bug activity + my ($raw_activity) + = $bug->can('get_activity') + ? $bug->get_activity() + : Bugzilla::Bug::GetBugActivity($bug->id); + + # allow other extensions to alter history + Bugzilla::Hook::process('inline_history_activitiy', + {activity => $raw_activity}); + + my %attachment_cache; + foreach my $attachment (@{$bug->attachments}) { + $attachment_cache{$attachment->id} = $attachment; + } + +# build a list of bugs we need to check visibility of, so we can check with a single query + my %visible_bug_ids; + + # envelope, augment and tweak + foreach my $operation (@$raw_activity) { + + # make operation.who an object + $operation->{who} + = Bugzilla::User->new({name => $operation->{who}, cache => 1}); + + # we need to track operations which are just cc changes + $operation->{cc_only} = 1; + + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = $attachment_cache{$change->{attachid}}; + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + # load field object (only required for custom fields), and set the + # field type for custom fields + my $field_obj; + if ($change->{fieldname} =~ /^cf_/) { + $field_obj = Bugzilla::Field->new({name => $change->{fieldname}, cache => 1}); + $change->{fieldtype} = $field_obj->type; + } + + # identify buglist changes + if ( $change->{fieldname} eq 'blocked' + || $change->{fieldname} eq 'dependson' + || $change->{fieldname} eq 'dupe' + || ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)) + { + $change->{buglist} = 1; + foreach my $what (qw(removed added)) { + my @buglist = split(/[\s,]+/, $change->{$what}); + foreach my $id (@buglist) { + if ($id && $id =~ /^\d+$/) { + $visible_bug_ids{$id} = 1; } + } + } + } + + # split see-also + if ($change->{fieldname} eq 'see_also') { + my $url_base = Bugzilla->localconfig->{urlbase}; + foreach my $f (qw( added removed )) { + my @values; + foreach my $value (split(/, /, $change->{$f})) { + my ($bug_id) + = substr($value, 0, length($url_base)) eq $url_base + ? $value =~ /id=(\d+)$/ + : undef; + push @values, {url => $value, bug_id => $bug_id,}; + } + $change->{$f} = \@values; + } + } + + # track cc-only + if ($change->{fieldname} ne 'cc') { + $operation->{cc_only} = 0; + } + + # split multiple flag changes (must be processed last) + # set $change->{flagtype_name} to make searching the activity + # stream for flag changes easier and quicker + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + if (scalar(@added) <= 1 && scalar(@removed) <= 1) { + $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); + next; + } - # track cc-only - if ($change->{fieldname} ne 'cc') { - $operation->{cc_only} = 0; - } + # remove current change + splice(@{$operation->{changes}}, $i, 1); - # split multiple flag changes (must be processed last) - # set $change->{flagtype_name} to make searching the activity - # stream for flag changes easier and quicker - if ($change->{fieldname} eq 'flagtypes.name') { - my @added = split(/, /, $change->{added}); - my @removed = split(/, /, $change->{removed}); - if (scalar(@added) <= 1 && scalar(@removed) <= 1) { - $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); - next; - } - # remove current change - splice(@{$operation->{changes}}, $i, 1); - # restructure into added/removed for each flag - my %flags; - foreach my $flag (@added) { - $flags{$flag}{added} = $flag; - $flags{$flag}{removed} = ''; - } - foreach my $flag (@removed) { - $flags{$flag}{added} = ''; - $flags{$flag}{removed} = $flag; - } - # clone current change, modify and insert - foreach my $flag (sort keys %flags) { - my $flag_change = {}; - foreach my $key (keys %$change) { - $flag_change->{$key} = $change->{$key}; - } - $flag_change->{removed} = $flags{$flag}{removed}; - $flag_change->{added} = $flags{$flag}{added}; - $flag_change->{flagtype_name} = _extract_flagtype($flag); - splice(@{$operation->{changes}}, $i, 0, $flag_change); - } - $i--; - } + # restructure into added/removed for each flag + my %flags; + foreach my $flag (@added) { + $flags{$flag}{added} = $flag; + $flags{$flag}{removed} = ''; + } + foreach my $flag (@removed) { + $flags{$flag}{added} = ''; + $flags{$flag}{removed} = $flag; } - _add_activity_to_stream($stream, date_str_to_time($operation->{when}), $operation->{who}->id, $operation); + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + $flag_change->{flagtype_name} = _extract_flagtype($flag); + splice(@{$operation->{changes}}, $i, 0, $flag_change); + } + $i--; + } } - # prime the visible-bugs cache - $user->visible_bugs([keys %visible_bug_ids]); + _add_activity_to_stream( + $stream, + date_str_to_time($operation->{when}), + $operation->{who}->id, $operation + ); + } + + # prime the visible-bugs cache + $user->visible_bugs([keys %visible_bug_ids]); } sub _extract_flagtype { - my ($value) = @_; - return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; + my ($value) = @_; + return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; } # display 'duplicate of this bug' as an activity entry, not a comment sub _add_duplicates_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT longdescs.who, - UNIX_TIMESTAMP(bug_when), " . - $dbh->sql_date_format('bug_when') . ", + UNIX_TIMESTAMP(bug_when), " . $dbh->sql_date_format('bug_when') . ", type, extra_data FROM longdescs @@ -360,19 +374,22 @@ sub _add_duplicates_to_stream { WHERE bug_id = ? AND (type = ? OR type = ?) ORDER BY bug_when "); - $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); - - while (my($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { - _add_activity_to_stream($stream, $time, $who, { - who => Bugzilla::User->new({ id => $who, cache => 1 }), - when => $when, - changes => [{ - fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), - added => $dupe_id, - buglist => 1, - }], - }); - } + $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); + + while (my ($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { + _add_activity_to_stream( + $stream, $time, $who, + { + who => Bugzilla::User->new({id => $who, cache => 1}), + when => $when, + changes => [{ + fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), + added => $dupe_id, + buglist => 1, + }], + } + ); + } } 1; diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm index 54bd6e560..042dabc38 100644 --- a/extensions/BugModal/lib/MonkeyPatches.pm +++ b/extensions/BugModal/lib/MonkeyPatches.pm @@ -17,10 +17,10 @@ use warnings; use Bugzilla::User; sub treeherder_user { - return Bugzilla->process_cache->{treeherder_user} //= - Bugzilla::User->new({ name => 'tbplbot@gmail.com', cache => 1 }) - || Bugzilla::User->new({ name => 'orangefactor@bots.tld', cache => 1 }) - || Bugzilla::User->new(); + return Bugzilla->process_cache->{treeherder_user} + //= Bugzilla::User->new({name => 'tbplbot@gmail.com', cache => 1}) + || Bugzilla::User->new({name => 'orangefactor@bots.tld', cache => 1}) + || Bugzilla::User->new(); } package Bugzilla::Bug; @@ -32,10 +32,11 @@ use warnings; use Bugzilla::Attachment; sub active_attachments { - my ($self) = @_; - return [] if $self->{error}; - return $self->{active_attachments} //= Bugzilla::Attachment->get_attachments_by_bug( - $self, { exclude_obsolete => 1, preload => 1 }); + my ($self) = @_; + return [] if $self->{error}; + return $self->{active_attachments} + //= Bugzilla::Attachment->get_attachments_by_bug($self, + {exclude_obsolete => 1, preload => 1}); } 1; @@ -47,8 +48,8 @@ use strict; use warnings; sub is_image { - my ($self) = @_; - return substr($self->contenttype, 0, 6) eq 'image/'; + my ($self) = @_; + return substr($self->contenttype, 0, 6) eq 'image/'; } 1; diff --git a/extensions/BugModal/lib/Util.pm b/extensions/BugModal/lib/Util.pm index 6a453159e..b1d7068d8 100644 --- a/extensions/BugModal/lib/Util.pm +++ b/extensions/BugModal/lib/Util.pm @@ -21,19 +21,21 @@ use DateTime::TimeZone; use Time::Local qw(timelocal); sub date_str_to_time { - my ($date) = @_; - # avoid creating a DateTime object - if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { - return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); - } - state $tz //= DateTime::TimeZone->new( name => 'local' ); - my $dt = datetime_from($date, $tz); - if (!$dt) { - # this should never happen - warn("invalid datetime '$date'"); - return undef; - } - return $dt->epoch; + my ($date) = @_; + + # avoid creating a DateTime object + if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { + return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); + } + state $tz //= DateTime::TimeZone->new(name => 'local'); + my $dt = datetime_from($date, $tz); + if (!$dt) { + + # this should never happen + warn("invalid datetime '$date'"); + return undef; + } + return $dt->epoch; } 1; diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm index b69d609dd..5f3308327 100644 --- a/extensions/BugModal/lib/WebService.pm +++ b/extensions/BugModal/lib/WebService.pm @@ -27,353 +27,357 @@ use Taint::Util qw(untaint); # these methods are much lighter than our public API calls sub rest_resources { - return [ - # return all the products accessible by the user. - # required by new-bug - qr{^/bug_modal/initial_field_values}, { - GET => { - method => 'initial_field_values' - }, + return [ + # return all the products accessible by the user. + # required by new-bug + qr{^/bug_modal/initial_field_values}, + {GET => {method => 'initial_field_values'},}, + + # return all the components pertaining to the product. + # required by new-bug + qr{^/bug_modal/product_info}, + { + GET => { + method => 'product_info', + params => sub { + return {product_name => Bugzilla->input_params->{product}}; }, + }, + }, - # return all the components pertaining to the product. - # required by new-bug - qr{^/bug_modal/product_info}, { - GET => { - method => 'product_info', - params => sub { - return { product_name => Bugzilla->input_params->{product} } - }, - }, + # return all the lazy-loaded data; kept in sync with the UI's + # requirements. + qr{^/bug_modal/edit/(\d+)$}, + { + GET => { + method => 'edit', + params => sub { + return {id => $_[0]}; }, + }, + }, - # return all the lazy-loaded data; kept in sync with the UI's - # requirements. - qr{^/bug_modal/edit/(\d+)$}, { - GET => { - method => 'edit', - params => sub { - return { id => $_[0] } - }, - }, + # returns pre-formatted html, enabling reuse of the user template + qr{^/bug_modal/cc/(\d+)$}, + { + GET => { + method => 'cc', + params => sub { + return {id => $_[0]}; }, + }, + }, - # returns pre-formatted html, enabling reuse of the user template - qr{^/bug_modal/cc/(\d+)$}, { - GET => { - method => 'cc', - params => sub { - return { id => $_[0] } - }, - }, - }, + # returns fields that require touching when the product is changed + qw{^/bug_modal/new_product/(\d+)$}, + { + GET => { + method => 'new_product', + params => sub { - # returns fields that require touching when the product is changed - qw{^/bug_modal/new_product/(\d+)$}, { - GET => { - method => 'new_product', - params => sub { - # products with slashes in their name means we have to grab - # the product from the query-string instead of the path - return { id => $_[0], product_name => Bugzilla->input_params->{product} } - }, - }, + # products with slashes in their name means we have to grab + # the product from the query-string instead of the path + return {id => $_[0], product_name => Bugzilla->input_params->{product}}; }, - ] + }, + }, + ]; } sub initial_field_values { - my $user = Bugzilla->user; - return { - products => _name($user->get_enterable_products), - keywords => _name([Bugzilla::Keyword->get_all()]), - }; + my $user = Bugzilla->user; + return { + products => _name($user->get_enterable_products), + keywords => _name([Bugzilla::Keyword->get_all()]), + }; } sub product_info { - my ( $self, $params ) = @_; - if ( !ref $params->{product_name} ) { - untaint( $params->{product_name} ); - } - else { - ThrowCodeError( 'params_required', { function => 'BugModal.components', params => ['product'] } ); - } - my $product = Bugzilla::Product->check( { name => $params->{product_name}, cache => 1 } ); - $product = Bugzilla->user->can_enter_product( $product, 1 ); - my @components = map { - { - name => $_->name, - description => $_->description, - } - } @{ $product->components }; - return { - components => \@components, - versions => _name($product->versions), - }; + my ($self, $params) = @_; + if (!ref $params->{product_name}) { + untaint($params->{product_name}); + } + else { + ThrowCodeError('params_required', + {function => 'BugModal.components', params => ['product']}); + } + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + $product = Bugzilla->user->can_enter_product($product, 1); + my @components = map { {name => $_->name, description => $_->description,} } + @{$product->components}; + return {components => \@components, versions => _name($product->versions),}; } # everything we need for edit mode in a single call, returning just the fields # that the ui requires. sub edit { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - - # the keys of the options hash must match the field id in the ui - my %options; - - my @products = @{ $user->get_enterable_products }; - unless (grep { $_->id == $bug->product_id } @products) { - unshift @products, $bug->product_obj; - } - $options{product} = [ map { { name => $_->name } } @products ]; - - $options{component} = _name($bug->product_obj->components, $bug->component); - $options{version} = _name($bug->product_obj->versions, $bug->version); - $options{target_milestone} = _name($bug->product_obj->milestones, $bug->target_milestone); - $options{priority} = _name('priority', $bug->priority); - $options{bug_severity} = _name('bug_severity', $bug->bug_severity); - $options{rep_platform} = _name('rep_platform', $bug->rep_platform); - $options{op_sys} = _name('op_sys', $bug->op_sys); - - # custom select fields - my @custom_fields = - grep { $_->type == FIELD_TYPE_SINGLE_SELECT || $_->type == FIELD_TYPE_MULTI_SELECT } - Bugzilla->active_custom_fields({ product => $bug->product_obj, component => $bug->component_obj }); - foreach my $field (@custom_fields) { - my $field_name = $field->name; - my @values = map { { name => $_->name } } - grep { $bug->$field_name eq $_->name - || ($_->is_active - && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) } - @{ $field->legal_values }; - $options{$field_name} = \@values; - } - - # keywords - my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); - - # results - return { - options => \%options, - keywords => [ map { $_->name } @keywords ], - }; + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + + # the keys of the options hash must match the field id in the ui + my %options; + + my @products = @{$user->get_enterable_products}; + unless (grep { $_->id == $bug->product_id } @products) { + unshift @products, $bug->product_obj; + } + $options{product} = [map { {name => $_->name} } @products]; + + $options{component} = _name($bug->product_obj->components, $bug->component); + $options{version} = _name($bug->product_obj->versions, $bug->version); + $options{target_milestone} + = _name($bug->product_obj->milestones, $bug->target_milestone); + $options{priority} = _name('priority', $bug->priority); + $options{bug_severity} = _name('bug_severity', $bug->bug_severity); + $options{rep_platform} = _name('rep_platform', $bug->rep_platform); + $options{op_sys} = _name('op_sys', $bug->op_sys); + + # custom select fields + my @custom_fields = grep { + $_->type == FIELD_TYPE_SINGLE_SELECT + || $_->type == FIELD_TYPE_MULTI_SELECT + } Bugzilla->active_custom_fields( + {product => $bug->product_obj, component => $bug->component_obj}); + foreach my $field (@custom_fields) { + my $field_name = $field->name; + my @values = map { {name => $_->name} } grep { + $bug->$field_name eq $_->name + || ($_->is_active + && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) + } @{$field->legal_values}; + $options{$field_name} = \@values; + } + + # keywords + my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); + + # results + return {options => \%options, keywords => [map { $_->name } @keywords],}; } sub _name { - my ($values, $current) = @_; - # values can either be an array-ref of values, or a field name, which - # result in that field's legal-values being used. - if (!ref($values)) { - $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values; - } - return [ - map { { name => $_->name } } - grep { (defined $current && $_->name eq $current) || $_->is_active } - @$values - ]; + my ($values, $current) = @_; + + # values can either be an array-ref of values, or a field name, which + # result in that field's legal-values being used. + if (!ref($values)) { + $values = Bugzilla::Field->new({name => $values, cache => 1})->legal_values; + } + return [map { {name => $_->name} } + grep { (defined $current && $_->name eq $current) || $_->is_active } + @$values]; } sub cc { - my ($self, $params) = @_; - my $template = Bugzilla->template; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $vars = { - bug => $bug, - cc_list => [ - sort { lc($a->identity) cmp lc($b->identity) } - @{ $bug->cc_users } - ] - }; - - my $html = ''; - $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) - || ThrowTemplateError($template->error); - return { html => $html }; + my ($self, $params) = @_; + my $template = Bugzilla->template; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $vars = { + bug => $bug, + cc_list => [sort { lc($a->identity) cmp lc($b->identity) } @{$bug->cc_users}] + }; + + my $html = ''; + $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) + || ThrowTemplateError($template->error); + return {html => $html}; } sub new_product { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $product = Bugzilla::Product->check({ name => $params->{product_name}, cache => 1 }); - my $true = $self->type('boolean', 1); - my %result; - - # components - - my $components = _name($product->components); - my $current_component = $bug->component; - if (my $component = first_value { $_->{name} eq $current_component} @$components) { - # identical component in both products - $component->{selected} = $true; - } - else { - # default to a blank value - unshift @$components, { - name => '', - selected => $true, - }; - } - $result{component} = $components; - - # milestones - - my $milestones = _name($product->milestones); - my $current_milestone = $bug->target_milestone; - if ($bug->check_can_change_field('target_milestone', 0, 1) - && (my $milestone = first_value { $_->{name} eq $current_milestone} @$milestones)) - { - # identical milestone in both products - $milestone->{selected} = $true; - } - else { - # use default milestone - my $default_milestone = $product->default_milestone; - my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; - $milestone->{selected} = $true; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + my $true = $self->type('boolean', 1); + my %result; + + # components + + my $components = _name($product->components); + my $current_component = $bug->component; + if (my $component + = first_value { $_->{name} eq $current_component } @$components) + { + # identical component in both products + $component->{selected} = $true; + } + else { + # default to a blank value + unshift @$components, {name => '', selected => $true,}; + } + $result{component} = $components; + + # milestones + + my $milestones = _name($product->milestones); + my $current_milestone = $bug->target_milestone; + if ($bug->check_can_change_field('target_milestone', 0, 1) + && (my $milestone + = first_value { $_->{name} eq $current_milestone } @$milestones)) + { + # identical milestone in both products + $milestone->{selected} = $true; + } + else { + # use default milestone + my $default_milestone = $product->default_milestone; + my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; + $milestone->{selected} = $true; + } + $result{target_milestone} = $milestones; + + # versions + + my $versions = _name($product->versions); + my $current_version = $bug->version; + my $selected_version; + if (my $version = first_value { $_->{name} eq $current_version } @$versions) { + + # identical version in both products + $version->{selected} = $true; + $selected_version = $version; + } + elsif ($current_version =~ /^(\d+) Branch$/ + || $current_version =~ /^Firefox (\d+)$/ + || $current_version =~ /^(\d+)$/) + { + # firefox, with its three version naming schemes + my $branch = $1; + foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { + if (my $version = first_value { $_->{name} eq $test_version } @$versions) { + $version->{selected} = $true; + $selected_version = $version; + last; + } } - $result{target_milestone} = $milestones; - - # versions + } + if (!$selected_version) { - my $versions = _name($product->versions); - my $current_version = $bug->version; - my $selected_version; - if (my $version = first_value { $_->{name} eq $current_version } @$versions) { - # identical version in both products + # "unspecified", "other" + foreach my $test_version ("unspecified", "other") { + if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { $version->{selected} = $true; $selected_version = $version; + last; + } } - elsif ( - $current_version =~ /^(\d+) Branch$/ - || $current_version =~ /^Firefox (\d+)$/ - || $current_version =~ /^(\d+)$/) + } + if (!$selected_version) { + + # default to a blank value + unshift @$versions, {name => '', selected => $true,}; + } + $result{version} = $versions; + + # groups + + my @groups; + + # find invalid groups + push @groups, + map { {type => 'invalid', group => $_, checked => 0,} } + @{Bugzilla::Bug->get_invalid_groups( + {bug_ids => [$bug->id], product => $product})}; + + # logic lifted from bug/process/verify-new-product.html.tmpl + my $current_groups = $bug->groups_in; + my $group_controls = $product->group_controls; + foreach my $group_id (keys %$group_controls) { + my $group_control = $group_controls->{$group_id}; + if ( + $group_control->{membercontrol} == CONTROLMAPMANDATORY + || ($group_control->{othercontrol} == CONTROLMAPMANDATORY + && !$user->in_group($group_control->{name})) + ) { - # firefox, with its three version naming schemes - my $branch = $1; - foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { - if (my $version = first_value { $_->{name} eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # "unspecified", "other" - foreach my $test_version ("unspecified", "other") { - if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # default to a blank value - unshift @$versions, { - name => '', - selected => $true, - }; - } - $result{version} = $versions; - - # groups - - my @groups; - - # find invalid groups - push @groups, - map {{ - type => 'invalid', - group => $_, - checked => 0, - }} - @{ Bugzilla::Bug->get_invalid_groups({ bug_ids => [ $bug->id ], product => $product }) }; - - # logic lifted from bug/process/verify-new-product.html.tmpl - my $current_groups = $bug->groups_in; - my $group_controls = $product->group_controls; - foreach my $group_id (keys %$group_controls) { - my $group_control = $group_controls->{$group_id}; - if ($group_control->{membercontrol} == CONTROLMAPMANDATORY - || ($group_control->{othercontrol} == CONTROLMAPMANDATORY && !$user->in_group($group_control->{name}))) - { - # mandatory, always checked - push @groups, { - type => 'mandatory', - group => $group_control->{group}, - checked => 1, - }; - } - elsif ( - ($group_control->{membercontrol} != CONTROLMAPNA && $user->in_group($group_control->{name})) - || $group_control->{othercontrol} != CONTROLMAPNA) - { - # optional, checked if.. - my $group = $group_control->{group}; - my $checked = - # same group as current product - (any { $_->id == $group->id } @$current_groups) - # member default - || $group_control->{membercontrol} == CONTROLMAPDEFAULT && $user->in_group($group_control->{name}) - # or other default - || $group_control->{othercontrol} == CONTROLMAPDEFAULT && !$user->in_group($group_control->{name}) - ; - push @groups, { - type => 'optional', - group => $group_control->{group}, - checked => $checked || 0, - }; - } + # mandatory, always checked + push @groups, + {type => 'mandatory', group => $group_control->{group}, checked => 1,}; } + elsif ( + ( + $group_control->{membercontrol} != CONTROLMAPNA + && $user->in_group($group_control->{name}) + ) + || $group_control->{othercontrol} != CONTROLMAPNA + ) + { + # optional, checked if.. + my $group = $group_control->{group}; + my $checked = - my $default_group_name = $product->default_security_group; - if (my $default_group = first_value { $_->{group}->name eq $default_group_name } @groups) { - # because we always allow the default product group to be selected, it's never invalid - $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; - } - else { - # add the product's default group if it's missing - unshift @groups, { - type => 'optional', - group => $product->default_security_group_obj, - checked => 0, - }; - } + # same group as current product + (any { $_->id == $group->id } @$current_groups) - # if the bug is currently in a group, ensure a group is checked by default - # by checking the product's default group if no other groups apply - if (@$current_groups && !any { $_->{checked} } @groups) { - foreach my $g (@groups) { - next unless $g->{group}->name eq $default_group_name; - $g->{checked} = 1; - last; - } - } + # member default + || $group_control->{membercontrol} == CONTROLMAPDEFAULT + && $user->in_group($group_control->{name}) - # group by type and flatten - my $vars = { - product => $product, - groups => { invalid => [], mandatory => [], optional => [] }, - }; - foreach my $g (@groups) { - push @{ $vars->{groups}->{$g->{type}} }, { - id => $g->{group}->id, - name => $g->{group}->name, - description => $g->{group}->description, - checked => $g->{checked}, + # or other default + || $group_control->{othercontrol} == CONTROLMAPDEFAULT + && !$user->in_group($group_control->{name}); + push @groups, + { + type => 'optional', + group => $group_control->{group}, + checked => $checked || 0, }; } - - # build group selection html - my $template = Bugzilla->template; - $template->process('bug_modal/new_product_groups.html.tmpl', $vars, \$result{groups}) - || ThrowTemplateError($template->error); - - return \%result; + } + + my $default_group_name = $product->default_security_group; + if (my $default_group + = first_value { $_->{group}->name eq $default_group_name } @groups) + { +# because we always allow the default product group to be selected, it's never invalid + $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; + } + else { + # add the product's default group if it's missing + unshift @groups, + { + type => 'optional', + group => $product->default_security_group_obj, + checked => 0, + }; + } + + # if the bug is currently in a group, ensure a group is checked by default + # by checking the product's default group if no other groups apply + if (@$current_groups && !any { $_->{checked} } @groups) { + foreach my $g (@groups) { + next unless $g->{group}->name eq $default_group_name; + $g->{checked} = 1; + last; + } + } + + # group by type and flatten + my $vars = { + product => $product, + groups => {invalid => [], mandatory => [], optional => []}, + }; + foreach my $g (@groups) { + push @{$vars->{groups}->{$g->{type}}}, + { + id => $g->{group}->id, + name => $g->{group}->name, + description => $g->{group}->description, + checked => $g->{checked}, + }; + } + + # build group selection html + my $template = Bugzilla->template; + $template->process('bug_modal/new_product_groups.html.tmpl', + $vars, \$result{groups}) + || ThrowTemplateError($template->error); + + return \%result; } 1; diff --git a/extensions/BugmailFilter/Config.pm b/extensions/BugmailFilter/Config.pm index 5948c3b64..5b9585cee 100644 --- a/extensions/BugmailFilter/Config.pm +++ b/extensions/BugmailFilter/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'BugmailFilter'; +use constant NAME => 'BugmailFilter'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/BugmailFilter/Extension.pm b/extensions/BugmailFilter/Extension.pm index d4d7bb790..063cb273d 100644 --- a/extensions/BugmailFilter/Extension.pm +++ b/extensions/BugmailFilter/Extension.pm @@ -34,185 +34,172 @@ use Sys::Syslog qw(:DEFAULT); # sub user_preferences { - my ($self, $args) = @_; - return unless $args->{current_tab} eq 'bugmail_filter'; - - if ($args->{save_changes}) { - my $input = Bugzilla->input_params; - - if ($input->{add_filter}) { - - # add a new filter - - my $params = { - user_id => Bugzilla->user->id, - }; - $params->{field_name} = $input->{field} || IS_NULL; - if ($params->{field_name} eq '~') { - $params->{field_name} = '~' . $input->{field_contains}; - } - $params->{relationship} = $input->{relationship} || IS_NULL; - if ($input->{changer}) { - Bugzilla::User::match_field({ changer => { type => 'single'} }); - $params->{changer_id} = Bugzilla::User->check({ - name => $input->{changer}, - cache => 1, - })->id; - } - else { - $params->{changer_id} = IS_NULL; - } - if (my $product_name = $input->{product}) { - my $product = Bugzilla::Product->check({ - name => $product_name, cache => 1 - }); - $params->{product_id} = $product->id; - - if (my $component_name = $input->{component}) { - $params->{component_id} = Bugzilla::Component->check({ - name => $component_name, product => $product, - cache => 1 - })->id; - } - else { - $params->{component_id} = IS_NULL; - } - } - else { - $params->{product_id} = IS_NULL; - $params->{component_id} = IS_NULL; - } - - if (@{ Bugzilla::Extension::BugmailFilter::Filter->match($params) }) { - ThrowUserError('bugmail_filter_exists'); - } - $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0; - foreach my $name (keys %$params) { - $params->{$name} = undef - if $params->{$name} eq IS_NULL; - } - Bugzilla::Extension::BugmailFilter::Filter->create($params); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'bugmail_filter'; + + if ($args->{save_changes}) { + my $input = Bugzilla->input_params; + + if ($input->{add_filter}) { + + # add a new filter + + my $params = {user_id => Bugzilla->user->id,}; + $params->{field_name} = $input->{field} || IS_NULL; + if ($params->{field_name} eq '~') { + $params->{field_name} = '~' . $input->{field_contains}; + } + $params->{relationship} = $input->{relationship} || IS_NULL; + if ($input->{changer}) { + Bugzilla::User::match_field({changer => {type => 'single'}}); + $params->{changer_id} + = Bugzilla::User->check({name => $input->{changer}, cache => 1,})->id; + } + else { + $params->{changer_id} = IS_NULL; + } + if (my $product_name = $input->{product}) { + my $product = Bugzilla::Product->check({name => $product_name, cache => 1}); + $params->{product_id} = $product->id; + + if (my $component_name = $input->{component}) { + $params->{component_id} + = Bugzilla::Component->check({ + name => $component_name, product => $product, cache => 1 + })->id; } - - elsif ($input->{remove_filter}) { - - # remove filter(s) - - my $ids = ref($input->{remove}) ? $input->{remove} : [ $input->{remove} ]; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - my $filters = Bugzilla::Extension::BugmailFilter::Filter->match({ id => $ids, user_id => $user->id }); - $dbh->bz_start_transaction; - foreach my $filter (@$filters) { - $filter->remove_from_db(); - } - $dbh->bz_commit_transaction; + else { + $params->{component_id} = IS_NULL; } + } + else { + $params->{product_id} = IS_NULL; + $params->{component_id} = IS_NULL; + } + + if (@{Bugzilla::Extension::BugmailFilter::Filter->match($params)}) { + ThrowUserError('bugmail_filter_exists'); + } + $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0; + foreach my $name (keys %$params) { + $params->{$name} = undef if $params->{$name} eq IS_NULL; + } + Bugzilla::Extension::BugmailFilter::Filter->create($params); } - my $vars = $args->{vars}; - my $field_descs = template_var('field_descs'); + elsif ($input->{remove_filter}) { - # load all fields into a hash for easy manipulation - my %fields = - map { $_->name => $field_descs->{$_->name} } - @{ Bugzilla->fields({ obsolete => 0 }) }; + # remove filter(s) - # remove time trackinger fields - if (!Bugzilla->user->is_timetracker) { - foreach my $field (TIMETRACKING_FIELDS) { - delete $fields{$field}; - } - } + my $ids = ref($input->{remove}) ? $input->{remove} : [$input->{remove}]; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # remove fields which don't make any sense to filter on - foreach my $field (IGNORE_FIELDS) { - delete $fields{$field}; + my $filters = Bugzilla::Extension::BugmailFilter::Filter->match( + {id => $ids, user_id => $user->id}); + $dbh->bz_start_transaction; + foreach my $filter (@$filters) { + $filter->remove_from_db(); + } + $dbh->bz_commit_transaction; } + } - # remove all tracking flag fields. these change too frequently to be of - # value, so they only add noise to the list. - foreach my $field (Bugzilla->tracking_flag_names) { - delete $fields{$field}; - } + my $vars = $args->{vars}; + my $field_descs = template_var('field_descs'); - # add tracking flag types instead - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() } - ) { - $fields{$field->name} = $field->description; - } + # load all fields into a hash for easy manipulation + my %fields = map { $_->name => $field_descs->{$_->name} } + @{Bugzilla->fields({obsolete => 0})}; - # adjust the description for selected fields - foreach my $field (keys %{ FIELD_DESCRIPTION_OVERRIDE() }) { - $fields{$field} = FIELD_DESCRIPTION_OVERRIDE->{$field}; + # remove time trackinger fields + if (!Bugzilla->user->is_timetracker) { + foreach my $field (TIMETRACKING_FIELDS) { + delete $fields{$field}; } - - # some fields are present in the changed-fields x-header but are not real - # bugzilla fields - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() } - ) { - $fields{$field->name} = $field->description; + } + + # remove fields which don't make any sense to filter on + foreach my $field (IGNORE_FIELDS) { + delete $fields{$field}; + } + + # remove all tracking flag fields. these change too frequently to be of + # value, so they only add noise to the list. + foreach my $field (Bugzilla->tracking_flag_names) { + delete $fields{$field}; + } + + # add tracking flag types instead + foreach my $field ( + @{Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields()}) + { + $fields{$field->name} = $field->description; + } + + # adjust the description for selected fields + foreach my $field (keys %{FIELD_DESCRIPTION_OVERRIDE()}) { + $fields{$field} = FIELD_DESCRIPTION_OVERRIDE->{$field}; + } + + # some fields are present in the changed-fields x-header but are not real + # bugzilla fields + foreach + my $field (@{Bugzilla::Extension::BugmailFilter::FakeField->fake_fields()}) + { + $fields{$field->name} = $field->description; + } + + $vars->{fields} = \%fields; + $vars->{field_list} = [sort { lc($a->{description}) cmp lc($b->{description}) } + map { {name => $_, description => $fields{$_}} } keys %fields]; + + $vars->{relationships} = FILTER_RELATIONSHIPS(); + + $vars->{filters} = [ + sort { + $a->product_name cmp $b->product_name + || $a->component_name cmp $b->component_name + || $a->field_name cmp $b->field_name + } @{Bugzilla::Extension::BugmailFilter::Filter->match({ + user_id => Bugzilla->user->id, + }) } + ]; - $vars->{fields} = \%fields; - $vars->{field_list} = [ - sort { lc($a->{description}) cmp lc($b->{description}) } - map { { name => $_, description => $fields{$_} } } - keys %fields - ]; - - $vars->{relationships} = FILTER_RELATIONSHIPS(); - - $vars->{filters} = [ - sort { - $a->product_name cmp $b->product_name - || $a->component_name cmp $b->component_name - || $a->field_name cmp $b->field_name - } - @{ Bugzilla::Extension::BugmailFilter::Filter->match({ - user_id => Bugzilla->user->id, - }) } - ]; - - # set field_description - foreach my $filter (@{ $vars->{filters} }) { - my $field_name = $filter->field_name; - if (!$field_name) { - $filter->field_description('Any'); - } - elsif (substr($field_name, 0, 1) eq '~') { - $filter->field_description('~ ' . substr($field_name, 1)); - } - else { - $filter->field_description($fields{$field_name} || $filter->field->description); - } + # set field_description + foreach my $filter (@{$vars->{filters}}) { + my $field_name = $filter->field_name; + if (!$field_name) { + $filter->field_description('Any'); } - - # build a list of tracking-flags, grouped by type - require Bugzilla::Extension::TrackingFlags::Constants; - require Bugzilla::Extension::TrackingFlags::Flag; - my %flag_types = - map { $_->{name} => $_->{description} } - @{ Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES() }; - my %tracking_flags_by_type; - foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all) { - my $type = $flag_types{$flag->flag_type}; - $tracking_flags_by_type{$type} //= []; - push @{ $tracking_flags_by_type{$type} }, $flag; + elsif (substr($field_name, 0, 1) eq '~') { + $filter->field_description('~ ' . substr($field_name, 1)); } - my @tracking_flags_by_type; - foreach my $type (sort keys %tracking_flags_by_type) { - push @tracking_flags_by_type, { - name => $type, - flags => $tracking_flags_by_type{$type}, - }; + else { + $filter->field_description($fields{$field_name} || $filter->field->description); } - $vars->{tracking_flags_by_type} = \@tracking_flags_by_type; - - ${ $args->{handled} } = 1; + } + + # build a list of tracking-flags, grouped by type + require Bugzilla::Extension::TrackingFlags::Constants; + require Bugzilla::Extension::TrackingFlags::Flag; + my %flag_types = map { $_->{name} => $_->{description} } + @{Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES()}; + my %tracking_flags_by_type; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all) { + my $type = $flag_types{$flag->flag_type}; + $tracking_flags_by_type{$type} //= []; + push @{$tracking_flags_by_type{$type}}, $flag; + } + my @tracking_flags_by_type; + foreach my $type (sort keys %tracking_flags_by_type) { + push @tracking_flags_by_type, + {name => $type, flags => $tracking_flags_by_type{$type},}; + } + $vars->{tracking_flags_by_type} = \@tracking_flags_by_type; + + ${$args->{handled}} = 1; } # @@ -220,219 +207,212 @@ sub user_preferences { # sub user_wants_mail { - my ($self, $args) = @_; - my ($user, $wants_mail, $diffs, $comments) - = @$args{qw( user wants_mail fieldDiffs comments )}; - - # already filtered by email prefs - return unless $$wants_mail; + my ($self, $args) = @_; + my ($user, $wants_mail, $diffs, $comments) + = @$args{qw( user wants_mail fieldDiffs comments )}; + + # already filtered by email prefs + return unless $$wants_mail; + + # avoid recursion + my $depth = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + $depth++ if $sub eq 'Bugzilla::User::wants_bug_mail'; + } + return if $depth > 1; + + my $cache = Bugzilla->request_cache->{bugmail_filters} //= {}; + my $filters = $cache->{$user->id} + //= Bugzilla::Extension::BugmailFilter::Filter->match({user_id => $user->id}); + return unless @$filters; + + my $fields = [ + map { { + filter_field => $_->{field_name}, # filter's field_name + field_name => $_->{field_name}, # raw bugzilla field_name + } } grep { + + # flags are added later + $_->{field_name} ne 'flagtypes.name' + } @$diffs + ]; + + # if more than one field was changed we need to check if the normal email + # preferences would have excluded the field. + if (@$fields > 1) { + + # check each field individually and create filter objects if required + my @arg_list + = @$args{qw( bug relationship fieldDiffs comments dep_mail changer )}; + foreach my $field (@$fields) { - # avoid recursion - my $depth = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - $depth++ if $sub eq 'Bugzilla::User::wants_bug_mail'; - } - return if $depth > 1; - - my $cache = Bugzilla->request_cache->{bugmail_filters} //= {}; - my $filters = $cache->{$user->id} //= - Bugzilla::Extension::BugmailFilter::Filter->match({ - user_id => $user->id - }); - return unless @$filters; - - my $fields = [ - map { { - filter_field => $_->{field_name}, # filter's field_name - field_name => $_->{field_name}, # raw bugzilla field_name - } } - grep { - # flags are added later - $_->{field_name} ne 'flagtypes.name' - } - @$diffs - ]; - - # if more than one field was changed we need to check if the normal email - # preferences would have excluded the field. - if (@$fields > 1) { - # check each field individually and create filter objects if required - my @arg_list = @$args{qw( bug relationship fieldDiffs comments dep_mail changer )}; - foreach my $field (@$fields) { - # just a single diff - foreach my $diff (@$diffs) { - next unless $diff->{field_name} eq $field->{field_name}; - $arg_list[2] = [ $diff ]; - last; - } - if (!$user->wants_bug_mail(@arg_list)) { - # changes to just this field would have been dropped by email - # preferences. build a corresponding filter object so we - # interact with email preferences correctly. - push @$filters, Bugzilla::Extension::BugmailFilter::Filter->new_from_hash({ - field_name => $field->{field_name}, - action => 1, - }); - } - } + # just a single diff + foreach my $diff (@$diffs) { + next unless $diff->{field_name} eq $field->{field_name}; + $arg_list[2] = [$diff]; + last; + } + if (!$user->wants_bug_mail(@arg_list)) { + + # changes to just this field would have been dropped by email + # preferences. build a corresponding filter object so we + # interact with email preferences correctly. + push @$filters, + Bugzilla::Extension::BugmailFilter::Filter->new_from_hash({ + field_name => $field->{field_name}, action => 1, + }); + } } + } - # insert fake fields for new attachments and comments - if (@$comments) { - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - push @$fields, { field_name => 'attachment.created', - filter_field => 'attachment.created' }; - } - if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) { - push @$fields, { field_name => 'comment.created', - filter_field => 'comment.created' }; - } + # insert fake fields for new attachments and comments + if (@$comments) { + if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { + push @$fields, + {field_name => 'attachment.created', filter_field => 'attachment.created'}; } - - # insert fake fields for flags - foreach my $diff (@$diffs) { - next unless $diff->{field_name} eq 'flagtypes.name'; - foreach my $change (split(/, /, join(', ', ($diff->{old}, $diff->{new})))) { - next unless $change =~ /^(.+)[\?\-+]/; - push @$fields, { - filter_field => $1, - field_name => 'flagtypes.name', - }; - } + if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) { + push @$fields, + {field_name => 'comment.created', filter_field => 'comment.created'}; } - - # set filter_field on tracking flags to tracking.$type - require Bugzilla::Extension::TrackingFlags::Flag; - my @tracking_flags = Bugzilla->tracking_flags; - foreach my $field (@$fields) { - next unless my $field_name = $field->{field_name}; - foreach my $tracking_flag (@tracking_flags) { - if ($field_name eq $tracking_flag->name) { - $field->{filter_field} = 'tracking.'. $tracking_flag->flag_type; - } - } + } + + # insert fake fields for flags + foreach my $diff (@$diffs) { + next unless $diff->{field_name} eq 'flagtypes.name'; + foreach my $change (split(/, /, join(', ', ($diff->{old}, $diff->{new})))) { + next unless $change =~ /^(.+)[\?\-+]/; + push @$fields, {filter_field => $1, field_name => 'flagtypes.name',}; } - - if (_should_drop($fields, $filters, $args)) { - $$wants_mail = 0; - openlog('apache', 'cons,pid', 'local4'); - syslog('notice', encode_utf8(sprintf( - '[bugmail] %s (filtered) bug-%s %s', - $args->{user}->login, - $args->{bug}->id, - $args->{bug}->short_desc, - ))); - closelog(); + } + + # set filter_field on tracking flags to tracking.$type + require Bugzilla::Extension::TrackingFlags::Flag; + my @tracking_flags = Bugzilla->tracking_flags; + foreach my $field (@$fields) { + next unless my $field_name = $field->{field_name}; + foreach my $tracking_flag (@tracking_flags) { + if ($field_name eq $tracking_flag->name) { + $field->{filter_field} = 'tracking.' . $tracking_flag->flag_type; + } } + } + + if (_should_drop($fields, $filters, $args)) { + $$wants_mail = 0; + openlog('apache', 'cons,pid', 'local4'); + syslog( + 'notice', + encode_utf8(sprintf( + '[bugmail] %s (filtered) bug-%s %s', + $args->{user}->login, + $args->{bug}->id, $args->{bug}->short_desc, + )) + ); + closelog(); + } } sub _should_drop { - my ($fields, $filters, $args) = @_; - - # calculate relationships - - my ($user, $bug, $relationship, $changer) = @$args{qw( user bug relationship changer )}; - my ($user_id, $login) = ($user->id, $user->login); - my $bit_direct = Bugzilla::BugMail::BIT_DIRECT; - my $bit_watching = Bugzilla::BugMail::BIT_WATCHING; - my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching - - # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS - my @rel_map; - $rel_map[1] = $bug->assigned_to->id == $user_id; - $rel_map[2] = !$rel_map[1]; - $rel_map[3] = $bug->reporter->id == $user_id; - $rel_map[4] = !$rel_map[3]; - if ($bug->qa_contact) { - $rel_map[5] = $bug->qa_contact->id == $user_id; - $rel_map[6] = !$rel_map[6]; + my ($fields, $filters, $args) = @_; + + # calculate relationships + + my ($user, $bug, $relationship, $changer) + = @$args{qw( user bug relationship changer )}; + my ($user_id, $login) = ($user->id, $user->login); + my $bit_direct = Bugzilla::BugMail::BIT_DIRECT; + my $bit_watching = Bugzilla::BugMail::BIT_WATCHING; + my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching + + # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS + my @rel_map; + $rel_map[1] = $bug->assigned_to->id == $user_id; + $rel_map[2] = !$rel_map[1]; + $rel_map[3] = $bug->reporter->id == $user_id; + $rel_map[4] = !$rel_map[3]; + if ($bug->qa_contact) { + $rel_map[5] = $bug->qa_contact->id == $user_id; + $rel_map[6] = !$rel_map[6]; + } + $rel_map[7] = $bug->cc ? grep { $_ eq $login } @{$bug->cc} : 0; + $rel_map[8] = !$rel_map[8]; + $rel_map[9] = ($relationship & $bit_watching or $relationship & $bit_compwatch); + $rel_map[10] = !$rel_map[9]; + $rel_map[11] = $bug->is_mentor($user); + $rel_map[12] = !$rel_map[11]; + foreach my $bool (@rel_map) { + $bool = $bool ? 1 : 0; + } + + # exclusions + # drop email where we are excluding all changed fields + + my $params = { + product_id => $bug->product_id, + component_id => $bug->component_id, + rel_map => \@rel_map, + changer_id => $changer->id, + }; + + foreach my $field (@$fields) { + $params->{field} = $field; + foreach my $filter (grep { $_->is_exclude } @$filters) { + if ($filter->matches($params)) { + $field->{exclude} = 1; + last; + } } - $rel_map[7] = $bug->cc - ? grep { $_ eq $login } @{ $bug->cc } - : 0; - $rel_map[8] = !$rel_map[8]; - $rel_map[9] = ( - $relationship & $bit_watching - or $relationship & $bit_compwatch - ); - $rel_map[10] = !$rel_map[9]; - $rel_map[11] = $bug->is_mentor($user); - $rel_map[12] = !$rel_map[11]; - foreach my $bool (@rel_map) { - $bool = $bool ? 1 : 0; - } - - # exclusions - # drop email where we are excluding all changed fields - - my $params = { - product_id => $bug->product_id, - component_id => $bug->component_id, - rel_map => \@rel_map, - changer_id => $changer->id, - }; - - foreach my $field (@$fields) { - $params->{field} = $field; - foreach my $filter (grep { $_->is_exclude } @$filters) { - if ($filter->matches($params)) { - $field->{exclude} = 1; - last; - } - } + } + + # no need to process includes if nothing was excluded + if (!grep { $_->{exclude} } @$fields) { + return 0; + } + + # inclusions + # flip the bit for fields that should be included + + foreach my $field (@$fields) { + $params->{field} = $field; + foreach my $filter (grep { $_->is_include } @$filters) { + if ($filter->matches($params)) { + $field->{exclude} = 0; + last; + } } + } - # no need to process includes if nothing was excluded - if (!grep { $_->{exclude} } @$fields) { - return 0; - } - - # inclusions - # flip the bit for fields that should be included - - foreach my $field (@$fields) { - $params->{field} = $field; - foreach my $filter (grep { $_->is_include } @$filters) { - if ($filter->matches($params)) { - $field->{exclude} = 0; - last; - } - } - } - - # drop if all fields are still excluded - return !(grep { !$_->{exclude} } @$fields); + # drop if all fields are still excluded + return !(grep { !$_->{exclude} } @$fields); } # catch when fields are renamed, and update the field_name entires sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; + my ($self, $args) = @_; + my $object = $args->{object}; - return unless $object->isa('Bugzilla::Field') - || $object->isa('Bugzilla::Extension::TrackingFlags::Flag'); + return + unless $object->isa('Bugzilla::Field') + || $object->isa('Bugzilla::Extension::TrackingFlags::Flag'); - return unless exists $args->{changes}->{name}; + return unless exists $args->{changes}->{name}; - my $old_name = $args->{changes}->{name}->[0]; - my $new_name = $args->{changes}->{name}->[1]; + my $old_name = $args->{changes}->{name}->[0]; + my $new_name = $args->{changes}->{name}->[1]; - Bugzilla->dbh->do( - "UPDATE bugmail_filters SET field_name=? WHERE field_name=?", - undef, - $new_name, $old_name); + Bugzilla->dbh->do("UPDATE bugmail_filters SET field_name=? WHERE field_name=?", + undef, $new_name, $old_name); } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE bugmail_filters SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE bugmail_filters SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } # @@ -440,92 +420,61 @@ sub reorg_move_component { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{schema}->{bugmail_filters} = { + my ($self, $args) = @_; + $args->{schema}->{bugmail_filters} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}, + }, + field_name => { + + # due to fake fields, this can't be field_id + TYPE => 'VARCHAR(64)', + NOTNULL => 0, + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}, + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'}, + }, + changer_id => { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}, + }, + relationship => {TYPE => 'INT2', NOTNULL => 0,}, + action => {TYPE => 'INT1', NOTNULL => 1,}, + ], + INDEXES => [ + bugmail_filters_unique_idx => { FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE' - }, - }, - field_name => { - # due to fake fields, this can't be field_id - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE' - }, - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE' - }, - }, - changer_id => { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE' - }, - }, - relationship => { - TYPE => 'INT2', - NOTNULL => 0, - }, - action => { - TYPE => 'INT1', - NOTNULL => 1, - }, + qw( user_id field_name product_id component_id + relationship ) ], - INDEXES => [ - bugmail_filters_unique_idx => { - FIELDS => [ qw( user_id field_name product_id component_id - relationship ) ], - TYPE => 'UNIQUE', - }, - bugmail_filters_user_idx => [ - 'user_id', - ], - ], - }; + TYPE => 'UNIQUE', + }, + bugmail_filters_user_idx => ['user_id',], + ], + }; } sub install_update_db { - Bugzilla->dbh->bz_add_column( - 'bugmail_filters', - 'changer_id', - { - TYPE => 'INT3', - NOTNULL => 0, - } - ); + Bugzilla->dbh->bz_add_column('bugmail_filters', 'changer_id', + {TYPE => 'INT3', NOTNULL => 0,}); } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting bugmail filters...\n"; - $dbh->do("DELETE FROM bugmail_filters"); + my $dbh = Bugzilla->dbh; + print "Deleting bugmail filters...\n"; + $dbh->do("DELETE FROM bugmail_filters"); } __PACKAGE__->NAME; diff --git a/extensions/BugmailFilter/lib/Constants.pm b/extensions/BugmailFilter/lib/Constants.pm index a6636dda7..bed8a37d3 100644 --- a/extensions/BugmailFilter/lib/Constants.pm +++ b/extensions/BugmailFilter/lib/Constants.pm @@ -14,10 +14,10 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FAKE_FIELD_NAMES - IGNORE_FIELDS - FIELD_DESCRIPTION_OVERRIDE - FILTER_RELATIONSHIPS + FAKE_FIELD_NAMES + IGNORE_FIELDS + FIELD_DESCRIPTION_OVERRIDE + FILTER_RELATIONSHIPS ); use Bugzilla::Constants; @@ -26,98 +26,54 @@ use Bugzilla::Constants; # header but are not real fields use constant FAKE_FIELD_NAMES => [ - { - name => 'comment.created', - description => 'Comment created', - }, - { - name => 'attachment.created', - description => 'Attachment created', - }, + {name => 'comment.created', description => 'Comment created',}, + {name => 'attachment.created', description => 'Attachment created',}, ]; # these fields don't make any sense to filter on use constant IGNORE_FIELDS => qw( - assignee_last_login - attach_data.thedata - attachments.submitter - cf_last_resolved - commenter - comment_tag - creation_ts - days_elapsed - delta_ts - everconfirmed - last_visit_ts - longdesc - longdescs.count - owner_idle_time - reporter - reporter_accessible - setters.login_name - tag - votes + assignee_last_login + attach_data.thedata + attachments.submitter + cf_last_resolved + commenter + comment_tag + creation_ts + days_elapsed + delta_ts + everconfirmed + last_visit_ts + longdesc + longdescs.count + owner_idle_time + reporter + reporter_accessible + setters.login_name + tag + votes ); # override the description of some fields -use constant FIELD_DESCRIPTION_OVERRIDE => { - bug_id => 'Bug Created', -}; +use constant FIELD_DESCRIPTION_OVERRIDE => {bug_id => 'Bug Created',}; # relationship / int mappings # _should_drop() also needs updating when this const is changed use constant FILTER_RELATIONSHIPS => [ - { - name => 'Assignee', - value => 1, - }, - { - name => 'Not Assignee', - value => 2, - }, - { - name => 'Reporter', - value => 3, - }, - { - name => 'Not Reporter', - value => 4, - }, - { - name => 'QA Contact', - value => 5, - }, - { - name => 'Not QA Contact', - value => 6, - }, - { - name => "CC'ed", - value => 7, - }, - { - name => "Not CC'ed", - value => 8, - }, - { - name => 'Watching', - value => 9, - }, - { - name => 'Not Watching', - value => 10, - }, - { - name => 'Mentoring', - value => 11, - }, - { - name => 'Not Mentoring', - value => 12, - }, + {name => 'Assignee', value => 1,}, + {name => 'Not Assignee', value => 2,}, + {name => 'Reporter', value => 3,}, + {name => 'Not Reporter', value => 4,}, + {name => 'QA Contact', value => 5,}, + {name => 'Not QA Contact', value => 6,}, + {name => "CC'ed", value => 7,}, + {name => "Not CC'ed", value => 8,}, + {name => 'Watching', value => 9,}, + {name => 'Not Watching', value => 10,}, + {name => 'Mentoring', value => 11,}, + {name => 'Not Mentoring', value => 12,}, ]; 1; diff --git a/extensions/BugmailFilter/lib/FakeField.pm b/extensions/BugmailFilter/lib/FakeField.pm index e9f8b1808..cf82aec85 100644 --- a/extensions/BugmailFilter/lib/FakeField.pm +++ b/extensions/BugmailFilter/lib/FakeField.pm @@ -16,8 +16,8 @@ use Bugzilla::Extension::BugmailFilter::Constants; # object sub new { - my ($class, $params) = @_; - return bless($params, $class); + my ($class, $params) = @_; + return bless($params, $class); } sub name { $_[0]->{name} } @@ -26,33 +26,35 @@ sub description { $_[0]->{description} } # static methods sub fake_fields { - my $cache = Bugzilla->request_cache->{bugmail_filter}; - if (!$cache->{fake_fields}) { - my @fields; - foreach my $rh (@{ FAKE_FIELD_NAMES() }) { - push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh); - } - $cache->{fake_fields} = \@fields; + my $cache = Bugzilla->request_cache->{bugmail_filter}; + if (!$cache->{fake_fields}) { + my @fields; + foreach my $rh (@{FAKE_FIELD_NAMES()}) { + push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh); } - return $cache->{fake_fields}; + $cache->{fake_fields} = \@fields; + } + return $cache->{fake_fields}; } sub tracking_flag_fields { - my $cache = Bugzilla->request_cache->{bugmail_filter}; - if (!$cache->{tracking_flag_fields}) { - require Bugzilla::Extension::TrackingFlags::Constants; - my @fields; - my $tracking_types = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES(); - foreach my $tracking_type (@$tracking_types) { - push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new({ - name => 'tracking.' . $tracking_type->{name}, - description => $tracking_type->{description}, - sortkey => $tracking_type->{sortkey}, - }); - } - $cache->{tracking_flag_fields} = \@fields; + my $cache = Bugzilla->request_cache->{bugmail_filter}; + if (!$cache->{tracking_flag_fields}) { + require Bugzilla::Extension::TrackingFlags::Constants; + my @fields; + my $tracking_types + = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES(); + foreach my $tracking_type (@$tracking_types) { + push @fields, + Bugzilla::Extension::BugmailFilter::FakeField->new({ + name => 'tracking.' . $tracking_type->{name}, + description => $tracking_type->{description}, + sortkey => $tracking_type->{sortkey}, + }); } - return $cache->{tracking_flag_fields}; + $cache->{tracking_flag_fields} = \@fields; + } + return $cache->{tracking_flag_fields}; } 1; diff --git a/extensions/BugmailFilter/lib/Filter.pm b/extensions/BugmailFilter/lib/Filter.pm index 7f2f4cb87..fa2c708cd 100644 --- a/extensions/BugmailFilter/lib/Filter.pm +++ b/extensions/BugmailFilter/lib/Filter.pm @@ -25,14 +25,14 @@ use Bugzilla::Util qw(trim); use constant DB_TABLE => 'bugmail_filters'; use constant DB_COLUMNS => qw( - id - user_id - product_id - component_id - field_name - relationship - changer_id - action + id + user_id + product_id + component_id + field_name + relationship + changer_id + action ); use constant LIST_ORDER => 'id'; @@ -40,13 +40,11 @@ use constant LIST_ORDER => 'id'; use constant UPDATE_COLUMNS => (); use constant VALIDATORS => { - user_id => \&_check_user, - field_name => \&_check_field_name, - action => \&Bugzilla::Object::check_boolean, -}; -use constant VALIDATOR_DEPENDENCIES => { - component_id => [ 'product_id' ], + user_id => \&_check_user, + field_name => \&_check_field_name, + action => \&Bugzilla::Object::check_boolean, }; +use constant VALIDATOR_DEPENDENCIES => {component_id => ['product_id'],}; use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -56,163 +54,164 @@ use constant USE_MEMCACHED => 0; # getters sub user { - my ($self) = @_; - return Bugzilla::User->new({ id => $self->{user_id}, cache => 1 }); + my ($self) = @_; + return Bugzilla::User->new({id => $self->{user_id}, cache => 1}); } sub product { - my ($self) = @_; - return $self->{product_id} - ? Bugzilla::Product->new({ id => $self->{product_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{product_id} + ? Bugzilla::Product->new({id => $self->{product_id}, cache => 1}) + : undef; } sub product_name { - my ($self) = @_; - return $self->{product_name} //= $self->{product_id} ? $self->product->name : ''; + my ($self) = @_; + return $self->{product_name} + //= $self->{product_id} ? $self->product->name : ''; } sub component { - my ($self) = @_; - return $self->{component_id} - ? Bugzilla::Component->new({ id => $self->{component_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{component_id} + ? Bugzilla::Component->new({id => $self->{component_id}, cache => 1}) + : undef; } sub component_name { - my ($self) = @_; - return $self->{component_name} //= $self->{component_id} ? $self->component->name : ''; + my ($self) = @_; + return $self->{component_name} + //= $self->{component_id} ? $self->component->name : ''; } sub field_name { - return $_[0]->{field_name} //= ''; + return $_[0]->{field_name} //= ''; } sub field_description { - my ($self, $value) = @_; - $self->{field_description} = $value if defined($value); - return $self->{field_description}; + my ($self, $value) = @_; + $self->{field_description} = $value if defined($value); + return $self->{field_description}; } sub field { - my ($self) = @_; - return unless $self->{field_name}; - if (!$self->{field}) { - if (substr($self->{field_name}, 0, 1) eq '~') { - # this should never happen - die "not implemented"; - } - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() }, - @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() }, - ) { - if ($field->{name} eq $self->{field_name}) { - return $self->{field} = $field; - } - } - $self->{field} = Bugzilla::Field->new({ name => $self->{field_name}, cache => 1 }); + my ($self) = @_; + return unless $self->{field_name}; + if (!$self->{field}) { + if (substr($self->{field_name}, 0, 1) eq '~') { + + # this should never happen + die "not implemented"; } - return $self->{field}; + foreach my $field ( + @{Bugzilla::Extension::BugmailFilter::FakeField->fake_fields()}, + @{Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields()}, + ) + { + if ($field->{name} eq $self->{field_name}) { + return $self->{field} = $field; + } + } + $self->{field} + = Bugzilla::Field->new({name => $self->{field_name}, cache => 1}); + } + return $self->{field}; } sub relationship { - return $_[0]->{relationship}; + return $_[0]->{relationship}; } sub changer_id { - return $_[0]->{changer_id}; + return $_[0]->{changer_id}; } sub changer { - my ($self) = @_; - return $self->{changer_id} - ? Bugzilla::User->new({ id => $self->{changer_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{changer_id} + ? Bugzilla::User->new({id => $self->{changer_id}, cache => 1}) + : undef; } sub relationship_name { - my ($self) = @_; - foreach my $rel (@{ FILTER_RELATIONSHIPS() }) { - return $rel->{name} - if $rel->{value} == $self->{relationship}; - } - return '?'; + my ($self) = @_; + foreach my $rel (@{FILTER_RELATIONSHIPS()}) { + return $rel->{name} if $rel->{value} == $self->{relationship}; + } + return '?'; } sub is_exclude { - return $_[0]->{action} == 1; + return $_[0]->{action} == 1; } sub is_include { - return $_[0]->{action} == 0; + return $_[0]->{action} == 0; } # validators sub _check_user { - my ($class, $user) = @_; - $user || ThrowCodeError('param_required', { param => 'user' }); + my ($class, $user) = @_; + $user || ThrowCodeError('param_required', {param => 'user'}); } sub _check_field_name { - my ($class, $field_name) = @_; - return undef unless $field_name; - if (substr($field_name, 0, 1) eq '~') { - $field_name = lc(trim($field_name)); - $field_name =~ /^~[a-z0-9_\.\-]+$/ - || ThrowUserError('bugmail_filter_invalid'); - length($field_name) <= 64 - || ThrowUserError('bugmail_filter_too_long'); - return $field_name; - } - foreach my $rh (@{ FAKE_FIELD_NAMES() }) { - return $field_name if $rh->{name} eq $field_name; - } - return $field_name - if $field_name =~ /^tracking\./; - Bugzilla::Field->check({ name => $field_name, cache => 1}); + my ($class, $field_name) = @_; + return undef unless $field_name; + if (substr($field_name, 0, 1) eq '~') { + $field_name = lc(trim($field_name)); + $field_name =~ /^~[a-z0-9_\.\-]+$/ || ThrowUserError('bugmail_filter_invalid'); + length($field_name) <= 64 || ThrowUserError('bugmail_filter_too_long'); return $field_name; + } + foreach my $rh (@{FAKE_FIELD_NAMES()}) { + return $field_name if $rh->{name} eq $field_name; + } + return $field_name if $field_name =~ /^tracking\./; + Bugzilla::Field->check({name => $field_name, cache => 1}); + return $field_name; } # methods sub matches { - my ($self, $args) = @_; - - if (my $field_name = $self->{field_name}) { - if ($args->{field}->{field_name} && substr($field_name, 0, 1) eq '~') { - my $substring = quotemeta(substr($field_name, 1)); - if ($args->{field}->{filter_field} !~ /$substring/i) { - return 0; - } - } - elsif ($field_name eq 'flagtypes.name') { - if ($args->{field}->{field_name} ne $field_name) { - return 0; - } - } - elsif ($field_name ne $args->{field}->{filter_field}) { - return 0; - } - } + my ($self, $args) = @_; - if ($self->{product_id} && $self->{product_id} != $args->{product_id}) { + if (my $field_name = $self->{field_name}) { + if ($args->{field}->{field_name} && substr($field_name, 0, 1) eq '~') { + my $substring = quotemeta(substr($field_name, 1)); + if ($args->{field}->{filter_field} !~ /$substring/i) { return 0; + } } - - if ($self->{component_id} && $self->{component_id} != $args->{component_id}) { + elsif ($field_name eq 'flagtypes.name') { + if ($args->{field}->{field_name} ne $field_name) { return 0; + } } - - if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) { - return 0; + elsif ($field_name ne $args->{field}->{filter_field}) { + return 0; } + } - if ($self->{changer_id} && $self->{changer_id} != $args->{changer_id}) { - return 0; - } + if ($self->{product_id} && $self->{product_id} != $args->{product_id}) { + return 0; + } + + if ($self->{component_id} && $self->{component_id} != $args->{component_id}) { + return 0; + } + + if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) { + return 0; + } + + if ($self->{changer_id} && $self->{changer_id} != $args->{changer_id}) { + return 0; + } - return 1; + return 1; } 1; diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm index ac9502fcb..e7812194e 100644 --- a/extensions/BzAPI/Extension.pm +++ b/extensions/BzAPI/Extension.pm @@ -33,15 +33,13 @@ our $VERSION = '0.1'; ################ sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } ################## @@ -49,14 +47,14 @@ sub install_filesystem { ################## sub template_before_process { - my ($self, $args) = @_; - my $vars = $args->{'vars'}; - my $file = $args->{'file'}; - - if ($file =~ /config\.json\.tmpl$/) { - $vars->{'initial_status'} = Bugzilla::Status->can_change_to; - $vars->{'status_objects'} = [ Bugzilla::Status->get_all ]; - } + my ($self, $args) = @_; + my $vars = $args->{'vars'}; + my $file = $args->{'file'}; + + if ($file =~ /config\.json\.tmpl$/) { + $vars->{'initial_status'} = Bugzilla::Status->can_change_to; + $vars->{'status_objects'} = [Bugzilla::Status->get_all]; + } } ############## @@ -64,137 +62,136 @@ sub template_before_process { ############## sub bug_start_of_update { - my ($self, $args) = @_; - my $old_bug = $args->{old_bug}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $old_bug = $args->{old_bug}; + my $params = Bugzilla->input_params; - return if !Bugzilla->request_cache->{bzapi}; + return if !Bugzilla->request_cache->{bzapi}; - # Check for a mid-air collision. Currently this only works when updating - # an individual bug and if last_changed_time is provided. Otherwise it - # allows the changes. - my $delta_ts = $params->{last_change_time} || ''; + # Check for a mid-air collision. Currently this only works when updating + # an individual bug and if last_changed_time is provided. Otherwise it + # allows the changes. + my $delta_ts = $params->{last_change_time} || ''; - if ($delta_ts && exists $params->{ids} && @{ $params->{ids} } == 1) { - _midair_check($delta_ts, $old_bug->delta_ts); - } + if ($delta_ts && exists $params->{ids} && @{$params->{ids}} == 1) { + _midair_check($delta_ts, $old_bug->delta_ts); + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $object = $args->{object}; + my $params = Bugzilla->input_params; - return if !Bugzilla->request_cache->{bzapi}; - return if !$object->isa('Bugzilla::Attachment'); + return if !Bugzilla->request_cache->{bzapi}; + return if !$object->isa('Bugzilla::Attachment'); - # Check for a mid-air collision. Currently this only works when updating - # an individual attachment and if last_changed_time is provided. Otherwise it - # allows the changes. - my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; - my $delta_ts = $stash->{last_change_time}; + # Check for a mid-air collision. Currently this only works when updating + # an individual attachment and if last_changed_time is provided. Otherwise it + # allows the changes. + my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; + my $delta_ts = $stash->{last_change_time}; - _midair_check($delta_ts, $object->modification_time) if $delta_ts; + _midair_check($delta_ts, $object->modification_time) if $delta_ts; } sub _midair_check { - my ($delta_ts, $old_delta_ts) = @_; - my $delta_ts_z = datetime_from($delta_ts) - || ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); - my $old_delta_tz_z = datetime_from($old_delta_ts); - if ($old_delta_tz_z ne $delta_ts_z) { - ThrowUserError('bzapi_midair_collision'); - } + my ($delta_ts, $old_delta_ts) = @_; + my $delta_ts_z = datetime_from($delta_ts) + || ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts}); + my $old_delta_tz_z = datetime_from($old_delta_ts); + if ($old_delta_tz_z ne $delta_ts_z) { + ThrowUserError('bzapi_midair_collision'); + } } sub webservice_error_codes { - my ($self, $args) = @_; - my $error_map = $args->{error_map}; - $error_map->{'bzapi_midair_collision'} = 400; + my ($self, $args) = @_; + my $error_map = $args->{error_map}; + $error_map->{'bzapi_midair_collision'} = 400; } sub webservice_fix_credentials { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $params = $args->{params}; - return if !Bugzilla->request_cache->{bzapi}; - fix_credentials($params); + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $params = $args->{params}; + return if !Bugzilla->request_cache->{bzapi}; + fix_credentials($params); } sub webservice_rest_request { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $params = $args->{params}; - my $cache = Bugzilla->request_cache; + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $params = $args->{params}; + my $cache = Bugzilla->request_cache; - return if !$cache->{bzapi}; + return if !$cache->{bzapi}; - # Stash certain values for later use - $cache->{bzapi_rpc} = $rpc; + # Stash certain values for later use + $cache->{bzapi_rpc} = $rpc; - # Internal websevice method being used - $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name; + # Internal websevice method being used + $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name; - # Load the appropriate request handler based on path and type - if (my $handler = _find_handler($rpc, 'request')) { - &$handler($params); - } + # Load the appropriate request handler based on path and type + if (my $handler = _find_handler($rpc, 'request')) { + &$handler($params); + } } sub webservice_rest_response { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $result = $args->{result}; - my $response = $args->{response}; - my $cache = Bugzilla->request_cache; - - # Stash certain values for later use - $cache->{bzapi_rpc} ||= $rpc; - - return if !Bugzilla->request_cache->{bzapi} - || ref $$result ne 'HASH'; - - if (exists $$result->{error}) { - $$result->{documentation} = BZAPI_DOC; - return; - } - - # Load the appropriate response handler based on path and type - if (my $handler = _find_handler($rpc, 'response')) { - &$handler($result, $response); - } - - # Add a Location header if a newly created resource - # such as a bug or comment. - if ($rpc->bz_success_code - && $rpc->bz_success_code == STATUS_CREATED - && $$result->{ref}) - { - $response->header("Location", $$result->{ref}); - } + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $result = $args->{result}; + my $response = $args->{response}; + my $cache = Bugzilla->request_cache; + + # Stash certain values for later use + $cache->{bzapi_rpc} ||= $rpc; + + return if !Bugzilla->request_cache->{bzapi} || ref $$result ne 'HASH'; + + if (exists $$result->{error}) { + $$result->{documentation} = BZAPI_DOC; + return; + } + + # Load the appropriate response handler based on path and type + if (my $handler = _find_handler($rpc, 'response')) { + &$handler($result, $response); + } + + # Add a Location header if a newly created resource + # such as a bug or comment. + if ( $rpc->bz_success_code + && $rpc->bz_success_code == STATUS_CREATED + && $$result->{ref}) + { + $response->header("Location", $$result->{ref}); + } } sub webservice_rest_resources { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $resources = $args->{resources}; + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $resources = $args->{resources}; - return if !Bugzilla->request_cache->{bzapi}; + return if !Bugzilla->request_cache->{bzapi}; - _add_resources($rpc, $resources); + _add_resources($rpc, $resources); } sub webservice_status_code_map { - my ($self, $args) = @_; - my $status_code_map = $args->{status_code_map}; - $status_code_map->{51} = STATUS_BAD_REQUEST; + my ($self, $args) = @_; + my $status_code_map = $args->{status_code_map}; + $status_code_map->{51} = STATUS_BAD_REQUEST; } sub psgi_builder { - my ($self, $args) = @_; - my $mount = $args->{mount}; + my ($self, $args) = @_; + my $mount = $args->{mount}; - $mount->{'bzapi'} = compile_cgi('extensions/BzAPI/bin/rest.cgi'); + $mount->{'bzapi'} = compile_cgi('extensions/BzAPI/bin/rest.cgi'); } @@ -203,93 +200,95 @@ sub psgi_builder { ##################### sub _find_handler { - my ($rpc, $type) = @_; + my ($rpc, $type) = @_; - my $path_info = $rpc->cgi->path_info; - my $request_method = $rpc->request->method; + my $path_info = $rpc->cgi->path_info; + my $request_method = $rpc->request->method; - my $module = $rpc->bz_class_name || ''; - $module =~ s/^Bugzilla::WebService:://; + my $module = $rpc->bz_class_name || ''; + $module =~ s/^Bugzilla::WebService:://; - my $cache = _preload_handlers(); + my $cache = _preload_handlers(); - return undef if !exists $cache->{$module}; + return undef if !exists $cache->{$module}; - # Make a copy of the handler array so - # as to not alter the actual cached data. - my @handlers = @{ $cache->{$module} }; + # Make a copy of the handler array so + # as to not alter the actual cached data. + my @handlers = @{$cache->{$module}}; - while (my $regex = shift @handlers) { - my $data = shift @handlers; - next if ref $data ne 'HASH'; - if ($path_info =~ $regex - && exists $data->{$request_method} - && exists $data->{$request_method}->{$type}) - { - return $data->{$request_method}->{$type}; - } + while (my $regex = shift @handlers) { + my $data = shift @handlers; + next if ref $data ne 'HASH'; + if ( $path_info =~ $regex + && exists $data->{$request_method} + && exists $data->{$request_method}->{$type}) + { + return $data->{$request_method}->{$type}; } + } - return undef; + return undef; } sub _add_resources { - my ($rpc, $native_resources) = @_; - - my $cache = _preload_handlers(); - - foreach my $module (keys %$cache) { - my $native_module = "Bugzilla::WebService::$module"; - next if !$native_resources->{$native_module}; - - # Make a copy of the handler array so - # as to not alter the actual cached data. - my @handlers = @{ $cache->{$module} }; - - my @ext_resources = (); - while (my $regex = shift @handlers) { - my $data = shift @handlers; - next if ref $data ne 'HASH'; - my $new_data = {}; - foreach my $request_method (keys %$data) { - next if !exists $data->{$request_method}->{resource}; - $new_data->{$request_method} = $data->{$request_method}->{resource}; - } - push(@ext_resources, $regex, $new_data); - } - - # Places the new resources at the beginning of the list - # so we can capture specific paths before the native resources - unshift(@{$native_resources->{$native_module}}, @ext_resources); + my ($rpc, $native_resources) = @_; + + my $cache = _preload_handlers(); + + foreach my $module (keys %$cache) { + my $native_module = "Bugzilla::WebService::$module"; + next if !$native_resources->{$native_module}; + + # Make a copy of the handler array so + # as to not alter the actual cached data. + my @handlers = @{$cache->{$module}}; + + my @ext_resources = (); + while (my $regex = shift @handlers) { + my $data = shift @handlers; + next if ref $data ne 'HASH'; + my $new_data = {}; + foreach my $request_method (keys %$data) { + next if !exists $data->{$request_method}->{resource}; + $new_data->{$request_method} = $data->{$request_method}->{resource}; + } + push(@ext_resources, $regex, $new_data); } + + # Places the new resources at the beginning of the list + # so we can capture specific paths before the native resources + unshift(@{$native_resources->{$native_module}}, @ext_resources); + } } sub _resource_modules { - my $extdir = bz_locations()->{extensionsdir}; - return map { basename($_, '.pm') } glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm"); + my $extdir = bz_locations()->{extensionsdir}; + return + map { basename($_, '.pm') } + glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm"); } # preload all handlers into cache # since we don't want to parse all # this multiple times sub _preload_handlers { - my $cache = Bugzilla->request_cache; - - if (!exists $cache->{rest_handlers}) { - my $all_handlers = {}; - foreach my $module (_resource_modules()) { - my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module"; - trick_taint($resource_class); - eval { require_module($resource_class) }; - next if ($@ || !$resource_class->can('rest_handlers')); - my $handlers = $resource_class->rest_handlers; - next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0); - $all_handlers->{$module} = $handlers; - } - $cache->{rest_handlers} = $all_handlers; + my $cache = Bugzilla->request_cache; + + if (!exists $cache->{rest_handlers}) { + my $all_handlers = {}; + foreach my $module (_resource_modules()) { + my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module"; + trick_taint($resource_class); + eval { require_module($resource_class) }; + next if ($@ || !$resource_class->can('rest_handlers')); + my $handlers = $resource_class->rest_handlers; + next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0); + $all_handlers->{$module} = $handlers; } + $cache->{rest_handlers} = $all_handlers; + } - return $cache->{rest_handlers}; + return $cache->{rest_handlers}; } __PACKAGE__->NAME; diff --git a/extensions/BzAPI/bin/rest.cgi b/extensions/BzAPI/bin/rest.cgi index 5642ad550..5a895bc1d 100755 --- a/extensions/BzAPI/bin/rest.cgi +++ b/extensions/BzAPI/bin/rest.cgi @@ -14,12 +14,11 @@ use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::WebService::Constants; + BEGIN { - if (!Bugzilla->feature('rest') - || !Bugzilla->feature('jsonrpc')) - { - ThrowUserError('feature_disabled', { feature => 'rest' }); - } + if (!Bugzilla->feature('rest') || !Bugzilla->feature('jsonrpc')) { + ThrowUserError('feature_disabled', {feature => 'rest'}); + } } # Set request_cache bzapi value to true in order to enable the @@ -30,9 +29,10 @@ Bugzilla->request_cache->{bzapi} = 1; # otherwise native REST will complain my $path_info = Bugzilla->cgi->path_info; if ($path_info =~ s'/$'') { - # Remove first slash as cgi->path_info expects it to - # not be there when setting a new path. - Bugzilla->cgi->path_info(substr($path_info, 1)); + + # Remove first slash as cgi->path_info expects it to + # not be there when setting a new path. + Bugzilla->cgi->path_info(substr($path_info, 1)); } use Bugzilla::WebService::Server::REST; diff --git a/extensions/BzAPI/lib/Constants.pm b/extensions/BzAPI/lib/Constants.pm index fb611aae6..f31dac7a8 100644 --- a/extensions/BzAPI/lib/Constants.pm +++ b/extensions/BzAPI/lib/Constants.pm @@ -13,142 +13,144 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - USER_FIELDS - BUG_FIELD_MAP - BOOLEAN_TYPE_MAP - ATTACHMENT_FIELD_MAP - DEFAULT_BUG_FIELDS - DEFAULT_ATTACHMENT_FIELDS + USER_FIELDS + BUG_FIELD_MAP + BOOLEAN_TYPE_MAP + ATTACHMENT_FIELD_MAP + DEFAULT_BUG_FIELDS + DEFAULT_ATTACHMENT_FIELDS - BZAPI_DOC + BZAPI_DOC ); # These are fields that are normally exported as a single value such # as the user's email. BzAPI needs to convert them to user objects # where possible. -use constant USER_FIELDS => (qw( +use constant USER_FIELDS => ( + qw( assigned_to cc creator qa_contact reporter -)); + ) +); # Convert old field names from old to new use constant BUG_FIELD_MAP => { - 'opendate' => 'creation_time', # query - 'creation_ts' => 'creation_time', - 'changeddate' => 'last_change_time', # query - 'delta_ts' => 'last_change_time', - 'bug_id' => 'id', - 'rep_platform' => 'platform', - 'bug_severity' => 'severity', - 'bug_status' => 'status', - 'short_desc' => 'summary', - 'bug_file_loc' => 'url', - 'status_whiteboard' => 'whiteboard', - 'reporter' => 'creator', - 'reporter_realname' => 'creator_realname', - 'cclist_accessible' => 'is_cc_accessible', - 'reporter_accessible' => 'is_creator_accessible', - 'everconfirmed' => 'is_confirmed', - 'dependson' => 'depends_on', - 'blocked' => 'blocks', - 'attachment' => 'attachments', - 'flag' => 'flags', - 'flagtypes.name' => 'flag', - 'bug_group' => 'group', - 'group' => 'groups', - 'longdesc' => 'comment', - 'bug_file_loc_type' => 'url_type', - 'bugidtype' => 'id_mode', - 'longdesc_type' => 'comment_type', - 'short_desc_type' => 'summary_type', - 'status_whiteboard_type' => 'whiteboard_type', - 'emailassigned_to1' => 'email1_assigned_to', - 'emailassigned_to2' => 'email2_assigned_to', - 'emailcc1' => 'email1_cc', - 'emailcc2' => 'email2_cc', - 'emailqa_contact1' => 'email1_qa_contact', - 'emailqa_contact2' => 'email2_qa_contact', - 'emailreporter1' => 'email1_creator', - 'emailreporter2' => 'email2_creator', - 'emaillongdesc1' => 'email1_comment_creator', - 'emaillongdesc2' => 'email2_comment_creator', - 'emailtype1' => 'email1_type', - 'emailtype2' => 'email2_type', - 'chfieldfrom' => 'changed_after', - 'chfieldto' => 'changed_before', - 'chfield' => 'changed_field', - 'chfieldvalue' => 'changed_field_to', - 'deadlinefrom' => 'deadline_after', - 'deadlineto' => 'deadline_before', - 'attach_data.thedata' => 'attachment.data', - 'longdescs.isprivate' => 'comment.is_private', - 'commenter' => 'comment.creator', - 'requestees.login_name' => 'flag.requestee', - 'setters.login_name' => 'flag.setter', - 'days_elapsed' => 'idle', - 'owner_idle_time' => 'assignee_idle', - 'dup_id' => 'dupe_of', - 'isopened' => 'is_open', - 'flag_type' => 'flag_types', - 'attachments.submitter' => 'attachment.attacher', - 'attachments.filename' => 'attachment.file_name', - 'attachments.description' => 'attachment.description', - 'attachments.delta_ts' => 'attachment.last_change_time', - 'attachments.isobsolete' => 'attachment.is_obsolete', - 'attachments.ispatch' => 'attachment.is_patch', - 'attachments.isprivate' => 'attachment.is_private', - 'attachments.mimetype' => 'attachment.content_type', - 'attachments.date' => 'attachment.creation_time', - 'attachments.attachid' => 'attachment.id', - 'attachments.flag' => 'attachment.flags', - 'attachments.token' => 'attachment.update_token' + 'opendate' => 'creation_time', # query + 'creation_ts' => 'creation_time', + 'changeddate' => 'last_change_time', # query + 'delta_ts' => 'last_change_time', + 'bug_id' => 'id', + 'rep_platform' => 'platform', + 'bug_severity' => 'severity', + 'bug_status' => 'status', + 'short_desc' => 'summary', + 'bug_file_loc' => 'url', + 'status_whiteboard' => 'whiteboard', + 'reporter' => 'creator', + 'reporter_realname' => 'creator_realname', + 'cclist_accessible' => 'is_cc_accessible', + 'reporter_accessible' => 'is_creator_accessible', + 'everconfirmed' => 'is_confirmed', + 'dependson' => 'depends_on', + 'blocked' => 'blocks', + 'attachment' => 'attachments', + 'flag' => 'flags', + 'flagtypes.name' => 'flag', + 'bug_group' => 'group', + 'group' => 'groups', + 'longdesc' => 'comment', + 'bug_file_loc_type' => 'url_type', + 'bugidtype' => 'id_mode', + 'longdesc_type' => 'comment_type', + 'short_desc_type' => 'summary_type', + 'status_whiteboard_type' => 'whiteboard_type', + 'emailassigned_to1' => 'email1_assigned_to', + 'emailassigned_to2' => 'email2_assigned_to', + 'emailcc1' => 'email1_cc', + 'emailcc2' => 'email2_cc', + 'emailqa_contact1' => 'email1_qa_contact', + 'emailqa_contact2' => 'email2_qa_contact', + 'emailreporter1' => 'email1_creator', + 'emailreporter2' => 'email2_creator', + 'emaillongdesc1' => 'email1_comment_creator', + 'emaillongdesc2' => 'email2_comment_creator', + 'emailtype1' => 'email1_type', + 'emailtype2' => 'email2_type', + 'chfieldfrom' => 'changed_after', + 'chfieldto' => 'changed_before', + 'chfield' => 'changed_field', + 'chfieldvalue' => 'changed_field_to', + 'deadlinefrom' => 'deadline_after', + 'deadlineto' => 'deadline_before', + 'attach_data.thedata' => 'attachment.data', + 'longdescs.isprivate' => 'comment.is_private', + 'commenter' => 'comment.creator', + 'requestees.login_name' => 'flag.requestee', + 'setters.login_name' => 'flag.setter', + 'days_elapsed' => 'idle', + 'owner_idle_time' => 'assignee_idle', + 'dup_id' => 'dupe_of', + 'isopened' => 'is_open', + 'flag_type' => 'flag_types', + 'attachments.submitter' => 'attachment.attacher', + 'attachments.filename' => 'attachment.file_name', + 'attachments.description' => 'attachment.description', + 'attachments.delta_ts' => 'attachment.last_change_time', + 'attachments.isobsolete' => 'attachment.is_obsolete', + 'attachments.ispatch' => 'attachment.is_patch', + 'attachments.isprivate' => 'attachment.is_private', + 'attachments.mimetype' => 'attachment.content_type', + 'attachments.date' => 'attachment.creation_time', + 'attachments.attachid' => 'attachment.id', + 'attachments.flag' => 'attachment.flags', + 'attachments.token' => 'attachment.update_token' }; # Convert from old boolean chart type names to new names use constant BOOLEAN_TYPE_MAP => { - 'equals' => 'equals', - 'not_equals' => 'notequals', - 'equals_any' => 'anyexact', - 'contains' => 'substring', - 'not_contains' => 'notsubstring', - 'case_contains' => 'casesubstring', - 'contains_any' => 'anywordssubstr', - 'not_contains_any' => 'nowordssubstr', - 'contains_all' => 'allwordssubstr', - 'contains_any_words' => 'anywords', - 'not_contains_any_words' => 'nowords', - 'contains_all_words' => 'allwords', - 'regex' => 'regexp', - 'not_regex' => 'notregexp', - 'less_than' => 'lessthan', - 'greater_than' => 'greaterthan', - 'changed_before' => 'changedbefore', - 'changed_after' => 'changedafter', - 'changed_from' => 'changedfrom', - 'changed_to' => 'changedto', - 'changed_by' => 'changedby', - 'matches' => 'matches' + 'equals' => 'equals', + 'not_equals' => 'notequals', + 'equals_any' => 'anyexact', + 'contains' => 'substring', + 'not_contains' => 'notsubstring', + 'case_contains' => 'casesubstring', + 'contains_any' => 'anywordssubstr', + 'not_contains_any' => 'nowordssubstr', + 'contains_all' => 'allwordssubstr', + 'contains_any_words' => 'anywords', + 'not_contains_any_words' => 'nowords', + 'contains_all_words' => 'allwords', + 'regex' => 'regexp', + 'not_regex' => 'notregexp', + 'less_than' => 'lessthan', + 'greater_than' => 'greaterthan', + 'changed_before' => 'changedbefore', + 'changed_after' => 'changedafter', + 'changed_from' => 'changedfrom', + 'changed_to' => 'changedto', + 'changed_by' => 'changedby', + 'matches' => 'matches' }; # Convert old attachment field names from old to new use constant ATTACHMENT_FIELD_MAP => { - 'submitter' => 'attacher', - 'description' => 'description', - 'filename' => 'file_name', - 'delta_ts' => 'last_change_time', - 'isobsolete' => 'is_obsolete', - 'ispatch' => 'is_patch', - 'isprivate' => 'is_private', - 'mimetype' => 'content_type', - 'contenttypeentry' => 'content_type', - 'date' => 'creation_time', - 'attachid' => 'id', - 'desc' => 'description', - 'flag' => 'flags', - 'type' => 'content_type', + 'submitter' => 'attacher', + 'description' => 'description', + 'filename' => 'file_name', + 'delta_ts' => 'last_change_time', + 'isobsolete' => 'is_obsolete', + 'ispatch' => 'is_patch', + 'isprivate' => 'is_private', + 'mimetype' => 'content_type', + 'contenttypeentry' => 'content_type', + 'date' => 'creation_time', + 'attachid' => 'id', + 'desc' => 'description', + 'flag' => 'flags', + 'type' => 'content_type', }; # A base link to the current BzAPI Documentation. diff --git a/extensions/BzAPI/lib/Resources/Bug.pm b/extensions/BzAPI/lib/Resources/Bug.pm index 4fd59824c..ee76ddbcd 100644 --- a/extensions/BzAPI/lib/Resources/Bug.pm +++ b/extensions/BzAPI/lib/Resources/Bug.pm @@ -28,111 +28,86 @@ use List::Util qw(max); ################# BEGIN { - require Bugzilla::WebService::Bug; - *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource; + require Bugzilla::WebService::Bug; + *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource; } sub rest_handlers { - my $rest_handlers = [ - qr{^/bug$}, { - GET => { - request => \&search_bugs_request, - response => \&search_bugs_response - }, - POST => { - request => \&create_bug_request, - response => \&create_bug_response - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - response => \&get_bug_response - }, - PUT => { - request => \&update_bug_request, - response => \&update_bug_response - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - response => \&get_comments_response - }, - POST => { - request => \&add_comment_request, - response => \&add_comment_response - } - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - response => \&get_history_response - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - response => \&get_attachments_response - }, - POST => { - request => \&add_attachment_request, - response => \&add_attachment_response - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - response => \&get_attachment_response - }, - PUT => { - request => \&update_attachment_request, - response => \&update_attachment_response - } - }, - qr{^/attachment/([^/]+)$}, { - GET => { - response => \&get_attachment_response - }, - PUT => { - request => \&update_attachment_request, - response => \&update_attachment_response - } - }, - qr{^/bug/([^/]+)/flag$}, { - GET => { - resource => { - method => 'get', - params => sub { - return { ids => [ $_[0] ], - include_fields => ['flags'] }; - } - }, - response => \&get_bug_flags_response, - } - }, - qr{^/count$}, { - GET => { - resource => { - method => 'get_bug_count' - } - } + my $rest_handlers = [ + qr{^/bug$}, + { + GET => {request => \&search_bugs_request, response => \&search_bugs_response}, + POST => {request => \&create_bug_request, response => \&create_bug_response} + }, + qr{^/bug/([^/]+)$}, + { + GET => {response => \&get_bug_response}, + PUT => {request => \&update_bug_request, response => \&update_bug_response} + }, + qr{^/bug/([^/]+)/comment$}, + { + GET => {response => \&get_comments_response}, + POST => {request => \&add_comment_request, response => \&add_comment_response} + }, + qr{^/bug/([^/]+)/history$}, + {GET => {response => \&get_history_response}}, + qr{^/bug/([^/]+)/attachment$}, + { + GET => {response => \&get_attachments_response}, + POST => + {request => \&add_attachment_request, response => \&add_attachment_response} + }, + qr{^/bug/attachment/([^/]+)$}, + { + GET => {response => \&get_attachment_response}, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/attachment/([^/]+)$}, + { + GET => {response => \&get_attachment_response}, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/bug/([^/]+)/flag$}, + { + GET => { + resource => { + method => 'get', + params => sub { + return {ids => [$_[0]], include_fields => ['flags']}; + } }, - qr{^/attachment/([^/]+)$}, { - GET => { - resource => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - } - }, - PUT => { - resource => { - method => 'update_attachment', - params => sub { - return { ids => [ $_[0] ] }; - } - } - } + response => \&get_bug_flags_response, + } + }, + qr{^/count$}, + {GET => {resource => {method => 'get_bug_count'}}}, + qr{^/attachment/([^/]+)$}, + { + GET => { + resource => { + method => 'attachments', + params => sub { + return {attachment_ids => [$_[0]]}; + } } - ]; - return $rest_handlers; + }, + PUT => { + resource => { + method => 'update_attachment', + params => sub { + return {ids => [$_[0]]}; + } + } + } + } + ]; + return $rest_handlers; } ######################### @@ -144,205 +119,203 @@ sub rest_handlers { # this should be broken into it's own module so that report.cgi # and here can share the same code. sub get_bug_count_resource { - my ($self, $params) = @_; - - Bugzilla->switch_to_shadow_db(); - - my $col_field = $params->{x_axis_field} || ''; - my $row_field = $params->{y_axis_field} || ''; - my $tbl_field = $params->{z_axis_field} || ''; - - my $dimensions = $col_field ? - $row_field ? - $tbl_field ? 3 : 2 : 1 : 0; - - if ($dimensions == 0) { - $col_field = "bug_status"; - $params->{x_axis_field} = "bug_status"; - } - - # Valid bug fields that can be reported on. - my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; - - # Convert external names to internal if necessary - $params = Bugzilla::Bug::map_fields($params); - $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field; - $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field; - $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field; - - # Validate the values in the axis fields or throw an error. - !$row_field - || ($valid_columns->{$row_field} && trick_taint($row_field)) - || ThrowCodeError("report_axis_invalid", { fld => "x", val => $row_field }); - !$col_field - || ($valid_columns->{$col_field} && trick_taint($col_field)) - || ThrowCodeError("report_axis_invalid", { fld => "y", val => $col_field }); - !$tbl_field - || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) - || ThrowCodeError("report_axis_invalid", { fld => "z", val => $tbl_field }); - - my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field); - - my $search = new Bugzilla::Search( - fields => \@axis_fields, - params => $params, - allow_unlimited => 1, - ); - - my ($results, $extra_data) = $search->data; - - # We have a hash of hashes for the data itself, and a hash to hold the - # row/col/table names. - my %data; - my %names; - - # Read the bug data and count the bugs for each possible value of row, column - # and table. - # - # We detect a numerical field, and sort appropriately, if all the values are - # numeric. - my $col_isnumeric = 1; - my $row_isnumeric = 1; - my $tbl_isnumeric = 1; - - foreach my $result (@$results) { - # handle empty dimension member names - my $row = check_value($row_field, $result); - my $col = check_value($col_field, $result); - my $tbl = check_value($tbl_field, $result); - - $data{$tbl}{$col}{$row}++; - $names{"col"}{$col}++; - $names{"row"}{$row}++; - $names{"tbl"}{$tbl}++; - - $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); - $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); - $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); - } - - my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); - my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); - my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); - - push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); - - my @data; - foreach my $tbl (@tbl_names) { - my @tbl_data; - foreach my $row (@row_names) { - my @col_data; - foreach my $col (@col_names) { - $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; - push(@col_data, $data{$tbl}{$col}{$row}); - if ($tbl ne "-total-") { - # This is a bit sneaky. We spend every loop except the last - # building up the -total- data, and then last time round, - # we process it as another tbl, and push() the total values - # into the image_data array. - $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; - } - } - push(@tbl_data, \@col_data); - } - push(@data, \@tbl_data); - } - - my $result = {}; - if ($dimensions == 0) { - my $sum = 0; - - # If the search returns no results, we just get an 0-byte file back - # and so there is no data at all. - if (@data) { - foreach my $value (@{ $data[0][0] }) { - $sum += $value; - } - } - - $result = { - 'data' => $sum - }; - } - elsif ($dimensions == 1) { - $result = { - 'x_labels' => \@col_names, - 'data' => $data[0][0] || [] - }; - } - elsif ($dimensions == 2) { - $result = { - 'x_labels' => \@col_names, - 'y_labels' => \@row_names, - 'data' => $data[0] || [[]] - }; - } - elsif ($dimensions == 3) { - if (@data > 1 && $tbl_names[-1] eq "-total-") { - # Last table is a total, which we discard - pop(@data); - pop(@tbl_names); + my ($self, $params) = @_; + + Bugzilla->switch_to_shadow_db(); + + my $col_field = $params->{x_axis_field} || ''; + my $row_field = $params->{y_axis_field} || ''; + my $tbl_field = $params->{z_axis_field} || ''; + + my $dimensions = $col_field ? $row_field ? $tbl_field ? 3 : 2 : 1 : 0; + + if ($dimensions == 0) { + $col_field = "bug_status"; + $params->{x_axis_field} = "bug_status"; + } + + # Valid bug fields that can be reported on. + my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; + + # Convert external names to internal if necessary + $params = Bugzilla::Bug::map_fields($params); + $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field; + $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field; + $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field; + + # Validate the values in the axis fields or throw an error. + !$row_field + || ($valid_columns->{$row_field} && trick_taint($row_field)) + || ThrowCodeError("report_axis_invalid", {fld => "x", val => $row_field}); + !$col_field + || ($valid_columns->{$col_field} && trick_taint($col_field)) + || ThrowCodeError("report_axis_invalid", {fld => "y", val => $col_field}); + !$tbl_field + || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) + || ThrowCodeError("report_axis_invalid", {fld => "z", val => $tbl_field}); + + my @axis_fields = grep {$_} ($row_field, $col_field, $tbl_field); + + my $search = new Bugzilla::Search( + fields => \@axis_fields, + params => $params, + allow_unlimited => 1, + ); + + my ($results, $extra_data) = $search->data; + + # We have a hash of hashes for the data itself, and a hash to hold the + # row/col/table names. + my %data; + my %names; + + # Read the bug data and count the bugs for each possible value of row, column + # and table. + # + # We detect a numerical field, and sort appropriately, if all the values are + # numeric. + my $col_isnumeric = 1; + my $row_isnumeric = 1; + my $tbl_isnumeric = 1; + + foreach my $result (@$results) { + + # handle empty dimension member names + my $row = check_value($row_field, $result); + my $col = check_value($col_field, $result); + my $tbl = check_value($tbl_field, $result); + + $data{$tbl}{$col}{$row}++; + $names{"col"}{$col}++; + $names{"row"}{$row}++; + $names{"tbl"}{$tbl}++; + + $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); + $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); + $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); + } + + my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); + my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); + my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); + + push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); + + my @data; + foreach my $tbl (@tbl_names) { + my @tbl_data; + foreach my $row (@row_names) { + my @col_data; + foreach my $col (@col_names) { + $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; + push(@col_data, $data{$tbl}{$col}{$row}); + if ($tbl ne "-total-") { + + # This is a bit sneaky. We spend every loop except the last + # building up the -total- data, and then last time round, + # we process it as another tbl, and push() the total values + # into the image_data array. + $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; } - - $result = { - 'x_labels' => \@col_names, - 'y_labels' => \@row_names, - 'z_labels' => \@tbl_names, - 'data' => @data ? \@data : [[[]]] - }; - } - - return $result; + } + push(@tbl_data, \@col_data); + } + push(@data, \@tbl_data); + } + + my $result = {}; + if ($dimensions == 0) { + my $sum = 0; + + # If the search returns no results, we just get an 0-byte file back + # and so there is no data at all. + if (@data) { + foreach my $value (@{$data[0][0]}) { + $sum += $value; + } + } + + $result = {'data' => $sum}; + } + elsif ($dimensions == 1) { + $result = {'x_labels' => \@col_names, 'data' => $data[0][0] || []}; + } + elsif ($dimensions == 2) { + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'data' => $data[0] || [[]] + }; + } + elsif ($dimensions == 3) { + if (@data > 1 && $tbl_names[-1] eq "-total-") { + + # Last table is a total, which we discard + pop(@data); + pop(@tbl_names); + } + + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'z_labels' => \@tbl_names, + 'data' => @data ? \@data : [[[]]] + }; + } + + return $result; } sub get_names { - my ($names, $isnumeric, $field_name) = @_; - my ($field, @sorted); - # XXX - This is a hack to handle the actual_time/work_time field, - # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. - $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; - - # _realname fields aren't real Bugzilla::Field objects, but they are a - # valid axis, so we don't vailidate them as Bugzilla::Field objects. - $field = Bugzilla::Field->check($field_name) - if ($field_name && $field_name !~ /_realname$/); - - if ($field && $field->is_select) { - foreach my $value (@{$field->legal_values}) { - push(@sorted, $value->name) if $names->{$value->name}; - } - unshift(@sorted, '---') if $field_name eq 'resolution'; - @sorted = uniq @sorted; - } - elsif ($isnumeric) { - # It's not a field we are preserving the order of, so sort it - # numerically... - @sorted = sort { $a <=> $b } keys %$names; - } - else { - # ...or alphabetically, as appropriate. - @sorted = sort keys %$names; - } - - return @sorted; + my ($names, $isnumeric, $field_name) = @_; + my ($field, @sorted); + + # XXX - This is a hack to handle the actual_time/work_time field, + # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. + $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; + + # _realname fields aren't real Bugzilla::Field objects, but they are a + # valid axis, so we don't vailidate them as Bugzilla::Field objects. + $field = Bugzilla::Field->check($field_name) + if ($field_name && $field_name !~ /_realname$/); + + if ($field && $field->is_select) { + foreach my $value (@{$field->legal_values}) { + push(@sorted, $value->name) if $names->{$value->name}; + } + unshift(@sorted, '---') if $field_name eq 'resolution'; + @sorted = uniq @sorted; + } + elsif ($isnumeric) { + + # It's not a field we are preserving the order of, so sort it + # numerically... + @sorted = sort { $a <=> $b } keys %$names; + } + else { + # ...or alphabetically, as appropriate. + @sorted = sort keys %$names; + } + + return @sorted; } sub check_value { - my ($field, $result) = @_; - - my $value; - if (!defined $field) { - $value = ''; - } - elsif ($field eq '') { - $value = ' '; - } - else { - $value = shift @$result; - $value = ' ' if (!defined $value || $value eq ''); - $value = '---' if ($field eq 'resolution' && $value eq ' '); - } - return $value; + my ($field, $result) = @_; + + my $value; + if (!defined $field) { + $value = ''; + } + elsif ($field eq '') { + $value = ' '; + } + else { + $value = shift @$result; + $value = ' ' if (!defined $value || $value eq ''); + $value = '---' if ($field eq 'resolution' && $value eq ' '); + } + return $value; } ######################## @@ -350,282 +323,290 @@ sub check_value { ######################## sub search_bugs_request { - my ($params) = @_; - - if (defined $params->{changed_field} - && $params->{changed_field} eq "creation_time") - { - $params->{changed_field} = "[Bug creation]"; - } + my ($params) = @_; - my $FIELD_NEW_TO_OLD = { reverse %{ BUG_FIELD_MAP() } }; + if (defined $params->{changed_field} + && $params->{changed_field} eq "creation_time") + { + $params->{changed_field} = "[Bug creation]"; + } - # Update values of various forms. - foreach my $key (keys %$params) { - # First, search types. These are found in the value of any field ending - # _type, and the value of any field matching type\d-\d-\d. - if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) { - $params->{$key} - = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key}; - } + my $FIELD_NEW_TO_OLD = {reverse %{BUG_FIELD_MAP()}}; - # Field names hiding in values instead of keys: changed_field, boolean - # charts and axis names. - if ($key =~ /^(field\d+-\d+-\d+| - changed_field| - (x|y|z)_axis_field)$ - /x) { - $params->{$key} - = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key}; - } - } + # Update values of various forms. + foreach my $key (keys %$params) { - # Update field names - foreach my $field (keys %$FIELD_NEW_TO_OLD) { - if (defined $params->{$field}) { - $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field}; - } - } - - if (exists $params->{bug_id_type}) { - $params->{bug_id_type} - = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type}; + # First, search types. These are found in the value of any field ending + # _type, and the value of any field matching type\d-\d-\d. + if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) { + $params->{$key} = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key}; } - # Time field names are screwy, and got reused. We can't put this mapping - # in NEW2OLD as everything will go haywire. actual_time has to be queried - # as work_time even though work_time is the submit-only field for _adding_ - # to actual_time, which can't be arbitrarily manipulated. - if (defined $params->{work_time}) { - $params->{actual_time} = delete $params->{work_time}; - } - - # Other convenience search ariables used by BzAPI - my @field_ids = grep(/^f(\d+)$/, keys %$params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; - foreach my $field (qw(setters.login_name requestees.login_name)) { - if (my $value = delete $params->{$field}) { - $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field; - $params->{"o${last_field_id}"} = 'equals'; - $params->{"v${last_field_id}"} = $value; - $last_field_id++; - } - } + # Field names hiding in values instead of keys: changed_field, boolean + # charts and axis names. + if ( + $key =~ /^(field\d+-\d+-\d+| + changed_field| + (x|y|z)_axis_field)$ + /x + ) + { + $params->{$key} = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key}; + } + } + + # Update field names + foreach my $field (keys %$FIELD_NEW_TO_OLD) { + if (defined $params->{$field}) { + $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field}; + } + } + + if (exists $params->{bug_id_type}) { + $params->{bug_id_type} + = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type}; + } + + # Time field names are screwy, and got reused. We can't put this mapping + # in NEW2OLD as everything will go haywire. actual_time has to be queried + # as work_time even though work_time is the submit-only field for _adding_ + # to actual_time, which can't be arbitrarily manipulated. + if (defined $params->{work_time}) { + $params->{actual_time} = delete $params->{work_time}; + } + + # Other convenience search ariables used by BzAPI + my @field_ids = grep(/^f(\d+)$/, keys %$params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + foreach my $field (qw(setters.login_name requestees.login_name)) { + if (my $value = delete $params->{$field}) { + $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field; + $params->{"o${last_field_id}"} = 'equals'; + $params->{"v${last_field_id}"} = $value; + $last_field_id++; + } + } } sub create_bug_request { - my ($params) = @_; - - # User roles such as assigned_to and qa_contact should be just the - # email (login) of the user you want to set to. - foreach my $field (qw(assigned_to qa_contact)) { - if (exists $params->{$field}) { - $params->{$field} = $params->{$field}->{name}; - } - } - - # CC should just be a list of bugzilla logins - if (exists $params->{cc}) { - $params->{cc} = [ map { $_->{name} } @{ $params->{cc} } ]; - } - - # Comment - if (exists $params->{comments}) { - $params->{comment_is_private} = $params->{comments}->[0]->{is_private}; - $params->{description} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } - - # Some fields are not supported by Bugzilla::Bug->create but are supported - # by Bugzilla::Bug->update :( - my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {}; - foreach my $field (qw(remaining_time)) { - next if !exists $params->{$field}; - $cache->{$field} = delete $params->{$field}; - } - - # remove username/password - delete $params->{username}; - delete $params->{password}; + my ($params) = @_; + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to set to. + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + $params->{$field} = $params->{$field}->{name}; + } + } + + # CC should just be a list of bugzilla logins + if (exists $params->{cc}) { + $params->{cc} = [map { $_->{name} } @{$params->{cc}}]; + } + + # Comment + if (exists $params->{comments}) { + $params->{comment_is_private} = $params->{comments}->[0]->{is_private}; + $params->{description} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } + + # Some fields are not supported by Bugzilla::Bug->create but are supported + # by Bugzilla::Bug->update :( + my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {}; + foreach my $field (qw(remaining_time)) { + next if !exists $params->{$field}; + $cache->{$field} = delete $params->{$field}; + } + + # remove username/password + delete $params->{username}; + delete $params->{password}; } sub update_bug_request { - my ($params) = @_; - - my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids}; - my $bug = Bugzilla::Bug->check($bug_id); - - # Convert groups to proper add/remove lists - if (exists $params->{groups}) { - my @new_groups = map { $_->{name} } @{ $params->{groups} }; - my @old_groups = map { $_->name } @{ $bug->groups_in }; - my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups); - if (@$added || @$removed) { - my $groups_data = {}; - $groups_data->{add} = $added if @$added; - $groups_data->{remove} = $removed if @$removed; - $params->{groups} = $groups_data; - } - else { - delete $params->{groups}; - } + my ($params) = @_; + + my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids}; + my $bug = Bugzilla::Bug->check($bug_id); + + # Convert groups to proper add/remove lists + if (exists $params->{groups}) { + my @new_groups = map { $_->{name} } @{$params->{groups}}; + my @old_groups = map { $_->name } @{$bug->groups_in}; + my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups); + if (@$added || @$removed) { + my $groups_data = {}; + $groups_data->{add} = $added if @$added; + $groups_data->{remove} = $removed if @$removed; + $params->{groups} = $groups_data; } - - # Other fields such as keywords, blocks depends_on - # support 'set' which will make the list exactly what - # the user passes in. - foreach my $field (qw(blocks depends_on dependson keywords)) { - if (exists $params->{$field}) { - $params->{$field} = { set => $params->{$field} }; - } + else { + delete $params->{groups}; + } + } + + # Other fields such as keywords, blocks depends_on + # support 'set' which will make the list exactly what + # the user passes in. + foreach my $field (qw(blocks depends_on dependson keywords)) { + if (exists $params->{$field}) { + $params->{$field} = {set => $params->{$field}}; + } + } + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to change to. Also if defined + # but set to NULL then we reset them to default + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + if (!$params->{$field}) { + $params->{"reset_$field"} = 1; + delete $params->{$field}; + } + else { + $params->{$field} = $params->{$field}->{name}; + } + } + } + + # CC is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{cc}) { + my $new_cc = [map { $_->{name} } @{$params->{cc}}]; + my ($removed, $added) = diff_arrays($bug->cc, $new_cc); + if (@$added || @$removed) { + my $cc_data = {}; + $cc_data->{add} = $added if @$added; + $cc_data->{remove} = $removed if @$removed; + $params->{cc} = $cc_data; } - - # User roles such as assigned_to and qa_contact should be just the - # email (login) of the user you want to change to. Also if defined - # but set to NULL then we reset them to default - foreach my $field (qw(assigned_to qa_contact)) { - if (exists $params->{$field}) { - if (!$params->{$field}) { - $params->{"reset_$field"} = 1; - delete $params->{$field}; - } - else { - $params->{$field} = $params->{$field}->{name}; - } - } + else { + delete $params->{cc}; } + } - # CC is treated like groups in that we need 'add' and 'remove' keys - if (exists $params->{cc}) { - my $new_cc = [ map { $_->{name} } @{ $params->{cc} } ]; - my ($removed, $added) = diff_arrays($bug->cc, $new_cc); - if (@$added || @$removed) { - my $cc_data = {}; - $cc_data->{add} = $added if @$added; - $cc_data->{remove} = $removed if @$removed; - $params->{cc} = $cc_data; - } - else { - delete $params->{cc}; - } + # see_also is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{see_also}) { + my $old_see_also = [map { $_->name } @{$bug->see_also}]; + my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also}); + if (@$added || @$removed) { + my $data = {}; + $data->{add} = $added if @$added; + $data->{remove} = $removed if @$removed; + $params->{see_also} = $data; } - - # see_also is treated like groups in that we need 'add' and 'remove' keys - if (exists $params->{see_also}) { - my $old_see_also = [ map { $_->name } @{ $bug->see_also } ]; - my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also}); - if (@$added || @$removed) { - my $data = {}; - $data->{add} = $added if @$added; - $data->{remove} = $removed if @$removed; - $params->{see_also} = $data; - } - else { - delete $params->{see_also}; - } + else { + delete $params->{see_also}; } + } - # BzAPI allows for adding comments by appending to the list of current - # comments and passing the whole list back. - # 1. If a comment id is specified, the user can update the comment privacy - # 2. If no id is specified it is considered a new comment but only the last - # one will be accepted. - my %comment_is_private; - foreach my $comment (@{ $params->{'comments'} }) { - if (my $id = $comment->{'id'}) { - # Existing comment; tweak privacy flags if necessary - $comment_is_private{$id} - = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0; - } - else { - # New comment to be added - # If multiple new comments are specified, only the last one will be - # added. - $params->{comment} = { - body => $comment->{text}, - is_private => ($comment->{'is_private'} && - $comment->{'is_private'} eq "true") ? 1 : 0 - }; - } - } - $params->{comment_is_private} = \%comment_is_private if %comment_is_private; - - # Remove setter and convert requestee to just name - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - # If no flag id provided, assume it is new - if (!exists $flag->{id}) { - $flag->{new} = 1; - } - } + # BzAPI allows for adding comments by appending to the list of current + # comments and passing the whole list back. + # 1. If a comment id is specified, the user can update the comment privacy + # 2. If no id is specified it is considered a new comment but only the last + # one will be accepted. + my %comment_is_private; + foreach my $comment (@{$params->{'comments'}}) { + if (my $id = $comment->{'id'}) { + + # Existing comment; tweak privacy flags if necessary + $comment_is_private{$id} + = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0; } + else { + # New comment to be added + # If multiple new comments are specified, only the last one will be + # added. + $params->{comment} = { + body => $comment->{text}, + is_private => ($comment->{'is_private'} && $comment->{'is_private'} eq "true") + ? 1 + : 0 + }; + } + } + $params->{comment_is_private} = \%comment_is_private if %comment_is_private; + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + + # If no flag id provided, assume it is new + if (!exists $flag->{id}) { + $flag->{new} = 1; + } + } + } } sub add_comment_request { - my ($params) = @_; - $params->{comment} = delete $params->{text} if $params->{text}; + my ($params) = @_; + $params->{comment} = delete $params->{text} if $params->{text}; } sub add_attachment_request { - my ($params) = @_; - - # Bug.add_attachment uses 'summary' for description. - if ($params->{description}) { - $params->{summary} = $params->{description}; - delete $params->{description}; - } - - # Remove setter and convert requestee to just name - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - } - } - - # Add comment if one is provided - if (exists $params->{comments} && scalar @{ $params->{comments} }) { - $params->{comment} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } + my ($params) = @_; + + # Bug.add_attachment uses 'summary' for description. + if ($params->{description}) { + $params->{summary} = $params->{description}; + delete $params->{description}; + } + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } + + # Add comment if one is provided + if (exists $params->{comments} && scalar @{$params->{comments}}) { + $params->{comment} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } } sub update_attachment_request { - my ($params) = @_; - - # Stash away for midair checking later - if ($params->{last_change_time}) { - my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; - $stash->{last_change_time} = delete $params->{last_change_time}; - } - - # Immutable values - foreach my $key (qw(attacher bug_id bug_ref creation_time - encoding id ref size update_token)) { - delete $params->{$key}; - } - - # Convert setter and requestee to standard values - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - } - } - - # Add comment if one is provided - if (exists $params->{comments} && scalar @{ $params->{comments} }) { - $params->{comment} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } + my ($params) = @_; + + # Stash away for midair checking later + if ($params->{last_change_time}) { + my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; + $stash->{last_change_time} = delete $params->{last_change_time}; + } + + # Immutable values + foreach my $key ( + qw(attacher bug_id bug_ref creation_time + encoding id ref size update_token) + ) + { + delete $params->{$key}; + } + + # Convert setter and requestee to standard values + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } + + # Add comment if one is provided + if (exists $params->{comments} && scalar @{$params->{comments}}) { + $params->{comment} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } } ######################### @@ -633,237 +614,247 @@ sub update_attachment_request { ######################### sub search_bugs_response { - my ($result, $response) = @_; - my $cache = Bugzilla->request_cache; - my $params = Bugzilla->input_params; + my ($result, $response) = @_; + my $cache = Bugzilla->request_cache; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; + return if !exists $$result->{bugs}; - my $bug_objs = $cache->{bzapi_search_bugs}; + my $bug_objs = $cache->{bzapi_search_bugs}; - my @fixed_bugs; - my $stash = {}; - foreach my $bug_data (@{$$result->{bugs}}) { - my $bug_obj = shift @$bug_objs; - my $fixed = fix_bug($bug_data, $bug_obj, $stash); + my @fixed_bugs; + my $stash = {}; + foreach my $bug_data (@{$$result->{bugs}}) { + my $bug_obj = shift @$bug_objs; + my $fixed = fix_bug($bug_data, $bug_obj, $stash); - # CC count and Dupe count - if (filter_wants_nocache($params, 'cc_count')) { - $fixed->{cc_count} = scalar @{ $bug_obj->cc } - if $bug_obj->cc; - } - if (filter_wants_nocache($params, 'dupe_count')) { - $fixed->{dupe_count} = scalar @{ $bug_obj->duplicate_ids } - if $bug_obj->duplicate_ids; - } - - push(@fixed_bugs, $fixed); + # CC count and Dupe count + if (filter_wants_nocache($params, 'cc_count')) { + $fixed->{cc_count} = scalar @{$bug_obj->cc} if $bug_obj->cc; + } + if (filter_wants_nocache($params, 'dupe_count')) { + $fixed->{dupe_count} = scalar @{$bug_obj->duplicate_ids} + if $bug_obj->duplicate_ids; } - $$result->{bugs} = \@fixed_bugs; + push(@fixed_bugs, $fixed); + } + + $$result->{bugs} = \@fixed_bugs; } sub create_bug_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{id}; - my $bug_id = $$result->{id}; + return if !exists $$result->{id}; + my $bug_id = $$result->{id}; - $$result->{ref} = $rpc->type('string', ref_urlbase() . "/bug/$bug_id"); - $response->code(STATUS_CREATED); + $$result->{ref} = $rpc->type('string', ref_urlbase() . "/bug/$bug_id"); + $response->code(STATUS_CREATED); } sub get_bug_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{bugs}; - my $bug_data = $$result->{bugs}->[0]; + return if !exists $$result->{bugs}; + my $bug_data = $$result->{bugs}->[0]; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug_obj = Bugzilla::Bug->check($bug_id); - my $fixed = fix_bug($bug_data, $bug_obj); + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug_obj = Bugzilla::Bug->check($bug_id); + my $fixed = fix_bug($bug_data, $bug_obj); - $$result = $fixed; + $$result = $fixed; } sub update_bug_response { - my ($result) = @_; - return if !exists $$result->{bugs} - || !scalar @{$$result->{bugs}}; - $$result = { ok => 1 }; + my ($result) = @_; + return if !exists $$result->{bugs} || !scalar @{$$result->{bugs}}; + $$result = {ok => 1}; } # Get all comments for a bug sub get_comments_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; + return if !exists $$result->{bugs}; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); - my $comment_objs = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @filtered_comment_objs; - foreach my $comment (@$comment_objs) { - next if $comment->is_private && !Bugzilla->user->is_insider; - push(@filtered_comment_objs, $comment); - } + my $comment_objs + = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since} + }); + my @filtered_comment_objs; + foreach my $comment (@$comment_objs) { + next if $comment->is_private && !Bugzilla->user->is_insider; + push(@filtered_comment_objs, $comment); + } - my $comments_data = $$result->{bugs}->{$bug_id}->{comments}; + my $comments_data = $$result->{bugs}->{$bug_id}->{comments}; - my @fixed_comments; - foreach my $comment_data (@$comments_data) { - my $comment_obj = shift @filtered_comment_objs; - my $fixed = fix_comment($comment_data, $comment_obj); + my @fixed_comments; + foreach my $comment_data (@$comments_data) { + my $comment_obj = shift @filtered_comment_objs; + my $fixed = fix_comment($comment_data, $comment_obj); - if (exists $fixed->{creator}) { - # /bug//comment returns full login for creator but not for /bug/?include_fields=comments :( - $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login); - # /bug//comment does not return real_name for creator but returns ref - $fixed->{creator}->{'ref'} = $rpc->type('string', ref_urlbase() . "/user/" . $comment_obj->author->login); - delete $fixed->{creator}->{real_name}; - } + if (exists $fixed->{creator}) { + +# /bug//comment returns full login for creator but not for /bug/?include_fields=comments :( + $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login); - push(@fixed_comments, filter($params, $fixed)); + # /bug//comment does not return real_name for creator but returns ref + $fixed->{creator}->{'ref'} = $rpc->type('string', + ref_urlbase() . "/user/" . $comment_obj->author->login); + delete $fixed->{creator}->{real_name}; } - $$result = { comments => \@fixed_comments }; + push(@fixed_comments, filter($params, $fixed)); + } + + $$result = {comments => \@fixed_comments}; } # Format the return response on successful comment creation sub add_comment_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{id}; - my $bug_id = $rpc->bz_rest_params->{id}; + return if !exists $$result->{id}; + my $bug_id = $rpc->bz_rest_params->{id}; - $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment") }; - $response->code(STATUS_CREATED); + $$result + = {ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment")}; + $response->code(STATUS_CREATED); } # Get the history for a bug sub get_history_response { - my ($result) = @_; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; - my $history = $$result->{bugs}->[0]->{history}; + return if !exists $$result->{bugs}; + my $history = $$result->{bugs}->[0]->{history}; - my @new_history; - foreach my $changeset (@$history) { - $changeset = fix_changeset($changeset); - push(@new_history, filter($params, $changeset)); - } + my @new_history; + foreach my $changeset (@$history) { + $changeset = fix_changeset($changeset); + push(@new_history, filter($params, $changeset)); + } - $$result = { history => \@new_history }; + $$result = {history => \@new_history}; } # Get all attachments for a bug sub get_attachments_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; - - return if !exists $$result->{bugs}; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug = Bugzilla::Bug->check($bug_id); - my $attachment_objs = $bug->attachments; - - my $attachments_data = $$result->{bugs}->{$bug_id}; - - my @fixed_attachments; - foreach my $attachment (@$attachments_data) { - my $attachment_obj = shift @$attachment_objs; - my $fixed = fix_attachment($attachment, $attachment_obj); - - if ((filter_wants_nocache($params, 'data', 'extra') - || filter_wants_nocache($params, 'encoding', 'extra') - || $params->{attachmentdata})) - { - if (!$fixed->{data}) { - $fixed->{data} = $rpc->type('base64', $attachment_obj->data); - $fixed->{encoding} = $rpc->type('string', 'base64'); - } - } - else { - delete $fixed->{data}; - delete $fixed->{encoding}; - } - - push(@fixed_attachments, filter($params, $fixed)); + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); + my $attachment_objs = $bug->attachments; + + my $attachments_data = $$result->{bugs}->{$bug_id}; + + my @fixed_attachments; + foreach my $attachment (@$attachments_data) { + my $attachment_obj = shift @$attachment_objs; + my $fixed = fix_attachment($attachment, $attachment_obj); + + if (( + filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata} + )) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; } - $$result = { attachments => \@fixed_attachments }; + push(@fixed_attachments, filter($params, $fixed)); + } + + $$result = {attachments => \@fixed_attachments}; } # Format the return response on successful attachment creation sub add_attachment_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my ($attach_id) = keys %{ $$result->{attachments} }; + my ($attach_id) = keys %{$$result->{attachments}}; - $$result = { ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), id => $attach_id }; - $response->code(STATUS_CREATED); + $$result = { + ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), + id => $attach_id + }; + $response->code(STATUS_CREATED); } # Update an attachment's metadata sub update_attachment_response { - my ($result) = @_; - $$result = { ok => 1 }; + my ($result) = @_; + $$result = {ok => 1}; } # Get a single attachment by attachment_id sub get_attachment_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; - - return if !exists $$result->{attachments}; - my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0]; - my $attachment_data = $$result->{attachments}->{$attach_id}; - my $attachment_obj = Bugzilla::Attachment->new($attach_id); - my $fixed = fix_attachment($attachment_data, $attachment_obj); - - if ((filter_wants_nocache($params, 'data', 'extra') - || filter_wants_nocache($params, 'encoding', 'extra') - || $params->{attachmentdata})) - { - if (!$fixed->{data}) { - $fixed->{data} = $rpc->type('base64', $attachment_obj->data); - $fixed->{encoding} = $rpc->type('string', 'base64'); - } - } - else { - delete $fixed->{data}; - delete $fixed->{encoding}; - } - - $fixed = filter($params, $fixed); - - $$result = $fixed; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{attachments}; + my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0]; + my $attachment_data = $$result->{attachments}->{$attach_id}; + my $attachment_obj = Bugzilla::Attachment->new($attach_id); + my $fixed = fix_attachment($attachment_data, $attachment_obj); + + if (( + filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata} + )) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; + } + + $fixed = filter($params, $fixed); + + $$result = $fixed; } # Get a list of flags for a bug sub get_bug_flags_response { - my ($result) = @_; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; - my $flags = $$result->{bugs}->[0]->{flags}; + return if !exists $$result->{bugs}; + my $flags = $$result->{bugs}->[0]->{flags}; - my @new_flags; - foreach my $flag (@$flags) { - push(@new_flags, fix_flag($flag)); - } + my @new_flags; + foreach my $flag (@$flags) { + push(@new_flags, fix_flag($flag)); + } - $$result = { flags => \@new_flags }; + $$result = {flags => \@new_flags}; } 1; diff --git a/extensions/BzAPI/lib/Resources/Bugzilla.pm b/extensions/BzAPI/lib/Resources/Bugzilla.pm index 6e350d839..23d423d5a 100644 --- a/extensions/BzAPI/lib/Resources/Bugzilla.pm +++ b/extensions/BzAPI/lib/Resources/Bugzilla.pm @@ -29,124 +29,118 @@ use Digest::MD5 qw(md5_base64); ######################### BEGIN { - require Bugzilla::WebService::Bugzilla; - *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration; - *Bugzilla::WebService::Bugzilla::get_empty = \&get_empty; + require Bugzilla::WebService::Bugzilla; + *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration; + *Bugzilla::WebService::Bugzilla::get_empty = \&get_empty; } sub rest_handlers { - my $rest_handlers = [ - qr{^/$}, { - GET => { - resource => { - method => 'get_empty' - } - } - }, - qr{^/configuration$}, { - GET => { - resource => { - method => 'get_configuration' - } - } - } - ]; - return $rest_handlers; + my $rest_handlers = [ + qr{^/$}, {GET => {resource => {method => 'get_empty'}}}, + qr{^/configuration$}, {GET => {resource => {method => 'get_configuration'}}} + ]; + return $rest_handlers; } sub get_configuration { - my ($self) = @_; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; - - my $can_cache = !exists $params->{product} && !exists $params->{flags}; - my $cache_key = 'bzapi_get_configuration'; - - if ($can_cache) { - my $result = Bugzilla->memcached->get_config({key => $cache_key}); - return $result if defined $result; - } - - # Get data from the shadow DB as they don't change very often. - Bugzilla->switch_to_shadow_db; - - # Pass a bunch of Bugzilla configuration to the templates. - my $vars = {}; - $vars->{'priority'} = get_legal_field_values('priority'); - $vars->{'severity'} = get_legal_field_values('bug_severity'); - $vars->{'platform'} = get_legal_field_values('rep_platform'); - $vars->{'op_sys'} = get_legal_field_values('op_sys'); - $vars->{'keyword'} = [ map($_->name, Bugzilla::Keyword->get_all) ]; - $vars->{'resolution'} = get_legal_field_values('resolution'); - $vars->{'status'} = get_legal_field_values('bug_status'); - $vars->{'custom_fields'} = - [ grep {$_->is_select} Bugzilla->active_custom_fields ]; - - # Include a list of product objects. - if ($params->{'product'}) { - my @products = $params->{'product'}; - foreach my $product_name (@products) { - my $product = new Bugzilla::Product({ name => $product_name }); - if ($product && $user->can_see_product($product->name)) { - push (@{$vars->{'products'}}, $product); - } - } - } else { - $vars->{'products'} = $user->get_selectable_products; - } - - # We set the 2nd argument to 1 to also preload flag types. - Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 }); - - # Allow consumers to specify whether or not they want flag data. - if (defined $params->{'flags'}) { - $vars->{'show_flags'} = $params->{'flags'}; - } - else { - # We default to sending flag data. - $vars->{'show_flags'} = 1; - } - - # Create separate lists of open versus resolved statuses. This should really - # be made part of the configuration. - my @open_status; - my @closed_status; - foreach my $status (@{$vars->{'status'}}) { - is_open_state($status) ? push(@open_status, $status) - : push(@closed_status, $status); + my ($self) = @_; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + + my $can_cache = !exists $params->{product} && !exists $params->{flags}; + my $cache_key = 'bzapi_get_configuration'; + + if ($can_cache) { + my $result = Bugzilla->memcached->get_config({key => $cache_key}); + return $result if defined $result; + } + + # Get data from the shadow DB as they don't change very often. + Bugzilla->switch_to_shadow_db; + + # Pass a bunch of Bugzilla configuration to the templates. + my $vars = {}; + $vars->{'priority'} = get_legal_field_values('priority'); + $vars->{'severity'} = get_legal_field_values('bug_severity'); + $vars->{'platform'} = get_legal_field_values('rep_platform'); + $vars->{'op_sys'} = get_legal_field_values('op_sys'); + $vars->{'keyword'} = [map($_->name, Bugzilla::Keyword->get_all)]; + $vars->{'resolution'} = get_legal_field_values('resolution'); + $vars->{'status'} = get_legal_field_values('bug_status'); + $vars->{'custom_fields'} + = [grep { $_->is_select } Bugzilla->active_custom_fields]; + + # Include a list of product objects. + if ($params->{'product'}) { + my @products = $params->{'product'}; + foreach my $product_name (@products) { + my $product = new Bugzilla::Product({name => $product_name}); + if ($product && $user->can_see_product($product->name)) { + push(@{$vars->{'products'}}, $product); + } } - $vars->{'open_status'} = \@open_status; - $vars->{'closed_status'} = \@closed_status; - - # Generate a list of fields that can be queried. - my @fields = @{Bugzilla::Field->match({obsolete => 0})}; - # Exclude fields the user cannot query. - if (!Bugzilla->user->is_timetracker) { - @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields; - } - $vars->{'field'} = \@fields; - - my $json; - Bugzilla->template->process('config.json.tmpl', $vars, \$json); - if ($json) { - my $result = $self->json->decode($json); - if ($can_cache) { - Bugzilla->memcached->set_config({key => $cache_key, data => $result}); - } - return $result; - } - else { - return {}; + } + else { + $vars->{'products'} = $user->get_selectable_products; + } + + # We set the 2nd argument to 1 to also preload flag types. + Bugzilla::Product::preload($vars->{'products'}, 1, {is_active => 1}); + + # Allow consumers to specify whether or not they want flag data. + if (defined $params->{'flags'}) { + $vars->{'show_flags'} = $params->{'flags'}; + } + else { + # We default to sending flag data. + $vars->{'show_flags'} = 1; + } + + # Create separate lists of open versus resolved statuses. This should really + # be made part of the configuration. + my @open_status; + my @closed_status; + foreach my $status (@{$vars->{'status'}}) { + is_open_state($status) + ? push(@open_status, $status) + : push(@closed_status, $status); + } + $vars->{'open_status'} = \@open_status; + $vars->{'closed_status'} = \@closed_status; + + # Generate a list of fields that can be queried. + my @fields = @{Bugzilla::Field->match({obsolete => 0})}; + + # Exclude fields the user cannot query. + if (!Bugzilla->user->is_timetracker) { + @fields = grep { + $_->name + !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ + } @fields; + } + $vars->{'field'} = \@fields; + + my $json; + Bugzilla->template->process('config.json.tmpl', $vars, \$json); + if ($json) { + my $result = $self->json->decode($json); + if ($can_cache) { + Bugzilla->memcached->set_config({key => $cache_key, data => $result}); } + return $result; + } + else { + return {}; + } } sub get_empty { - my ($self) = @_; - return { - ref => $self->type('string', Bugzilla->localconfig->{urlbase} . "bzapi/"), - documentation => $self->type('string', BZAPI_DOC), - version => $self->type('string', BUGZILLA_VERSION) - }; + my ($self) = @_; + return { + ref => $self->type('string', Bugzilla->localconfig->{urlbase} . "bzapi/"), + documentation => $self->type('string', BZAPI_DOC), + version => $self->type('string', BUGZILLA_VERSION) + }; } 1; diff --git a/extensions/BzAPI/lib/Resources/User.pm b/extensions/BzAPI/lib/Resources/User.pm index 550a61d28..7a7a183a9 100644 --- a/extensions/BzAPI/lib/Resources/User.pm +++ b/extensions/BzAPI/lib/Resources/User.pm @@ -14,67 +14,58 @@ use warnings; use Bugzilla::Extension::BzAPI::Util; sub rest_handlers { - my $rest_handlers = [ - qr{/user$}, { - GET => { - response => \&get_users, - }, - }, - qr{/user/([^/]+)$}, { - GET => { - response => \&get_user, - }, - } - ]; - return $rest_handlers; + my $rest_handlers = [ + qr{/user$}, {GET => {response => \&get_users,},}, + qr{/user/([^/]+)$}, {GET => {response => \&get_user,},} + ]; + return $rest_handlers; } sub get_users { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{users}; + return if !exists $$result->{users}; - my @users; - foreach my $user (@{$$result->{users}}) { - my $object = Bugzilla::User->new( - { id => $user->{id}, cache => 1 }); + my @users; + foreach my $user (@{$$result->{users}}) { + my $object = Bugzilla::User->new({id => $user->{id}, cache => 1}); - $user = fix_user($user, $object); + $user = fix_user($user, $object); - # Use userid instead of email for 'ref' for /user calls - $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); - # Emails are not filtered even if user is not logged in - $user->{name} = $rpc->type('string', $object->login); + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); - push(@users, filter($params, $user)); - } + push(@users, filter($params, $user)); + } - $$result->{users} = \@users; + $$result->{users} = \@users; } sub get_user { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{users}; - my $user = $$result->{users}->[0] || return; - my $object = Bugzilla::User->new({ id => $user->{id}, cache => 1 }); + return if !exists $$result->{users}; + my $user = $$result->{users}->[0] || return; + my $object = Bugzilla::User->new({id => $user->{id}, cache => 1}); - $user = fix_user($user, $object); + $user = fix_user($user, $object); - # Use userid instead of email for 'ref' for /user calls - $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); - # Emails are not filtered even if user is not logged in - $user->{name} = $rpc->type('string', $object->login); + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); - $user = filter($params, $user); + $user = filter($params, $user); - $$result = $user; + $$result = $user; } 1; diff --git a/extensions/BzAPI/lib/Util.pm b/extensions/BzAPI/lib/Util.pm index d50679a6c..6f7e2c025 100644 --- a/extensions/BzAPI/lib/Util.pm +++ b/extensions/BzAPI/lib/Util.pm @@ -28,425 +28,437 @@ use MIME::Base64; use base qw(Exporter); our @EXPORT = qw( - ref_urlbase - fix_bug - fix_user - fix_flag - fix_comment - fix_changeset - fix_attachment - filter_wants_nocache - filter - fix_credentials - filter_email + ref_urlbase + fix_bug + fix_user + fix_flag + fix_comment + fix_changeset + fix_attachment + filter_wants_nocache + filter + fix_credentials + filter_email ); # Return an URL base appropriate for constructing a ref link # normally required by REST API calls. sub ref_urlbase { - return Bugzilla->localconfig->{urlbase} . "bzapi"; + return Bugzilla->localconfig->{urlbase} . "bzapi"; } # convert certain fields within a bug object # from a simple scalar value to their respective objects sub fix_bug { - my ($data, $bug, $stash) = @_; - my $dbh = $stash->{dbh} //= Bugzilla->dbh; - my $params = $stash->{params} //= Bugzilla->input_params; - my $rpc = $stash->{rpc} //= Bugzilla->request_cache->{bzapi_rpc}; - my $method = $stash->{method} //= Bugzilla->request_cache->{bzapi_rpc_method}; - - $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id}); - - # Add REST API reference to the individual bug - if ($stash->{wants_ref} //= filter_wants_nocache($params, 'ref')) { - $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id; + my ($data, $bug, $stash) = @_; + my $dbh = $stash->{dbh} //= Bugzilla->dbh; + my $params = $stash->{params} //= Bugzilla->input_params; + my $rpc = $stash->{rpc} //= Bugzilla->request_cache->{bzapi_rpc}; + my $method = $stash->{method} //= Bugzilla->request_cache->{bzapi_rpc_method}; + + $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id}); + + # Add REST API reference to the individual bug + if ($stash->{wants_ref} //= filter_wants_nocache($params, 'ref')) { + $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id; + } + + # User fields + foreach my $field (USER_FIELDS) { + next if !exists $data->{$field}; + if ($field eq 'cc') { + my @new_cc; + foreach my $cc (@{$bug->cc_users}) { + my $cc_data = {name => filter_email($cc->email)}; + push(@new_cc, fix_user($cc_data, $cc)); + } + $data->{$field} = \@new_cc; } - - # User fields - foreach my $field (USER_FIELDS) { - next if !exists $data->{$field}; - if ($field eq 'cc') { - my @new_cc; - foreach my $cc (@{ $bug->cc_users }) { - my $cc_data = { name => filter_email($cc->email) }; - push(@new_cc, fix_user($cc_data, $cc)); - } - $data->{$field} = \@new_cc; - } - else { - my $field_name = $field; - if ($field eq 'creator') { - $field_name = 'reporter'; - } - $data->{$field} - = fix_user($data->{"${field}_detail"}, $bug->$field_name); - delete $data->{$field}->{id}; - delete $data->{$field}->{email}; - $data->{$field} = filter($params, $data->{$field}, undef, $field); - } - - # Get rid of extra detail hash if exists since redundant - delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"}; + else { + my $field_name = $field; + if ($field eq 'creator') { + $field_name = 'reporter'; + } + $data->{$field} = fix_user($data->{"${field}_detail"}, $bug->$field_name); + delete $data->{$field}->{id}; + delete $data->{$field}->{email}; + $data->{$field} = filter($params, $data->{$field}, undef, $field); } - # Groups - if ($stash->{wants_groups} //= filter_wants_nocache($params, 'groups')) { - my @new_groups; - foreach my $group (@{ $data->{groups} }) { - if (my $object = Bugzilla::Group->new({ name => $group, cache => 1 })) { - $group = { - id => $rpc->type('int', $object->id), - name => $rpc->type('string', $object->name), - }; - } - push(@new_groups, $group); - } - $data->{groups} = \@new_groups; + # Get rid of extra detail hash if exists since redundant + delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"}; + } + + # Groups + if ($stash->{wants_groups} //= filter_wants_nocache($params, 'groups')) { + my @new_groups; + foreach my $group (@{$data->{groups}}) { + if (my $object = Bugzilla::Group->new({name => $group, cache => 1})) { + $group = { + id => $rpc->type('int', $object->id), + name => $rpc->type('string', $object->name), + }; + } + push(@new_groups, $group); } - - # Flags - if (exists $data->{flags}) { - my @new_flags; - foreach my $flag (@{ $data->{flags} }) { - push(@new_flags, fix_flag($flag)); - } - $data->{flags} = \@new_flags; + $data->{groups} = \@new_groups; + } + + # Flags + if (exists $data->{flags}) { + my @new_flags; + foreach my $flag (@{$data->{flags}}) { + push(@new_flags, fix_flag($flag)); } - - # Attachment metadata is included by default but not data - if ($stash->{wants_attachments} //= filter_wants_nocache($params, 'attachments')) { - my $attachment_params = { ids => $bug->id }; - if (!filter_wants_nocache($params, 'data', 'extra', 'attachments') - && !$params->{attachmentdata}) - { - $attachment_params->{exclude_fields} = ['data']; - } - - my $attachments = $rpc->attachments($attachment_params); - - my @fixed_attachments; - foreach my $attachment (@{ $attachments->{bugs}->{$bug->id} }) { - my $fixed = fix_attachment($attachment); - push(@fixed_attachments, filter($params, $fixed, undef, 'attachments')); - } - - $data->{attachments} = \@fixed_attachments; + $data->{flags} = \@new_flags; + } + + # Attachment metadata is included by default but not data + if ($stash->{wants_attachments} + //= filter_wants_nocache($params, 'attachments')) + { + my $attachment_params = {ids => $bug->id}; + if ( !filter_wants_nocache($params, 'data', 'extra', 'attachments') + && !$params->{attachmentdata}) + { + $attachment_params->{exclude_fields} = ['data']; } - # Comments and history are not part of _default and have to be requested + my $attachments = $rpc->attachments($attachment_params); - # Comments - if ($stash->{wants_comments} //= filter_wants_nocache($params, 'comments', 'extra', 'comments')) { - my $comments = $rpc->comments({ ids => $bug->id }); - $comments = $comments->{bugs}->{$bug->id}->{comments}; - my @new_comments; - foreach my $comment (@$comments) { - $comment = fix_comment($comment); - push(@new_comments, filter($params, $comment, 'extra', 'comments')); - } - $data->{comments} = \@new_comments; + my @fixed_attachments; + foreach my $attachment (@{$attachments->{bugs}->{$bug->id}}) { + my $fixed = fix_attachment($attachment); + push(@fixed_attachments, filter($params, $fixed, undef, 'attachments')); } - # History - if ($stash->{wants_history} //= filter_wants_nocache($params, 'history', 'extra', 'history')) { - my $history = $rpc->history({ ids => [ $bug->id ] }); - my @new_history; - foreach my $changeset (@{ $history->{bugs}->[0]->{history} }) { - push(@new_history, fix_changeset($changeset, $bug)); - } - $data->{history} = \@new_history; + $data->{attachments} = \@fixed_attachments; + } + + # Comments and history are not part of _default and have to be requested + + # Comments + if ($stash->{wants_comments} + //= filter_wants_nocache($params, 'comments', 'extra', 'comments')) + { + my $comments = $rpc->comments({ids => $bug->id}); + $comments = $comments->{bugs}->{$bug->id}->{comments}; + my @new_comments; + foreach my $comment (@$comments) { + $comment = fix_comment($comment); + push(@new_comments, filter($params, $comment, 'extra', 'comments')); + } + $data->{comments} = \@new_comments; + } + + # History + if ($stash->{wants_history} + //= filter_wants_nocache($params, 'history', 'extra', 'history')) + { + my $history = $rpc->history({ids => [$bug->id]}); + my @new_history; + foreach my $changeset (@{$history->{bugs}->[0]->{history}}) { + push(@new_history, fix_changeset($changeset, $bug)); + } + $data->{history} = \@new_history; + } + + # Add in all custom fields even if not set or visible on this bug + my $custom_fields = $stash->{custom_fields} + //= Bugzilla->fields({custom => 1, obsolete => 0, by_name => 1}); + foreach my $field (values %$custom_fields) { + my $name = $field->name; + my $type = $field->type; + if (!filter_wants_nocache($params, $name, ['default', 'custom'])) { + delete $custom_fields->{$name}; + next; + } + if ($type == FIELD_TYPE_BUG_ID) { + $data->{$name} = $rpc->type('int', $bug->$name); + } + elsif ($type == FIELD_TYPE_DATETIME || $type == FIELD_TYPE_DATE) { + $data->{$name} = $rpc->type('dateTime', $bug->$name); } + elsif ($type == FIELD_TYPE_MULTI_SELECT) { + +# Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :( + if ($method eq 'Bug.search' + && !grep($_ eq '_all', @{$params->{include_fields}})) + { + $data->{$name} = $rpc->type('string', join(', ', @{$bug->$name})); + } + else { + my @values = map { $rpc->type('string', $_) } @{$bug->$name}; + $data->{$name} = \@values; + } + } + else { + $data->{$name} = $rpc->type('string', $bug->$name); + } + } - # Add in all custom fields even if not set or visible on this bug - my $custom_fields = $stash->{custom_fields} //= - Bugzilla->fields({ custom => 1, obsolete => 0, by_name => 1 }); - foreach my $field (values %$custom_fields) { - my $name = $field->name; - my $type = $field->type; - if (!filter_wants_nocache($params, $name, ['default','custom'])) { - delete $custom_fields->{$name}; - next; - } - if ($type == FIELD_TYPE_BUG_ID) { - $data->{$name} = $rpc->type('int', $bug->$name); - } - elsif ($type == FIELD_TYPE_DATETIME - || $type == FIELD_TYPE_DATE) - { - $data->{$name} = $rpc->type('dateTime', $bug->$name); - } - elsif ($type == FIELD_TYPE_MULTI_SELECT) { - # Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :( - if ($method eq 'Bug.search' && !grep($_ eq '_all', @{ $params->{include_fields} })) { - $data->{$name} = $rpc->type('string', join(', ', @{ $bug->$name })); - } - else { - my @values = map { $rpc->type('string', $_) } @{ $bug->$name }; - $data->{$name} = \@values; - } - } - else { - $data->{$name} = $rpc->type('string', $bug->$name); - } + # Remove empty values in some cases + foreach my $key (keys %$data) { + + # QA Contact is null if single bug or "" if doing search + if ($key eq 'qa_contact' && !$data->{$key}->{name}) { + if ($method eq 'Bug.search') { + $data->{$key}->{name} = $rpc->type('string', ''); + } + next; } - # Remove empty values in some cases - foreach my $key (keys %$data) { - # QA Contact is null if single bug or "" if doing search - if ($key eq 'qa_contact' && !$data->{$key}->{name}) { - if ($method eq 'Bug.search') { - $data->{$key}->{name} = $rpc->type('string', ''); - } - next; - } + next if $method eq 'Bug.search' && $key eq 'url'; # Return url even if empty + next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty + next if $method eq 'Bug.search' && $key eq 'whiteboard'; # Return whiteboard even if empty + next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS); - next if $method eq 'Bug.search' && $key eq 'url'; # Return url even if empty - next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty - next if $method eq 'Bug.search' && $key eq 'whiteboard'; # Return whiteboard even if empty - next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS); + next + if ($method eq 'Bug.search' + && $key =~ /^(resolution|cc_count|dupe_count)$/ + && !grep($_ eq '_all', @{$params->{include_fields}})); - next if ($method eq 'Bug.search' - && $key =~ /^(resolution|cc_count|dupe_count)$/ - && !grep($_ eq '_all', @{ $params->{include_fields} })); + if (!ref $data->{$key}) { + delete $data->{$key} if !$data->{$key}; + } + else { + if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) { - if (!ref $data->{$key}) { - delete $data->{$key} if !$data->{$key}; + # Return empty string if blocks or depends_on is empty + if ($method eq 'Bug.search' && ($key eq 'depends_on' || $key eq 'blocks')) { + $data->{$key} = ''; } else { - if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) { - # Return empty string if blocks or depends_on is empty - if ($method eq 'Bug.search' && ($key eq 'depends_on' || $key eq 'blocks')) { - $data->{$key} = ''; - } - else { - delete $data->{$key}; - } - } - elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) { - delete $data->{$key}; - } + delete $data->{$key}; } + } + elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) { + delete $data->{$key}; + } } + } - return $data; + return $data; } # convert a user related field from being just login # names to user objects sub fix_user { - my ($data, $object) = @_; - my $user = Bugzilla->user; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($data, $object) = @_; + my $user = Bugzilla->user; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return { name => undef } if !$data; + return {name => undef} if !$data; - if (!ref $data) { - $data = { - name => filter_email($object->login) - }; - $data->{real_name} = $rpc->type('string', $object->name); - } - else { - $data->{name} = filter_email($data->{name}); - } + if (!ref $data) { + $data = {name => filter_email($object->login)}; + $data->{real_name} = $rpc->type('string', $object->name); + } + else { + $data->{name} = filter_email($data->{name}); + } - if ($user->id) { - $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login); - } + if ($user->id) { + $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login); + } - return $data; + return $data; } # convert certain attributes of a comment to objects # and also remove other unwanted key/values. sub fix_comment { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $method = Bugzilla->request_cache->{bzapi_rpc_method}; + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $method = Bugzilla->request_cache->{bzapi_rpc_method}; - $object ||= Bugzilla::Comment->new({ id => $data->{id}, cache => 1 }); + $object ||= Bugzilla::Comment->new({id => $data->{id}, cache => 1}); - if (exists $data->{creator}) { - $data->{creator} = fix_user($data->{creator}, $object->author); - } + if (exists $data->{creator}) { + $data->{creator} = fix_user($data->{creator}, $object->author); + } - if ($data->{attachment_id} && $method ne 'Bug.search') { - $data->{attachment_ref} = $rpc->type('string', ref_urlbase() . - "/attachment/" . $object->extra_data); - } - else { - delete $data->{attachment_id}; - } + if ($data->{attachment_id} && $method ne 'Bug.search') { + $data->{attachment_ref} + = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->extra_data); + } + else { + delete $data->{attachment_id}; + } - delete $data->{author}; - delete $data->{time}; - delete $data->{raw_text}; + delete $data->{author}; + delete $data->{time}; + delete $data->{raw_text}; - return $data; + return $data; } # convert certain attributes of a changeset object from # scalar values to related objects. Also remove other unwanted # key/values. sub fix_changeset { - my ($data, $object) = @_; - my $user = Bugzilla->user; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - - if ($data->{who}) { - $data->{changer} = { - name => $rpc->type('string', $data->{who}), - ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who}) - }; - delete $data->{who}; - } - - if ($data->{when}) { - $data->{change_time} = $rpc->type('dateTime', $data->{when}); - delete $data->{when}; - } - - foreach my $change (@{ $data->{changes} }) { - $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name'; - } - - return $data; + my ($data, $object) = @_; + my $user = Bugzilla->user; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + if ($data->{who}) { + $data->{changer} = { + name => $rpc->type('string', $data->{who}), + ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who}) + }; + delete $data->{who}; + } + + if ($data->{when}) { + $data->{change_time} = $rpc->type('dateTime', $data->{when}); + delete $data->{when}; + } + + foreach my $change (@{$data->{changes}}) { + $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name'; + } + + return $data; } # convert certain attributes of an attachment object from # scalar values to related objects. Also add in additional # key/values. sub fix_attachment { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $method = Bugzilla->request_cache->{bzapi_rpc_method}; - my $params = Bugzilla->input_params; - my $user = Bugzilla->user; - - $object ||= Bugzilla::Attachment->new({ id => $data->{id}, cache => 1 }); - - if (exists $data->{attacher}) { - $data->{attacher} = fix_user($data->{attacher}, $object->attacher); - if ($method eq 'Bug.search') { - delete $data->{attacher}->{real_name}; - } - else { - $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name); - } + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $method = Bugzilla->request_cache->{bzapi_rpc_method}; + my $params = Bugzilla->input_params; + my $user = Bugzilla->user; + + $object ||= Bugzilla::Attachment->new({id => $data->{id}, cache => 1}); + + if (exists $data->{attacher}) { + $data->{attacher} = fix_user($data->{attacher}, $object->attacher); + if ($method eq 'Bug.search') { + delete $data->{attacher}->{real_name}; } - - if (exists $data->{data}) { - $data->{encoding} = $rpc->type('string', 'base64'); - if ($params->{attachmentdata} - || filter_wants_nocache($params, 'attachments.data')) - { - $data->{encoding} = $rpc->type('string', 'base64'); - } - else { - delete $data->{data}; - } - } - - if (exists $data->{bug_id}) { - $data->{bug_ref} = $rpc->type('string', ref_urlbase() . "/bug/" . $object->bug_id); - } - - # Upstream API returns these as integers where bzapi returns as booleans - if (exists $data->{is_patch}) { - $data->{is_patch} = $rpc->type('boolean', $data->{is_patch}); - } - if (exists $data->{is_obsolete}) { - $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete}); - } - if (exists $data->{is_private}) { - $data->{is_private} = $rpc->type('boolean', $data->{is_private}); + else { + $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name); } - - if (exists $data->{flags} && @{ $data->{flags} }) { - my @new_flags; - foreach my $flag (@{ $data->{flags} }) { - push(@new_flags, fix_flag($flag)); - } - $data->{flags} = \@new_flags; + } + + if (exists $data->{data}) { + $data->{encoding} = $rpc->type('string', 'base64'); + if ($params->{attachmentdata} + || filter_wants_nocache($params, 'attachments.data')) + { + $data->{encoding} = $rpc->type('string', 'base64'); } else { - delete $data->{flags}; + delete $data->{data}; } + } + + if (exists $data->{bug_id}) { + $data->{bug_ref} + = $rpc->type('string', ref_urlbase() . "/bug/" . $object->bug_id); + } + + # Upstream API returns these as integers where bzapi returns as booleans + if (exists $data->{is_patch}) { + $data->{is_patch} = $rpc->type('boolean', $data->{is_patch}); + } + if (exists $data->{is_obsolete}) { + $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete}); + } + if (exists $data->{is_private}) { + $data->{is_private} = $rpc->type('boolean', $data->{is_private}); + } + + if (exists $data->{flags} && @{$data->{flags}}) { + my @new_flags; + foreach my $flag (@{$data->{flags}}) { + push(@new_flags, fix_flag($flag)); + } + $data->{flags} = \@new_flags; + } + else { + delete $data->{flags}; + } - $data->{ref} = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->id); + $data->{ref} + = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->id); - # Add update token if we are getting an attachment outside of Bug.get and user is logged in - if ($user->id && ($method eq 'Bug.attachments'|| $method eq 'Bug.search')) { - $data->{update_token} = issue_hash_token([ $object->id, $object->modification_time ]); - } +# Add update token if we are getting an attachment outside of Bug.get and user is logged in + if ($user->id && ($method eq 'Bug.attachments' || $method eq 'Bug.search')) { + $data->{update_token} + = issue_hash_token([$object->id, $object->modification_time]); + } - delete $data->{creator}; - delete $data->{summary}; + delete $data->{creator}; + delete $data->{summary}; - return $data; + return $data; } # convert certain attributes of a flag object from # scalar values to related objects. Also remove other unwanted # key/values. sub fix_flag { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - $object ||= Bugzilla::Flag->new({ id => $data->{id}, cache => 1 }); + $object ||= Bugzilla::Flag->new({id => $data->{id}, cache => 1}); - if (exists $data->{setter}) { - $data->{setter} = fix_user($data->{setter}, $object->setter); - delete $data->{setter}->{real_name}; - } + if (exists $data->{setter}) { + $data->{setter} = fix_user($data->{setter}, $object->setter); + delete $data->{setter}->{real_name}; + } - if (exists $data->{requestee}) { - $data->{requestee} = fix_user($data->{requestee}, $object->requestee); - delete $data->{requestee}->{real_name}; - } + if (exists $data->{requestee}) { + $data->{requestee} = fix_user($data->{requestee}, $object->requestee); + delete $data->{requestee}->{real_name}; + } - return $data; + return $data; } # Calls Bugzilla::WebService::Util::filter_wants but disables caching # as we make several webservice calls in a single REST call and the # caching can cause unexpected results. sub filter_wants_nocache { - my ($params, $field, $types, $prefix) = @_; - delete Bugzilla->request_cache->{filter_wants}; - return filter_wants($params, $field, $types, $prefix); + my ($params, $field, $types, $prefix) = @_; + delete Bugzilla->request_cache->{filter_wants}; + return filter_wants($params, $field, $types, $prefix); } sub filter { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix); - } - return \%newhash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix); + } + return \%newhash; } sub fix_credentials { - my ($params) = @_; - # Allow user to pass in username=foo&password=bar to be compatible - $params->{'Bugzilla_login'} = $params->{'login'} = delete $params->{'username'} - if exists $params->{'username'}; - $params->{'Bugzilla_password'} = $params->{'password'} if exists $params->{'password'}; - - # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI - if (exists $params->{'userid'} && exists $params->{'cookie'}) { - my $userid = delete $params->{'userid'}; - my $cookie = delete $params->{'cookie'}; - $params->{'Bugzilla_token'} = "${userid}-${cookie}"; - } + my ($params) = @_; + + # Allow user to pass in username=foo&password=bar to be compatible + $params->{'Bugzilla_login'} = $params->{'login'} = delete $params->{'username'} + if exists $params->{'username'}; + $params->{'Bugzilla_password'} = $params->{'password'} + if exists $params->{'password'}; + + # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI + if (exists $params->{'userid'} && exists $params->{'cookie'}) { + my $userid = delete $params->{'userid'}; + my $cookie = delete $params->{'cookie'}; + $params->{'Bugzilla_token'} = "${userid}-${cookie}"; + } } # Filter email addresses by default ignoring the system # webservice_email_filter setting sub filter_email { - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return $rpc->type('string', email_filter($_[0])); + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + return $rpc->type('string', email_filter($_[0])); } 1; diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm index fdeedff98..654094191 100644 --- a/extensions/ComponentWatching/Extension.pm +++ b/extensions/ComponentWatching/Extension.pm @@ -32,92 +32,58 @@ use constant REL_COMPONENT_WATCHER => 15; # sub db_schema_abstract_schema { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - # Bugzilla 5.0+, the components.id type - # is INT3, while earlier versions used INT2 - my $component_id_type = 'INT2'; - my $len = scalar @{ $args->{schema}->{components}->{FIELDS} }; - for (my $i = 0; $i < $len - 1; $i+=2) { - next if $args->{schema}->{components}->{FIELDS}->[$i] ne 'id'; - $component_id_type = 'INT3' - if $args->{schema}->{components}->{FIELDS}->[$i+1]->{TYPE} eq 'MEDIUMSERIAL'; - last; - } - $args->{'schema'}->{'component_watch'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - component_id => { - TYPE => $component_id_type, - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - component_prefix => { - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - }, - ], - }; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + # Bugzilla 5.0+, the components.id type + # is INT3, while earlier versions used INT2 + my $component_id_type = 'INT2'; + my $len = scalar @{$args->{schema}->{components}->{FIELDS}}; + for (my $i = 0; $i < $len - 1; $i += 2) { + next if $args->{schema}->{components}->{FIELDS}->[$i] ne 'id'; + $component_id_type = 'INT3' + if $args->{schema}->{components}->{FIELDS}->[$i + 1]->{TYPE} eq + 'MEDIUMSERIAL'; + last; + } + $args->{'schema'}->{'component_watch'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + component_id => { + TYPE => $component_id_type, + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',} + }, + component_prefix => {TYPE => 'VARCHAR(64)', NOTNULL => 0,}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column( - 'components', - 'watch_user', - { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL', - } - } - ); - $dbh->bz_add_column( - 'component_watch', - 'id', - { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - ); - $dbh->bz_add_column( - 'component_watch', - 'component_prefix', - { - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - } - ); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'components', + 'watch_user', + { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL',} + } + ); + $dbh->bz_add_column('component_watch', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + ); + $dbh->bz_add_column('component_watch', 'component_prefix', + {TYPE => 'VARCHAR(64)', NOTNULL => 0,}); } # @@ -125,16 +91,16 @@ sub install_update_db { # sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{VARIABLES}{constants}; + $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; } sub template_before_process { - my ($self, $args) = @_; - return unless $args->{file} eq 'admin/components/create.html.tmpl'; - $args->{vars}{comp}{default_assignee}{login} = DEFAULT_ASSIGNEE; + my ($self, $args) = @_; + return unless $args->{file} eq 'admin/components/create.html.tmpl'; + $args->{vars}{comp}{default_assignee}{login} = DEFAULT_ASSIGNEE; } # @@ -142,147 +108,147 @@ sub template_before_process { # BEGIN { - *Bugzilla::Component::watch_user = \&_component_watch_user; + *Bugzilla::Component::watch_user = \&_component_watch_user; } sub _component_watch_user { - my ($self) = @_; - return unless $self->{watch_user}; - $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user}); - return $self->{watch_user_object}; + my ($self) = @_; + return unless $self->{watch_user}; + $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user}); + return $self->{watch_user_object}; } sub object_columns { - my ($self, $args) = @_; - my $class = $args->{class}; - my $columns = $args->{columns}; - return unless $class->isa('Bugzilla::Component'); - - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'watch_user')) { - push @$columns, 'watch_user'; - } + my ($self, $args) = @_; + my $class = $args->{class}; + my $columns = $args->{columns}; + return unless $class->isa('Bugzilla::Component'); + + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'watch_user')) { + push @$columns, 'watch_user'; + } } sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{object}; - my $columns = $args->{columns}; - return unless $object->isa('Bugzilla::Component'); + my ($self, $args) = @_; + my $object = $args->{object}; + my $columns = $args->{columns}; + return unless $object->isa('Bugzilla::Component'); - push(@$columns, 'watch_user'); + push(@$columns, 'watch_user'); - # add the user if not yet exists and user chooses 'automatic' - $self->_create_watch_user(); + # add the user if not yet exists and user chooses 'automatic' + $self->_create_watch_user(); - # editcomponents.cgi doesn't call set_all, so we have to do this here - my $input = Bugzilla->input_params; - $object->set('watch_user', $input->{watch_user}); + # editcomponents.cgi doesn't call set_all, so we have to do this here + my $input = Bugzilla->input_params; + $object->set('watch_user', $input->{watch_user}); } sub object_validators { - my ($self, $args) = @_; - my $class = $args->{class}; - my $validators = $args->{validators}; - return unless $class->isa('Bugzilla::Component'); + my ($self, $args) = @_; + my $class = $args->{class}; + my $validators = $args->{validators}; + return unless $class->isa('Bugzilla::Component'); - $validators->{watch_user} = \&_check_watch_user; + $validators->{watch_user} = \&_check_watch_user; } sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{class}; - my $params = $args->{params}; - return unless $class->isa('Bugzilla::Component'); - - # We need to create a watch user for the default product/component - # if we are creating the database for the first time. - my $dbh = Bugzilla->dbh; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - && !$dbh->selectrow_array('SELECT 1 FROM components')) - { - my $watch_user = Bugzilla::User->create({ - login_name => 'testcomponent@testproduct.bugs', - cryptpassword => '*', - disable_mail => 1 - }); - $params->{watch_user} = $watch_user->login; - } - else { - my $input = Bugzilla->input_params; - $params->{watch_user} = $input->{watch_user}; - $self->_create_watch_user(); - } + my ($self, $args) = @_; + my $class = $args->{class}; + my $params = $args->{params}; + return unless $class->isa('Bugzilla::Component'); + + # We need to create a watch user for the default product/component + # if we are creating the database for the first time. + my $dbh = Bugzilla->dbh; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE + && !$dbh->selectrow_array('SELECT 1 FROM components')) + { + my $watch_user = Bugzilla::User->create({ + login_name => 'testcomponent@testproduct.bugs', + cryptpassword => '*', + disable_mail => 1 + }); + $params->{watch_user} = $watch_user->login; + } + else { + my $input = Bugzilla->input_params; + $params->{watch_user} = $input->{watch_user}; + $self->_create_watch_user(); + } } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - my $old_object = $args->{old_object}; - my $changes = $args->{changes}; - return unless $object->isa('Bugzilla::Component'); - - my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0; - my $new_id = $object->watch_user ? $object->watch_user->id : 0; - if ($old_id != $new_id) { - $changes->{watch_user} = [ $old_id ? $old_id : undef, $new_id ? $new_id : undef ]; - } - - # when a component is renamed, update the watch-user to follow - # this only happens when the user appears to have been auto-generated from the old name - if ($changes->{name} - && $old_object->watch_user - && $object->watch_user - && $old_object->watch_user->id == $object->watch_user->id - && _generate_watch_user_name($old_object) eq $object->watch_user->login - ) - { - my $old_login = $object->watch_user->login; - $object->watch_user->set_login(_generate_watch_user_name($object)); - $object->watch_user->update(); - $changes->{watch_user_login} = [ $old_login, $object->watch_user->login ]; - } + my ($self, $args) = @_; + my $object = $args->{object}; + my $old_object = $args->{old_object}; + my $changes = $args->{changes}; + return unless $object->isa('Bugzilla::Component'); + + my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0; + my $new_id = $object->watch_user ? $object->watch_user->id : 0; + if ($old_id != $new_id) { + $changes->{watch_user} = [$old_id ? $old_id : undef, $new_id ? $new_id : undef]; + } + +# when a component is renamed, update the watch-user to follow +# this only happens when the user appears to have been auto-generated from the old name + if ( $changes->{name} + && $old_object->watch_user + && $object->watch_user + && $old_object->watch_user->id == $object->watch_user->id + && _generate_watch_user_name($old_object) eq $object->watch_user->login) + { + my $old_login = $object->watch_user->login; + $object->watch_user->set_login(_generate_watch_user_name($object)); + $object->watch_user->update(); + $changes->{watch_user_login} = [$old_login, $object->watch_user->login]; + } } sub _generate_watch_user_name { - # this is mirrored in template/en/default/hook/admin/components/edit-common-rows.html.tmpl - # that javascript needs to be kept in sync with this perl - my ($component) = @_; - return _sanitise_name($component->name) - . '@' . _sanitise_name($component->product->name) . '.bugs'; + +# this is mirrored in template/en/default/hook/admin/components/edit-common-rows.html.tmpl +# that javascript needs to be kept in sync with this perl + my ($component) = @_; + return + _sanitise_name($component->name) . '@' + . _sanitise_name($component->product->name) . '.bugs'; } sub _sanitise_name { - my ($name) = @_; - $name = lc($name); - $name =~ s/[^a-z0-9_]/-/g; - $name =~ s/-+/-/g; - $name =~ s/(^-|-$)//g; - return $name; + my ($name) = @_; + $name = lc($name); + $name =~ s/[^a-z0-9_]/-/g; + $name =~ s/-+/-/g; + $name =~ s/(^-|-$)//g; + return $name; } sub _create_watch_user { - my $input = Bugzilla->input_params; - if ($input->{watch_user_auto} - && !Bugzilla::User->new({ name => $input->{watch_user} })) - { - Bugzilla::User->create({ - login_name => $input->{watch_user}, - cryptpassword => '*', - }); - } + my $input = Bugzilla->input_params; + if ($input->{watch_user_auto} + && !Bugzilla::User->new({name => $input->{watch_user}})) + { + Bugzilla::User->create({ + login_name => $input->{watch_user}, cryptpassword => '*', + }); + } } sub _check_watch_user { - my ($self, $value, $field) = @_; - $value = trim($value || ''); - return undef if !REQUIRE_WATCH_USER && $value eq ''; - if ($value eq '') { - ThrowUserError('component_watch_missing_watch_user'); - } - if ($value !~ /\.bugs$/i) { - ThrowUserError('component_watch_invalid_watch_user'); - } - return Bugzilla::User->check($value)->id; + my ($self, $value, $field) = @_; + $value = trim($value || ''); + return undef if !REQUIRE_WATCH_USER && $value eq ''; + if ($value eq '') { + ThrowUserError('component_watch_missing_watch_user'); + } + if ($value !~ /\.bugs$/i) { + ThrowUserError('component_watch_invalid_watch_user'); + } + return Bugzilla::User->check($value)->id; } # @@ -290,74 +256,79 @@ sub _check_watch_user { # sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'component_watch'; - - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $input = Bugzilla->input_params; + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'component_watch'; - if ($save) { - if ($input->{'add'} && $input->{'add_product'}) { - # add watch + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $input = Bugzilla->input_params; - # load product and verify access - my $productName = $input->{'add_product'}; - my $product = Bugzilla::Product->new({ name => $productName, cache => 1 }); - unless ($product && $user->can_access_product($product)) { - ThrowUserError('product_access_denied', { product => $productName }); - } + if ($save) { + if ($input->{'add'} && $input->{'add_product'}) { - # starting-with - if (my $prefix = $input->{add_starting}) { - _addPrefixWatch($user, $product, $prefix); - - } else { - my $ra_componentNames = $input->{'add_component'}; - $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - - if (grep { $_ eq '' } @$ra_componentNames) { - # watching a product - _addProductWatch($user, $product); - - } else { - # watching specific components - foreach my $componentName (@$ra_componentNames) { - my $component = Bugzilla::Component->new({ - name => $componentName, product => $product, cache => 1 - }); - unless ($component) { - ThrowUserError('product_access_denied', { product => $productName }); - } - _addComponentWatch($user, $component); - } - } - } + # add watch + + # load product and verify access + my $productName = $input->{'add_product'}; + my $product = Bugzilla::Product->new({name => $productName, cache => 1}); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', {product => $productName}); + } - _addDefaultSettings($user); + # starting-with + if (my $prefix = $input->{add_starting}) { + _addPrefixWatch($user, $product, $prefix); - } else { - # remove watch(s) + } + else { + my $ra_componentNames = $input->{'add_component'}; + $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - my $delete = ref $input->{del_watch} - ? $input->{del_watch} - : [ $input->{del_watch} ]; - foreach my $id (@$delete) { - _deleteWatch($user, $id); + if (grep { $_ eq '' } @$ra_componentNames) { + + # watching a product + _addProductWatch($user, $product); + + } + else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component + = Bugzilla::Component->new({ + name => $componentName, product => $product, cache => 1 + }); + unless ($component) { + ThrowUserError('product_access_denied', {product => $productName}); } + _addComponentWatch($user, $component); + } } + } + + _addDefaultSettings($user); } + else { + # remove watch(s) - $vars->{'add_product'} = $input->{'product'}; - $vars->{'add_component'} = $input->{'component'}; - $vars->{'watches'} = _getWatches($user); - $vars->{'user_watches'} = _getUserWatches($user); + my $delete + = ref $input->{del_watch} ? $input->{del_watch} : [$input->{del_watch}]; + foreach my $id (@$delete) { + _deleteWatch($user, $id); + } + } + + } - $$handled = 1; + $vars->{'add_product'} = $input->{'product'}; + $vars->{'add_component'} = $input->{'component'}; + $vars->{'watches'} = _getWatches($user); + $vars->{'user_watches'} = _getUserWatches($user); + + $$handled = 1; } # @@ -365,44 +336,45 @@ sub user_preferences { # sub bugmail_recipients { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $recipients = $args->{'recipients'}; - my $diffs = $args->{'diffs'}; - - my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); - my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id); - - # notify when the product/component is switch from one being watched - if (@$diffs) { - # we need the product to process the component, so scan for that first - my $product; - foreach my $ra (@$diffs) { - next if !(exists $ra->{'old'} - && exists $ra->{'field_name'}); - if ($ra->{'field_name'} eq 'product') { - $product = Bugzilla::Product->new({ name => $ra->{'old'}, cache => 1 }); - $oldProductId = $product->id; - } - } - if (!$product) { - $product = Bugzilla::Product->new({ id => $oldProductId, cache => 1 }); - } - foreach my $ra (@$diffs) { - next if !(exists $ra->{'old'} - && exists $ra->{'field_name'}); - if ($ra->{'field_name'} eq 'component') { - my $component = Bugzilla::Component->new({ - name => $ra->{'old'}, product => $product, cache => 1 - }); - $oldComponentId = $component->id; - } - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); + my ($oldComponentId, $newComponentId) + = ($bug->component_id, $bug->component_id); + + # notify when the product/component is switch from one being watched + if (@$diffs) { + + # we need the product to process the component, so scan for that first + my $product; + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'product') { + $product = Bugzilla::Product->new({name => $ra->{'old'}, cache => 1}); + $oldProductId = $product->id; + } } + if (!$product) { + $product = Bugzilla::Product->new({id => $oldProductId, cache => 1}); + } + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'component') { + my $component + = Bugzilla::Component->new({ + name => $ra->{'old'}, product => $product, cache => 1 + }); + $oldComponentId = $component->id; + } + } + } - # add component watchers - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + # add component watchers + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" SELECT user_id FROM component_watch WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL) @@ -416,52 +388,52 @@ sub bugmail_recipients { AND components.name LIKE @{[$dbh->sql_string_concat('component_prefix', q{'%'})]} AND (components.id = ? OR components.id = ?) "); - $sth->execute( - $oldProductId, $newProductId, - $oldComponentId, $newComponentId, - $oldProductId, $newProductId, - $oldComponentId, $newComponentId, - ); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); - } + $sth->execute( + $oldProductId, $newProductId, $oldComponentId, $newComponentId, + $oldProductId, $newProductId, $oldComponentId, $newComponentId, + ); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} + = Bugzilla::BugMail::BIT_WATCHING(); } + } - # add component watchers from watch-users - my $uidList = join(',', keys %$recipients); - $sth = $dbh->prepare(" + # add component watchers from watch-users + my $uidList = join(',', keys %$recipients); + $sth = $dbh->prepare(" SELECT component_watch.user_id FROM components INNER JOIN component_watch ON component_watch.component_id = components.id WHERE components.watch_user in ($uidList) "); - $sth->execute(); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); - } + $sth->execute(); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} + = Bugzilla::BugMail::BIT_WATCHING(); } + } - # add watch-users from component watchers - $sth = $dbh->prepare(" + # add watch-users from component watchers + $sth = $dbh->prepare(" SELECT watch_user FROM components WHERE (id = ? OR id = ?) AND (watch_user IS NOT NULL) "); - $sth->execute($oldComponentId, $newComponentId); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT(); - } + $sth->execute($oldComponentId, $newComponentId); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT(); } + } } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; } # @@ -469,141 +441,140 @@ sub bugmail_relationships { # sub _getWatches { - my ($user, $watch_id) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $watch_id) = @_; + my $dbh = Bugzilla->dbh; - $watch_id = (defined $watch_id && $watch_id =~ /^(\d+)$/) ? $1 : undef; + $watch_id = (defined $watch_id && $watch_id =~ /^(\d+)$/) ? $1 : undef; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT id, product_id, component_id, component_prefix FROM component_watch - WHERE user_id = ?" . ($watch_id ? " AND id = ?" : "") + WHERE user_id = ?" . ($watch_id ? " AND id = ?" : "")); + $watch_id ? $sth->execute($user->id, $watch_id) : $sth->execute($user->id); + + my @watches; + while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new({id => $productId, cache => 1}); + next unless $product && $user->can_access_product($product); + + my %watch = ( + id => $id, + product => $product, + product_name => $product->name, + component_name => '', + component_prefix => $prefix, ); - $watch_id ? $sth->execute($user->id, $watch_id) : $sth->execute($user->id); - - my @watches; - while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) { - my $product = Bugzilla::Product->new({ id => $productId, cache => 1 }); - next unless $product && $user->can_access_product($product); - - my %watch = ( - id => $id, - product => $product, - product_name => $product->name, - component_name => '', - component_prefix => $prefix, - ); - if ($componentId) { - my $component = Bugzilla::Component->new({ id => $componentId, cache => 1 }); - next unless $component; - $watch{'component'} = $component; - $watch{'component_name'} = $component->name; - } - - push @watches, \%watch; + if ($componentId) { + my $component = Bugzilla::Component->new({id => $componentId, cache => 1}); + next unless $component; + $watch{'component'} = $component; + $watch{'component_name'} = $component->name; } - if ($watch_id) { - return $watches[0] || {}; - } + push @watches, \%watch; + } + + if ($watch_id) { + return $watches[0] || {}; + } - @watches = sort { - $a->{'product_name'} cmp $b->{'product_name'} - || $a->{'component_name'} cmp $b->{'component_name'} - || $a->{'component_prefix'} cmp $b->{'component_prefix'} - } @watches; + @watches = sort { + $a->{'product_name'} cmp $b->{'product_name'} + || $a->{'component_name'} cmp $b->{'component_name'} + || $a->{'component_prefix'} cmp $b->{'component_prefix'} + } @watches; - return \@watches; + return \@watches; } sub _getUserWatches { - my ($user) = @_; - my $dbh = Bugzilla->dbh; + my ($user) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT components.product_id, components.id as component, profiles.login_name FROM watch INNER JOIN components ON components.watch_user = watched INNER JOIN profiles ON profiles.userid = watched WHERE watcher = ? "); - $sth->execute($user->id); - my @watches; - while (my ($productId, $componentId, $login) = $sth->fetchrow_array) { - my $product = Bugzilla::Product->new({ id => $productId, cache => 1 }); - next unless $product && $user->can_access_product($product); - - my %watch = ( - product => $product, - component => Bugzilla::Component->new({ id => $componentId, cache => 1 }), - user => Bugzilla::User->check($login), - ); - push @watches, \%watch; - } + $sth->execute($user->id); + my @watches; + while (my ($productId, $componentId, $login) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new({id => $productId, cache => 1}); + next unless $product && $user->can_access_product($product); + + my %watch = ( + product => $product, + component => Bugzilla::Component->new({id => $componentId, cache => 1}), + user => Bugzilla::User->check($login), + ); + push @watches, \%watch; + } - @watches = sort { - $a->{'product'}->name cmp $b->{'product'}->name - || $a->{'component'}->name cmp $b->{'component'}->name - } @watches; + @watches = sort { + $a->{'product'}->name cmp $b->{'product'}->name + || $a->{'component'}->name cmp $b->{'component'}->name + } @watches; - return \@watches; + return \@watches; } sub _addProductWatch { - my ($user, $product) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $product) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? AND product_id = ? AND component_id IS NULL "); - $sth->execute($user->id, $product->id); - return if $sth->fetchrow_array; + $sth->execute($user->id, $product->id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" DELETE FROM component_watch WHERE user_id = ? AND product_id = ? "); - $sth->execute($user->id, $product->id); + $sth->execute($user->id, $product->id); - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id) VALUES (?, ?) "); - $sth->execute($user->id, $product->id); + $sth->execute($user->id, $product->id); - return _getWatches($user, $dbh->bz_last_key()); + return _getWatches($user, $dbh->bz_last_key()); } sub _addComponentWatch { - my ($user, $component) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $component) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? AND (component_id = ? OR (product_id = ? AND component_id IS NULL)) "); - $sth->execute($user->id, $component->id, $component->product_id); - return if $sth->fetchrow_array; + $sth->execute($user->id, $component->id, $component->product_id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id, component_id) VALUES (?, ?, ?) "); - $sth->execute($user->id, $component->product_id, $component->id); + $sth->execute($user->id, $component->product_id, $component->id); - return _getWatches($user, $dbh->bz_last_key()); + return _getWatches($user, $dbh->bz_last_key()); } sub _addPrefixWatch { - my ($user, $product, $prefix) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $product, $prefix) = @_; + my $dbh = Bugzilla->dbh; - trick_taint($prefix); - my $sth = $dbh->prepare(" + trick_taint($prefix); + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? @@ -612,121 +583,102 @@ sub _addPrefixWatch { OR (product_id = ? AND component_id IS NULL) ) "); - $sth->execute( - $user->id, - $product->id, $prefix, - $product->id - ); - return if $sth->fetchrow_array; + $sth->execute($user->id, $product->id, $prefix, $product->id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id, component_prefix) VALUES (?, ?, ?) "); - $sth->execute($user->id, $product->id, $prefix); + $sth->execute($user->id, $product->id, $prefix); } sub _deleteWatch { - my ($user, $id) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $id) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($id) || ThrowCodeError("component_watch_invalid_id"); + detaint_natural($id) || ThrowCodeError("component_watch_invalid_id"); - return $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?", - undef, $id, $user->id); + return $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?", + undef, $id, $user->id); } sub _addDefaultSettings { - my ($user) = @_; - my $dbh = Bugzilla->dbh; + my ($user) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM email_setting WHERE user_id = ? AND relationship = ? "); - $sth->execute($user->id, REL_COMPONENT_WATCHER); - return if $sth->fetchrow_array; - - my @defaultEvents = ( - EVT_OTHER, - EVT_COMMENT, - EVT_ATTACHMENT, - EVT_ATTACHMENT_DATA, - EVT_PROJ_MANAGEMENT, - EVT_OPENED_CLOSED, - EVT_KEYWORD, - EVT_DEPEND_BLOCK, - EVT_BUG_CREATED, - ); - foreach my $event (@defaultEvents) { - $dbh->do( - "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", - undef, - $user->id, REL_COMPONENT_WATCHER, $event - ); - } + $sth->execute($user->id, REL_COMPONENT_WATCHER); + return if $sth->fetchrow_array; + + my @defaultEvents = (EVT_OTHER, EVT_COMMENT, + EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, + EVT_KEYWORD, EVT_DEPEND_BLOCK, + EVT_BUG_CREATED, + ); + foreach my $event (@defaultEvents) { + $dbh->do("INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, $user->id, REL_COMPONENT_WATCHER, $event); + } } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE component_watch SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE component_watch SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; + my ($self, $args) = @_; + my $status = $args->{status}; - $status->('component_watching_check'); + $status->('component_watching_check'); - my ($count) = Bugzilla->dbh->selectrow_array(" + my ($count) = Bugzilla->dbh->selectrow_array(" SELECT COUNT(*) FROM component_watch INNER JOIN components ON components.id = component_watch.component_id WHERE component_watch.product_id <> components.product_id "); - if ($count) { - $status->('component_watching_alert', undef, 'alert'); - $status->('component_watching_repair'); - } + if ($count) { + $status->('component_watching_alert', undef, 'alert'); + $status->('component_watching_repair'); + } } sub sanitycheck_repair { - my ($self, $args) = @_; - return unless Bugzilla->cgi->param('component_watching_repair'); + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('component_watching_repair'); - my $status = $args->{'status'}; - my $dbh = Bugzilla->dbh; - $status->('component_watching_repairing'); + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('component_watching_repairing'); - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT DISTINCT component_watch.product_id AS bad_product_id, components.product_id AS good_product_id, component_watch.component_id FROM component_watch INNER JOIN components ON components.id = component_watch.component_id WHERE component_watch.product_id <> components.product_id - ", - { Slice => {} } - ); - foreach my $row (@$rows) { - $dbh->do(" + ", {Slice => {}}); + foreach my $row (@$rows) { + $dbh->do(" UPDATE component_watch SET product_id=? WHERE product_id=? AND component_id=? - ", undef, - $row->{good_product_id}, - $row->{bad_product_id}, - $row->{component_id}, - ); - } + ", undef, $row->{good_product_id}, $row->{bad_product_id}, + $row->{component_id},); + } } # @@ -734,8 +686,9 @@ sub sanitycheck_repair { # sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{ComponentWatching} = "Bugzilla::Extension::ComponentWatching::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{ComponentWatching} + = "Bugzilla::Extension::ComponentWatching::WebService"; } __PACKAGE__->NAME; diff --git a/extensions/ComponentWatching/lib/WebService.pm b/extensions/ComponentWatching/lib/WebService.pm index ba4cb0225..331842f7b 100644 --- a/extensions/ComponentWatching/lib/WebService.pm +++ b/extensions/ComponentWatching/lib/WebService.pm @@ -21,30 +21,25 @@ use Bugzilla::Product; use Bugzilla::User; sub rest_resources { - return [ - qr{^/component-watching$}, { - GET => { - method => 'list', - }, - POST => { - method => 'add', - }, + return [ + qr{^/component-watching$}, + {GET => {method => 'list',}, POST => {method => 'add',},}, + qr{^/component-watching/(\d+)$}, + { + GET => { + method => 'get', + params => sub { + return {id => $_[0]}; }, - qr{^/component-watching/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { id => $_[0] } - }, - }, - DELETE => { - method => 'remove', - params => sub { - return { id => $_[0] } - }, - }, + }, + DELETE => { + method => 'remove', + params => sub { + return {id => $_[0]}; }, - ]; + }, + }, + ]; } # @@ -52,62 +47,69 @@ sub rest_resources { # sub list { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla::Extension::ComponentWatching::_getWatches($user); + return Bugzilla::Extension::ComponentWatching::_getWatches($user); } sub add { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $result; - - # load product and verify access - my $productName = $params->{'product'}; - my $product = Bugzilla::Product->new({ name => $productName, cache => 1 }); - unless ($product && $user->can_access_product($product)) { - ThrowUserError('product_access_denied', { product => $productName }); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $result; + + # load product and verify access + my $productName = $params->{'product'}; + my $product = Bugzilla::Product->new({name => $productName, cache => 1}); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', {product => $productName}); + } + + my $ra_componentNames = $params->{'component'}; + $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); + + if (grep { $_ eq '' } @$ra_componentNames) { + + # watching a product + $result + = Bugzilla::Extension::ComponentWatching::_addProductWatch($user, $product); + + } + else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component + = Bugzilla::Component->new({ + name => $componentName, product => $product, cache => 1 + }); + unless ($component) { + ThrowUserError('product_access_denied', {product => $productName}); + } + $result = Bugzilla::Extension::ComponentWatching::_addComponentWatch($user, + $component); } + } - my $ra_componentNames = $params->{'component'}; - $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - - if (grep { $_ eq '' } @$ra_componentNames) { - # watching a product - $result = Bugzilla::Extension::ComponentWatching::_addProductWatch($user, $product); - - } else { - # watching specific components - foreach my $componentName (@$ra_componentNames) { - my $component = Bugzilla::Component->new({ - name => $componentName, product => $product, cache => 1 - }); - unless ($component) { - ThrowUserError('product_access_denied', { product => $productName }); - } - $result = Bugzilla::Extension::ComponentWatching::_addComponentWatch($user, $component); - } - } - - Bugzilla::Extension::ComponentWatching::_addDefaultSettings($user); + Bugzilla::Extension::ComponentWatching::_addDefaultSettings($user); - return $result; + return $result; } sub get { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla::Extension::ComponentWatching::_getWatches($user, $params->{'id'}); + return Bugzilla::Extension::ComponentWatching::_getWatches($user, + $params->{'id'}); } sub remove { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my %result = (status => Bugzilla::Extension::ComponentWatching::_deleteWatch($user, $params->{'id'})); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my %result = (status => + Bugzilla::Extension::ComponentWatching::_deleteWatch($user, $params->{'id'})); - return \%result; + return \%result; } 1; diff --git a/extensions/ContributorEngagement/Config.pm b/extensions/ContributorEngagement/Config.pm index d48de3bc6..5f46efdb0 100644 --- a/extensions/ContributorEngagement/Config.pm +++ b/extensions/ContributorEngagement/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'ContributorEngagement'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/ContributorEngagement/Extension.pm b/extensions/ContributorEngagement/Extension.pm index 35eba24ab..ae5c1b809 100644 --- a/extensions/ContributorEngagement/Extension.pm +++ b/extensions/ContributorEngagement/Extension.pm @@ -23,105 +23,110 @@ use Bugzilla::Extension::ContributorEngagement::Constants; our $VERSION = '2.0'; BEGIN { - *Bugzilla::User::first_patch_reviewed_id = \&_first_patch_reviewed_id; + *Bugzilla::User::first_patch_reviewed_id = \&_first_patch_reviewed_id; } sub _first_patch_reviewed_id { return $_[0]->{'first_patch_reviewed_id'}; } sub install_update_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_column_info('profiles', 'first_patch_approved_id')) { - $dbh->bz_drop_column('profiles', 'first_patch_approved_id'); - } - if (!$dbh->bz_column_info('profiles', 'first_patch_reviewed_id')) { - $dbh->bz_add_column('profiles', 'first_patch_reviewed_id', { TYPE => 'INT3' }); - _populate_first_reviewed_ids(); - } + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_column_info('profiles', 'first_patch_approved_id')) { + $dbh->bz_drop_column('profiles', 'first_patch_approved_id'); + } + if (!$dbh->bz_column_info('profiles', 'first_patch_reviewed_id')) { + $dbh->bz_add_column('profiles', 'first_patch_reviewed_id', {TYPE => 'INT3'}); + _populate_first_reviewed_ids(); + } } sub _populate_first_reviewed_ids { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?'); - my $ra = $dbh->selectall_arrayref("SELECT attachments.submitter_id, + my $sth = $dbh->prepare( + 'UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?'); + my $ra = $dbh->selectall_arrayref( + "SELECT attachments.submitter_id, attachments.attach_id FROM attachments INNER JOIN flags ON attachments.attach_id = flags.attach_id INNER JOIN flagtypes ON flags.type_id = flagtypes.id WHERE flagtypes.name LIKE 'review%' AND flags.status = '+' - ORDER BY flags.modification_date"); - my $count = 1; - my $total = scalar @$ra; - my %user_seen; - foreach my $ra_row (@$ra) { - my ($user_id, $attach_id) = @$ra_row; - indicate_progress({ current => $count++, total => $total, every => 25 }); - next if $user_seen{$user_id}; - $sth->execute($attach_id, $user_id); - $user_seen{$user_id} = 1; - } - - print "done\n"; + ORDER BY flags.modification_date" + ); + my $count = 1; + my $total = scalar @$ra; + my %user_seen; + foreach my $ra_row (@$ra) { + my ($user_id, $attach_id) = @$ra_row; + indicate_progress({current => $count++, total => $total, every => 25}); + next if $user_seen{$user_id}; + $sth->execute($attach_id, $user_id); + $user_seen{$user_id} = 1; + } + + print "done\n"; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info($class->DB_TABLE, 'first_patch_reviewed_id')) { - push @$columns, 'first_patch_reviewed_id'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info($class->DB_TABLE, 'first_patch_reviewed_id')) { + push @$columns, 'first_patch_reviewed_id'; } + } } sub flag_end_of_update { - my ($self, $args) = @_; - my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)}; - - if ($object->isa('Bugzilla::Attachment') - && @$new_flags - && !$object->attacher->first_patch_reviewed_id - && grep($_ eq $object->bug->product, ENABLED_PRODUCTS)) - { - my $attachment = $object; - - foreach my $orig_change (@$new_flags) { - my $change = $orig_change; - $change =~ s/^[^:]+://; # get rid of setter - $change =~ s/\([^\)]+\)$//; # get rid of requestee - my ($name, $value) = $change =~ /^(.+)(.)$/; - - # Only interested in review flags set to + - next unless $name =~ /^review/ && $value eq '+'; - - _send_mail($attachment, $timestamp); - - Bugzilla->dbh->do("UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?", - undef, $attachment->id, $attachment->attacher->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $attachment->attacher->id }); - last; - } + my ($self, $args) = @_; + my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)}; + + if ( $object->isa('Bugzilla::Attachment') + && @$new_flags + && !$object->attacher->first_patch_reviewed_id + && grep($_ eq $object->bug->product, ENABLED_PRODUCTS)) + { + my $attachment = $object; + + foreach my $orig_change (@$new_flags) { + my $change = $orig_change; + $change =~ s/^[^:]+://; # get rid of setter + $change =~ s/\([^\)]+\)$//; # get rid of requestee + my ($name, $value) = $change =~ /^(.+)(.)$/; + + # Only interested in review flags set to + + next unless $name =~ /^review/ && $value eq '+'; + + _send_mail($attachment, $timestamp); + + Bugzilla->dbh->do( + "UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?", + undef, $attachment->id, $attachment->attacher->id); + Bugzilla->memcached->clear( + {table => 'profiles', id => $attachment->attacher->id}); + last; } + } } sub _send_mail { - my ($attachment, $timestamp) = @_; + my ($attachment, $timestamp) = @_; - my $vars = { - date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'), - attachment => $attachment, - from_user => EMAIL_FROM, - }; + my $vars = { + date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'), + attachment => $attachment, + from_user => EMAIL_FROM, + }; - my $msg; - my $template = Bugzilla->template_inner($attachment->attacher->setting('lang')); - $template->process("contributor/email.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + my $msg; + my $template = Bugzilla->template_inner($attachment->attacher->setting('lang')); + $template->process("contributor/email.txt.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); } __PACKAGE__->NAME; diff --git a/extensions/ContributorEngagement/lib/Constants.pm b/extensions/ContributorEngagement/lib/Constants.pm index dd379adcd..030a463a4 100644 --- a/extensions/ContributorEngagement/lib/Constants.pm +++ b/extensions/ContributorEngagement/lib/Constants.pm @@ -14,20 +14,17 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - EMAIL_FROM - ENABLED_PRODUCTS + EMAIL_FROM + ENABLED_PRODUCTS ); use constant EMAIL_FROM => 'bugzilla-daemon@mozilla.org'; use constant ENABLED_PRODUCTS => ( - "Cloud Services", - "Core", - "Firefox for Android", - "Firefox for Metro", - "Firefox", - "Testing", - "Toolkit", + "Cloud Services", "Core", + "Firefox for Android", "Firefox for Metro", + "Firefox", "Testing", + "Toolkit", ); 1; diff --git a/extensions/EditComments/Config.pm b/extensions/EditComments/Config.pm index b95647940..e310dda84 100644 --- a/extensions/EditComments/Config.pm +++ b/extensions/EditComments/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'EditComments'; +use constant NAME => 'EditComments'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/EditComments/Extension.pm b/extensions/EditComments/Extension.pm index e2ace3f23..b7c398967 100644 --- a/extensions/EditComments/Extension.pm +++ b/extensions/EditComments/Extension.pm @@ -28,33 +28,36 @@ our $VERSION = '0.01'; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - my $schema = $args->{schema}; - - $schema->{'longdescs_activity'} = { - FIELDS => [ - comment_id => {TYPE => 'INT', NOTNULL => 1, - REFERENCES => {TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - change_when => {TYPE => 'DATETIME', NOTNULL => 1}, - old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - longdescs_activity_comment_id_idx => ['comment_id'], - longdescs_activity_change_when_idx => ['change_when'], - longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)], - ], - }; + my ($self, $args) = @_; + my $schema = $args->{schema}; + + $schema->{'longdescs_activity'} = { + FIELDS => [ + comment_id => { + TYPE => 'INT', + NOTNULL => 1, + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + change_when => {TYPE => 'DATETIME', NOTNULL => 1}, + old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => [ + longdescs_activity_comment_id_idx => ['comment_id'], + longdescs_activity_change_when_idx => ['change_when'], + longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)], + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('longdescs', 'edit_count', { TYPE => 'INT3', DEFAULT => 0 }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('longdescs', 'edit_count', {TYPE => 'INT3', DEFAULT => 0}); } #################### @@ -62,36 +65,33 @@ sub install_update_db { #################### sub page_before_template { - my ($self, $args) = @_; + my ($self, $args) = @_; - return if $args->{'page_id'} ne 'editcomments.html'; + return if $args->{'page_id'} ne 'editcomments.html'; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; - # validate group membership - my $edit_comments_group = Bugzilla->params->{edit_comments_group}; - if (!$edit_comments_group || !$user->in_group($edit_comments_group)) { - ThrowUserError('auth_failure', { group => $edit_comments_group, - action => 'view', - object => 'editcomments' }); - } + # validate group membership + my $edit_comments_group = Bugzilla->params->{edit_comments_group}; + if (!$edit_comments_group || !$user->in_group($edit_comments_group)) { + ThrowUserError('auth_failure', + {group => $edit_comments_group, action => 'view', object => 'editcomments'}); + } - my $bug_id = $params->{bug_id}; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $params->{bug_id}; + my $bug = Bugzilla::Bug->check($bug_id); - my $comment_id = $params->{comment_id}; + my $comment_id = $params->{comment_id}; - my ($comment) = grep($_->id == $comment_id, @{ $bug->comments }); - if (!$comment - || ($comment->is_private && !$user->is_insider)) - { - ThrowUserError("edit_comment_invalid_comment_id", { comment_id => $comment_id }); - } + my ($comment) = grep($_->id == $comment_id, @{$bug->comments}); + if (!$comment || ($comment->is_private && !$user->is_insider)) { + ThrowUserError("edit_comment_invalid_comment_id", {comment_id => $comment_id}); + } - $vars->{'bug'} = $bug; - $vars->{'comment'} = $comment; + $vars->{'bug'} = $bug; + $vars->{'comment'} = $comment; } ################## @@ -99,88 +99,94 @@ sub page_before_template { ################## BEGIN { - no warnings 'redefine'; - *Bugzilla::Comment::activity = \&_get_activity; - *Bugzilla::Comment::edit_count = \&_edit_count; - *Bugzilla::WebService::Bug::_super_translate_comment = \&Bugzilla::WebService::Bug::_translate_comment; - *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment; + no warnings 'redefine'; + *Bugzilla::Comment::activity = \&_get_activity; + *Bugzilla::Comment::edit_count = \&_edit_count; + *Bugzilla::WebService::Bug::_super_translate_comment + = \&Bugzilla::WebService::Bug::_translate_comment; + *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment; } sub _new_translate_comment { - my ($self, $comment, $filters) = @_; + my ($self, $comment, $filters) = @_; - my $comment_hash = $self->_super_translate_comment($comment, $filters); + my $comment_hash = $self->_super_translate_comment($comment, $filters); - if (filter_wants $filters, 'raw_text') { - $comment_hash->{raw_text} = $self->type('string', $comment->body); - } + if (filter_wants $filters, 'raw_text') { + $comment_hash->{raw_text} = $self->type('string', $comment->body); + } - return $comment_hash; + return $comment_hash; } sub _edit_count { return $_[0]->{'edit_count'}; } sub _get_activity { - my ($self, $activity_sort_order) = @_; + my ($self, $activity_sort_order) = @_; - return $self->{'activity'} if $self->{'activity'}; + return $self->{'activity'} if $self->{'activity'}; - my $dbh = Bugzilla->dbh; - my $query = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' . - $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') . ' + my $dbh = Bugzilla->dbh; + my $query + = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' + . $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') + . ' AS time, longdescs_activity.old_comment AS old FROM longdescs_activity INNER JOIN profiles ON profiles.userid = longdescs_activity.who WHERE longdescs_activity.comment_id = ?'; - $query .= " ORDER BY longdescs_activity.change_when DESC"; - my $sth = $dbh->prepare($query); - $sth->execute($self->id); - - # We are shifting each comment activity body 1 back. The reason this - # has to be done is that the longdescs_activity table stores the comment - # body that the comment was before the edit, not the actual new version - # of the comment. - my @activity; - my $new_comment; - my $last_old_comment; - my $count = 0; - while (my $change_ref = $sth->fetchrow_hashref()) { - my %change = %$change_ref; - $change{'author'} = Bugzilla::User->new({ id => $change{'userid'}, cache => 1 }); - if ($count == 0) { - $change{new} = $self->body; - } - else { - $change{new} = $new_comment; - } - $new_comment = $change{old}; - $last_old_comment = $change{old}; - push (@activity, \%change); - $count++; + $query .= " ORDER BY longdescs_activity.change_when DESC"; + my $sth = $dbh->prepare($query); + $sth->execute($self->id); + + # We are shifting each comment activity body 1 back. The reason this + # has to be done is that the longdescs_activity table stores the comment + # body that the comment was before the edit, not the actual new version + # of the comment. + my @activity; + my $new_comment; + my $last_old_comment; + my $count = 0; + while (my $change_ref = $sth->fetchrow_hashref()) { + my %change = %$change_ref; + $change{'author'} = Bugzilla::User->new({id => $change{'userid'}, cache => 1}); + if ($count == 0) { + $change{new} = $self->body; } + else { + $change{new} = $new_comment; + } + $new_comment = $change{old}; + $last_old_comment = $change{old}; + push(@activity, \%change); + $count++; + } + + return [] if !@activity; + + # Store the original comment as the first or last entry + # depending on sort order + push( + @activity, + { + author => $self->author, + body => $last_old_comment, + time => $self->creation_ts, + original => 1 + } + ); - return [] if !@activity; - - # Store the original comment as the first or last entry - # depending on sort order - push(@activity, { - author => $self->author, - body => $last_old_comment, - time => $self->creation_ts, - original => 1 - }); - - $activity_sort_order - ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'}; + $activity_sort_order + ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'}; - if ($activity_sort_order eq "oldest_to_newest") { - @activity = reverse @activity; - } + if ($activity_sort_order eq "oldest_to_newest") { + @activity = reverse @activity; + } - $self->{'activity'} = \@activity; + $self->{'activity'} = \@activity; - return $self->{'activity'}; + return $self->{'activity'}; } ######### @@ -188,86 +194,91 @@ sub _get_activity { ######### sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Comment')) { - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'edit_count')) { - push @$columns, 'edit_count'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Comment')) { + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'edit_count')) { + push @$columns, 'edit_count'; } + } } sub bug_end_of_update { - my ($self, $args) = @_; - - # Silently return if not in the proper group - # or if editing comments is disabled - my $user = Bugzilla->user; - my $edit_comments_group = Bugzilla->params->{edit_comments_group}; - return if (!$edit_comments_group || !$user->in_group($edit_comments_group)); - - my $bug = $args->{bug}; - my $timestamp = $args->{timestamp}; - my $params = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - my $updated = 0; - foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) { - my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/; - next if !detaint_natural($comment_id); - - # The comment ID must belong to this bug. - my ($comment_obj) = grep($_->id == $comment_id, @{ $bug->comments}); - next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider)); - - my $new_comment = $comment_obj->_check_thetext($params->{$param}); - - my $old_comment = $comment_obj->body; - next if $old_comment eq $new_comment; - - trick_taint($new_comment); - $dbh->do("UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1 - WHERE comment_id = ?", - undef, $new_comment, $comment_id); - Bugzilla->memcached->clear({ table => 'longdescs', id => $comment_id }); - - # Log old comment to the longdescs activity table - $timestamp ||= $dbh->selectrow_array("SELECT NOW()"); - $dbh->do("INSERT INTO longdescs_activity " . - "(comment_id, who, change_when, old_comment) " . - "VALUES (?, ?, ?, ?)", - undef, ($comment_id, $user->id, $timestamp, $old_comment)); - - $comment_obj->{thetext} = $new_comment; - - $updated = 1; - } - - $bug->_sync_fulltext( update_comments => 1 ) if $updated; + my ($self, $args) = @_; + + # Silently return if not in the proper group + # or if editing comments is disabled + my $user = Bugzilla->user; + my $edit_comments_group = Bugzilla->params->{edit_comments_group}; + return if (!$edit_comments_group || !$user->in_group($edit_comments_group)); + + my $bug = $args->{bug}; + my $timestamp = $args->{timestamp}; + my $params = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + my $updated = 0; + foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) { + my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/; + next if !detaint_natural($comment_id); + + # The comment ID must belong to this bug. + my ($comment_obj) = grep($_->id == $comment_id, @{$bug->comments}); + next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider)); + + my $new_comment = $comment_obj->_check_thetext($params->{$param}); + + my $old_comment = $comment_obj->body; + next if $old_comment eq $new_comment; + + trick_taint($new_comment); + $dbh->do( + "UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1 + WHERE comment_id = ?", undef, $new_comment, $comment_id + ); + Bugzilla->memcached->clear({table => 'longdescs', id => $comment_id}); + + # Log old comment to the longdescs activity table + $timestamp ||= $dbh->selectrow_array("SELECT NOW()"); + $dbh->do( + "INSERT INTO longdescs_activity " + . "(comment_id, who, change_when, old_comment) " + . "VALUES (?, ?, ?, ?)", + undef, + ($comment_id, $user->id, $timestamp, $old_comment) + ); + + $comment_obj->{thetext} = $new_comment; + + $updated = 1; + } + + $bug->_sync_fulltext(update_comments => 1) if $updated; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{groupsecurity}->{params} }, { - name => 'edit_comments_group', - type => 's', - choices => \&get_all_group_names, - default => 'admin', - checker => \&check_group + my ($self, $args) = @_; + push @{$args->{panels}->{groupsecurity}->{params}}, + { + name => 'edit_comments_group', + type => 's', + choices => \&get_all_group_names, + default => 'admin', + checker => \&check_group }; } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService"; } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting edited comment histories...\n"; - $dbh->do("DELETE FROM longdescs_activity"); - $dbh->do("UPDATE longdescs SET edit_count=0"); + my $dbh = Bugzilla->dbh; + print "Deleting edited comment histories...\n"; + $dbh->do("DELETE FROM longdescs_activity"); + $dbh->do("UPDATE longdescs SET edit_count=0"); } __PACKAGE__->NAME; diff --git a/extensions/EditComments/lib/WebService.pm b/extensions/EditComments/lib/WebService.pm index 6969ca742..97e40b8b6 100644 --- a/extensions/EditComments/lib/WebService.pm +++ b/extensions/EditComments/lib/WebService.pm @@ -18,64 +18,61 @@ use Bugzilla::Util qw(trim); use Bugzilla::WebService::Util qw(validate); use constant PUBLIC_METHODS => qw( - comments + comments ); sub comments { - my ($self, $params) = validate(@_, 'comment_ids'); - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - if (!defined $params->{comment_ids}) { - ThrowCodeError('param_required', - { function => 'Bug.comments', - param => 'comment_ids' }); + my ($self, $params) = validate(@_, 'comment_ids'); + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + if (!defined $params->{comment_ids}) { + ThrowCodeError('param_required', + {function => 'Bug.comments', param => 'comment_ids'}); + } + + my @ids = map { trim($_) } @{$params->{comment_ids} || []}; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); } + } - my @ids = map { trim($_) } @{ $params->{comment_ids} || [] }; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + $user->visible_bugs([keys %got_bug_ids]); # preload cache for visibility check + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } + my %comments; + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); } + $comments{$comment->id} = $comment->body; + } - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - $user->visible_bugs([ keys %got_bug_ids ]); # preload cache for visibility check - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - - my %comments; - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = $comment->body; - } - - return { comments => \%comments }; + return {comments => \%comments}; } sub rest_resources { - return [ - qr{^/editcomments/comment/(\d+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => $_[0] }; - }, - }, + return [ + qr{^/editcomments/comment/(\d+)$}, + { + GET => { + method => 'comments', + params => sub { + return {comment_ids => $_[0]}; }, - qr{^/editcomments/comment$}, { - GET => { - method => 'comments', - }, - }, - ]; -}; + }, + }, + qr{^/editcomments/comment$}, + {GET => {method => 'comments',},}, + ]; +} 1; diff --git a/extensions/EditTable/Config.pm b/extensions/EditTable/Config.pm index b9bd003b2..f62e5f0c0 100644 --- a/extensions/EditTable/Config.pm +++ b/extensions/EditTable/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'EditTable'; +use constant NAME => 'EditTable'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/EditTable/Extension.pm b/extensions/EditTable/Extension.pm index 1eb0d82f9..a6d01be65 100644 --- a/extensions/EditTable/Extension.pm +++ b/extensions/EditTable/Extension.pm @@ -49,137 +49,128 @@ our $VERSION = '1'; # }, sub EDITABLE_TABLES { - my $tables = {}; - Bugzilla::Hook::process("editable_tables", { tables => $tables }); - return $tables; + my $tables = {}; + Bugzilla::Hook::process("editable_tables", {tables => $tables}); + return $tables; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'edit_table.html'; - my $input = Bugzilla->input_params; - - # we only support editing a particular set of tables - my $table_name = $input->{table}; - exists $self->EDITABLE_TABLES()->{$table_name} - || ThrowUserError('edittable_unsupported', { table => $table_name } ); - my $table = $self->EDITABLE_TABLES()->{$table_name}; - my $id_field = $table->{id_field}; - my $order_by = $table->{order_by} || $id_field; - my $group = $table->{group} || 'admin'; - trick_taint($table_name); - - Bugzilla->user->in_group($group) - || ThrowUserError('auth_failure', { group => $group, - action => 'edit', - object => 'tables' }); - - # load columns - my $dbh = Bugzilla->dbh; - my @fields = sort - grep { $_ ne $id_field && $_ ne $order_by; } - $dbh->bz_table_columns($table_name); - if ($order_by ne $id_field) { - unshift @fields, $order_by; - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'edit_table.html'; + my $input = Bugzilla->input_params; + + # we only support editing a particular set of tables + my $table_name = $input->{table}; + exists $self->EDITABLE_TABLES()->{$table_name} + || ThrowUserError('edittable_unsupported', {table => $table_name}); + my $table = $self->EDITABLE_TABLES()->{$table_name}; + my $id_field = $table->{id_field}; + my $order_by = $table->{order_by} || $id_field; + my $group = $table->{group} || 'admin'; + trick_taint($table_name); + + Bugzilla->user->in_group($group) + || ThrowUserError('auth_failure', + {group => $group, action => 'edit', object => 'tables'}); + + # load columns + my $dbh = Bugzilla->dbh; + my @fields = sort grep { $_ ne $id_field && $_ ne $order_by; } + $dbh->bz_table_columns($table_name); + if ($order_by ne $id_field) { + unshift @fields, $order_by; + } + + # update table + my $data = $input->{table_data}; + my $edits = []; + if ($data) { + check_hash_token($input->{token}, [$table_name]); + + $data = from_json($data)->{data}; + $edits = dclone($data); + eval { + $dbh->bz_start_transaction; + + foreach my $row (@$data) { + map { trick_taint($_) } @$row; + if ($row->[0] eq '-') { + + # add + shift @$row; + next unless grep { $_ ne '' } @$row; + my $placeholders = join(',', split(//, '?' x scalar(@fields))); + $dbh->do( + "INSERT INTO $table_name(" + . join(',', @fields) . ") " + . "VALUES ($placeholders)", + undef, @$row + ); + } + elsif ($row->[0] < 0) { - # update table - my $data = $input->{table_data}; - my $edits = []; - if ($data) { - check_hash_token($input->{token}, [$table_name]); - - $data = from_json($data)->{data}; - $edits = dclone($data); - eval { - $dbh->bz_start_transaction; - - foreach my $row (@$data) { - map { trick_taint($_) } @$row; - if ($row->[0] eq '-') { - # add - shift @$row; - next unless grep { $_ ne '' } @$row; - my $placeholders = join(',', split(//, '?' x scalar(@fields))); - $dbh->do( - "INSERT INTO $table_name(" . join(',', @fields) . ") " . - "VALUES ($placeholders)", - undef, - @$row - ); - } - elsif ($row->[0] < 0) { - # delete - $dbh->do( - "DELETE FROM $table_name WHERE $id_field=?", - undef, - -$row->[0] - ); - } - else { - # update - my $id = shift @$row; - $dbh->do( - "UPDATE $table_name " . - "SET " . join(',', map { "$_ = ?" } @fields) . " " . - "WHERE $id_field = ?", - undef, - @$row, $id - ); - } - } - - $dbh->bz_commit_transaction; - $vars->{updated} = 1; - $edits = []; - }; - if ($@) { - my $error = $@; - $error =~ s/^DBD::[^:]+::db do failed: //; - $error =~ s/^(.+) \[for Statement ".+$/$1/s; - $vars->{error} = $error; - $dbh->bz_rollback_transaction; + # delete + $dbh->do("DELETE FROM $table_name WHERE $id_field=?", undef, -$row->[0]); + } + else { + # update + my $id = shift @$row; + $dbh->do( + "UPDATE $table_name " . "SET " + . join(',', map {"$_ = ?"} @fields) . " " + . "WHERE $id_field = ?", + undef, @$row, $id + ); } + } + + $dbh->bz_commit_transaction; + $vars->{updated} = 1; + $edits = []; + }; + if ($@) { + my $error = $@; + $error =~ s/^DBD::[^:]+::db do failed: //; + $error =~ s/^(.+) \[for Statement ".+$/$1/s; + $vars->{error} = $error; + $dbh->bz_rollback_transaction; } + } - # load data from table - unshift @fields, $id_field; - $data = $dbh->selectall_arrayref( - "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by" - ); + # load data from table + unshift @fields, $id_field; + $data = $dbh->selectall_arrayref( + "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by"); - # we don't support nulls currently - foreach my $row (@$data) { - if (grep { !defined($_) } @$row) { - ThrowUserError('edittable_nulls', { table => $table_name } ); - } + # we don't support nulls currently + foreach my $row (@$data) { + if (grep { !defined($_) } @$row) { + ThrowUserError('edittable_nulls', {table => $table_name}); } + } - # apply failed edits - foreach my $edit (@$edits) { - if ($edit->[0] eq '-') { - push @$data, $edit; - } - else { - my $id = $edit->[0]; - foreach my $row (@$data) { - if ($row->[0] == $id) { - @$row = @$edit; - last; - } - } + # apply failed edits + foreach my $edit (@$edits) { + if ($edit->[0] eq '-') { + push @$data, $edit; + } + else { + my $id = $edit->[0]; + foreach my $row (@$data) { + if ($row->[0] == $id) { + @$row = @$edit; + last; } + } } + } - $vars->{table_name} = $table_name; - $vars->{blurb} = $table->{blurb}; - $vars->{token} = issue_hash_token([$table_name]); - $vars->{table_data} = to_json({ - fields => \@fields, - id_field => $id_field, - data => $data, - }); + $vars->{table_name} = $table_name; + $vars->{blurb} = $table->{blurb}; + $vars->{token} = issue_hash_token([$table_name]); + $vars->{table_data} + = to_json({fields => \@fields, id_field => $id_field, data => $data,}); } __PACKAGE__->NAME; diff --git a/extensions/Ember/Extension.pm b/extensions/Ember/Extension.pm index 8bb558c6f..d3e322919 100644 --- a/extensions/Ember/Extension.pm +++ b/extensions/Ember/Extension.pm @@ -16,9 +16,9 @@ use parent qw(Bugzilla::Extension); our $VERSION = '0.01'; sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService"; } __PACKAGE__->NAME; diff --git a/extensions/Ember/lib/FakeBug.pm b/extensions/Ember/lib/FakeBug.pm index 46fef4ea7..32cbb5287 100644 --- a/extensions/Ember/lib/FakeBug.pm +++ b/extensions/Ember/lib/FakeBug.pm @@ -16,62 +16,64 @@ use Bugzilla::Bug; our $AUTOLOAD; sub new { - my $class = shift; - my $self = shift; - bless $self, $class; - return $self; + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; } sub AUTOLOAD { - my $self = shift; - my $name = $AUTOLOAD; - $name =~ s/.*://; - return exists $self->{$name} ? $self->{$name} : undef; + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; } sub check_can_change_field { - return Bugzilla::Bug::check_can_change_field(@_); + return Bugzilla::Bug::check_can_change_field(@_); } -sub id { return undef; } +sub id { return undef; } sub product_obj { return $_[0]->{product_obj}; } -sub reporter { return Bugzilla->user; } +sub reporter { return Bugzilla->user; } sub choices { - my $self = shift; - return $self->{'choices'} if exists $self->{'choices'}; - return {} if $self->{'error'}; - my $user = Bugzilla->user; - - my @products = @{ $user->get_enterable_products }; - # The current product is part of the popup, even if new bugs are no longer - # allowed for that product - if (!grep($_->name eq $self->product_obj->name, @products)) { - unshift(@products, $self->product_obj); - } - - my @statuses = @{ Bugzilla::Status->can_change_to }; - - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$self->product_obj->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } - - my %choices = ( - bug_status => \@statuses, - product => \@products, - component => $self->product_obj->components, - version => $self->product_obj->versions, - target_milestone => $self->product_obj->milestones, - ); - - my $resolution_field = new Bugzilla::Field({ name => 'resolution' }); - # Don't include the empty resolution in drop-downs. - my @resolutions = grep($_->name, @{ $resolution_field->legal_values }); - $choices{'resolution'} = \@resolutions; - - $self->{'choices'} = \%choices; - return $self->{'choices'}; + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + my @products = @{$user->get_enterable_products}; + + # The current product is part of the popup, even if new bugs are no longer + # allowed for that product + if (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + + my @statuses = @{Bugzilla::Status->can_change_to}; + + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$self->product_obj->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } + + my %choices = ( + bug_status => \@statuses, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + ); + + my $resolution_field = new Bugzilla::Field({name => 'resolution'}); + + # Don't include the empty resolution in drop-downs. + my @resolutions = grep($_->name, @{$resolution_field->legal_values}); + $choices{'resolution'} = \@resolutions; + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } 1; diff --git a/extensions/Ember/lib/WebService.pm b/extensions/Ember/lib/WebService.pm index 10c828537..6ad33cd81 100644 --- a/extensions/Ember/lib/WebService.pm +++ b/extensions/Ember/lib/WebService.pm @@ -12,8 +12,8 @@ use strict; use warnings; use parent qw(Bugzilla::WebService - Bugzilla::WebService::Bug - Bugzilla::WebService::Product); + Bugzilla::WebService::Bug + Bugzilla::WebService::Product); use Bugzilla::Bug; use Bugzilla::Component; @@ -29,69 +29,68 @@ use Scalar::Util qw(blessed); use Storable qw(dclone); use constant PUBLIC_METHODS => qw( - bug - create - get_attachments - search - show + bug + create + get_attachments + search + show ); -use constant DATE_FIELDS => { - show => ['last_updated'], -}; +use constant DATE_FIELDS => {show => ['last_updated'],}; use constant FIELD_TYPE_MAP => { - 0 => 'unknown', - 1 => 'freetext', - 2 => 'single_select', - 3 => 'multiple_select', - 4 => 'textarea', - 5 => 'datetime', - 6 => 'date', - 7 => 'bug_id', - 8 => 'bug_urls', - 9 => 'keywords', - 99 => 'extension' + 0 => 'unknown', + 1 => 'freetext', + 2 => 'single_select', + 3 => 'multiple_select', + 4 => 'textarea', + 5 => 'datetime', + 6 => 'date', + 7 => 'bug_id', + 8 => 'bug_urls', + 9 => 'keywords', + 99 => 'extension' }; use constant NON_EDIT_FIELDS => qw( - assignee_accessible - bug_group - bug_id - commenter - cclist_accessible - content - creation_ts - days_elapsed - everconfirmed - qacontact_accessible - reporter - reporter_accessible - restrict_comments - tag - votes + assignee_accessible + bug_group + bug_id + commenter + cclist_accessible + content + creation_ts + days_elapsed + everconfirmed + qacontact_accessible + reporter + reporter_accessible + restrict_comments + tag + votes ); use constant BUG_CHOICE_FIELDS => qw( - bug_status - component - product - resolution - target_milestone - version + bug_status + component + product + resolution + target_milestone + version ); use constant DEFAULT_VALUE_MAP => { - op_sys => 'defaultopsys', - rep_platform => 'defaultplatform', - priority => 'defaultpriority', - bug_severity => 'defaultseverity' + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity' }; sub API_NAMES { - # Internal field names converted to the API equivalents - my %api_names = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - return \%api_names; + + # Internal field names converted to the API equivalents + my %api_names = reverse %{Bugzilla::Bug::FIELD_MAP()}; + return \%api_names; } ############### @@ -99,227 +98,221 @@ sub API_NAMES { ############### sub create { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->switch_to_shadow_db(); + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->switch_to_shadow_db(); - my $product = delete $params->{product}; - $product || ThrowCodeError('params_required', - { function => 'Ember.create', params => ['product'] }); + my $product = delete $params->{product}; + $product + || ThrowCodeError('params_required', + {function => 'Ember.create', params => ['product']}); - my $product_obj = Bugzilla::Product->check($product); + my $product_obj = Bugzilla::Product->check($product); - my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new( - { product_obj => $product_obj, reporter_id => Bugzilla->user->id }); + my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new( + {product_obj => $product_obj, reporter_id => Bugzilla->user->id}); - my @fields = $self->_get_fields($fake_bug); + my @fields = $self->_get_fields($fake_bug); - return { - fields => \@fields - }; + return {fields => \@fields}; } sub show { - my ($self, $params) = @_; - my (@fields, $attachments, $comments, $data); - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($self, $params) = @_; + my (@fields, $attachments, $comments, $data); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - # Throw error if token was provided and user is not logged - # in meaning token was invalid/expired. - if (exists $params->{token} && !$user->id) { - ThrowUserError('invalid_token'); - } + # Throw error if token was provided and user is not logged + # in meaning token was invalid/expired. + if (exists $params->{token} && !$user->id) { + ThrowUserError('invalid_token'); + } - my $bug_id = delete $params->{id}; - $bug_id || ThrowCodeError('params_required', - { function => 'Ember.show', params => ['id'] }); + my $bug_id = delete $params->{id}; + $bug_id + || ThrowCodeError('params_required', + {function => 'Ember.show', params => ['id']}); - my $bug = Bugzilla::Bug->check($bug_id); + my $bug = Bugzilla::Bug->check($bug_id); - my $bug_hash = $self->_bug_to_hash($bug, $params); + my $bug_hash = $self->_bug_to_hash($bug, $params); - # Only return changes since last_updated if provided - my $last_updated = delete $params->{last_updated}; - if ($last_updated) { - trick_taint($last_updated); + # Only return changes since last_updated if provided + my $last_updated = delete $params->{last_updated}; + if ($last_updated) { + trick_taint($last_updated); - my $updated_fields = - $dbh->selectcol_arrayref('SELECT fieldid FROM bugs_activity - WHERE bug_when > ? AND bug_id = ?', - undef, ($last_updated, $bug->id)); + my $updated_fields = $dbh->selectcol_arrayref( + 'SELECT fieldid FROM bugs_activity + WHERE bug_when > ? AND bug_id = ?', undef, + ($last_updated, $bug->id) + ); - if (@$updated_fields) { - # Also add in the delta_ts value which is in the bugs_activity - # entries - push(@$updated_fields, get_field_id('delta_ts')); - @fields = $self->_get_fields($bug, $updated_fields); - } - } - # Return all the things - else { - @fields = $self->_get_fields($bug); - } + if (@$updated_fields) { - # Place the fields current value along with the field definition - foreach my $field (@fields) { - if (($field->{name} eq 'depends_on' - || $field->{name} eq 'blocks') - && scalar @{ $bug_hash->{$field->{name}} }) - { - my $bug_ids = delete $bug_hash->{$field->{name}}; - $user->visible_bugs($bug_ids); - my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids); - - my @new_list; - foreach my $bug (@$bug_objs) { - my $data; - if ($user->can_see_bug($bug)) { - $data = { - id => $bug->id, - status => $bug->bug_status, - summary => $bug->short_desc - }; - } - else { - $data = { id => $bug->id }; - } - push(@new_list, $data); - } - $field->{current_value} = \@new_list; + # Also add in the delta_ts value which is in the bugs_activity + # entries + push(@$updated_fields, get_field_id('delta_ts')); + @fields = $self->_get_fields($bug, $updated_fields); + } + } + + # Return all the things + else { + @fields = $self->_get_fields($bug); + } + + # Place the fields current value along with the field definition + foreach my $field (@fields) { + if (($field->{name} eq 'depends_on' || $field->{name} eq 'blocks') + && scalar @{$bug_hash->{$field->{name}}}) + { + my $bug_ids = delete $bug_hash->{$field->{name}}; + $user->visible_bugs($bug_ids); + my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids); + + my @new_list; + foreach my $bug (@$bug_objs) { + my $data; + if ($user->can_see_bug($bug)) { + $data + = {id => $bug->id, status => $bug->bug_status, summary => $bug->short_desc}; } else { - $field->{current_value} = delete $bug_hash->{$field->{name}} || ''; + $data = {id => $bug->id}; } + push(@new_list, $data); + } + $field->{current_value} = \@new_list; } - - # Any left over bug values will be added to the field list - # These are extra fields that do not have a corresponding - # Field.pm object - if (!$last_updated) { - foreach my $key (keys %$bug_hash) { - my $field = { - name => $key, - current_value => $bug_hash->{$key} - }; - my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key; - $field->{can_edit} = $self->_can_change_field($name, $bug); - push(@fields, $field); - } + else { + $field->{current_value} = delete $bug_hash->{$field->{name}} || ''; + } + } + + # Any left over bug values will be added to the field list + # These are extra fields that do not have a corresponding + # Field.pm object + if (!$last_updated) { + foreach my $key (keys %$bug_hash) { + my $field = {name => $key, current_value => $bug_hash->{$key}}; + my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key; + $field->{can_edit} = $self->_can_change_field($name, $bug); + push(@fields, $field); } + } - # Complete the return data - my $data = { id => $bug->id, fields => \@fields }; + # Complete the return data + my $data = {id => $bug->id, fields => \@fields}; - return $data; + return $data; } sub search { - my ($self, $params) = @_; - - my $total; - if (exists $params->{offset} && exists $params->{limit}) { - my $count_params = dclone($params); - delete $count_params->{offset}; - delete $count_params->{limit}; - $count_params->{count_only} = 1; - $total = $self->SUPER::search($count_params); - } - - my $result = $self->SUPER::search($params); - $result->{total} = defined $total ? $total : scalar(@{ $result->{bugs} }); - return $result; + my ($self, $params) = @_; + + my $total; + if (exists $params->{offset} && exists $params->{limit}) { + my $count_params = dclone($params); + delete $count_params->{offset}; + delete $count_params->{limit}; + $count_params->{count_only} = 1; + $total = $self->SUPER::search($count_params); + } + + my $result = $self->SUPER::search($params); + $result->{total} = defined $total ? $total : scalar(@{$result->{bugs}}); + return $result; } sub bug { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - my $bug_id = delete $params->{id}; - $bug_id || ThrowCodeError('param_required', - { function => 'Ember.bug', param => 'id' }); - - my ($comments, $attachments) = ([], []); - my $bug = $self->get({ ids => [ $bug_id ] }); - $bug = $bug->{bugs}->[0]; - - # Only return changes since last_updated if provided - my $last_updated = delete $params->{last_updated}; - if ($last_updated) { - trick_taint($last_updated); - my $updated_fields = $dbh->selectcol_arrayref('SELECT fielddefs.name + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + my $bug_id = delete $params->{id}; + $bug_id + || ThrowCodeError('param_required', {function => 'Ember.bug', param => 'id'}); + + my ($comments, $attachments) = ([], []); + my $bug = $self->get({ids => [$bug_id]}); + $bug = $bug->{bugs}->[0]; + + # Only return changes since last_updated if provided + my $last_updated = delete $params->{last_updated}; + if ($last_updated) { + trick_taint($last_updated); + my $updated_fields = $dbh->selectcol_arrayref( + 'SELECT fielddefs.name FROM fielddefs INNER JOIN bugs_activity ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_when > ? AND bugs_activity.bug_id = ?', - undef, ($last_updated, $bug->{id})); - - my %field_map = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $field_map{'flagtypes.name'} = 'flags'; - - my $changed_bug = {}; - foreach my $field (@$updated_fields) { - my $field_name = $field_map{$field} || $field; - if ($bug->{$field_name}) { - $changed_bug->{$field_name} = $bug->{$field_name}; - } - } - $bug = $changed_bug; + undef, ($last_updated, $bug->{id}) + ); + + my %field_map = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $field_map{'flagtypes.name'} = 'flags'; + + my $changed_bug = {}; + foreach my $field (@$updated_fields) { + my $field_name = $field_map{$field} || $field; + if ($bug->{$field_name}) { + $changed_bug->{$field_name} = $bug->{$field_name}; + } + } + $bug = $changed_bug; - # Find any comments created since the last_updated date - $comments = $self->comments({ ids => $bug_id, new_since => $last_updated }); + # Find any comments created since the last_updated date + $comments = $self->comments({ids => $bug_id, new_since => $last_updated}); - # Find any new attachments or modified attachments since the - # last_updated date - my $updated_attachments = - $dbh->selectcol_arrayref('SELECT attach_id FROM attachments + # Find any new attachments or modified attachments since the + # last_updated date + my $updated_attachments = $dbh->selectcol_arrayref( + 'SELECT attach_id FROM attachments WHERE (creation_ts > ? OR modification_time > ?) - AND bug_id = ?', - undef, ($last_updated, $last_updated, $bug->{id})); - if ($updated_attachments) { - $attachments = $self->_get_attachments({ attachment_ids => $updated_attachments, - exclude_fields => ['data'] }); - } + AND bug_id = ?', undef, + ($last_updated, $last_updated, $bug->{id}) + ); + if ($updated_attachments) { + $attachments + = $self->_get_attachments({ + attachment_ids => $updated_attachments, exclude_fields => ['data'] + }); } - else { - $comments = $self->comments({ ids => [ $bug_id ] }); - $attachments = $self->_get_attachments({ ids => [ $bug_id ], - exclude_fields => ['data'] }); + } + else { + $comments = $self->comments({ids => [$bug_id]}); + $attachments + = $self->_get_attachments({ids => [$bug_id], exclude_fields => ['data']}); - } + } - $comments = $comments->{bugs}->{$bug_id}->{comments}; + $comments = $comments->{bugs}->{$bug_id}->{comments}; - return { - bug => $bug, - comments => $comments, - attachments => $attachments, - }; + return {bug => $bug, comments => $comments, attachments => $attachments,}; } sub get_attachments { - my ($self, $params) = @_; - my $attachments = $self->_get_attachments($params); - my $flag_types = []; - my $bug; - if ($params->{ids}) { - $bug = Bugzilla::Bug->check($params->{ids}->[0]); - $flag_types = $self->_get_flag_types_bug($bug, 'attachment'); - } - elsif ($params->{attachment_ids} && @$attachments) { - $bug = Bugzilla::Bug->check($attachments->[0]->{bug_id}); - $flag_types = $self->_get_flag_types_all($bug, 'attachment')->{attachment}; - } - if (@$flag_types) { - @$flag_types = map { $self->_flagtype_to_hash($_, $bug) } @$flag_types; - } - return { - attachments => $attachments, - flag_types => $flag_types - }; + my ($self, $params) = @_; + my $attachments = $self->_get_attachments($params); + my $flag_types = []; + my $bug; + if ($params->{ids}) { + $bug = Bugzilla::Bug->check($params->{ids}->[0]); + $flag_types = $self->_get_flag_types_bug($bug, 'attachment'); + } + elsif ($params->{attachment_ids} && @$attachments) { + $bug = Bugzilla::Bug->check($attachments->[0]->{bug_id}); + $flag_types = $self->_get_flag_types_all($bug, 'attachment')->{attachment}; + } + if (@$flag_types) { + @$flag_types = map { $self->_flagtype_to_hash($_, $bug) } @$flag_types; + } + return {attachments => $attachments, flag_types => $flag_types}; } ################### @@ -327,449 +320,477 @@ sub get_attachments { ################### sub _get_attachments { - my ($self, $params) = @_; - my $user = Bugzilla->user; - - my $attachments = $self->attachments($params); - - if ($params->{ids}) { - $attachments = [ map { @{ $attachments->{bugs}->{$_} } } - keys %{ $attachments->{bugs} } ]; - } - elsif ($params->{attachment_ids}) { - $attachments = [ map { $attachments->{attachments}->{$_} } - keys %{ $attachments->{attachments} } ]; - } - - foreach my $attachment (@$attachments) { - $attachment->{can_edit} - = ($user->login eq $attachment->{creator} || $user->in_group('editbugs')) ? 1 : 0; - } - - return $attachments; + my ($self, $params) = @_; + my $user = Bugzilla->user; + + my $attachments = $self->attachments($params); + + if ($params->{ids}) { + $attachments + = [map { @{$attachments->{bugs}->{$_}} } keys %{$attachments->{bugs}}]; + } + elsif ($params->{attachment_ids}) { + $attachments = [map { $attachments->{attachments}->{$_} } + keys %{$attachments->{attachments}}]; + } + + foreach my $attachment (@$attachments) { + $attachment->{can_edit} + = ($user->login eq $attachment->{creator} || $user->in_group('editbugs')) + ? 1 + : 0; + } + + return $attachments; } sub _get_fields { - my ($self, $bug, $field_ids) = @_; - my $user = Bugzilla->user; - - # Load the field objects we need - my @field_objs; - if ($field_ids) { - # Load just the fields that match the ids provided - @field_objs = @{ Bugzilla::Field->match({ id => $field_ids }) }; - + my ($self, $bug, $field_ids) = @_; + my $user = Bugzilla->user; + + # Load the field objects we need + my @field_objs; + if ($field_ids) { + + # Load just the fields that match the ids provided + @field_objs = @{Bugzilla::Field->match({id => $field_ids})}; + + } + else { + # load up standard fields + @field_objs = @{Bugzilla->fields({custom => 0})}; + + # Load custom fields + my $cf_params = {product => $bug->product_obj}; + $cf_params->{component} = $bug->component_obj if $bug->can('component_obj'); + $cf_params->{bug_id} = $bug->id if $bug->id; + push(@field_objs, Bugzilla->active_custom_fields($cf_params)); + } + + my $return_groups = my $return_flags = $field_ids ? 0 : 1; + my @fields; + foreach my $field (@field_objs) { + $return_groups = 1 if $field->name eq 'bug_group'; + $return_flags = 1 if $field->name eq 'flagtypes.name'; + + # Skip any special fields containing . in the name such as + # for attachments.*, etc. + next if $field->name =~ /\./; + + # Remove time tracking fields if the user is privileged + next + if (grep($field->name eq $_, TIMETRACKING_FIELDS) + && !Bugzilla->user->is_timetracker); + + # These fields should never be set by the user + next if grep($field->name eq $_, NON_EDIT_FIELDS); + + # We already selected a product so no need to display all choices + # Might as well skip classification for new bugs as well. + next + if (!$bug->id + && ($field->name eq 'product' || $field->name eq 'classification')); + + # Skip assigned_to and qa_contact for new bugs if user not in + # editbugs group + next + if (!$bug->id + && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact') + && !$user->in_group('editbugs', $bug->product_obj->id)); + +# Do not display obsolete fields or fields that should be displayed for create bug form + next + if (!$bug->id && $field->custom && ($field->obsolete || !$field->enter_bug)); + + push(@fields, $self->_field_to_hash($field, $bug)); + } + + # Add group information as separate field + if ($return_groups) { + push( + @fields, + { + description => $self->type('string', 'Groups'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'groups'), + values => [ + map { $self->_group_to_hash($_, $bug) } @{$bug->product_obj->groups_available} + ] + } + ); + } + + # Add flag information as separate field + if ($return_flags) { + my $flag_hash; + if ($bug->id) { + foreach my $flag_type ('bug', 'attachment') { + $flag_hash->{$flag_type} = $self->_get_flag_types_bug($bug, $flag_type); + } } else { - # load up standard fields - @field_objs = @{ Bugzilla->fields({ custom => 0 }) }; - - # Load custom fields - my $cf_params = { product => $bug->product_obj }; - $cf_params->{component} = $bug->component_obj if $bug->can('component_obj'); - $cf_params->{bug_id} = $bug->id if $bug->id; - push(@field_objs, Bugzilla->active_custom_fields($cf_params)); - } - - my $return_groups = my $return_flags = $field_ids ? 0 : 1; - my @fields; - foreach my $field (@field_objs) { - $return_groups = 1 if $field->name eq 'bug_group'; - $return_flags = 1 if $field->name eq 'flagtypes.name'; - - # Skip any special fields containing . in the name such as - # for attachments.*, etc. - next if $field->name =~ /\./; - - # Remove time tracking fields if the user is privileged - next if (grep($field->name eq $_, TIMETRACKING_FIELDS) - && !Bugzilla->user->is_timetracker); - - # These fields should never be set by the user - next if grep($field->name eq $_, NON_EDIT_FIELDS); - - # We already selected a product so no need to display all choices - # Might as well skip classification for new bugs as well. - next if (!$bug->id && ($field->name eq 'product' || $field->name eq 'classification')); - - # Skip assigned_to and qa_contact for new bugs if user not in - # editbugs group - next if (!$bug->id - && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact') - && !$user->in_group('editbugs', $bug->product_obj->id)); - - # Do not display obsolete fields or fields that should be displayed for create bug form - next if (!$bug->id && $field->custom - && ($field->obsolete || !$field->enter_bug)); - - push(@fields, $self->_field_to_hash($field, $bug)); + $flag_hash = $self->_get_flag_types_all($bug); } - - # Add group information as separate field - if ($return_groups) { - push(@fields, { - description => $self->type('string', 'Groups'), - is_custom => $self->type('boolean', 0), - is_mandatory => $self->type('boolean', 0), - name => $self->type('string', 'groups'), - values => [ map { $self->_group_to_hash($_, $bug) } - @{ $bug->product_obj->groups_available } ] - }); - } - - # Add flag information as separate field - if ($return_flags) { - my $flag_hash; - if ($bug->id) { - foreach my $flag_type ('bug', 'attachment') { - $flag_hash->{$flag_type} = $self->_get_flag_types_bug($bug, $flag_type); - } - } - else { - $flag_hash = $self->_get_flag_types_all($bug); - } - my @flag_values; - foreach my $flag_type ('bug', 'attachment') { - foreach my $flag (@{ $flag_hash->{$flag_type} }) { - push(@flag_values, $self->_flagtype_to_hash($flag, $bug)); - } - } - - push(@fields, { - description => $self->type('string', 'Flags'), - is_custom => $self->type('boolean', 0), - is_mandatory => $self->type('boolean', 0), - name => $self->type('string', 'flags'), - values => \@flag_values - }); + my @flag_values; + foreach my $flag_type ('bug', 'attachment') { + foreach my $flag (@{$flag_hash->{$flag_type}}) { + push(@flag_values, $self->_flagtype_to_hash($flag, $bug)); + } } - return @fields; + push( + @fields, + { + description => $self->type('string', 'Flags'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'flags'), + values => \@flag_values + } + ); + } + + return @fields; } sub _get_flag_types_all { - my ($self, $bug, $type) = @_; - my $params = { is_active => 1 }; - $params->{target_type} = $type if $type; - return $bug->product_obj->flag_types($params); + my ($self, $bug, $type) = @_; + my $params = {is_active => 1}; + $params->{target_type} = $type if $type; + return $bug->product_obj->flag_types($params); } sub _get_flag_types_bug { - my ($self, $bug, $type) = @_; - my $params = { - target_type => $type, - product_id => $bug->product_obj->id, - component_id => $bug->component_obj->id, - bug_id => $bug->id, - active_or_has_flags => $bug->id, - }; - return Bugzilla::Flag->_flag_types($params); + my ($self, $bug, $type) = @_; + my $params = { + target_type => $type, + product_id => $bug->product_obj->id, + component_id => $bug->component_obj->id, + bug_id => $bug->id, + active_or_has_flags => $bug->id, + }; + return Bugzilla::Flag->_flag_types($params); } sub _group_to_hash { - my ($self, $group, $bug) = @_; + my ($self, $group, $bug) = @_; - my $data = { - description => $self->type('string', $group->description), - name => $self->type('string', $group->name) - }; + my $data = { + description => $self->type('string', $group->description), + name => $self->type('string', $group->name) + }; - if ($group->name eq $bug->product_obj->default_security_group) { - $data->{security_default} = $self->type('boolean', 1); - } + if ($group->name eq $bug->product_obj->default_security_group) { + $data->{security_default} = $self->type('boolean', 1); + } - return $data; + return $data; } sub _field_to_hash { - my ($self, $field, $bug) = @_; - - my $data = { - is_custom => $self->type('boolean', $field->custom), - description => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - }; - - if ($field->custom) { - $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type}); - } - - # Use the API name if one is present instead of the internal field name - my $field_name = $field->name; - $field_name = API_NAMES->{$field_name} || $field_name; - - if ($field_name eq 'longdesc') { - $field_name = $bug->id ? 'comment' : 'description'; - } - - $data->{name} = $self->type('string', $field_name); - - # Set can_edit true or false if we are editing a current bug - if ($bug->id) { - # 'delta_ts's can_edit is incorrectly set in fielddefs - $data->{can_edit} = $field->name eq 'delta_ts' - ? $self->type('boolean', 0) - : $self->_can_change_field($field, $bug); - } - - # description for creating a new bug, otherwise comment - - # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs - if ($field->is_select || $field->name eq 'version' || $field->name eq 'target_milestone') { - $data->{values} = [ $self->_get_field_values($field, $bug) ]; - } - - # Add default values for specific fields if new bug - if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) { - my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}}; - $data->{default_value} = $default_value; - } - - return $data; + my ($self, $field, $bug) = @_; + + my $data = { + is_custom => $self->type('boolean', $field->custom), + description => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + }; + + if ($field->custom) { + $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type}); + } + + # Use the API name if one is present instead of the internal field name + my $field_name = $field->name; + $field_name = API_NAMES->{$field_name} || $field_name; + + if ($field_name eq 'longdesc') { + $field_name = $bug->id ? 'comment' : 'description'; + } + + $data->{name} = $self->type('string', $field_name); + + # Set can_edit true or false if we are editing a current bug + if ($bug->id) { + + # 'delta_ts's can_edit is incorrectly set in fielddefs + $data->{can_edit} + = $field->name eq 'delta_ts' + ? $self->type('boolean', 0) + : $self->_can_change_field($field, $bug); + } + + # description for creating a new bug, otherwise comment + + # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs + if ( $field->is_select + || $field->name eq 'version' + || $field->name eq 'target_milestone') + { + $data->{values} = [$self->_get_field_values($field, $bug)]; + } + + # Add default values for specific fields if new bug + if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) { + my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}}; + $data->{default_value} = $default_value; + } + + return $data; } sub _value_to_hash { - my ($self, $value, $bug) = @_; + my ($self, $value, $bug) = @_; - my $data = { name=> $self->type('string', $value->name) }; + my $data = {name => $self->type('string', $value->name)}; - if ($bug->{bug_id}) { - $data->{is_active} = $self->type('boolean', $value->is_active); - } + if ($bug->{bug_id}) { + $data->{is_active} = $self->type('boolean', $value->is_active); + } - if ($value->can('sortkey')) { - $data->{sort_key} = $self->type('int', $value->sortkey || 0); - } + if ($value->can('sortkey')) { + $data->{sort_key} = $self->type('int', $value->sortkey || 0); + } - if ($value->isa('Bugzilla::Component')) { - $data->{default_assignee} = $self->_user_to_hash($value->default_assignee); - $data->{initial_cc} = [ map { $self->_user_to_hash($_) } @{ $value->initial_cc } ]; - if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) { - $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact); - } + if ($value->isa('Bugzilla::Component')) { + $data->{default_assignee} = $self->_user_to_hash($value->default_assignee); + $data->{initial_cc} = [map { $self->_user_to_hash($_) } @{$value->initial_cc}]; + if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) { + $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact); } + } - if ($value->can('description')) { - $data->{description} = $self->type('string', $value->description); - } + if ($value->can('description')) { + $data->{description} = $self->type('string', $value->description); + } - return $data; + return $data; } sub _user_to_hash { - my ($self, $user) = @_; + my ($self, $user) = @_; - my $data = { - real_name => $self->type('string', $user->name) - }; + my $data = {real_name => $self->type('string', $user->name)}; - if (Bugzilla->user->id) { - $data->{email} = $self->type('string', $user->email); - } + if (Bugzilla->user->id) { + $data->{email} = $self->type('string', $user->email); + } - return $data; + return $data; } sub _get_field_values { - my ($self, $field, $bug) = @_; - - # Certain fields are special and should use $bug->choices - # to determine editability and not $bug->check_can_change_field - my @values; - if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) { - @values = @{ $bug->choices->{$field->name} }; + my ($self, $field, $bug) = @_; + + # Certain fields are special and should use $bug->choices + # to determine editability and not $bug->check_can_change_field + my @values; + if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) { + @values = @{$bug->choices->{$field->name}}; + } + else { + # We need to get the values from the product for + # component, version, and milestones. + if ($field->name eq 'component') { + @values = @{$bug->product_obj->components}; + } + elsif ($field->name eq 'target_milestone') { + @values = @{$bug->product_obj->milestones}; + } + elsif ($field->name eq 'version') { + @values = @{$bug->product_obj->versions}; } else { - # We need to get the values from the product for - # component, version, and milestones. - if ($field->name eq 'component') { - @values = @{ $bug->product_obj->components }; - } - elsif ($field->name eq 'target_milestone') { - @values = @{ $bug->product_obj->milestones }; - } - elsif ($field->name eq 'version') { - @values = @{ $bug->product_obj->versions }; - } - else { - @values = @{ $field->legal_values }; - } + @values = @{$field->legal_values}; } + } - my @filtered_values; - foreach my $value (@values) { - next if !$bug->id && !$value->is_active; - next if $bug->id && !$self->_can_change_field($field, $bug, $value->name); - push(@filtered_values, $value); - } + my @filtered_values; + foreach my $value (@values) { + next if !$bug->id && !$value->is_active; + next if $bug->id && !$self->_can_change_field($field, $bug, $value->name); + push(@filtered_values, $value); + } - return map { $self->_value_to_hash($_, $bug) } @filtered_values; + return map { $self->_value_to_hash($_, $bug) } @filtered_values; } sub _can_change_field { - my ($self, $field, $bug, $value) = @_; - my $user = Bugzilla->user; - my $field_name = blessed $field ? $field->name : $field; - - # Cannot set resolution on bug creation - return $self->type('boolean', 0) if ($field_name eq 'resolution' && !$bug->{bug_id}); - - # Cannot edit an obsolete or inactive custom field - return $self->type('boolean', 0) if (blessed $field && $field->custom && $field->obsolete); - - # If not a multi-select or single-select, value is not provided - # and we just check if the field itself is editable by the user. - if (!defined $value) { - return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1)); - } - - return $self->type('boolean', $bug->check_can_change_field($field_name, '', $value)); + my ($self, $field, $bug, $value) = @_; + my $user = Bugzilla->user; + my $field_name = blessed $field ? $field->name : $field; + + # Cannot set resolution on bug creation + return $self->type('boolean', 0) + if ($field_name eq 'resolution' && !$bug->{bug_id}); + + # Cannot edit an obsolete or inactive custom field + return $self->type('boolean', 0) + if (blessed $field && $field->custom && $field->obsolete); + + # If not a multi-select or single-select, value is not provided + # and we just check if the field itself is editable by the user. + if (!defined $value) { + return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1)); + } + + return $self->type('boolean', + $bug->check_can_change_field($field_name, '', $value)); } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $data = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id; - } - - $data->{type} = $flag->attach_id ? 'attachment' : 'bug'; - $data->{attach_id} = $flag->attach_id if $flag->attach_id; - - return $data; + my ($self, $flag) = @_; + + my $data = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id; + } + + $data->{type} = $flag->attach_id ? 'attachment' : 'bug'; + $data->{attach_id} = $flag->attach_id if $flag->attach_id; + + return $data; } sub _flagtype_to_hash { - my ($self, $flagtype, $bug) = @_; - my $user = Bugzilla->user; - - my $cansetflag = $user->can_set_flag($flagtype); - my $canrequestflag = $user->can_request_flag($flagtype); - - my $data = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - is_requestable => $self->type('boolean', $flagtype->is_requestable), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), - can_set_flag => $self->type('boolean', $cansetflag), - can_request_flag => $self->type('boolean', $canrequestflag) - }; - - my @values; - foreach my $value ('?','+','-') { - push(@values, $self->type('string', $value)); - } - $data->{values} = \@values; - - # if we're creating a bug, we need to return all valid flags for - # this product, as well as inclusions & exclusions so ember can - # display relevant flags once the component is selected - if (!$bug->id) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $bug->product_obj->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $bug->product_obj->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $data; + my ($self, $flagtype, $bug) = @_; + my $user = Bugzilla->user; + + my $cansetflag = $user->can_set_flag($flagtype); + my $canrequestflag = $user->can_request_flag($flagtype); + + my $data = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + is_requestable => $self->type('boolean', $flagtype->is_requestable), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), + can_set_flag => $self->type('boolean', $cansetflag), + can_request_flag => $self->type('boolean', $canrequestflag) + }; + + my @values; + foreach my $value ('?', '+', '-') { + push(@values, $self->type('string', $value)); + } + $data->{values} = \@values; + + # if we're creating a bug, we need to return all valid flags for + # this product, as well as inclusions & exclusions so ember can + # display relevant flags once the component is selected + if (!$bug->id) { + my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, + $bug->product_obj->id); + my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, + $bug->product_obj->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $data; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub rest_resources { - return [ - # create page - single product name - qr{^/ember/create/(.*)$}, { - GET => { - method => 'create', - params => sub { - return { product => $_[0] }; - } - } - }, - # create page - one or more products - qr{^/ember/create$}, { - GET => { - method => 'create' - } - }, - # show bug page - single bug id - qr{^/ember/show/(\d+)$}, { - GET => { - method => 'show', - params => sub { - return { id => $_[0] }; - } - } - }, - # search - wrapper around SUPER::search which also includes the total - # number of bugs when using pagination - qr{^/ember/search$}, { - GET => { - method => 'search', - }, - }, - # get current bug attributes without field information - single bug id - qr{^/ember/bug/(\d+)$}, { - GET => { - method => 'bug', - params => sub { - return { id => $_[0] }; - } - } - }, - # attachments - wrapper around SUPER::attachments that also includes - # can_edit attribute - qr{^/ember/bug/(\d+)/attachments$}, { - GET => { - method => 'get_attachments', - params => sub { - return { ids => $_[0] }; - } - } - }, - qr{^/ember/bug/attachments/(\d+)$}, { - GET => { - method => 'get_attachments', - params => sub { - return { attachment_ids => $_[0] }; - } - } + return [ + # create page - single product name + qr{^/ember/create/(.*)$}, + { + GET => { + method => 'create', + params => sub { + return {product => $_[0]}; } - ]; -}; + } + }, + + # create page - one or more products + qr{^/ember/create$}, + {GET => {method => 'create'}}, + + # show bug page - single bug id + qr{^/ember/show/(\d+)$}, + { + GET => { + method => 'show', + params => sub { + return {id => $_[0]}; + } + } + }, + + # search - wrapper around SUPER::search which also includes the total + # number of bugs when using pagination + qr{^/ember/search$}, + {GET => {method => 'search',},}, + + # get current bug attributes without field information - single bug id + qr{^/ember/bug/(\d+)$}, + { + GET => { + method => 'bug', + params => sub { + return {id => $_[0]}; + } + } + }, + + # attachments - wrapper around SUPER::attachments that also includes + # can_edit attribute + qr{^/ember/bug/(\d+)/attachments$}, + { + GET => { + method => 'get_attachments', + params => sub { + return {ids => $_[0]}; + } + } + }, + qr{^/ember/bug/attachments/(\d+)$}, + { + GET => { + method => 'get_attachments', + params => sub { + return {attachment_ids => $_[0]}; + } + } + } + ]; +} 1; diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm index e7782ef6c..696da2de9 100644 --- a/extensions/Example/Config.pm +++ b/extensions/Example/Config.pm @@ -12,21 +12,16 @@ use strict; use warnings; use constant NAME => 'Example'; -use constant REQUIRED_MODULES => [ - { - package => 'Data-Dumper', - module => 'Data::Dumper', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Data-Dumper', module => 'Data::Dumper', version => 0,},]; use constant OPTIONAL_MODULES => [ - { - package => 'Acme', - module => 'Acme', - version => 1.11, - feature => ['example_acme'], - }, + { + package => 'Acme', + module => 'Acme', + version => 1.11, + feature => ['example_acme'], + }, ]; __PACKAGE__->NAME; diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm index 22c4042b5..9ecbf25a6 100644 --- a/extensions/Example/Extension.pm +++ b/extensions/Example/Extension.pm @@ -33,338 +33,355 @@ use constant REL_EXAMPLE => -127; our $VERSION = '1.0'; sub admin_editusers_action { - my ($self, $args) = @_; - my ($vars, $action, $user) = @$args{qw(vars action user)}; - my $template = Bugzilla->template; - - if ($action eq 'my_action') { - # Allow to restrict the search to any group the user is allowed to bless. - $vars->{'restrictablegroups'} = $user->bless_groups(); - $template->process('admin/users/search.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } + my ($self, $args) = @_; + my ($vars, $action, $user) = @$args{qw(vars action user)}; + my $template = Bugzilla->template; + + if ($action eq 'my_action') { + + # Allow to restrict the search to any group the user is allowed to bless. + $vars->{'restrictablegroups'} = $user->bless_groups(); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } } sub attachment_process_data { - my ($self, $args) = @_; - my $type = $args->{attributes}->{mimetype}; - my $filename = $args->{attributes}->{filename}; - - # Make sure images have the correct extension. - # Uncomment the two lines below to make this check effective. - if ($type =~ /^image\/(\w+)$/) { - my $format = $1; - if ($filename =~ /^(.+)(:?\.[^\.]+)$/) { - my $name = $1; - #$args->{attributes}->{filename} = "${name}.$format"; - } - else { - # The file has no extension. We append it. - #$args->{attributes}->{filename} .= ".$format"; - } + my ($self, $args) = @_; + my $type = $args->{attributes}->{mimetype}; + my $filename = $args->{attributes}->{filename}; + + # Make sure images have the correct extension. + # Uncomment the two lines below to make this check effective. + if ($type =~ /^image\/(\w+)$/) { + my $format = $1; + if ($filename =~ /^(.+)(:?\.[^\.]+)$/) { + my $name = $1; + + #$args->{attributes}->{filename} = "${name}.$format"; } + else { + # The file has no extension. We append it. + #$args->{attributes}->{filename} .= ".$format"; + } + } } sub auth_login_methods { - my ($self, $args) = @_; - my $modules = $args->{modules}; - if (exists $modules->{Example}) { - $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{modules}; + if (exists $modules->{Example}) { + $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm'; + } } sub auth_verify_methods { - my ($self, $args) = @_; - my $modules = $args->{modules}; - if (exists $modules->{Example}) { - $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{modules}; + if (exists $modules->{Example}) { + $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm'; + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - - my ($bug, $field, $new_value, $old_value, $priv_results) - = @$args{qw(bug field new_value old_value priv_results)}; - - my $user = Bugzilla->user; - - # Disallow a bug from being reopened if currently closed unless user - # is in 'admin' group - if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') { - if (!is_open_state($old_value) && is_open_state($new_value) - && !$user->in_group('admin')) - { - push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - return; - } - } + my ($self, $args) = @_; - # Disallow a bug's keywords from being edited unless user is the - # reporter of the bug - if ($field eq 'keywords' && $bug->product_obj->name eq 'Example' - && $user->login ne $bug->reporter->login) - { - push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER); - return; - } + my ($bug, $field, $new_value, $old_value, $priv_results) + = @$args{qw(bug field new_value old_value priv_results)}; + + my $user = Bugzilla->user; - # Allow updating of priority even if user cannot normally edit the bug - # and they are in group 'engineering' - if ($field eq 'priority' && $bug->product_obj->name eq 'Example' - && $user->in_group('engineering')) + # Disallow a bug from being reopened if currently closed unless user + # is in 'admin' group + if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') { + if (!is_open_state($old_value) + && is_open_state($new_value) + && !$user->in_group('admin')) { - push(@$priv_results, PRIVILEGES_REQUIRED_NONE); - return; + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + return; } + } + + # Disallow a bug's keywords from being edited unless user is the + # reporter of the bug + if ( $field eq 'keywords' + && $bug->product_obj->name eq 'Example' + && $user->login ne $bug->reporter->login) + { + push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER); + return; + } + + # Allow updating of priority even if user cannot normally edit the bug + # and they are in group 'engineering' + if ( $field eq 'priority' + && $bug->product_obj->name eq 'Example' + && $user->in_group('engineering')) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + return; + } } sub bug_columns { - my ($self, $args) = @_; - my $columns = $args->{'columns'}; - push (@$columns, "delta_ts AS example") + my ($self, $args) = @_; + my $columns = $args->{'columns'}; + push(@$columns, "delta_ts AS example"); } sub bug_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $bug = $args->{'bug'}; - my $timestamp = $args->{'timestamp'}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; - my $bug_id = $bug->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you file a bug. - # warn "Bug $bug_id has been filed!"; + my $bug_id = $bug->id; + + # Uncomment this line to see a line in your webserver's error log whenever + # you file a bug. + # warn "Bug $bug_id has been filed!"; } sub bug_end_of_create_validators { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $bug_params = $args->{'params'}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $bug_params = $args->{'params'}; - # Uncomment this line below to see a line in your webserver's error log - # containing all validated bug field values every time you file a bug. - # warn Dumper($bug_params); + # Uncomment this line below to see a line in your webserver's error log + # containing all validated bug field values every time you file a bug. + # warn Dumper($bug_params); - # This would remove all ccs from the bug, preventing ANY ccs from being - # added on bug creation. - # $bug_params->{cc} = []; + # This would remove all ccs from the bug, preventing ANY ccs from being + # added on bug creation. + # $bug_params->{cc} = []; } sub bug_start_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $old_summary = $old_bug->short_desc; + + my $status_message; + if (my $status_change = $changes->{'bug_status'}) { + my $old_status = new Bugzilla::Status({name => $status_change->[0]}); + my $new_status = new Bugzilla::Status({name => $status_change->[1]}); + if ($new_status->is_open && !$old_status->is_open) { + $status_message = "Bug re-opened!"; } - - my $old_summary = $old_bug->short_desc; - - my $status_message; - if (my $status_change = $changes->{'bug_status'}) { - my $old_status = new Bugzilla::Status({ name => $status_change->[0] }); - my $new_status = new Bugzilla::Status({ name => $status_change->[1] }); - if ($new_status->is_open && !$old_status->is_open) { - $status_message = "Bug re-opened!"; - } - if (!$new_status->is_open && $old_status->is_open) { - $status_message = "Bug closed!"; - } + if (!$new_status->is_open && $old_status->is_open) { + $status_message = "Bug closed!"; } + } + + my $bug_id = $bug->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on bug $bug_id" + . " at $timestamp."; - my $bug_id = $bug->id; - my $num_changes = scalar keys %$changes; - my $result = "There were $num_changes changes to fields on bug $bug_id" - . " at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a bug. - # warn $result; + # Uncomment this line to see $result in your webserver's error log whenever + # you update a bug. + # warn $result; } sub bug_end_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $old_summary = $old_bug->short_desc; + + my $status_message; + if (my $status_change = $changes->{'bug_status'}) { + my $old_status = new Bugzilla::Status({name => $status_change->[0]}); + my $new_status = new Bugzilla::Status({name => $status_change->[1]}); + if ($new_status->is_open && !$old_status->is_open) { + $status_message = "Bug re-opened!"; } - - my $old_summary = $old_bug->short_desc; - - my $status_message; - if (my $status_change = $changes->{'bug_status'}) { - my $old_status = new Bugzilla::Status({ name => $status_change->[0] }); - my $new_status = new Bugzilla::Status({ name => $status_change->[1] }); - if ($new_status->is_open && !$old_status->is_open) { - $status_message = "Bug re-opened!"; - } - if (!$new_status->is_open && $old_status->is_open) { - $status_message = "Bug closed!"; - } + if (!$new_status->is_open && $old_status->is_open) { + $status_message = "Bug closed!"; } + } - my $bug_id = $bug->id; - my $num_changes = scalar keys %$changes; - my $result = "There were $num_changes changes to fields on bug $bug_id" - . " at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a bug. - # warn $result; + my $bug_id = $bug->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on bug $bug_id" + . " at $timestamp."; + + # Uncomment this line to see $result in your webserver's error log whenever + # you update a bug. + # warn $result; } sub bug_fields { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, "example") + my $fields = $args->{'fields'}; + push(@$fields, "example"); } sub bug_format_comment { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This replaces every occurrence of the word "foo" with the word - # "bar" + # This replaces every occurrence of the word "foo" with the word + # "bar" - my $regexes = $args->{'regexes'}; - push(@$regexes, { match => qr/\bfoo\b/, replace => 'bar' }); + my $regexes = $args->{'regexes'}; + push(@$regexes, {match => qr/\bfoo\b/, replace => 'bar'}); - # And this links every occurrence of the word "bar" to example.com, - # but it won't affect "foo"s that have already been turned into "bar" - # above (because each regex is run in order, and later regexes don't modify - # earlier matches, due to some cleverness in Bugzilla's internals). - # - # For example, the phrase "foo bar" would become: - # bar bar - my $bar_match = qr/\b(bar)\b/; - push(@$regexes, { match => $bar_match, replace => \&_replace_bar }); + # And this links every occurrence of the word "bar" to example.com, + # but it won't affect "foo"s that have already been turned into "bar" + # above (because each regex is run in order, and later regexes don't modify + # earlier matches, due to some cleverness in Bugzilla's internals). + # + # For example, the phrase "foo bar" would become: + # bar bar + my $bar_match = qr/\b(bar)\b/; + push(@$regexes, {match => $bar_match, replace => \&_replace_bar}); } # Used by bug_format_comment--see its code for an explanation. sub _replace_bar { - my $args = shift; - # $match is the first parentheses match in the $bar_match regex - # in bug-format_comment.pl. We get up to 10 regex matches as - # arguments to this function. - my $match = $args->{matches}->[0]; - # Remember, you have to HTML-escape any data that you are returning! - $match = html_quote($match); - return qq{$match}; -}; + my $args = shift; + + # $match is the first parentheses match in the $bar_match regex + # in bug-format_comment.pl. We get up to 10 regex matches as + # arguments to this function. + my $match = $args->{matches}->[0]; + + # Remember, you have to HTML-escape any data that you are returning! + $match = html_quote($match); + return qq{$match}; +} sub buglist_columns { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $columns = $args->{'columns'}; - $columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' }; - $columns->{'product_desc'} = { 'name' => 'prod_desc.description', - 'title' => 'Product Description' }; + my $columns = $args->{'columns'}; + $columns->{'example'} = {'name' => 'bugs.delta_ts', 'title' => 'Example'}; + $columns->{'product_desc'} + = {'name' => 'prod_desc.description', 'title' => 'Product Description'}; } sub buglist_column_joins { - my ($self, $args) = @_; - my $joins = $args->{'column_joins'}; + my ($self, $args) = @_; + my $joins = $args->{'column_joins'}; - # This column is added using the "buglist_columns" hook - $joins->{'product_desc'} = { - from => 'product_id', - to => 'id', - table => 'products', - as => 'prod_desc', - join => 'INNER', - }; + # This column is added using the "buglist_columns" hook + $joins->{'product_desc'} = { + from => 'product_id', + to => 'id', + table => 'products', + as => 'prod_desc', + join => 'INNER', + }; } sub search_operator_field_override { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $operators = $args->{'operators'}; + my $operators = $args->{'operators'}; - my $original = $operators->{component}->{_non_changed}; - $operators->{component} = { - _non_changed => sub { _component_nonchanged($original, @_) } - }; + my $original = $operators->{component}->{_non_changed}; + $operators->{component} = { + _non_changed => sub { _component_nonchanged($original, @_) } + }; } sub _component_nonchanged { - my $original = shift; - my ($invocant, $args) = @_; + my $original = shift; + my ($invocant, $args) = @_; + + $invocant->$original($args); - $invocant->$original($args); - # Actually, it does not change anything in the result, - # just an example. - $args->{term} = $args->{term} . " OR 1=2"; + # Actually, it does not change anything in the result, + # just an example. + $args->{term} = $args->{term} . " OR 1=2"; } sub bugmail_recipients { - my ($self, $args) = @_; - my $recipients = $args->{recipients}; - my $bug = $args->{bug}; - - my $user = - new Bugzilla::User({ name => Bugzilla->params->{'maintainer'} }); - - if ($bug->id == 1) { - # Uncomment the line below to add the maintainer to the recipients - # list of every bugmail from bug 1 as though that the maintainer - # were on the CC list. - #$recipients->{$user->id}->{+REL_CC} = 1; - - # And this line adds the maintainer as though he had the "REL_EXAMPLE" - # relationship from the bugmail_relationships hook below. - #$recipients->{$user->id}->{+REL_EXAMPLE} = 1; - } + my ($self, $args) = @_; + my $recipients = $args->{recipients}; + my $bug = $args->{bug}; + + my $user = new Bugzilla::User({name => Bugzilla->params->{'maintainer'}}); + + if ($bug->id == 1) { + + # Uncomment the line below to add the maintainer to the recipients + # list of every bugmail from bug 1 as though that the maintainer + # were on the CC list. + #$recipients->{$user->id}->{+REL_CC} = 1; + + # And this line adds the maintainer as though he had the "REL_EXAMPLE" + # relationship from the bugmail_relationships hook below. + #$recipients->{$user->id}->{+REL_EXAMPLE} = 1; + } } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_EXAMPLE} = 'Example'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_EXAMPLE} = 'Example'; } sub config_add_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{Example} = "Bugzilla::Extension::Example::Config"; + my $modules = $args->{panel_modules}; + $modules->{Example} = "Bugzilla::Extension::Example::Config"; } sub config_modify_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $panels = $args->{panels}; + my $panels = $args->{panels}; - # Add the "Example" auth methods. - my $auth_params = $panels->{'auth'}->{params}; - my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params); - my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params); + # Add the "Example" auth methods. + my $auth_params = $panels->{'auth'}->{params}; + my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params); + my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params); - push(@{ $info_class->{choices} }, 'CGI,Example'); - push(@{ $verify_class->{choices} }, 'Example'); + push(@{$info_class->{choices}}, 'CGI,Example'); + push(@{$verify_class->{choices}}, 'Example'); - push(@$auth_params, { name => 'param_example', - type => 't', - default => 0, - checker => \&check_numeric }); + push( + @$auth_params, + { + name => 'param_example', + type => 't', + default => 0, + checker => \&check_numeric + } + ); } sub db_schema_abstract_schema { - my ($self, $args) = @_; + my ($self, $args) = @_; + # $args->{'schema'}->{'example_table'} = { # FIELDS => [ # id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, @@ -383,639 +400,662 @@ sub db_schema_abstract_schema { } sub email_in_before_parse { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $subject = $args->{mail}->header('Subject'); - # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN]. - if ($subject =~ /\[.*(\d+)\].*/) { - $args->{fields}->{bug_id} = $1; - } + my $subject = $args->{mail}->header('Subject'); + + # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN]. + if ($subject =~ /\[.*(\d+)\].*/) { + $args->{fields}->{bug_id} = $1; + } } sub email_in_after_parse { - my ($self, $args) = @_; - my $reporter = $args->{fields}->{reporter}; - my $dbh = Bugzilla->dbh; - - # No other check needed if this is a valid regular user. - return if login_to_id($reporter); - - # The reporter is not a regular user. We create an account for him, - # but he can only comment on existing bugs. - # This is useful for people who reply by email to bugmails received - # in mailing-lists. - if ($args->{fields}->{bug_id}) { - # WARNING: we return now to skip the remaining code below. - # You must understand that removing this line would make the code - # below effective! Do it only if you are OK with the behavior - # described here. - return; - - Bugzilla::User->create({ login_name => $reporter, cryptpassword => '*' }); - - # For security reasons, delete all fields unrelated to comments. - foreach my $field (keys %{$args->{fields}}) { - next if $field =~ /^(?:bug_id|comment|reporter)$/; - delete $args->{fields}->{$field}; - } - } - else { - ThrowUserError('invalid_username', { name => $reporter }); + my ($self, $args) = @_; + my $reporter = $args->{fields}->{reporter}; + my $dbh = Bugzilla->dbh; + + # No other check needed if this is a valid regular user. + return if login_to_id($reporter); + + # The reporter is not a regular user. We create an account for him, + # but he can only comment on existing bugs. + # This is useful for people who reply by email to bugmails received + # in mailing-lists. + if ($args->{fields}->{bug_id}) { + + # WARNING: we return now to skip the remaining code below. + # You must understand that removing this line would make the code + # below effective! Do it only if you are OK with the behavior + # described here. + return; + + Bugzilla::User->create({login_name => $reporter, cryptpassword => '*'}); + + # For security reasons, delete all fields unrelated to comments. + foreach my $field (keys %{$args->{fields}}) { + next if $field =~ /^(?:bug_id|comment|reporter)$/; + delete $args->{fields}->{$field}; } + } + else { + ThrowUserError('invalid_username', {name => $reporter}); + } } sub enter_bug_entrydefaultvars { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub error_catch { - my ($self, $args) = @_; - # Customize the error message displayed when someone tries to access - # page.cgi with an invalid page ID, and keep track of this attempt - # in the web server log. - return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE; - return unless $args->{error} eq 'bad_page_cgi_id'; - - my $page_id = $args->{vars}->{page_id}; - my $login = Bugzilla->user->identity || "Someone"; - warn "$login attempted to access page.cgi with id = $page_id"; - - my $page = $args->{message}; - my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!"; - $new_error_msg = html_quote($new_error_msg); - # There are better tools to parse an HTML page, but it's just an example. - # Since Perl 5.16, we can no longer write "class" inside look-behind - # assertions, because "ss" is also seen as the german ß character, which - # makes Perl 5.16 complain. The right fix is to use the /aa modifier, - # but it's only understood since Perl 5.14. So the workaround is to write - # "clas[s]" instead of "class". Stupid and ugly hack, but it works with - # all Perl versions. - $$page =~ s/(?<=).*(?=<\/td>)/$new_error_msg/si; + my ($self, $args) = @_; + + # Customize the error message displayed when someone tries to access + # page.cgi with an invalid page ID, and keep track of this attempt + # in the web server log. + return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE; + return unless $args->{error} eq 'bad_page_cgi_id'; + + my $page_id = $args->{vars}->{page_id}; + my $login = Bugzilla->user->identity || "Someone"; + warn "$login attempted to access page.cgi with id = $page_id"; + + my $page = $args->{message}; + my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!"; + $new_error_msg = html_quote($new_error_msg); + + # There are better tools to parse an HTML page, but it's just an example. + # Since Perl 5.16, we can no longer write "class" inside look-behind + # assertions, because "ss" is also seen as the german ß character, which + # makes Perl 5.16 complain. The right fix is to use the /aa modifier, + # but it's only understood since Perl 5.14. So the workaround is to write + # "clas[s]" instead of "class". Stupid and ugly hack, but it works with + # all Perl versions. + $$page + =~ s/(?<=).*(?=<\/td>)/$new_error_msg/si; } sub flag_end_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $flag_params = $args; - my ($object, $timestamp, $old_flags, $new_flags) = - @$flag_params{qw(object timestamp old_flags new_flags)}; - my ($removed, $added) = diff_arrays($old_flags, $new_flags); - my ($granted, $denied) = (0, 0); - foreach my $new_flag (@$added) { - $granted++ if $new_flag =~ /\+$/; - $denied++ if $new_flag =~ /-$/; - } - my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id - : $object->bug_id; - my $result = "$granted flags were granted and $denied flags were denied" - . " on bug $bug_id at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update flags. - # warn $result; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $flag_params = $args; + my ($object, $timestamp, $old_flags, $new_flags) + = @$flag_params{qw(object timestamp old_flags new_flags)}; + my ($removed, $added) = diff_arrays($old_flags, $new_flags); + my ($granted, $denied) = (0, 0); + foreach my $new_flag (@$added) { + $granted++ if $new_flag =~ /\+$/; + $denied++ if $new_flag =~ /-$/; + } + my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id : $object->bug_id; + my $result = "$granted flags were granted and $denied flags were denied" + . " on bug $bug_id at $timestamp."; + + # Uncomment this line to see $result in your webserver's error log whenever + # you update flags. + # warn $result; } sub group_before_delete { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. - my $group = $args->{'group'}; - my $group_id = $group->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you file a bug. - # warn "Group $group_id is about to be deleted!"; + my $group = $args->{'group'}; + my $group_id = $group->id; + + # Uncomment this line to see a line in your webserver's error log whenever + # you file a bug. + # warn "Group $group_id is about to be deleted!"; } sub group_end_of_create { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $group = $args->{'group'}; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $group = $args->{'group'}; + + my $group_id = $group->id; - my $group_id = $group->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you create a new group. - #warn "Group $group_id has been created!"; + # Uncomment this line to see a line in your webserver's error log whenever + # you create a new group. + #warn "Group $group_id has been created!"; } sub group_end_of_update { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. + my ($self, $args) = @_; - my ($group, $changes) = @$args{qw(group changes)}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; - } + my ($group, $changes) = @$args{qw(group changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $group_id = $group->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on group $group_id."; - my $group_id = $group->id; - my $num_changes = scalar keys %$changes; - my $result = - "There were $num_changes changes to fields on group $group_id."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a group. - #warn $result; + # Uncomment this line to see $result in your webserver's error log whenever + # you update a group. + #warn $result; } sub install_before_final_checks { - my ($self, $args) = @_; - print "Install-before_final_checks hook\n" unless $args->{silent}; + my ($self, $args) = @_; + print "Install-before_final_checks hook\n" unless $args->{silent}; - # Add a new user setting like this: - # - # add_setting({ - # name => 'product_chooser', # setting name - # options => ['pretty', 'full', 'small'], # options - # category => 'pretty' # default - # }); - # To add descriptions for the setting and choices, add extra values to - # the hash defined in global/setting-descs.none.tmpl. Do this in a hook: - # hook/global/setting-descs-settings.none.tmpl . + # Add a new user setting like this: + # + # add_setting({ + # name => 'product_chooser', # setting name + # options => ['pretty', 'full', 'small'], # options + # category => 'pretty' # default + # }); + # To add descriptions for the setting and choices, add extra values to + # the hash defined in global/setting-descs.none.tmpl. Do this in a hook: + # hook/global/setting-descs-settings.none.tmpl . } sub install_filesystem { - my ($self, $args) = @_; - my $create_dirs = $args->{'create_dirs'}; - my $recurse_dirs = $args->{'recurse_dirs'}; - my $htaccess = $args->{'htaccess'}; - - # Create a new directory in datadir specifically for this extension. - # The directory will need to allow files to be created by the extension - # code as well as allow the webserver to server content from it. - # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; - # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE; - - # Update the permissions of any files and directories that currently reside - # in the extension's directory. - # $recurse_dirs->{$data_path} = { - # files => Bugzilla::Install::Filesystem::CGI_READ, - # dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE - # }; - - # Create a htaccess file that allows specific content to be served from the - # extension's directory. - # $htaccess->{"$data_path/.htaccess"} = { - # perms => Bugzilla::Install::Filesystem::WS_SERVE, - # contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY - # }; + my ($self, $args) = @_; + my $create_dirs = $args->{'create_dirs'}; + my $recurse_dirs = $args->{'recurse_dirs'}; + my $htaccess = $args->{'htaccess'}; + + # Create a new directory in datadir specifically for this extension. + # The directory will need to allow files to be created by the extension + # code as well as allow the webserver to server content from it. + # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; + # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE; + + # Update the permissions of any files and directories that currently reside + # in the extension's directory. + # $recurse_dirs->{$data_path} = { + # files => Bugzilla::Install::Filesystem::CGI_READ, + # dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE + # }; + + # Create a htaccess file that allows specific content to be served from the + # extension's directory. + # $htaccess->{"$data_path/.htaccess"} = { + # perms => Bugzilla::Install::Filesystem::WS_SERVE, + # contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY + # }; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + # $dbh->bz_add_column('example', 'new_column', # {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); # $dbh->bz_add_index('example', 'example_new_column_idx', [qw(value)]); } sub install_update_db_fielddefs { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + # $dbh->bz_add_column('fielddefs', 'example_column', # {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => ''}); } sub job_map { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $job_map = $args->{job_map}; + my $job_map = $args->{job_map}; - # This adds the named class (an instance of TheSchwartz::Worker) as a - # handler for when a job is added with the name "some_task". - $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass'; + # This adds the named class (an instance of TheSchwartz::Worker) as a + # handler for when a job is added with the name "some_task". + $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass'; - # Schedule a job like this: - # my $queue = Bugzilla->job_queue(); - # $queue->insert('some_task', { some_parameter => $some_variable }); + # Schedule a job like this: + # my $queue = Bugzilla->job_queue(); + # $queue->insert('some_task', { some_parameter => $some_variable }); } sub mailer_before_send { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $email = $args->{email}; - my $email = $args->{email}; - # If you add a header to an email, it's best to start it with - # 'X-Bugzilla-' so that you don't conflict with - # other extensions. - $email->header_set('X-Bugzilla-Example-Header', 'Example'); + # If you add a header to an email, it's best to start it with + # 'X-Bugzilla-' so that you don't conflict with + # other extensions. + $email->header_set('X-Bugzilla-Example-Header', 'Example'); } sub object_before_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object_params = $args->{'params'}; + my $class = $args->{'class'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($class->isa('Bugzilla::ExampleObject')) { - warn "About to create an ExampleObject!"; - warn "Got the following parameters: " - . join(', ', keys(%$object_params)); - } + # Note that this is a made-up class, for this example. + if ($class->isa('Bugzilla::ExampleObject')) { + warn "About to create an ExampleObject!"; + warn "Got the following parameters: " . join(', ', keys(%$object_params)); + } } sub object_before_delete { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = $args->{'object'}; + my $object = $args->{'object'}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - my $id = $object->id; - warn "An object with id $id is about to be deleted!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + my $id = $object->id; + warn "An object with id $id is about to be deleted!"; + } } sub object_before_set { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $field, $value) = @$args{qw(object field value)}; + my ($object, $field, $value) = @$args{qw(object field value)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - warn "The field $field is changing from " . $object->{$field} - . " to $value!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + warn "The field $field is changing from " . $object->{$field} . " to $value!"; + } } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::ExampleObject')) { - push(@$columns, 'example'); - } + if ($class->isa('Bugzilla::ExampleObject')) { + push(@$columns, 'example'); + } } sub object_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object = $args->{'object'}; + my $class = $args->{'class'}; + my $object = $args->{'object'}; - warn "Created a new $class object!"; + warn "Created a new $class object!"; } sub object_end_of_create_validators { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object_params = $args->{'params'}; + my $class = $args->{'class'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($class->isa('Bugzilla::ExampleObject')) { - # Always set example_field to 1, even if the validators said otherwise. - $object_params->{example_field} = 1; - } + # Note that this is a made-up class, for this example. + if ($class->isa('Bugzilla::ExampleObject')) { + + # Always set example_field to 1, even if the validators said otherwise. + $object_params->{example_field} = 1; + } } sub object_end_of_set { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; + my ($object, $field) = @$args{qw(object field)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - warn "The field $field has changed to " . $object->{$field}; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + warn "The field $field has changed to " . $object->{$field}; + } } sub object_end_of_set_all { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = $args->{'object'}; - my $object_params = $args->{'params'}; + my $object = $args->{'object'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - if ($object_params->{example_field} == 1) { - $object->{example_field} = 1; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + if ($object_params->{example_field} == 1) { + $object->{example_field} = 1; } + } } sub object_end_of_update { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $old_object, $changes) = - @$args{qw(object old_object changes)}; + my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - if (defined $changes->{'name'}) { - my ($old, $new) = @{ $changes->{'name'} }; - print "The name field changed from $old to $new!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + if (defined $changes->{'name'}) { + my ($old, $new) = @{$changes->{'name'}}; + print "The name field changed from $old to $new!"; } + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::ExampleObject')) { - push(@$columns, 'example'); - } + if ($object->isa('Bugzilla::ExampleObject')) { + push(@$columns, 'example'); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - - if ($class->isa('Bugzilla::Bug')) { - # This is an example of adding a new validator. - # See the _check_example subroutine below. - $validators->{example} = \&_check_example; - - # This is an example of overriding an existing validator. - # See the check_short_desc validator below. - my $original = $validators->{short_desc}; - $validators->{short_desc} = sub { _check_short_desc($original, @_) }; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + + if ($class->isa('Bugzilla::Bug')) { + + # This is an example of adding a new validator. + # See the _check_example subroutine below. + $validators->{example} = \&_check_example; + + # This is an example of overriding an existing validator. + # See the check_short_desc validator below. + my $original = $validators->{short_desc}; + $validators->{short_desc} = sub { _check_short_desc($original, @_) }; + } } sub _check_example { - my ($invocant, $value, $field) = @_; - warn "I was called to validate the value of $field."; - warn "The value of $field that I was passed in is: $value"; + my ($invocant, $value, $field) = @_; + warn "I was called to validate the value of $field."; + warn "The value of $field that I was passed in is: $value"; - # Make the value always be 1. - my $fixed_value = 1; - return $fixed_value; + # Make the value always be 1. + my $fixed_value = 1; + return $fixed_value; } sub _check_short_desc { - my $original = shift; - my $invocant = shift; - my $value = $invocant->$original(@_); - if ($value !~ /example/i) { - # Use this line to make Bugzilla throw an error every time - # you try to file a bug or update a bug without the word "example" - # in the summary. - if (0) { - ThrowUserError('example_short_desc_invalid'); - } + my $original = shift; + my $invocant = shift; + my $value = $invocant->$original(@_); + if ($value !~ /example/i) { + + # Use this line to make Bugzilla throw an error every time + # you try to file a bug or update a bug without the word "example" + # in the summary. + if (0) { + ThrowUserError('example_short_desc_invalid'); } - return $value; + } + return $value; } sub page_before_template { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; + my ($vars, $page) = @$args{qw(vars page_id)}; - # You can see this hook in action by loading page.cgi?id=example.html - if ($page eq 'example.html') { - $vars->{cgi_variables} = { Bugzilla->cgi->Vars }; - } + # You can see this hook in action by loading page.cgi?id=example.html + if ($page eq 'example.html') { + $vars->{cgi_variables} = {Bugzilla->cgi->Vars}; + } } sub path_info_whitelist { - my ($self, $args) = @_; - my $whitelist = $args->{whitelist}; - push(@$whitelist, "page.cgi"); + my ($self, $args) = @_; + my $whitelist = $args->{whitelist}; + push(@$whitelist, "page.cgi"); } sub post_bug_after_creation { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub product_confirm_delete { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub product_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $product = $args->{product}; - my $product = $args->{product}; + # For this example, any lines of code that actually make changes to your + # database have been commented out. - # For this example, any lines of code that actually make changes to your - # database have been commented out. + # This section will take a group that exists in your installation + # (possible called test_group) and automatically makes the new + # product hidden to only members of the group. Just remove + # the restriction if you want the new product to be public. - # This section will take a group that exists in your installation - # (possible called test_group) and automatically makes the new - # product hidden to only members of the group. Just remove - # the restriction if you want the new product to be public. + my $example_group = new Bugzilla::Group({name => 'example_group'}); - my $example_group = new Bugzilla::Group({ name => 'example_group' }); + if ($example_group) { + $product->set_group_controls( + $example_group, + { + entry => 1, + membercontrol => CONTROLMAPMANDATORY, + othercontrol => CONTROLMAPMANDATORY + } + ); - if ($example_group) { - $product->set_group_controls($example_group, - { entry => 1, - membercontrol => CONTROLMAPMANDATORY, - othercontrol => CONTROLMAPMANDATORY }); # $product->update(); - } + } - # This section will automatically add a default component - # to the new product called 'No Component'. + # This section will automatically add a default component + # to the new product called 'No Component'. - my $default_assignee = new Bugzilla::User( - { name => Bugzilla->params->{maintainer} }); + my $default_assignee + = new Bugzilla::User({name => Bugzilla->params->{maintainer}}); + + if ($default_assignee) { - if ($default_assignee) { # Bugzilla::Component->create( # { name => 'No Component', # product => $product, # description => 'Select this component if one does not ' . # 'exist in the current list of components', # initialowner => $default_assignee }); - } + } } sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; + my ($self, $args) = @_; + my $map = $args->{'map'}; - # This demonstrates adding a shorter alias for a long custom field name. - $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'}; + # This demonstrates adding a shorter alias for a long custom field name. + $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'}; } sub sanitycheck_check { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $sth; + my $dbh = Bugzilla->dbh; + my $sth; - my $status = $args->{'status'}; + my $status = $args->{'status'}; - # Check that all users are Australian - $status->('example_check_au_user'); + # Check that all users are Australian + $status->('example_check_au_user'); - $sth = $dbh->prepare("SELECT userid, login_name + $sth = $dbh->prepare( + "SELECT userid, login_name FROM profiles - WHERE login_name NOT LIKE '%.au'"); - $sth->execute; - - my $seen_nonau = 0; - while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) { - $status->('example_check_au_user_alert', - { userid => $userid, login => $login }, - 'alert'); - $seen_nonau = 1; - } + WHERE login_name NOT LIKE '%.au'" + ); + $sth->execute; + + my $seen_nonau = 0; + while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) { + $status->( + 'example_check_au_user_alert', {userid => $userid, login => $login}, 'alert' + ); + $seen_nonau = 1; + } - $status->('example_check_au_user_prompt') if $seen_nonau; + $status->('example_check_au_user_prompt') if $seen_nonau; } sub sanitycheck_repair { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; - my $status = $args->{'status'}; + my $status = $args->{'status'}; - if ($cgi->param('example_repair_au_user')) { - $status->('example_repair_au_user_start'); + if ($cgi->param('example_repair_au_user')) { + $status->('example_repair_au_user_start'); - #$dbh->do("UPDATE profiles - # SET login_name = CONCAT(login_name, '.au') - # WHERE login_name NOT LIKE '%.au'"); + #$dbh->do("UPDATE profiles + # SET login_name = CONCAT(login_name, '.au') + # WHERE login_name NOT LIKE '%.au'"); - $status->('example_repair_au_user_end'); - } + $status->('example_repair_au_user_end'); + } } sub template_before_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $config = $args->{'config'}; - # This will be accessible as "example_global_variable" in every - # template in Bugzilla. See Bugzilla/Template.pm's create() function - # for more things that you can set. - $config->{VARIABLES}->{example_global_variable} = sub { return 'value' }; + my $config = $args->{'config'}; + + # This will be accessible as "example_global_variable" in every + # template in Bugzilla. See Bugzilla/Template.pm's create() function + # for more things that you can set. + $config->{VARIABLES}->{example_global_variable} = sub { return 'value' }; } sub template_before_process { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($vars, $file, $context) = @$args{qw(vars file context)}; + my ($vars, $file, $context) = @$args{qw(vars file context)}; - if ($file eq 'bug/edit.html.tmpl') { - $vars->{'viewing_the_bug_form'} = 1; - } + if ($file eq 'bug/edit.html.tmpl') { + $vars->{'viewing_the_bug_form'} = 1; + } } sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{current_tab}; - my $save = $args->{save_changes}; - my $handled = $args->{handled}; + my ($self, $args) = @_; + my $tab = $args->{current_tab}; + my $save = $args->{save_changes}; + my $handled = $args->{handled}; - return unless $tab eq 'my_tab'; + return unless $tab eq 'my_tab'; - my $value = Bugzilla->input_params->{'example_pref'}; - if ($save) { - # Validate your data and update the DB accordingly. - $value =~ s/\s+/:/g; - } - $args->{'vars'}->{example_pref} = $value; + my $value = Bugzilla->input_params->{'example_pref'}; + if ($save) { + + # Validate your data and update the DB accordingly. + $value =~ s/\s+/:/g; + } + $args->{'vars'}->{example_pref} = $value; - # Set the 'handled' scalar reference to true so that the caller - # knows the panel name is valid and that an extension took care of it. - $$handled = 1; + # Set the 'handled' scalar reference to true so that the caller + # knows the panel name is valid and that an extension took care of it. + $$handled = 1; } sub webservice { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Example} = "Bugzilla::Extension::Example::WebService"; + my $dispatch = $args->{dispatch}; + $dispatch->{Example} = "Bugzilla::Extension::Example::WebService"; } sub webservice_error_codes { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $error_map = $args->{error_map}; - $error_map->{'example_my_error'} = 10001; + my $error_map = $args->{error_map}; + $error_map->{'example_my_error'} = 10001; } sub webservice_status_code_map { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $status_code_map = $args->{status_code_map}; - my $status_code_map = $args->{status_code_map}; - # Uncomment this line to override the status code for the - # error 'object_does_not_exist' to STATUS_BAD_REQUEST - #$status_code_map->{51} = STATUS_BAD_REQUEST; + # Uncomment this line to override the status code for the + # error 'object_does_not_exist' to STATUS_BAD_REQUEST + #$status_code_map->{51} = STATUS_BAD_REQUEST; } sub webservice_before_call { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $method = $args->{method}; - my $full_method = $args->{full_method}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $method = $args->{method}; + my $full_method = $args->{full_method}; - # Uncomment this line to see a line in your webserver's error log whenever - # a webservice call is made - #warn "RPC call $full_method made by ", Bugzilla->user->login, "\n"; + # Uncomment this line to see a line in your webserver's error log whenever + # a webservice call is made + #warn "RPC call $full_method made by ", Bugzilla->user->login, "\n"; } sub webservice_fix_credentials { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $params = $args->{'params'}; - # Allow user to pass in username=foo&password=bar - if (exists $params->{'username'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = $params->{'username'}; - $params->{'Bugzilla_password'} = $params->{'password'}; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $params = $args->{'params'}; + + # Allow user to pass in username=foo&password=bar + if (exists $params->{'username'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = $params->{'username'}; + $params->{'Bugzilla_password'} = $params->{'password'}; + } } sub webservice_rest_request { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $params = $args->{'params'}; - # Internally we may have a field called 'cf_test_field' but we allow users - # to use the shorter 'test_field' name. - if (exists $params->{'test_field'}) { - $params->{'test_field'} = delete $params->{'cf_test_field'}; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $params = $args->{'params'}; + + # Internally we may have a field called 'cf_test_field' but we allow users + # to use the shorter 'test_field' name. + if (exists $params->{'test_field'}) { + $params->{'test_field'} = delete $params->{'cf_test_field'}; + } } sub webservice_rest_resources { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $resources = $args->{'resources'}; - # Add a new resource that allows for /rest/example/hello - # to call Example.hello - $resources->{'Bugzilla::Extension::Example::WebService'} = [ - qr{^/example/hello$}, { - GET => { - method => 'hello', - } - } - ]; + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $resources = $args->{'resources'}; + + # Add a new resource that allows for /rest/example/hello + # to call Example.hello + $resources->{'Bugzilla::Extension::Example::WebService'} + = [qr{^/example/hello$}, {GET => {method => 'hello',}}]; } sub webservice_rest_response { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $result = $args->{'result'}; - my $response = $args->{'response'}; - # Convert a list of bug hashes to a single bug hash if only one is - # being returned. - if (ref $$result eq 'HASH' - && exists $$result->{'bugs'} - && scalar @{ $$result->{'bugs'} } == 1) - { - $$result = $$result->{'bugs'}->[0]; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $result = $args->{'result'}; + my $response = $args->{'response'}; + + # Convert a list of bug hashes to a single bug hash if only one is + # being returned. + if ( ref $$result eq 'HASH' + && exists $$result->{'bugs'} + && scalar @{$$result->{'bugs'}} == 1) + { + $$result = $$result->{'bugs'}->[0]; + } } # This must be the last line of your extension. diff --git a/extensions/Example/lib/Auth/Login.pm b/extensions/Example/lib/Auth/Login.pm index 376fe21a8..10be62f85 100644 --- a/extensions/Example/lib/Auth/Login.pm +++ b/extensions/Example/lib/Auth/Login.pm @@ -15,7 +15,7 @@ use Bugzilla::Constants; # Always returns no data. sub get_login_info { - return { failure => AUTH_NODATA }; + return {failure => AUTH_NODATA}; } 1; diff --git a/extensions/Example/lib/Auth/Verify.pm b/extensions/Example/lib/Auth/Verify.pm index cac6d1019..4170b93c3 100644 --- a/extensions/Example/lib/Auth/Verify.pm +++ b/extensions/Example/lib/Auth/Verify.pm @@ -14,7 +14,7 @@ use Bugzilla::Constants; # A verifier that always fails. sub check_credentials { - return { failure => AUTH_NO_SUCH_USER }; + return {failure => AUTH_NO_SUCH_USER}; } 1; diff --git a/extensions/Example/lib/Config.pm b/extensions/Example/lib/Config.pm index fac0046af..360a57510 100644 --- a/extensions/Example/lib/Config.pm +++ b/extensions/Example/lib/Config.pm @@ -16,16 +16,11 @@ use Bugzilla::Config::Common; our $sortkey = 5000; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'example_string', - type => 't', - default => 'EXAMPLE', - }, - ); - return @param_list; + my @param_list + = ({name => 'example_string', type => 't', default => 'EXAMPLE',},); + return @param_list; } 1; diff --git a/extensions/Example/lib/WebService.pm b/extensions/Example/lib/WebService.pm index 7b1940462..f50c6e6cb 100644 --- a/extensions/Example/lib/WebService.pm +++ b/extensions/Example/lib/WebService.pm @@ -14,8 +14,8 @@ use base qw(Bugzilla::WebService); use Bugzilla::Error; use constant PUBLIC_METHODS => qw( - hello - throw_an_error + hello + throw_an_error ); # This can be called as Example.hello() from the WebService. diff --git a/extensions/Example/template/en/default/setup/strings.txt.pl b/extensions/Example/template/en/default/setup/strings.txt.pl index 7b19a9f4d..6ee4b5fdc 100644 --- a/extensions/Example/template/en/default/setup/strings.txt.pl +++ b/extensions/Example/template/en/default/setup/strings.txt.pl @@ -17,8 +17,6 @@ # Contributor(s): # Max Kanat-Alexander -%strings = ( - feature_example_acme => 'Example Extension: Acme Feature', -); +%strings = (feature_example_acme => 'Example Extension: Acme Feature',); 1; diff --git a/extensions/FlagDefaultRequestee/Extension.pm b/extensions/FlagDefaultRequestee/Extension.pm index b534f9904..df23d9cc4 100644 --- a/extensions/FlagDefaultRequestee/Extension.pm +++ b/extensions/FlagDefaultRequestee/Extension.pm @@ -27,14 +27,16 @@ our $VERSION = '1'; ################ sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('flagtypes', 'default_requestee', { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL' } - }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'flagtypes', + 'default_requestee', + { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + } + ); } ############# @@ -42,51 +44,51 @@ sub install_update_db { ############# sub template_before_process { - my ($self, $args) = @_; - return unless Bugzilla->user->id; - my ($vars, $file) = @$args{qw(vars file)}; - return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES; - - my $flag_types = []; - if (exists $vars->{bug} || exists $vars->{attachment}) { - my $bug; - if (exists $vars->{bug}) { - $bug = $vars->{'bug'}; - } elsif (exists $vars->{'attachment'}) { - $bug = $vars->{'attachment'}->{bug}; - } - - $flag_types = Bugzilla::FlagType::match({ - 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id, - 'bug_id' => $bug->id, - 'active_or_has_flags' => $bug->id, - }); - - $vars->{flag_currently_requested} ||= {}; - foreach my $type (@$flag_types) { - my $flags = Bugzilla::Flag->match({ - type_id => $type->id, - bug_id => $bug->id, - status => '?' - }); - map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags; - } + my ($self, $args) = @_; + return unless Bugzilla->user->id; + my ($vars, $file) = @$args{qw(vars file)}; + return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES; + + my $flag_types = []; + if (exists $vars->{bug} || exists $vars->{attachment}) { + my $bug; + if (exists $vars->{bug}) { + $bug = $vars->{'bug'}; } - elsif ($file =~ /^bug\/create/ && exists $vars->{product}) { - my $bug_flags = $vars->{product}->flag_types->{bug}; - my $attachment_flags = $vars->{product}->flag_types->{attachment}; - $flag_types = [ map { $_ } (@$bug_flags, @$attachment_flags) ]; + elsif (exists $vars->{'attachment'}) { + $bug = $vars->{'attachment'}->{bug}; } - return if !@$flag_types; + $flag_types = Bugzilla::FlagType::match({ + 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id, + 'bug_id' => $bug->id, + 'active_or_has_flags' => $bug->id, + }); - $vars->{flag_default_requestees} ||= {}; + $vars->{flag_currently_requested} ||= {}; foreach my $type (@$flag_types) { - next if !$type->default_requestee; - $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login; + my $flags + = Bugzilla::Flag->match({ + type_id => $type->id, bug_id => $bug->id, status => '?' + }); + map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags; } + } + elsif ($file =~ /^bug\/create/ && exists $vars->{product}) { + my $bug_flags = $vars->{product}->flag_types->{bug}; + my $attachment_flags = $vars->{product}->flag_types->{attachment}; + $flag_types = [map {$_} (@$bug_flags, @$attachment_flags)]; + } + + return if !@$flag_types; + + $vars->{flag_default_requestees} ||= {}; + foreach my $type (@$flag_types) { + next if !$type->default_requestee; + $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login; + } } ################## @@ -94,83 +96,79 @@ sub template_before_process { ################## BEGIN { - *Bugzilla::FlagType::default_requestee = \&_default_requestee; + *Bugzilla::FlagType::default_requestee = \&_default_requestee; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::FlagType')) { - push(@$columns, 'default_requestee'); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::FlagType')) { + push(@$columns, 'default_requestee'); + } } sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::FlagType'); + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::FlagType'); - my $columns = $args->{columns}; - push(@$columns, 'default_requestee'); + my $columns = $args->{columns}; + push(@$columns, 'default_requestee'); - # editflagtypes.cgi doesn't call set_all, so we have to do this here - my $input = Bugzilla->input_params; - $object->set('default_requestee', $input->{default_requestee}) - if exists $input->{default_requestee}; + # editflagtypes.cgi doesn't call set_all, so we have to do this here + my $input = Bugzilla->input_params; + $object->set('default_requestee', $input->{default_requestee}) + if exists $input->{default_requestee}; } sub object_validators { - my ($self, $args) = @_; - my $class = $args->{class}; - return unless $class->isa('Bugzilla::FlagType'); + my ($self, $args) = @_; + my $class = $args->{class}; + return unless $class->isa('Bugzilla::FlagType'); - my $validators = $args->{validators}; - $validators->{default_requestee} = \&_check_default_requestee; + my $validators = $args->{validators}; + $validators->{default_requestee} = \&_check_default_requestee; } sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{class}; - return unless $class->isa('Bugzilla::FlagType'); - - my $params = $args->{params}; - my $input = Bugzilla->input_params; - $params->{default_requestee} = $input->{default_requestee} - if exists $params->{default_requestee}; + my ($self, $args) = @_; + my $class = $args->{class}; + return unless $class->isa('Bugzilla::FlagType'); + + my $params = $args->{params}; + my $input = Bugzilla->input_params; + $params->{default_requestee} = $input->{default_requestee} + if exists $params->{default_requestee}; } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::FlagType'); - - my $old_object = $args->{old_object}; - my $changes = $args->{changes}; - my $old_id = $old_object->default_requestee - ? $old_object->default_requestee->id - : 0; - my $new_id = $object->default_requestee - ? $object->default_requestee->id - : 0; - return if $old_id == $new_id; - - $changes->{default_requestee} = [ $old_id, $new_id ]; + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::FlagType'); + + my $old_object = $args->{old_object}; + my $changes = $args->{changes}; + my $old_id + = $old_object->default_requestee ? $old_object->default_requestee->id : 0; + my $new_id = $object->default_requestee ? $object->default_requestee->id : 0; + return if $old_id == $new_id; + + $changes->{default_requestee} = [$old_id, $new_id]; } sub _check_default_requestee { - my ($self, $value, $field) = @_; - $value = trim($value // ''); - return undef if $value eq ''; - ThrowUserError("flag_default_requestee_review") - if $self->name eq 'review'; - return Bugzilla::User->check($value)->id; + my ($self, $value, $field) = @_; + $value = trim($value // ''); + return undef if $value eq ''; + ThrowUserError("flag_default_requestee_review") if $self->name eq 'review'; + return Bugzilla::User->check($value)->id; } sub _default_requestee { - my ($self) = @_; - return $self->{default_requestee} - ? Bugzilla::User->new({ id => $self->{default_requestee}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{default_requestee} + ? Bugzilla::User->new({id => $self->{default_requestee}, cache => 1}) + : undef; } __PACKAGE__->NAME; diff --git a/extensions/FlagDefaultRequestee/lib/Constants.pm b/extensions/FlagDefaultRequestee/lib/Constants.pm index 2c2cdf35c..26d3e0d9e 100644 --- a/extensions/FlagDefaultRequestee/lib/Constants.pm +++ b/extensions/FlagDefaultRequestee/lib/Constants.pm @@ -14,14 +14,12 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAGTYPE_TEMPLATES + FLAGTYPE_TEMPLATES ); use constant FLAGTYPE_TEMPLATES => ( - "attachment/edit.html.tmpl", - "attachment/createformcontents.html.tmpl", - "bug/edit.html.tmpl", - "bug/create/create.html.tmpl" + "attachment/edit.html.tmpl", "attachment/createformcontents.html.tmpl", + "bug/edit.html.tmpl", "bug/create/create.html.tmpl" ); 1; diff --git a/extensions/FlagTypeComment/Extension.pm b/extensions/FlagTypeComment/Extension.pm index e7b34113d..e26b08b27 100644 --- a/extensions/FlagTypeComment/Extension.pm +++ b/extensions/FlagTypeComment/Extension.pm @@ -39,31 +39,19 @@ our $VERSION = '1'; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'flagtype_comments'} = { - FIELDS => [ - type_id => { - TYPE => 'SMALLINT(6)', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - on_status => { - TYPE => 'CHAR(1)', - NOTNULL => 1 - }, - comment => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1 - }, - ], - INDEXES => [ - flagtype_comments_idx => ['type_id'], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'flagtype_comments'} = { + FIELDS => [ + type_id => { + TYPE => 'SMALLINT(6)', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + on_status => {TYPE => 'CHAR(1)', NOTNULL => 1}, + comment => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [flagtype_comments_idx => ['type_id'],], + }; } ############# @@ -71,83 +59,89 @@ sub db_schema_abstract_schema { ############# sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; - return unless Bugzilla->user->id; - if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) { - _set_ftc_states($file, $vars); - } + return unless Bugzilla->user->id; + if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) { + _set_ftc_states($file, $vars); + } } sub _set_ftc_states { - my ($file, $vars) = @_; - my $dbh = Bugzilla->dbh; - - my $ftc_flags; - my $db_result; - if ($file =~ /^admin\//) { - # admin - my $type = $vars->{'type'} || return; - my ($target_type, $id); - if (blessed($type)) { - ($target_type, $id) = ($type->target_type, $type->id); - } else { - ($target_type, $id) = ($type->{target_type}, $type->{id}); - trick_taint($id) if $id; - } - if ($target_type eq 'bug') { - return unless FLAGTYPE_COMMENT_BUG_FLAGS; - } else { - return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS; - } - if ($id) { - $db_result = $dbh->selectall_arrayref( - "SELECT type_id AS flagtype, on_status AS state, comment AS text + my ($file, $vars) = @_; + my $dbh = Bugzilla->dbh; + + my $ftc_flags; + my $db_result; + if ($file =~ /^admin\//) { + + # admin + my $type = $vars->{'type'} || return; + my ($target_type, $id); + if (blessed($type)) { + ($target_type, $id) = ($type->target_type, $type->id); + } + else { + ($target_type, $id) = ($type->{target_type}, $type->{id}); + trick_taint($id) if $id; + } + if ($target_type eq 'bug') { + return unless FLAGTYPE_COMMENT_BUG_FLAGS; + } + else { + return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS; + } + if ($id) { + $db_result = $dbh->selectall_arrayref( + "SELECT type_id AS flagtype, on_status AS state, comment AS text FROM flagtype_comments - WHERE type_id = ?", - { Slice => {} }, $id); - } - } else { - # creating/editing attachment / viewing bug - my $bug; - if (exists $vars->{'bug'}) { - $bug = $vars->{'bug'}; - } elsif (exists $vars->{'attachment'}) { - $bug = $vars->{'attachment'}->{bug}; - } else { - return; - } - - my $flag_types = Bugzilla::FlagType::match({ - 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id, - 'bug_id' => $bug->id, - 'active_or_has_flags' => $bug->id, - }); - - if (@$flag_types) { - my $types = join(',', map { $_->id } @$flag_types); - my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'"; - $db_result = $dbh->selectall_arrayref( - "SELECT type_id AS flagtype, on_status AS state, comment AS text - FROM flagtype_comments - WHERE type_id IN ($types) AND on_status IN ($states)", - { Slice => {} }); - } - else { - $db_result = []; - } + WHERE type_id = ?", {Slice => {}}, $id + ); + } + } + else { + # creating/editing attachment / viewing bug + my $bug; + if (exists $vars->{'bug'}) { + $bug = $vars->{'bug'}; + } + elsif (exists $vars->{'attachment'}) { + $bug = $vars->{'attachment'}->{bug}; + } + else { + return; } - foreach my $row (@$db_result) { - $ftc_flags->{$row->{'flagtype'}} ||= {}; - $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text}; + my $flag_types = Bugzilla::FlagType::match({ + 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id, + 'bug_id' => $bug->id, + 'active_or_has_flags' => $bug->id, + }); + + if (@$flag_types) { + my $types = join(',', map { $_->id } @$flag_types); + my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'"; + $db_result = $dbh->selectall_arrayref( + "SELECT type_id AS flagtype, on_status AS state, comment AS text + FROM flagtype_comments + WHERE type_id IN ($types) AND on_status IN ($states)", {Slice => {}} + ); + } + else { + $db_result = []; } + } - $vars->{'ftc_states'} = [ FLAGTYPE_COMMENT_STATES ]; - $vars->{'ftc_flags'} = $ftc_flags; + foreach my $row (@$db_result) { + $ftc_flags->{$row->{'flagtype'}} ||= {}; + $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text}; + } + + $vars->{'ftc_states'} = [FLAGTYPE_COMMENT_STATES]; + $vars->{'ftc_flags'} = $ftc_flags; } ######### @@ -155,55 +149,55 @@ sub _set_ftc_states { ######### sub flagtype_end_of_create { - my ($self, $args) = @_; - _set_flagtypes($args->{type}); + my ($self, $args) = @_; + _set_flagtypes($args->{type}); } sub flagtype_end_of_update { - my ($self, $args) = @_; - _set_flagtypes($args->{type}); + my ($self, $args) = @_; + _set_flagtypes($args->{type}); } sub _set_flagtypes { - my $flag_type = shift; - my $flagtype_id = $flag_type->id; - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - foreach my $state (FLAGTYPE_COMMENT_STATES) { - next if (!defined $input->{"ftc_${flagtype_id}_$state"} - && !defined $input->{"ftc_new_$state"}); - - my $text = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || ''; - $text =~ s/\r\n/\n/g; - trick_taint($text); - - if ($text ne '') { - if ($dbh->selectrow_array( - "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?", - undef, - $flagtype_id, $state) - ) { - $dbh->do( - "UPDATE flagtype_comments SET comment=? - WHERE type_id=? AND on_status=?", - undef, - $text, $flagtype_id, $state); - } else { - $dbh->do( - "INSERT INTO flagtype_comments(type_id, on_status, comment) - VALUES (?, ?, ?)", - undef, - $flagtype_id, $state, $text); - } - - } else { - $dbh->do( - "DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?", - undef, - $flagtype_id, $state); - } + my $flag_type = shift; + my $flagtype_id = $flag_type->id; + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + foreach my $state (FLAGTYPE_COMMENT_STATES) { + next + if (!defined $input->{"ftc_${flagtype_id}_$state"} + && !defined $input->{"ftc_new_$state"}); + + my $text + = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || ''; + $text =~ s/\r\n/\n/g; + trick_taint($text); + + if ($text ne '') { + if ($dbh->selectrow_array( + "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?", undef, + $flagtype_id, $state + )) + { + $dbh->do( + "UPDATE flagtype_comments SET comment=? + WHERE type_id=? AND on_status=?", undef, $text, $flagtype_id, $state + ); + } + else { + $dbh->do( + "INSERT INTO flagtype_comments(type_id, on_status, comment) + VALUES (?, ?, ?)", undef, $flagtype_id, $state, $text + ); + } + + } + else { + $dbh->do("DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?", + undef, $flagtype_id, $state); } + } } __PACKAGE__->NAME; diff --git a/extensions/FlagTypeComment/lib/Constants.pm b/extensions/FlagTypeComment/lib/Constants.pm index d6242b78b..ee3a54076 100644 --- a/extensions/FlagTypeComment/lib/Constants.pm +++ b/extensions/FlagTypeComment/lib/Constants.pm @@ -26,28 +26,26 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAGTYPE_COMMENT_TEMPLATES - FLAGTYPE_COMMENT_STATES - FLAGTYPE_COMMENT_BUG_FLAGS - FLAGTYPE_COMMENT_ATTACHMENT_FLAGS + FLAGTYPE_COMMENT_TEMPLATES + FLAGTYPE_COMMENT_STATES + FLAGTYPE_COMMENT_BUG_FLAGS + FLAGTYPE_COMMENT_ATTACHMENT_FLAGS ); -use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-"); -use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0; +use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-"); +use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0; use constant FLAGTYPE_COMMENT_ATTACHMENT_FLAGS => 1; sub FLAGTYPE_COMMENT_TEMPLATES { - my @result = ("admin/flag-type/edit.html.tmpl"); - if (FLAGTYPE_COMMENT_BUG_FLAGS) { - push @result, ("bug/comments.html.tmpl"); - } - if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) { - push @result, ( - "attachment/edit.html.tmpl", - "attachment/createformcontents.html.tmpl", - ); - } - return @result; + my @result = ("admin/flag-type/edit.html.tmpl"); + if (FLAGTYPE_COMMENT_BUG_FLAGS) { + push @result, ("bug/comments.html.tmpl"); + } + if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) { + push @result, + ("attachment/edit.html.tmpl", "attachment/createformcontents.html.tmpl",); + } + return @result; } 1; diff --git a/extensions/GitHubAuth/Extension.pm b/extensions/GitHubAuth/Extension.pm index d0d9f42f1..85d81b02a 100644 --- a/extensions/GitHubAuth/Extension.pm +++ b/extensions/GitHubAuth/Extension.pm @@ -24,69 +24,73 @@ use URI::QueryParam; our $VERSION = '0.01'; BEGIN { - # Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called. - # Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins. +# Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called. +# Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins. - *Bugzilla::Auth::Login::CGI::can = sub { - my ($stack, $method) = @_; + *Bugzilla::Auth::Login::CGI::can = sub { + my ($stack, $method) = @_; - return undef if $method eq 'fail_nodata'; - return $stack->SUPER::can($method); - }; + return undef if $method eq 'fail_nodata'; + return $stack->SUPER::can($method); + }; } sub install_before_final_checks { - Bugzilla::Group->create({ - name => 'no-github-auth', - description => 'Group containing groups whose members may not use GitHubAuth to log in', - isbuggroup => 0, - }) unless Bugzilla::Group->new({ name => 'no-github-auth' }); + Bugzilla::Group->create({ + name => 'no-github-auth', + description => + 'Group containing groups whose members may not use GitHubAuth to log in', + isbuggroup => 0, + }) + unless Bugzilla::Group->new({name => 'no-github-auth'}); } sub attachment_should_redirect_login { - my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $cgi = Bugzilla->cgi; - if ($cgi->param('github_state') || $cgi->param('github_email')) { - ${$args->{do_redirect}} = 1; - } + if ($cgi->param('github_state') || $cgi->param('github_email')) { + ${$args->{do_redirect}} = 1; + } } sub auth_login_methods { - my ($self, $args) = @_; - my $modules = $args->{'modules'}; - if (exists $modules->{'GitHubAuth'}) { - $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{'modules'}; + if (exists $modules->{'GitHubAuth'}) { + $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm'; + } } sub auth_verify_methods { - my ($self, $args) = @_; - my $modules = $args->{'modules'}; - if (exists $modules->{'GitHubAuth'}) { - $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{'modules'}; + if (exists $modules->{'GitHubAuth'}) { + $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm'; + } } sub config_modify_panels { - my ($self, $args) = @_; - my $auth_panel_params = $args->{panels}{auth}{params}; - - my $user_info_class = first { $_->{name} eq 'user_info_class' } @$auth_panel_params; - if ($user_info_class) { - push @{ $user_info_class->{choices} }, "GitHubAuth,CGI"; - } - - my $user_verify_class = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params; - if ($user_verify_class) { - unshift @{ $user_verify_class->{choices} }, "GitHubAuth"; - } + my ($self, $args) = @_; + my $auth_panel_params = $args->{panels}{auth}{params}; + + my $user_info_class + = first { $_->{name} eq 'user_info_class' } @$auth_panel_params; + if ($user_info_class) { + push @{$user_info_class->{choices}}, "GitHubAuth,CGI"; + } + + my $user_verify_class + = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params; + if ($user_verify_class) { + unshift @{$user_verify_class->{choices}}, "GitHubAuth"; + } } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config"; } __PACKAGE__->NAME; diff --git a/extensions/GitHubAuth/lib/Client.pm b/extensions/GitHubAuth/lib/Client.pm index 291501961..328bab48f 100644 --- a/extensions/GitHubAuth/lib/Client.pm +++ b/extensions/GitHubAuth/lib/Client.pm @@ -16,7 +16,8 @@ use URI; use URI::QueryParam; use Digest; -use Bugzilla::Extension::GitHubAuth::Client::Error qw(ThrowUserError ThrowCodeError); +use Bugzilla::Extension::GitHubAuth::Client::Error + qw(ThrowUserError ThrowCodeError); use Bugzilla::Util qw(remote_ip); use constant DIGEST_HASH => 'SHA1'; @@ -24,107 +25,108 @@ use constant DIGEST_HASH => 'SHA1'; use fields qw(user_agent); use constant { - GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token', - GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize', - GH_USER_EMAILS_URI => 'https://api.github.com/user/emails', + GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token', + GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize', + GH_USER_EMAILS_URI => 'https://api.github.com/user/emails', }; sub new { - my ($class, %init) = @_; - my $self = $class->fields::new(); + my ($class, %init) = @_; + my $self = $class->fields::new(); - return $self; + return $self; } sub login_uri { - my ($class, $target_uri) = @_; + my ($class, $target_uri) = @_; - my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); - $uri->query_form(target_uri => $target_uri); - return $uri; + my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); + $uri->query_form(target_uri => $target_uri); + return $uri; } sub authorize_uri { - my ($class, $state) = @_; + my ($class, $state) = @_; - my $uri = URI->new(GH_AUTHORIZE_URI); - $uri->query_form( - client_id => Bugzilla->params->{github_client_id}, - scope => 'user:email', - state => $state, - redirect_uri => Bugzilla->localconfig->{urlbase} . "github.cgi", - ); + my $uri = URI->new(GH_AUTHORIZE_URI); + $uri->query_form( + client_id => Bugzilla->params->{github_client_id}, + scope => 'user:email', + state => $state, + redirect_uri => Bugzilla->localconfig->{urlbase} . "github.cgi", + ); - return $uri; + return $uri; } sub get_email_key { - my ($class, $email) = @_; - - my $cgi = Bugzilla->cgi; - my $digest = Digest->new(DIGEST_HASH); - $digest->add($email); - $digest->add(remote_ip()); - $digest->add($cgi->cookie('Bugzilla_github_token') // Bugzilla->request_cache->{github_token} // ''); - $digest->add(Bugzilla->localconfig->{site_wide_secret}); - return $digest->hexdigest; + my ($class, $email) = @_; + + my $cgi = Bugzilla->cgi; + my $digest = Digest->new(DIGEST_HASH); + $digest->add($email); + $digest->add(remote_ip()); + $digest->add($cgi->cookie('Bugzilla_github_token') + // Bugzilla->request_cache->{github_token} // ''); + $digest->add(Bugzilla->localconfig->{site_wide_secret}); + return $digest->hexdigest; } sub _handle_response { - my ($self, $response) = @_; - my $data = eval { - decode_json($response->content); - }; - if ($@) { - ThrowCodeError("github_bad_response", { message => "Unable to parse json response" }); - } - - unless ($response->is_success) { - ThrowCodeError("github_error", { response => $response }); - } - return $data; + my ($self, $response) = @_; + my $data = eval { decode_json($response->content); }; + if ($@) { + ThrowCodeError("github_bad_response", + {message => "Unable to parse json response"}); + } + + unless ($response->is_success) { + ThrowCodeError("github_error", {response => $response}); + } + return $data; } sub get_access_token { - my ($self, $code) = @_; - - my $response = $self->user_agent->post( - GH_ACCESS_TOKEN_URI, - { client_id => Bugzilla->params->{github_client_id}, - client_secret => Bugzilla->params->{github_client_secret}, - code => $code }, - Accept => 'application/json', - ); - my $data = $self->_handle_response($response); - return $data->{access_token} if exists $data->{access_token}; + my ($self, $code) = @_; + + my $response = $self->user_agent->post(GH_ACCESS_TOKEN_URI, + { + client_id => Bugzilla->params->{github_client_id}, + client_secret => Bugzilla->params->{github_client_secret}, + code => $code + }, + Accept => 'application/json', + ); + my $data = $self->_handle_response($response); + return $data->{access_token} if exists $data->{access_token}; } sub get_user_emails { - my ($self, $access_token) = @_; - my $uri = URI->new(GH_USER_EMAILS_URI); - $uri->query_form(access_token => $access_token); + my ($self, $access_token) = @_; + my $uri = URI->new(GH_USER_EMAILS_URI); + $uri->query_form(access_token => $access_token); - my $response = $self->user_agent->get($uri, Accept => 'application/json'); + my $response = $self->user_agent->get($uri, Accept => 'application/json'); - return $self->_handle_response($response); + return $self->_handle_response($response); } sub user_agent { - my ($self) = @_; - $self->{user_agent} //= $self->_build_user_agent; + my ($self) = @_; + $self->{user_agent} //= $self->_build_user_agent; - return $self->{user_agent}; + return $self->{user_agent}; } sub _build_user_agent { - my ($self) = @_; - my $ua = LWP::UserAgent->new( timeout => 10 ); + my ($self) = @_; + my $ua = LWP::UserAgent->new(timeout => 10); - if (Bugzilla->params->{proxy_url}) { - $ua->proxy('https', Bugzilla->params->{proxy_url}); - } + if (Bugzilla->params->{proxy_url}) { + $ua->proxy('https', Bugzilla->params->{proxy_url}); + } - return $ua; + return $ua; } 1; diff --git a/extensions/GitHubAuth/lib/Client/Error.pm b/extensions/GitHubAuth/lib/Client/Error.pm index adb6ec07b..00e8415d1 100644 --- a/extensions/GitHubAuth/lib/Client/Error.pm +++ b/extensions/GitHubAuth/lib/Client/Error.pm @@ -16,39 +16,39 @@ use Bugzilla::Error (); use base qw(Exporter); use fields qw(type error vars); -our @EXPORT = qw(ThrowUserError ThrowCodeError); +our @EXPORT = qw(ThrowUserError ThrowCodeError); our $USE_EXCEPTION_OBJECTS = 0; sub _new { - my ($class, $type, $error, $vars) = @_; - my $self = $class->fields::new(); - $self->{type} = $type; - $self->{error} = $error; - $self->{vars} = $vars // {}; + my ($class, $type, $error, $vars) = @_; + my $self = $class->fields::new(); + $self->{type} = $type; + $self->{error} = $error; + $self->{vars} = $vars // {}; - return $self; + return $self; } -sub type { $_[0]->{type} } +sub type { $_[0]->{type} } sub error { $_[0]->{error} } -sub vars { $_[0]->{vars} } +sub vars { $_[0]->{vars} } sub ThrowUserError { - if ($USE_EXCEPTION_OBJECTS) { - die __PACKAGE__->_new('user', @_); - } - else { - Bugzilla::Error::ThrowUserError(@_); - } + if ($USE_EXCEPTION_OBJECTS) { + die __PACKAGE__->_new('user', @_); + } + else { + Bugzilla::Error::ThrowUserError(@_); + } } sub ThrowCodeError { - if ($USE_EXCEPTION_OBJECTS) { - die __PACKAGE__->_new('code', @_); - } - else { - Bugzilla::Error::ThrowCodeError(@_); - } + if ($USE_EXCEPTION_OBJECTS) { + die __PACKAGE__->_new('code', @_); + } + else { + Bugzilla::Error::ThrowCodeError(@_); + } } 1; diff --git a/extensions/GitHubAuth/lib/Config.pm b/extensions/GitHubAuth/lib/Config.pm index 0c8874129..b718b4be5 100644 --- a/extensions/GitHubAuth/lib/Config.pm +++ b/extensions/GitHubAuth/lib/Config.pm @@ -16,22 +16,14 @@ use Bugzilla::Config::Common; our $sortkey = 1350; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'github_client_id', - type => 't', - default => '', - }, - { - name => 'github_client_secret', - type => 't', - default => '', - }, - ); - - return @params; + my ($class) = @_; + + my @params = ( + {name => 'github_client_id', type => 't', default => '',}, + {name => 'github_client_secret', type => 't', default => '',}, + ); + + return @params; } 1; diff --git a/extensions/GitHubAuth/lib/Login.pm b/extensions/GitHubAuth/lib/Login.pm index 073fbfeea..df3195bc7 100644 --- a/extensions/GitHubAuth/lib/Login.pm +++ b/extensions/GitHubAuth/lib/Login.pm @@ -24,187 +24,208 @@ use Bugzilla::Extension::GitHubAuth::Client; use Bugzilla::Extension::GitHubAuth::Client::Error (); use Bugzilla::Error; -use constant { requires_verification => 1, - is_automatic => 1, - user_can_create_account => 1 }; +use constant {requires_verification => 1, is_automatic => 1, + user_can_create_account => 1}; sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $github_action = Bugzilla->request_cache->{github_action}; - - return { failure => AUTH_NODATA } unless $github_action; - - my $response; - if ($github_action eq 'login') { - $response = $self->_get_login_info_from_github(); - } - elsif ($github_action eq 'email') { - $response = $self->_get_login_info_from_email(); - } - - if (!exists $response->{failure}) { - if (exists $response->{user}) { - # existing account - my $user = $response->{user}; - return { failure => AUTH_ERROR, - user_error => 'github_auth_account_too_powerful' } if $user->in_group('no-github-auth'); - return { failure => AUTH_ERROR, - user_error => 'mfa_prevents_login', - details => { provider => 'GitHub' } } if $user->mfa; - $response = { - username => $user->login, - user_id => $user->id, - github_auth => 1, - }; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $github_action = Bugzilla->request_cache->{github_action}; + + return {failure => AUTH_NODATA} unless $github_action; + + my $response; + if ($github_action eq 'login') { + $response = $self->_get_login_info_from_github(); + } + elsif ($github_action eq 'email') { + $response = $self->_get_login_info_from_email(); + } + + if (!exists $response->{failure}) { + if (exists $response->{user}) { + + # existing account + my $user = $response->{user}; + return { + failure => AUTH_ERROR, + user_error => 'github_auth_account_too_powerful' } - else { - # new account - my $email = $response->{email}; - $response = { - username => $email, - github_auth => 1, - }; + if $user->in_group('no-github-auth'); + return { + failure => AUTH_ERROR, + user_error => 'mfa_prevents_login', + details => {provider => 'GitHub'} } + if $user->mfa; + $response = {username => $user->login, user_id => $user->id, github_auth => 1,}; } - return $response; + else { + # new account + my $email = $response->{email}; + $response = {username => $email, github_auth => 1,}; + } + } + return $response; } sub _get_login_info_from_github { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - my $code = $cgi->param('code'); - - return { failure => AUTH_ERROR, error => 'github_missing_code' } unless $code; - - trick_taint($code); - - my $client = Bugzilla::Extension::GitHubAuth::Client->new; - - my ($access_token, $emails); - eval { - # The following variable lets us catch and return (rather than throw) errors - # from our github client code, as required by the Auth API. - local $Bugzilla::Extension::GitHubAuth::Client::Error::USE_EXCEPTION_OBJECTS = 1; - $access_token = $client->get_access_token($code); - $emails = $client->get_user_emails($access_token); - }; - my $e = $@; - if (blessed $e && $e->isa('Bugzilla::Extension::GitHubAuth::Client::Error')) { - my $key = $e->type eq 'user' ? 'user_error' : 'error'; - return { failure => AUTH_ERROR, $key => $e->error, details => $e->vars }; - } - elsif ($e) { - die $e; - } - - my @emails = map { $_->{email} } - grep { $_->{verified} && $_->{email} !~ /\@users\.noreply\.github\.com$/ } @$emails; - - my @bugzilla_users; - my @github_emails; - foreach my $email (@emails) { - my $user = Bugzilla::User->new({name => $email, cache => 1}); - if ($user) { - push @bugzilla_users, $user; - } - else { - push @github_emails, $email; - } - } - my @allowed_bugzilla_users = grep { not $_->in_group('no-github-auth') } @bugzilla_users; - - if (@allowed_bugzilla_users == 1) { - my ($user) = @allowed_bugzilla_users; - return { user => $user }; - } - elsif (@allowed_bugzilla_users > 1) { - $self->{github_failure} = { - template => 'account/auth/github-verify-account.html.tmpl', - vars => { - bugzilla_users => \@allowed_bugzilla_users, - choose_email => _mk_choose_email(\@emails), - }, - }; - return { failure => AUTH_NODATA }; - } - elsif (@allowed_bugzilla_users == 0 && @bugzilla_users > 0 && @github_emails == 0) { - return { failure => AUTH_ERROR, - user_error => 'github_auth_account_too_powerful' }; - } - elsif (@github_emails) { - $self->{github_failure} = { - template => 'account/auth/github-verify-account.html.tmpl', - vars => { - github_emails => \@github_emails, - choose_email => _mk_choose_email(\@emails), - }, - }; - return { failure => AUTH_NODATA }; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + my $code = $cgi->param('code'); + + return {failure => AUTH_ERROR, error => 'github_missing_code'} unless $code; + + trick_taint($code); + + my $client = Bugzilla::Extension::GitHubAuth::Client->new; + + my ($access_token, $emails); + eval { + # The following variable lets us catch and return (rather than throw) errors + # from our github client code, as required by the Auth API. + local $Bugzilla::Extension::GitHubAuth::Client::Error::USE_EXCEPTION_OBJECTS + = 1; + $access_token = $client->get_access_token($code); + $emails = $client->get_user_emails($access_token); + }; + my $e = $@; + if (blessed $e && $e->isa('Bugzilla::Extension::GitHubAuth::Client::Error')) { + my $key = $e->type eq 'user' ? 'user_error' : 'error'; + return {failure => AUTH_ERROR, $key => $e->error, details => $e->vars}; + } + elsif ($e) { + die $e; + } + + my @emails + = map { $_->{email} } + grep { $_->{verified} && $_->{email} !~ /\@users\.noreply\.github\.com$/ } + @$emails; + + my @bugzilla_users; + my @github_emails; + foreach my $email (@emails) { + my $user = Bugzilla::User->new({name => $email, cache => 1}); + if ($user) { + push @bugzilla_users, $user; } else { - return { failure => AUTH_ERROR, user_error => 'github_no_emails' }; + push @github_emails, $email; } + } + my @allowed_bugzilla_users + = grep { not $_->in_group('no-github-auth') } @bugzilla_users; + + if (@allowed_bugzilla_users == 1) { + my ($user) = @allowed_bugzilla_users; + return {user => $user}; + } + elsif (@allowed_bugzilla_users > 1) { + $self->{github_failure} = { + template => 'account/auth/github-verify-account.html.tmpl', + vars => { + bugzilla_users => \@allowed_bugzilla_users, + choose_email => _mk_choose_email(\@emails), + }, + }; + return {failure => AUTH_NODATA}; + } + elsif (@allowed_bugzilla_users == 0 + && @bugzilla_users > 0 + && @github_emails == 0) + { + return { + failure => AUTH_ERROR, + user_error => 'github_auth_account_too_powerful' + }; + } + elsif (@github_emails) { + $self->{github_failure} = { + template => 'account/auth/github-verify-account.html.tmpl', + vars => { + github_emails => \@github_emails, + choose_email => _mk_choose_email(\@emails), + }, + }; + return {failure => AUTH_NODATA}; + } + else { + return {failure => AUTH_ERROR, user_error => 'github_no_emails'}; + } } sub _get_login_info_from_email { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $email = $cgi->param('email') or return { failure => AUTH_ERROR, - user_error => 'github_invalid_email', - details => { email => '' } }; - trick_taint($email); - - unless (any { $_ eq $email } @{ Bugzilla->request_cache->{github_emails} }) { - return { failure => AUTH_ERROR, - user_error => 'github_invalid_email', - details => { email => $email }}; - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $email = $cgi->param('email') + or return { + failure => AUTH_ERROR, + user_error => 'github_invalid_email', + details => {email => ''} + }; + trick_taint($email); - my $user = Bugzilla::User->new({name => $email, cache => 1}); - $cgi->remove_cookie('Bugzilla_github_token'); - return $user ? { user => $user } : { email => $email }; + unless (any { $_ eq $email } @{Bugzilla->request_cache->{github_emails}}) { + return { + failure => AUTH_ERROR, + user_error => 'github_invalid_email', + details => {email => $email} + }; + } + + my $user = Bugzilla::User->new({name => $email, cache => 1}); + $cgi->remove_cookie('Bugzilla_github_token'); + return $user ? {user => $user} : {email => $email}; } sub fail_nodata { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; - ThrowUserError('login_required') if Bugzilla->usage_mode != USAGE_MODE_BROWSER; + ThrowUserError('login_required') if Bugzilla->usage_mode != USAGE_MODE_BROWSER; - my $file = $self->{github_failure}{template} // "account/auth/login.html.tmpl"; - my $vars = $self->{github_failure}{vars} // { target => $cgi->url(-relative=>1) }; + my $file = $self->{github_failure}{template} // "account/auth/login.html.tmpl"; + my $vars = $self->{github_failure}{vars} + // {target => $cgi->url(-relative => 1)}; - print $cgi->header(); - $template->process($file, $vars) or ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process($file, $vars) or ThrowTemplateError($template->error()); + exit; } sub _store_emails { - my ($emails) = @_; - my $state = issue_short_lived_session_token("github_email"); - set_token_extra_data($state, { type => 'github_email', - emails => $emails, - target_uri => Bugzilla->request_cache->{github_target_uri} }); - - Bugzilla->cgi->send_cookie(-name => 'github_state', - -value => $state, - -httponly => 1); - return $state; + my ($emails) = @_; + my $state = issue_short_lived_session_token("github_email"); + set_token_extra_data( + $state, + { + type => 'github_email', + emails => $emails, + target_uri => Bugzilla->request_cache->{github_target_uri} + } + ); + + Bugzilla->cgi->send_cookie( + -name => 'github_state', + -value => $state, + -httponly => 1 + ); + return $state; } sub _mk_choose_email { - my ($emails) = @_; - my $state = _store_emails($emails); - - return sub { - my $email = shift; - my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); - $uri->query_form( state => $state, email => $email ); - return $uri; - }; + my ($emails) = @_; + my $state = _store_emails($emails); + + return sub { + my $email = shift; + my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); + $uri->query_form(state => $state, email => $email); + return $uri; + }; } 1; diff --git a/extensions/GitHubAuth/lib/Verify.pm b/extensions/GitHubAuth/lib/Verify.pm index f399af02e..078353c80 100644 --- a/extensions/GitHubAuth/lib/Verify.pm +++ b/extensions/GitHubAuth/lib/Verify.pm @@ -16,11 +16,11 @@ use base qw(Bugzilla::Auth::Verify); use Bugzilla::Constants qw( AUTH_NO_SUCH_USER ); sub check_credentials { - my ($self, $login_data) = @_; + my ($self, $login_data) = @_; - return { failure => AUTH_NO_SUCH_USER } unless $login_data->{github_auth}; + return {failure => AUTH_NO_SUCH_USER} unless $login_data->{github_auth}; - return $login_data; + return $login_data; } 1; diff --git a/extensions/GoogleAnalytics/Extension.pm b/extensions/GoogleAnalytics/Extension.pm index e9b144da4..fb7e8adae 100644 --- a/extensions/GoogleAnalytics/Extension.pm +++ b/extensions/GoogleAnalytics/Extension.pm @@ -15,9 +15,9 @@ use parent qw(Bugzilla::Extension); our $VERSION = '0.1'; sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{GoogleAnalytics} = "Bugzilla::Extension::GoogleAnalytics::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{GoogleAnalytics} = "Bugzilla::Extension::GoogleAnalytics::Config"; } __PACKAGE__->NAME; diff --git a/extensions/GoogleAnalytics/lib/Config.pm b/extensions/GoogleAnalytics/lib/Config.pm index f9e003ce0..1c453ff74 100644 --- a/extensions/GoogleAnalytics/lib/Config.pm +++ b/extensions/GoogleAnalytics/lib/Config.pm @@ -14,28 +14,25 @@ use warnings; use Bugzilla::Config::Common; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'google_analytics_tracking_id', - type => 't', - default => '', - checker => sub { - my ($tracking_id) = (@_); - - return 'must be like UA-XXXXXX-X' unless $tracking_id =~ m{^(UA-[[:xdigit:]]+-[[:xdigit:]]+)?$}; - return ''; - } - }, - { - name => 'google_analytics_debug', - type => 'b', - default => 0 - }, - ); - - return @params; + my ($class) = @_; + + my @params = ( + { + name => 'google_analytics_tracking_id', + type => 't', + default => '', + checker => sub { + my ($tracking_id) = (@_); + + return 'must be like UA-XXXXXX-X' + unless $tracking_id =~ m{^(UA-[[:xdigit:]]+-[[:xdigit:]]+)?$}; + return ''; + } + }, + {name => 'google_analytics_debug', type => 'b', default => 0}, + ); + + return @params; } 1; diff --git a/extensions/Gravatar/Config.pm b/extensions/Gravatar/Config.pm index e0c684c9b..8651966b5 100644 --- a/extensions/Gravatar/Config.pm +++ b/extensions/Gravatar/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'Gravatar'; +use constant NAME => 'Gravatar'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/Gravatar/Extension.pm b/extensions/Gravatar/Extension.pm index 97cf23b00..04d5f3090 100644 --- a/extensions/Gravatar/Extension.pm +++ b/extensions/Gravatar/Extension.pm @@ -20,37 +20,39 @@ use Digest::MD5 qw(md5_hex); use constant DEFAULT_URL => 'extensions/Gravatar/web/default.jpg'; BEGIN { - *Bugzilla::User::gravatar = \&_user_gravatar; + *Bugzilla::User::gravatar = \&_user_gravatar; } sub _user_gravatar { - my ($self, $size) = @_; - if ($self->setting('show_my_gravatar') eq 'Off') { - return DEFAULT_URL; - } - if (!$self->{gravatar}) { - my $email = $self->email; - $email = $gravatar_user_map{$self->email} if exists $gravatar_user_map{$self->email}; - $self->{gravatar} = 'https://secure.gravatar.com/avatar/' . md5_hex(lc($email)) . '?d=mm'; - } - $size ||= 64; - return $self->{gravatar} . '&size=' . $size; + my ($self, $size) = @_; + if ($self->setting('show_my_gravatar') eq 'Off') { + return DEFAULT_URL; + } + if (!$self->{gravatar}) { + my $email = $self->email; + $email = $gravatar_user_map{$self->email} + if exists $gravatar_user_map{$self->email}; + $self->{gravatar} + = 'https://secure.gravatar.com/avatar/' . md5_hex(lc($email)) . '?d=mm'; + } + $size ||= 64; + return $self->{gravatar} . '&size=' . $size; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'show_gravatars', - options => ['On', 'Off'], - default => 'Off', - category => 'Bug Editing' - }); - add_setting({ - name => 'show_my_gravatar', - options => ['On', 'Off'], - default => 'On', - category => 'Bug Editing' - }); + my ($self, $args) = @_; + add_setting({ + name => 'show_gravatars', + options => ['On', 'Off'], + default => 'Off', + category => 'Bug Editing' + }); + add_setting({ + name => 'show_my_gravatar', + options => ['On', 'Off'], + default => 'On', + category => 'Bug Editing' + }); } __PACKAGE__->NAME; diff --git a/extensions/Gravatar/lib/Data.pm b/extensions/Gravatar/lib/Data.pm index 763dba85b..13b004fa5 100644 --- a/extensions/Gravatar/lib/Data.pm +++ b/extensions/Gravatar/lib/Data.pm @@ -13,11 +13,9 @@ use warnings; use base 'Exporter'; our @EXPORT_OK = qw( - %gravatar_user_map + %gravatar_user_map ); -our %gravatar_user_map = ( - 'orangefactor@bots.tld' => 'tbplbot@gmail.com', -); +our %gravatar_user_map = ('orangefactor@bots.tld' => 'tbplbot@gmail.com',); 1; diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm index 316fc6cdc..7fca2ccf0 100644 --- a/extensions/GuidedBugEntry/Config.pm +++ b/extensions/GuidedBugEntry/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'GuidedBugEntry'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/GuidedBugEntry/Extension.pm b/extensions/GuidedBugEntry/Extension.pm index 2d58d506a..72bae0d84 100644 --- a/extensions/GuidedBugEntry/Extension.pm +++ b/extensions/GuidedBugEntry/Extension.pm @@ -23,111 +23,103 @@ use Bugzilla::Extension::BMO::Data; our $VERSION = '1'; sub enter_bug_start { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - - # hack for skipping old guided code when enabled - $vars->{'disable_guided'} = 1; - - # force guided format for new users - my $format = $cgi->param('format') || ''; - if ($cgi->param('maketemplate')) { - $format = '__default__'; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + + # hack for skipping old guided code when enabled + $vars->{'disable_guided'} = 1; + + # force guided format for new users + my $format = $cgi->param('format') || ''; + if ($cgi->param('maketemplate')) { + $format = '__default__'; + } + + if ($format eq 'guided' || ($format eq '' && !$user->in_group('editbugs'))) { + + # skip the first step if a product is provided + if ($cgi->param('product')) { + print $cgi->redirect('enter_bug.cgi?format=guided' + . ($cgi->param('format_forced') ? '&format_forced=1' : '') + . '#h=dupes' . '|' + . url_quote($cgi->param('product')) . '|' + . url_quote($cgi->param('component') || '')); + exit; } - if ( - $format eq 'guided' || - ( - $format eq '' && - !$user->in_group('editbugs') - ) - ) { - # skip the first step if a product is provided - if ($cgi->param('product')) { - print $cgi->redirect('enter_bug.cgi?format=guided' . - ($cgi->param('format_forced') ? '&format_forced=1' : '') . - '#h=dupes' . - '|' . url_quote($cgi->param('product')) . - '|' . url_quote($cgi->param('component') || '') - ); - exit; - } - - # Do not redirect to product forms if we came from there already - $vars->{'format_forced'} = 1 if $cgi->param('format_forced'); - - $self->_init_vars($vars); - print $cgi->header(); - $template->process('guided/guided.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } - - # we use the __default__ format to bypass the guided entry - # it isn't understood upstream, so remove it once a product - # has been selected. - if ( - ($cgi->param('format') && $cgi->param('format') eq "__default__") - && ($cgi->param('product') && $cgi->param('product') ne '') - ) { - $cgi->delete('format'); - } + # Do not redirect to product forms if we came from there already + $vars->{'format_forced'} = 1 if $cgi->param('format_forced'); + + $self->_init_vars($vars); + print $cgi->header(); + $template->process('guided/guided.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + + # we use the __default__ format to bypass the guided entry + # it isn't understood upstream, so remove it once a product + # has been selected. + if ( ($cgi->param('format') && $cgi->param('format') eq "__default__") + && ($cgi->param('product') && $cgi->param('product') ne '')) + { + $cgi->delete('format'); + } } sub _init_vars { - my ($self, $vars) = @_; - my $user = Bugzilla->user; - - my @enterable_products = @{$user->get_enterable_products}; - ThrowUserError('no_products') unless scalar(@enterable_products); - - my @classifications = ({object => undef, products => \@enterable_products}); - - my $class; - foreach my $product (@enterable_products) { - $class->{$product->classification_id}->{'object'} ||= - new Bugzilla::Classification($product->classification_id); - push(@{$class->{$product->classification_id}->{'products'}}, $product); - } - @classifications = - sort { - $a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) - } (values %$class); - $vars->{'classifications'} = \@classifications; - - my @open_states = BUG_STATE_OPEN(); - $vars->{'open_states'} = \@open_states; - - $vars->{'token'} = issue_session_token('create_bug'); - - $vars->{'platform'} = detect_platform(); - $vars->{'op_sys'} = detect_op_sys(); - $vars->{'webdev'} = Bugzilla->cgi->param('webdev'); + my ($self, $vars) = @_; + my $user = Bugzilla->user; + + my @enterable_products = @{$user->get_enterable_products}; + ThrowUserError('no_products') unless scalar(@enterable_products); + + my @classifications = ({object => undef, products => \@enterable_products}); + + my $class; + foreach my $product (@enterable_products) { + $class->{$product->classification_id}->{'object'} + ||= new Bugzilla::Classification($product->classification_id); + push(@{$class->{$product->classification_id}->{'products'}}, $product); + } + @classifications = sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class); + $vars->{'classifications'} = \@classifications; + + my @open_states = BUG_STATE_OPEN(); + $vars->{'open_states'} = \@open_states; + + $vars->{'token'} = issue_session_token('create_bug'); + + $vars->{'platform'} = detect_platform(); + $vars->{'op_sys'} = detect_op_sys(); + $vars->{'webdev'} = Bugzilla->cgi->param('webdev'); } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - my $cgi = Bugzilla->cgi; - - return unless $page eq 'guided_products.js'; - - if (!$cgi->param('format_forced')) { - my %bug_formats; - foreach my $product (keys %create_bug_formats) { - if (my $format = Bugzilla::Extension::BMO::forced_format($product)) { - $bug_formats{$product} = $format; - } - } - $vars->{'create_bug_formats'} = \%bug_formats; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + my $cgi = Bugzilla->cgi; + + return unless $page eq 'guided_products.js'; + + if (!$cgi->param('format_forced')) { + my %bug_formats; + foreach my $product (keys %create_bug_formats) { + if (my $format = Bugzilla::Extension::BMO::forced_format($product)) { + $bug_formats{$product} = $format; + } } + $vars->{'create_bug_formats'} = \%bug_formats; + } - $vars->{'webdev'} = $cgi->param('webdev'); + $vars->{'webdev'} = $cgi->param('webdev'); } __PACKAGE__->NAME; diff --git a/extensions/InlineHistory/Extension.pm b/extensions/InlineHistory/Extension.pm index adbfa4c74..45f1120f8 100644 --- a/extensions/InlineHistory/Extension.pm +++ b/extensions/InlineHistory/Extension.pm @@ -24,223 +24,227 @@ our $VERSION = '1.5'; use constant MAXIMUM_ACTIVITY_COUNT => 500; # don't show really long values -use constant MAXIMUM_VALUE_LENGTH => 256; +use constant MAXIMUM_VALUE_LENGTH => 256; sub template_before_create { - my ($self, $args) = @_; - $args->{config}->{FILTERS}->{ih_short_value} = sub { - my ($str) = @_; - return length($str) <= MAXIMUM_VALUE_LENGTH - ? $str - : substr($str, 0, MAXIMUM_VALUE_LENGTH - 3) . '...'; - }; + my ($self, $args) = @_; + $args->{config}->{FILTERS}->{ih_short_value} = sub { + my ($str) = @_; + return + length($str) <= MAXIMUM_VALUE_LENGTH + ? $str + : substr($str, 0, MAXIMUM_VALUE_LENGTH - 3) . '...'; + }; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - return if $file ne 'bug/edit.html.tmpl'; - - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - return unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on'; - - # note: bug/edit.html.tmpl doesn't support multiple bugs - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - my $bug_id = $bug->id; - - # build bug activity - my ($activity) = $bug->can('get_activity') - ? $bug->get_activity() - : Bugzilla::Bug::GetBugActivity($bug_id); - $activity = _add_duplicates($bug_id, $activity); - - if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) { - $activity = []; - $vars->{'ih_activity'} = 0; - $vars->{'ih_activity_max'} = 1; - return; - } - - # allow other extensions to alter history - Bugzilla::Hook::process('inline_history_activtiy', { activity => $activity }); - - my %attachment_cache; - foreach my $attachment (@{$bug->attachments}) { - $attachment_cache{$attachment->id} = $attachment; - } - - # build a list of bugs we need to check visibility of, so we can check with a single query - my %visible_bug_ids; - - # augment and tweak - foreach my $operation (@$activity) { - # make operation.who an object - $operation->{who} = - Bugzilla::User->new({ name => $operation->{who}, cache => 1 }); - - for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { - my $change = $operation->{changes}->[$i]; - - # make an attachment object - if ($change->{attachid}) { - $change->{attach} = $attachment_cache{$change->{attachid}}; - } - - # empty resolutions are displayed as --- by default - # make it explicit here to enable correct display of the change - if ($change->{fieldname} eq 'resolution') { - $change->{removed} = '---' if $change->{removed} eq ''; - $change->{added} = '---' if $change->{added} eq ''; - } - - # make boolean fields true/false instead of 1/0 - my ($table, $field) = ('bugs', $change->{fieldname}); - if ($field =~ /^([^\.]+)\.(.+)$/) { - ($table, $field) = ($1, $2); - } - my $column = $dbh->bz_column_info($table, $field); - if ($column && $column->{TYPE} eq 'BOOLEAN') { - $change->{removed} = ''; - $change->{added} = $change->{added} ? 'true' : 'false'; - } - - my $field_obj; - if ($change->{fieldname} =~ /^cf_/) { - $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 }); - $change->{fieldtype} = $field_obj->type; - } - - # identify buglist changes - if ($change->{fieldname} eq 'blocked' || - $change->{fieldname} eq 'dependson' || - $change->{fieldname} eq 'dupe' || - ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID) - ) { - $change->{buglist} = 1; - foreach my $what (qw(removed added)) { - my @buglist = split(/[\s,]+/, $change->{$what}); - foreach my $id (@buglist) { - if ($id && $id =~ /^\d+$/) { - $visible_bug_ids{$id} = 1; - } - } - } - } - - # split see-also - if ($change->{fieldname} eq 'see_also') { - my $url_base = Bugzilla->localconfig->{urlbase}; - foreach my $f (qw( added removed )) { - my @values; - foreach my $value (split(/, /, $change->{$f})) { - my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base - ? $value =~ /id=(\d+)$/ - : undef; - push @values, { - url => $value, - bug_id => $bug_id, - }; - } - $change->{$f} = \@values; - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + return if $file ne 'bug/edit.html.tmpl'; + + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + return + unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on'; + + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + my $bug_id = $bug->id; + + # build bug activity + my ($activity) + = $bug->can('get_activity') + ? $bug->get_activity() + : Bugzilla::Bug::GetBugActivity($bug_id); + $activity = _add_duplicates($bug_id, $activity); + + if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) { + $activity = []; + $vars->{'ih_activity'} = 0; + $vars->{'ih_activity_max'} = 1; + return; + } + + # allow other extensions to alter history + Bugzilla::Hook::process('inline_history_activtiy', {activity => $activity}); + + my %attachment_cache; + foreach my $attachment (@{$bug->attachments}) { + $attachment_cache{$attachment->id} = $attachment; + } + +# build a list of bugs we need to check visibility of, so we can check with a single query + my %visible_bug_ids; + + # augment and tweak + foreach my $operation (@$activity) { + + # make operation.who an object + $operation->{who} + = Bugzilla::User->new({name => $operation->{who}, cache => 1}); + + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = $attachment_cache{$change->{attachid}}; + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + my $field_obj; + if ($change->{fieldname} =~ /^cf_/) { + $field_obj = Bugzilla::Field->new({name => $change->{fieldname}, cache => 1}); + $change->{fieldtype} = $field_obj->type; + } + + # identify buglist changes + if ( $change->{fieldname} eq 'blocked' + || $change->{fieldname} eq 'dependson' + || $change->{fieldname} eq 'dupe' + || ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)) + { + $change->{buglist} = 1; + foreach my $what (qw(removed added)) { + my @buglist = split(/[\s,]+/, $change->{$what}); + foreach my $id (@buglist) { + if ($id && $id =~ /^\d+$/) { + $visible_bug_ids{$id} = 1; } + } + } + } + + # split see-also + if ($change->{fieldname} eq 'see_also') { + my $url_base = Bugzilla->localconfig->{urlbase}; + foreach my $f (qw( added removed )) { + my @values; + foreach my $value (split(/, /, $change->{$f})) { + my ($bug_id) + = substr($value, 0, length($url_base)) eq $url_base + ? $value =~ /id=(\d+)$/ + : undef; + push @values, {url => $value, bug_id => $bug_id,}; + } + $change->{$f} = \@values; + } + } + + # split multiple flag changes (must be processed last) + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + next if scalar(@added) <= 1 && scalar(@removed) <= 1; + + # remove current change + splice(@{$operation->{changes}}, $i, 1); + + # restructure into added/removed for each flag + my %flags; + foreach my $added (@added) { + my ($value, $name) = $added =~ /^((.+).)$/; + next unless defined $name; + $flags{$name}{added} = $value; + $flags{$name}{removed} |= ''; + } + foreach my $removed (@removed) { + my ($value, $name) = $removed =~ /^((.+).)$/; + next unless defined $name; + $flags{$name}{added} |= ''; + $flags{$name}{removed} = $value; + } - # split multiple flag changes (must be processed last) - if ($change->{fieldname} eq 'flagtypes.name') { - my @added = split(/, /, $change->{added}); - my @removed = split(/, /, $change->{removed}); - next if scalar(@added) <= 1 && scalar(@removed) <= 1; - # remove current change - splice(@{$operation->{changes}}, $i, 1); - # restructure into added/removed for each flag - my %flags; - foreach my $added (@added) { - my ($value, $name) = $added =~ /^((.+).)$/; - next unless defined $name; - $flags{$name}{added} = $value; - $flags{$name}{removed} |= ''; - } - foreach my $removed (@removed) { - my ($value, $name) = $removed =~ /^((.+).)$/; - next unless defined $name; - $flags{$name}{added} |= ''; - $flags{$name}{removed} = $value; - } - # clone current change, modify and insert - foreach my $flag (sort keys %flags) { - my $flag_change = {}; - foreach my $key (keys %$change) { - $flag_change->{$key} = $change->{$key}; - } - $flag_change->{removed} = $flags{$flag}{removed}; - $flag_change->{added} = $flags{$flag}{added}; - splice(@{$operation->{changes}}, $i, 0, $flag_change); - } - $i--; - } + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + splice(@{$operation->{changes}}, $i, 0, $flag_change); } + $i--; + } } + } - $user->visible_bugs([keys %visible_bug_ids]); + $user->visible_bugs([keys %visible_bug_ids]); - $vars->{'ih_activity'} = $activity; + $vars->{'ih_activity'} = $activity; } sub _add_duplicates { - # insert 'is a dupe of this bug' comment to allow js to display - # as activity - my ($bug_id, $activity) = @_; + # insert 'is a dupe of this bug' comment to allow js to display + # as activity - # we're ignoring pre-bugzilla 3.0 ".. has been marked as a duplicate .." - # comments because searching each comment's text is expensive. these - # legacy comments will not be visible at all in the bug's comment/activity - # stream. bug 928786 deals with migrating those comments to be stored as - # CMT_HAS_DUPE instead. + my ($bug_id, $activity) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" - SELECT profiles.login_name, " . - $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ", + # we're ignoring pre-bugzilla 3.0 ".. has been marked as a duplicate .." + # comments because searching each comment's text is expensive. these + # legacy comments will not be visible at all in the bug's comment/activity + # stream. bug 928786 deals with migrating those comments to be stored as + # CMT_HAS_DUPE instead. + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT profiles.login_name, " + . $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ", extra_data FROM longdescs INNER JOIN profiles ON profiles.userid = longdescs.who WHERE bug_id = ? AND type = ? ORDER BY bug_when "); - $sth->execute($bug_id, CMT_HAS_DUPE); - - while (my($who, $when, $dupe_id) = $sth->fetchrow_array) { - my $entry = { - 'when' => $when, - 'who' => $who, - 'changes' => [ - { - 'removed' => '', - 'added' => $dupe_id, - 'attachid' => undef, - 'fieldname' => 'dupe', - 'dupe' => 1, - } - ], - }; - push @$activity, $entry; - } + $sth->execute($bug_id, CMT_HAS_DUPE); + + while (my ($who, $when, $dupe_id) = $sth->fetchrow_array) { + my $entry = { + 'when' => $when, + 'who' => $who, + 'changes' => [{ + 'removed' => '', + 'added' => $dupe_id, + 'attachid' => undef, + 'fieldname' => 'dupe', + 'dupe' => 1, + }], + }; + push @$activity, $entry; + } - return [ sort { $a->{when} cmp $b->{when} } @$activity ]; + return [sort { $a->{when} cmp $b->{when} } @$activity]; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'inline_history', - options => ['on', 'off'], - default => 'off', - category => 'Bug Editing' - }); + my ($self, $args) = @_; + add_setting({ + name => 'inline_history', + options => ['on', 'off'], + default => 'off', + category => 'Bug Editing' + }); } __PACKAGE__->NAME; diff --git a/extensions/LastResolved/Config.pm b/extensions/LastResolved/Config.pm index 8fd8f106f..c981db7f0 100644 --- a/extensions/LastResolved/Config.pm +++ b/extensions/LastResolved/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'LastResolved'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/LastResolved/Extension.pm b/extensions/LastResolved/Extension.pm index 197bb15d9..798506ae9 100644 --- a/extensions/LastResolved/Extension.pm +++ b/extensions/LastResolved/Extension.pm @@ -22,94 +22,94 @@ use Bugzilla::Install::Util qw(indicate_progress); our $VERSION = '0.01'; sub install_update_db { - my ($self, $args) = @_; - my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'}); - if (!$last_resolved) { - Bugzilla::Field->create({ - name => 'cf_last_resolved', - description => 'Last Resolved', - type => FIELD_TYPE_DATETIME, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 1, - }); - _migrate_last_resolved(); - } + my ($self, $args) = @_; + my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'}); + if (!$last_resolved) { + Bugzilla::Field->create({ + name => 'cf_last_resolved', + description => 'Last Resolved', + type => FIELD_TYPE_DATETIME, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 1, + }); + _migrate_last_resolved(); + } } sub _migrate_last_resolved { - my $dbh = Bugzilla->dbh; - my $field_id = get_field_id('bug_status'); - my $resolved_activity = $dbh->selectall_arrayref( - "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who + my $dbh = Bugzilla->dbh; + my $field_id = get_field_id('bug_status'); + my $resolved_activity = $dbh->selectall_arrayref( + "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who FROM bugs_activity WHERE bugs_activity.fieldid = ? AND bugs_activity.added = 'RESOLVED' - ORDER BY bugs_activity.bug_when", - undef, $field_id); + ORDER BY bugs_activity.bug_when", undef, $field_id + ); - my $count = 1; - my $total = scalar @$resolved_activity; - my %current_last_resolved; - foreach my $activity (@$resolved_activity) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($id, $new, $who) = @$activity; - my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : ""; - $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", undef, $new, $id); - LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new); - $current_last_resolved{$id} = $new; - } + my $count = 1; + my $total = scalar @$resolved_activity; + my %current_last_resolved; + foreach my $activity (@$resolved_activity) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($id, $new, $who) = @$activity; + my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : ""; + $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", + undef, $new, $id); + LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new); + $current_last_resolved{$id} = $new; + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($field, $priv_results) = @$args{qw(field priv_results)}; - if ($field eq 'cf_last_resolved') { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my ($field, $priv_results) = @$args{qw(field priv_results)}; + if ($field eq 'cf_last_resolved') { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - if ($changes->{'bug_status'}) { - # If the bug has been resolved then update the cf_last_resolved - # value to the current timestamp if cf_last_resolved exists - if ($bug->bug_status eq 'RESOLVED') { - $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", - undef, $timestamp, $bug->id); - my $old_value = $bug->cf_last_resolved || ''; - LogActivityEntry($bug->id, 'cf_last_resolved', $old_value, - $timestamp, Bugzilla->user->id, $timestamp); - } + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + if ($changes->{'bug_status'}) { + + # If the bug has been resolved then update the cf_last_resolved + # value to the current timestamp if cf_last_resolved exists + if ($bug->bug_status eq 'RESOLVED') { + $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", + undef, $timestamp, $bug->id); + my $old_value = $bug->cf_last_resolved || ''; + LogActivityEntry($bug->id, 'cf_last_resolved', $old_value, $timestamp, + Bugzilla->user->id, $timestamp); } + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, 'cf_last_resolved') + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + push(@$fields, 'cf_last_resolved'); } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - push(@$columns, 'cf_last_resolved'); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + push(@$columns, 'cf_last_resolved'); + } } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - $columns->{'cf_last_resolved'} = { - name => 'bugs.cf_last_resolved', - title => 'Last Resolved', - }; + my ($self, $args) = @_; + my $columns = $args->{columns}; + $columns->{'cf_last_resolved'} + = {name => 'bugs.cf_last_resolved', title => 'Last Resolved',}; } __PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm index 94b9b10eb..d47fb4d91 100644 --- a/extensions/LimitedEmail/Config.pm +++ b/extensions/LimitedEmail/Config.pm @@ -11,15 +11,15 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'LimitedEmail'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'LimitedEmail'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; use constant FILTERS => [ - qr/^(?:glob|dkl|justdave|shyam)\@mozilla\.com$/i, - qr/^byron\.jones\@gmail\.com$/i, - qr/^gerv\@mozilla\.org$/i, - qr/^reed\@reedloden\.com$/i, + qr/^(?:glob|dkl|justdave|shyam)\@mozilla\.com$/i, + qr/^byron\.jones\@gmail\.com$/i, + qr/^gerv\@mozilla\.org$/i, + qr/^reed\@reedloden\.com$/i, ]; __PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/Extension.pm b/extensions/LimitedEmail/Extension.pm index 9b504db91..3831ac878 100644 --- a/extensions/LimitedEmail/Extension.pm +++ b/extensions/LimitedEmail/Extension.pm @@ -21,46 +21,46 @@ use Encode qw(encode_utf8); use Bugzilla::Constants qw(bz_locations); sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; - my $header = $email->{header}; - return if $header->header('to') eq ''; + my ($self, $args) = @_; + my $email = $args->{email}; + my $header = $email->{header}; + return if $header->header('to') eq ''; - my $blocked = ''; - if (!deliver_to($header->header('to'))) { - $blocked = $header->header('to'); - $header->header_set(to => ''); - } + my $blocked = ''; + if (!deliver_to($header->header('to'))) { + $blocked = $header->header('to'); + $header->header_set(to => ''); + } - my $log_filename = bz_locations->{'datadir'} . '/mail.log'; - my $fh = FileHandle->new(">>$log_filename"); - if ($fh) { - print $fh encode_utf8(sprintf( - "[%s] %s%s %s : %s\n", - time2str('%D %T', time), - ($blocked eq '' ? '' : '(blocked) '), - ($blocked eq '' ? $header->header('to') : $blocked), - $header->header('X-Bugzilla-Reason') || '-', - $header->header('subject') - )); - $fh->close(); - } + my $log_filename = bz_locations->{'datadir'} . '/mail.log'; + my $fh = FileHandle->new(">>$log_filename"); + if ($fh) { + print $fh encode_utf8(sprintf( + "[%s] %s%s %s : %s\n", + time2str('%D %T', time), + ($blocked eq '' ? '' : '(blocked) '), + ($blocked eq '' ? $header->header('to') : $blocked), + $header->header('X-Bugzilla-Reason') || '-', + $header->header('subject') + )); + $fh->close(); + } } sub deliver_to { - my $email = address_of(shift); - my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS; - foreach my $re (@$ra_filters) { - if ($email =~ $re) { - return 1; - } + my $email = address_of(shift); + my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS; + foreach my $re (@$ra_filters) { + if ($email =~ $re) { + return 1; } - return 0; + } + return 0; } sub address_of { - my $email = shift; - return $email =~ /<([^>]+)>/ ? $1 : $email; + my $email = shift; + return $email =~ /<([^>]+)>/ ? $1 : $email; } __PACKAGE__->NAME; diff --git a/extensions/MozProjectReview/Config.pm b/extensions/MozProjectReview/Config.pm index 1a5e14f3d..41d761a35 100644 --- a/extensions/MozProjectReview/Config.pm +++ b/extensions/MozProjectReview/Config.pm @@ -12,10 +12,8 @@ use warnings; use constant NAME => 'MozProjectReview'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/MozProjectReview/Extension.pm b/extensions/MozProjectReview/Extension.pm index 29d709ff4..c5cd27d21 100644 --- a/extensions/MozProjectReview/Extension.pm +++ b/extensions/MozProjectReview/Extension.pm @@ -22,139 +22,151 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub post_bug_after_creation { - my ($self, $args) = @_; - my $vars = $args->{'vars'}; - my $bug = $vars->{'bug'}; - my $timestamp = $args->{'timestamp'}; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; - my $template = Bugzilla->template; - - return if !($params->{format} && $params->{format} eq 'moz-project-review'); - - # do a match if applicable - Bugzilla::User::match_field({ - 'sow_vendor_mozcontact' => { 'type' => 'single' }, - }); - - my $do_sec_review = 0; - my @sec_review_needed = ( - 'Engaging a new vendor company', - 'Adding a new SOW with a vendor', - 'Extending a SOW or renewing a contract', - 'Purchasing software', - 'Signing up for an online service', - 'Other' - ); - if ((any { $_ eq $params->{contract_type} } @sec_review_needed) - || $params->{mozilla_data} eq 'Yes') { - $do_sec_review = 1; - } - - my ($sec_review_bug, $finance_bug, $error, @dep_comment, @dep_errors, @send_mail); - - # Common parameters always passed to _file_child_bug - # bug_data and template_suffix will be different for each bug - my $child_params = { - parent_bug => $bug, - template_vars => $vars, - dep_comment => \@dep_comment, - dep_errors => \@dep_errors, - send_mail => \@send_mail, - }; - - if ($do_sec_review) { - $child_params->{'bug_data'} = { - short_desc => 'RRA: ' . $params->{contract_type} . ' with ' . $params->{other_party}, - product => 'Enterprise Information Security', - component => 'Rapid Risk Analysis', - bug_severity => 'normal', - groups => [ 'mozilla-employee-confidential' ], - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $bug->bug_id, - cc => $params->{cc}, - }; - $child_params->{'template_suffix'} = 'sec-review'; - _file_child_bug($child_params); - } - + my ($self, $args) = @_; + my $vars = $args->{'vars'}; + my $bug = $vars->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + my $template = Bugzilla->template; + + return if !($params->{format} && $params->{format} eq 'moz-project-review'); + + # do a match if applicable + Bugzilla::User::match_field({'sow_vendor_mozcontact' => {'type' => 'single'},}); + + my $do_sec_review = 0; + my @sec_review_needed = ( + 'Engaging a new vendor company', + 'Adding a new SOW with a vendor', + 'Extending a SOW or renewing a contract', + 'Purchasing software', + 'Signing up for an online service', + 'Other' + ); + if ((any { $_ eq $params->{contract_type} } @sec_review_needed) + || $params->{mozilla_data} eq 'Yes') + { + $do_sec_review = 1; + } + + my ($sec_review_bug, $finance_bug, $error, @dep_comment, @dep_errors, + @send_mail); + + # Common parameters always passed to _file_child_bug + # bug_data and template_suffix will be different for each bug + my $child_params = { + parent_bug => $bug, + template_vars => $vars, + dep_comment => \@dep_comment, + dep_errors => \@dep_errors, + send_mail => \@send_mail, + }; + + if ($do_sec_review) { $child_params->{'bug_data'} = { - short_desc => 'Finance Review: ' . $params->{contract_type} . ' with ' . $params->{other_party}, - product => 'Finance', - component => 'Purchase Request Form', - bug_severity => 'normal', - priority => '--', - groups => [ 'finance' ], - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $bug->bug_id, - cc => $params->{cc}, + short_desc => 'RRA: ' + . $params->{contract_type} + . ' with ' + . $params->{other_party}, + product => 'Enterprise Information Security', + component => 'Rapid Risk Analysis', + bug_severity => 'normal', + groups => ['mozilla-employee-confidential'], + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $bug->bug_id, + cc => $params->{cc}, }; - $child_params->{'template_suffix'} = 'finance'; + $child_params->{'template_suffix'} = 'sec-review'; _file_child_bug($child_params); - + } + + $child_params->{'bug_data'} = { + short_desc => 'Finance Review: ' + . $params->{contract_type} + . ' with ' + . $params->{other_party}, + product => 'Finance', + component => 'Purchase Request Form', + bug_severity => 'normal', + priority => '--', + groups => ['finance'], + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $bug->bug_id, + cc => $params->{cc}, + }; + $child_params->{'template_suffix'} = 'finance'; + _file_child_bug($child_params); + + if (scalar @dep_errors) { + warn "[Bug " + . $bug->id + . "] Failed to create additional moz-project-review bugs:\n" + . join("\n", @dep_errors); + $vars->{'message'} = 'moz_project_review_creation_failed'; + } + + if (scalar @dep_comment) { + my $comment = join("\n", @dep_comment); if (scalar @dep_errors) { - warn "[Bug " . $bug->id . "] Failed to create additional moz-project-review bugs:\n" . - join("\n", @dep_errors); - $vars->{'message'} = 'moz_project_review_creation_failed'; - } - - if (scalar @dep_comment) { - my $comment = join("\n", @dep_comment); - if (scalar @dep_errors) { - $comment .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; - } - $bug->add_comment($comment); - $bug->update($bug->creation_ts); + $comment + .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; } + $bug->add_comment($comment); + $bug->update($bug->creation_ts); + } - foreach my $bug_id (@send_mail) { - Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user }); - } + foreach my $bug_id (@send_mail) { + Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + } } sub _file_child_bug { - my ($params) = @_; - my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, $dep_errors, $send_mail) - = @$params{qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail)}; - - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $new_bug; - eval { - my $comment; - my $full_template = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl"; - Bugzilla->template->process($full_template, $template_vars, \$comment) - || ThrowTemplateError(Bugzilla->template->error()); - $bug_data->{'comment'} = $comment; - if ($new_bug = Bugzilla::Bug->create($bug_data)) { - my $set_all = { - dependson => { add => [ $new_bug->bug_id ] } - }; - $parent_bug->set_all($set_all); - $parent_bug->update($parent_bug->creation_ts); - } + my ($params) = @_; + my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, + $dep_errors, $send_mail) + = @$params{ + qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail) }; - if ($@ || !($new_bug && $new_bug->{'bug_id'})) { - push(@$dep_comment, "Error creating $template_suffix review bug"); - push(@$dep_errors, "$template_suffix : $@") if $@; - # Since we performed Bugzilla::Bug::create in an eval block, we - # need to manually rollback the commit as this is not done - # in Bugzilla::Error automatically for eval'ed code. - Bugzilla->dbh->bz_rollback_transaction(); - } - else { - push(@$send_mail, $new_bug->id); - push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $new_bug; + eval { + my $comment; + my $full_template + = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl"; + Bugzilla->template->process($full_template, $template_vars, \$comment) + || ThrowTemplateError(Bugzilla->template->error()); + $bug_data->{'comment'} = $comment; + if ($new_bug = Bugzilla::Bug->create($bug_data)) { + my $set_all = {dependson => {add => [$new_bug->bug_id]}}; + $parent_bug->set_all($set_all); + $parent_bug->update($parent_bug->creation_ts); } - - undef $@; - Bugzilla->error_mode($old_error_mode); + }; + + if ($@ || !($new_bug && $new_bug->{'bug_id'})) { + push(@$dep_comment, "Error creating $template_suffix review bug"); + push(@$dep_errors, "$template_suffix : $@") if $@; + + # Since we performed Bugzilla::Bug::create in an eval block, we + # need to manually rollback the commit as this is not done + # in Bugzilla::Error automatically for eval'ed code. + Bugzilla->dbh->bz_rollback_transaction(); + } + else { + push(@$send_mail, $new_bug->id); + push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + } + + undef $@; + Bugzilla->error_mode($old_error_mode); } __PACKAGE__->NAME; diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm index fc3a689bf..ae7921af7 100644 --- a/extensions/MyDashboard/Extension.pm +++ b/extensions/MyDashboard/Extension.pm @@ -29,54 +29,53 @@ our $VERSION = BUGZILLA_VERSION; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - - my $schema = $args->{schema}; - - $schema->{'mydashboard'} = { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - mydashboard_user_id_idx => ['user_id'], - ], - }; - - $schema->{'bug_interest'} = { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1 }, - - bug_id => { TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' } }, - - user_id => { TYPE => 'INT3', - NOTNOLL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' } }, - - modification_time => { TYPE => 'DATETIME', - NOTNULL => 1 } - ], - INDEXES => [ - bug_interest_idx => { FIELDS => [qw(bug_id user_id)], - TYPE => 'UNIQUE' }, - bug_interest_user_id_idx => ['user_id'] - ], - }; + my ($self, $args) = @_; + + my $schema = $args->{schema}; + + $schema->{'mydashboard'} = { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + mydashboard_namedquery_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + mydashboard_user_id_idx => ['user_id'], + ], + }; + + $schema->{'bug_interest'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + + user_id => { + TYPE => 'INT3', + NOTNOLL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + + modification_time => {TYPE => 'DATETIME', NOTNULL => 1} + ], + INDEXES => [ + bug_interest_idx => {FIELDS => [qw(bug_id user_id)], TYPE => 'UNIQUE'}, + bug_interest_user_id_idx => ['user_id'] + ], + }; } ########### @@ -84,34 +83,35 @@ sub db_schema_abstract_schema { ########### BEGIN { - *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; - *Bugzilla::Component::watcher_ids = \&_component_watcher_ids; + *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; + *Bugzilla::Component::watcher_ids = \&_component_watcher_ids; } sub _in_mydashboard { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; - $self->{'in_mydashboard'} = $dbh->selectrow_array(" - SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", - undef, $self->id, Bugzilla->user->id); - return $self->{'in_mydashboard'}; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; + $self->{'in_mydashboard'} = $dbh->selectrow_array(" + SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", undef, + $self->id, Bugzilla->user->id); + return $self->{'in_mydashboard'}; } sub _component_watcher_ids { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - my $query = "SELECT user_id FROM component_watch + my $query = "SELECT user_id FROM component_watch WHERE product_id = ? AND (component_id = ? OR component_id IS NULL OR ? LIKE @{[$dbh->sql_string_concat('component_prefix', q{'%'})]})"; - $self->{watcher_ids} ||= $dbh->selectcol_arrayref($query, undef, - $self->product_id, $self->id, $self->name); + $self->{watcher_ids} + ||= $dbh->selectcol_arrayref($query, undef, $self->product_id, $self->id, + $self->name); - return $self->{watcher_ids}; + return $self->{watcher_ids}; } ############# @@ -119,16 +119,16 @@ sub _component_watcher_ids { ############# sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; - return if $page ne 'mydashboard.html'; + return if $page ne 'mydashboard.html'; - # require user to be logged in for this page - Bugzilla->login(LOGIN_REQUIRED); + # require user to be logged in for this page + Bugzilla->login(LOGIN_REQUIRED); - $vars->{queries} = [ QUERY_DEFS ]; + $vars->{queries} = [QUERY_DEFS]; } ######### @@ -136,115 +136,122 @@ sub page_before_template { ######### sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'saved-searches'; + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'saved-searches'; - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; - if ($save) { - my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard + if ($save) { + my $sth_insert_fp = $dbh->prepare( + 'INSERT INTO mydashboard (namedquery_id, user_id) - VALUES (?, ?)'); - my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard + VALUES (?, ?)' + ); + my $sth_delete_fp = $dbh->prepare( + 'DELETE FROM mydashboard WHERE namedquery_id = ? - AND user_id = ?'); - foreach my $q (@{$user->queries}) { - if (defined $params->{'in_mydashboard_' . $q->id}) { - $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard; - } - else { - $sth_delete_fp->execute($q->id, $user->id) if $q->in_mydashboard; - } - } + AND user_id = ?' + ); + foreach my $q (@{$user->queries}) { + if (defined $params->{'in_mydashboard_' . $q->id}) { + $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard; + } + else { + $sth_delete_fp->execute($q->id, $user->id) if $q->in_mydashboard; + } } + } } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; } sub bug_end_of_create { - my ($self, $args) = @_; - my ($bug, $params, $timestamp) = @$args{qw(bug params timestamp)}; - my $user = Bugzilla->user; - - # Anyone added to the CC list of a bug is now interested in that bug. - foreach my $cc_user (@{ $bug->cc_users }) { - next if $user->id == $cc_user->id; - Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, $timestamp); - } - - # Anyone that is watching a component is interested when a bug is filed into the component. - foreach my $watcher_id (@{ $bug->component_obj->watcher_ids }) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, $timestamp); - } + my ($self, $args) = @_; + my ($bug, $params, $timestamp) = @$args{qw(bug params timestamp)}; + my $user = Bugzilla->user; + + # Anyone added to the CC list of a bug is now interested in that bug. + foreach my $cc_user (@{$bug->cc_users}) { + next if $user->id == $cc_user->id; + Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, + $timestamp); + } + +# Anyone that is watching a component is interested when a bug is filed into the component. + foreach my $watcher_id (@{$bug->component_obj->watcher_ids}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, + $timestamp); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $changes, $timestamp) = @$args{qw(bug old_bug changes timestamp)}; - my $user = Bugzilla->user; - - # Anyone added to the CC list of a bug is now interested in that bug. - my %old_cc = map { $_->id => $_ } grep { defined } @{ $old_bug->cc_users }; - my @added = grep { not $old_cc{ $_->id } } grep { defined } @{ $bug->cc_users }; - foreach my $cc_user (@added) { - next if $user->id == $cc_user->id; - Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, $timestamp); + my ($self, $args) = @_; + my ($bug, $old_bug, $changes, $timestamp) + = @$args{qw(bug old_bug changes timestamp)}; + my $user = Bugzilla->user; + + # Anyone added to the CC list of a bug is now interested in that bug. + my %old_cc = map { $_->id => $_ } grep {defined} @{$old_bug->cc_users}; + my @added = grep { not $old_cc{$_->id} } grep {defined} @{$bug->cc_users}; + foreach my $cc_user (@added) { + next if $user->id == $cc_user->id; + Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, + $timestamp); + } + +# Anyone that is watching a component is interested when a bug is filed into the component. + if ($changes->{product} or $changes->{component}) { + + # All of the watchers would be interested in this bug update + foreach my $watcher_id (@{$bug->component_obj->watcher_ids}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, + $timestamp); } + } - # Anyone that is watching a component is interested when a bug is filed into the component. - if ($changes->{product} or $changes->{component}) { - # All of the watchers would be interested in this bug update - foreach my $watcher_id (@{ $bug->component_obj->watcher_ids }) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, $timestamp); - } - } + if ($changes->{bug_status}) { + my ($old_status, $new_status) = @{$changes->{bug_status}}; + if (is_open_state($old_status) && !is_open_state($new_status)) { + my @related_bugs = (@{$bug->blocks_obj}, @{$bug->depends_on_obj}); + my %involved; - if ($changes->{bug_status}) { - my ($old_status, $new_status) = @{ $changes->{bug_status} }; - if (is_open_state($old_status) && !is_open_state($new_status)) { - my @related_bugs = (@{ $bug->blocks_obj }, @{ $bug->depends_on_obj }); - my %involved; - - foreach my $related_bug (@related_bugs) { - my @users = grep { defined } $related_bug->assigned_to, - $related_bug->reporter, - $related_bug->qa_contact, - @{ $related_bug->cc_users }; - - foreach my $involved_user (@users) { - $involved{ $involved_user->id }{ $related_bug->id } = 1; - } - } - foreach my $involved_user_id (keys %involved) { - foreach my $related_bug_id (keys %{$involved{$involved_user_id}}) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($involved_user_id, - $related_bug_id, - $timestamp); - } - } + foreach my $related_bug (@related_bugs) { + my @users = grep {defined} $related_bug->assigned_to, $related_bug->reporter, + $related_bug->qa_contact, @{$related_bug->cc_users}; + + foreach my $involved_user (@users) { + $involved{$involved_user->id}{$related_bug->id} = 1; + } + } + foreach my $involved_user_id (keys %involved) { + foreach my $related_bug_id (keys %{$involved{$involved_user_id}}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($involved_user_id, + $related_bug_id, $timestamp); } + } } + } } sub merge_users_before { - my ($self, $args) = @_; - my $old_id = $args->{old_id}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $old_id = $args->{old_id}; + my $dbh = Bugzilla->dbh; - # If the bug_interest table has both the source user - # and destination user, then we remove the old user entry. - $dbh->do("DELETE FROM bug_interest WHERE user_id = ?", undef, $old_id); + # If the bug_interest table has both the source user + # and destination user, then we remove the old user entry. + $dbh->do("DELETE FROM bug_interest WHERE user_id = ?", undef, $old_id); } __PACKAGE__->NAME; diff --git a/extensions/MyDashboard/lib/BugInterest.pm b/extensions/MyDashboard/lib/BugInterest.pm index cf33900c5..2e427d612 100644 --- a/extensions/MyDashboard/lib/BugInterest.pm +++ b/extensions/MyDashboard/lib/BugInterest.pm @@ -25,47 +25,45 @@ use constant LIST_ORDER => 'id'; use constant NAME_FIELD => 'id'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; ##################################################################### # Provide accessors for our columns ##################################################################### -sub id { return $_[0]->{id} } -sub bug_id { return $_[0]->{bug_id} } -sub user_id { return $_[0]->{user_id} } +sub id { return $_[0]->{id} } +sub bug_id { return $_[0]->{bug_id} } +sub user_id { return $_[0]->{user_id} } sub modification_time { return $_[0]->{modification_time} } sub mark { - my ($class, $user_id, $bug_id, $timestamp) = @_; + my ($class, $user_id, $bug_id, $timestamp) = @_; - my ($interest) = @{ $class->match({ user_id => $user_id, - bug_id => $bug_id }) }; - if ($interest) { - $interest->set(modification_time => $timestamp); - $interest->update(); - return $interest; - } - else { - return $class->create({ - user_id => $user_id, - bug_id => $bug_id, - modification_time => $timestamp, - }); - } + my ($interest) = @{$class->match({user_id => $user_id, bug_id => $bug_id})}; + if ($interest) { + $interest->set(modification_time => $timestamp); + $interest->update(); + return $interest; + } + else { + return $class->create({ + user_id => $user_id, bug_id => $bug_id, modification_time => $timestamp, + }); + } } sub unmark { - my ($class, $user_id, $bug_id) = @_; + my ($class, $user_id, $bug_id) = @_; - my ($interest) = @{ $class->match({ user_id => $user_id, - bug_id => $bug_id }) }; - if ($interest) { - $interest->remove_from_db(); - } + my ($interest) = @{$class->match({user_id => $user_id, bug_id => $bug_id})}; + if ($interest) { + $interest->remove_from_db(); + } } 1; diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm index d77be7da4..59cb4f14e 100644 --- a/extensions/MyDashboard/lib/Queries.pm +++ b/extensions/MyDashboard/lib/Queries.pm @@ -25,11 +25,11 @@ use DateTime; use base qw(Exporter); our @EXPORT = qw( - QUERY_ORDER - SELECT_COLUMNS - QUERY_DEFS - query_bugs - query_flags + QUERY_ORDER + SELECT_COLUMNS + QUERY_DEFS + query_bugs + query_flags ); # Default sort order @@ -38,294 +38,300 @@ use constant QUERY_ORDER => ("changeddate desc", "bug_id"); # List of columns that we will be selecting. In the future this should be configurable # Share with buglist.cgi? use constant SELECT_COLUMNS => qw( - bug_id - bug_status - short_desc - changeddate + bug_id + bug_status + short_desc + changeddate ); sub QUERY_DEFS { - my $user = Bugzilla->user; - - my @query_defs = ( - { - name => 'assignedbugs', - heading => 'Assigned to You', - description => 'The bug has been assigned to you, and it is not resolved or closed.', - params => { - 'bug_status' => ['__open__'], - 'emailassigned_to1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'newbugs', - heading => 'New Reported by You', - description => 'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.', - params => { - 'bug_status' => ['UNCONFIRMED', 'NEW'], - 'emailreporter1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'inprogressbugs', - heading => "In Progress Reported by You", - description => 'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)', - params => { - 'bug_status' => [ map { $_->name } grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states()) ], - 'emailreporter1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'openccbugs', - heading => "You Are CC'd On", - description => 'You are in the CC list of the bug, so you are watching it.', - params => { - 'bug_status' => ['__open__'], - 'emailcc1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'mentorbugs', - heading => "You Are a Mentor", - description => 'You are one of the mentors for the bug.', - params => { - 'bug_status' => ['__open__'], - 'emailbug_mentor1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'lastvisitedbugs', - heading => 'Updated Since Last Visit', - description => 'Bugs updated since last visited', - mark_read => 'Mark Visited', - params => { - o1 => 'lessthan', - v1 => '%last_changed%', - f1 => 'last_visit_ts', - }, - }, - { - name => 'interestingbugs', - heading => 'Interesting Bugs', - description => 'Bugs that you may find interesting', - mark_read => 'Remove Interest', - params => { - j_top => 'OR', - f1 => 'bug_interest_ts', - o1 => 'isnotempty', - - f2 => 'last_visit_ts', - o2 => 'lessthan', - v2 => '%last_changed%', - } - }, - { - name => 'nevervisitbugs', - heading => 'Involved with and Never Visited', - description => "Bugs you've never visited, but are involved with", - mark_read => 'Mark Visited', - params => { - query_format => "advanced", - bug_status => ['__open__'],, - o1 => "isempty", - f1 => "last_visit_ts", - j2 => "OR", - f2 => "OP", - f3 => "assigned_to", - o3 => "equals", - v3 => $user->login, - o4 => "equals", - f4 => "reporter", - v4 => $user->login, - v5 => $user->login, - f5 => "qa_contact", - o5 => "equals", - o6 => "equals", - f6 => "cc", - v6 => $user->login, - f7 => "bug_mentor", - o7 => "equals", - v7 => $user->login, - f9 => "CP", - }, - }, + my $user = Bugzilla->user; + + my @query_defs = ( + { + name => 'assignedbugs', + heading => 'Assigned to You', + description => + 'The bug has been assigned to you, and it is not resolved or closed.', + params => { + 'bug_status' => ['__open__'], + 'emailassigned_to1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'newbugs', + heading => 'New Reported by You', + description => + 'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.', + params => { + 'bug_status' => ['UNCONFIRMED', 'NEW'], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'inprogressbugs', + heading => "In Progress Reported by You", + description => + 'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)', + params => { + 'bug_status' => [ + map { $_->name } + grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states()) + ], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'openccbugs', + heading => "You Are CC'd On", + description => 'You are in the CC list of the bug, so you are watching it.', + params => { + 'bug_status' => ['__open__'], + 'emailcc1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'mentorbugs', + heading => "You Are a Mentor", + description => 'You are one of the mentors for the bug.', + params => { + 'bug_status' => ['__open__'], + 'emailbug_mentor1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'lastvisitedbugs', + heading => 'Updated Since Last Visit', + description => 'Bugs updated since last visited', + mark_read => 'Mark Visited', + params => {o1 => 'lessthan', v1 => '%last_changed%', f1 => 'last_visit_ts',}, + }, + { + name => 'interestingbugs', + heading => 'Interesting Bugs', + description => 'Bugs that you may find interesting', + mark_read => 'Remove Interest', + params => { + j_top => 'OR', + f1 => 'bug_interest_ts', + o1 => 'isnotempty', + + f2 => 'last_visit_ts', + o2 => 'lessthan', + v2 => '%last_changed%', + } + }, + { + name => 'nevervisitbugs', + heading => 'Involved with and Never Visited', + description => "Bugs you've never visited, but are involved with", + mark_read => 'Mark Visited', + params => { + query_format => "advanced", + bug_status => ['__open__'], + , + o1 => "isempty", + f1 => "last_visit_ts", + j2 => "OR", + f2 => "OP", + f3 => "assigned_to", + o3 => "equals", + v3 => $user->login, + o4 => "equals", + f4 => "reporter", + v4 => $user->login, + v5 => $user->login, + f5 => "qa_contact", + o5 => "equals", + o6 => "equals", + f6 => "cc", + v6 => $user->login, + f7 => "bug_mentor", + o7 => "equals", + v7 => $user->login, + f9 => "CP", + }, + }, + ); + + if (Bugzilla->params->{'useqacontact'}) { + push( + @query_defs, + { + name => 'qacontactbugs', + heading => 'You Are QA Contact', + description => + 'You are the qa contact on this bug, and it is not resolved or closed.', + params => { + 'bug_status' => ['__open__'], + 'emailqa_contact1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + } ); - - if (Bugzilla->params->{'useqacontact'}) { - push(@query_defs, { - name => 'qacontactbugs', - heading => 'You Are QA Contact', - description => 'You are the qa contact on this bug, and it is not resolved or closed.', - params => { - 'bug_status' => ['__open__'], - 'emailqa_contact1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }); - } - - if ($user->showmybugslink) { - my $query = Bugzilla->params->{mybugstemplate}; - my $login = $user->login; - $query =~ s/%userid%/$login/; - $query =~ s/^buglist.cgi\?//; - push(@query_defs, { - name => 'mybugs', - heading => "My Bugs", - saved => 1, - params => $query, - }); - } - - foreach my $q (@{$user->queries}) { - next if !$q->in_mydashboard; - push(@query_defs, { name => $q->name, - saved => 1, - params => $q->url }); - } - - return @query_defs; + } + + if ($user->showmybugslink) { + my $query = Bugzilla->params->{mybugstemplate}; + my $login = $user->login; + $query =~ s/%userid%/$login/; + $query =~ s/^buglist.cgi\?//; + push(@query_defs, + {name => 'mybugs', heading => "My Bugs", saved => 1, params => $query,}); + } + + foreach my $q (@{$user->queries}) { + next if !$q->in_mydashboard; + push(@query_defs, {name => $q->name, saved => 1, params => $q->url}); + } + + return @query_defs; } sub query_bugs { - my $qdef = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $datetime_now = DateTime->now(time_zone => $user->timezone); - - ## HACK to remove POST - delete $ENV{REQUEST_METHOD}; - - my $params = new Bugzilla::CGI($qdef->{params}); - - my $search = new Bugzilla::Search( fields => [ SELECT_COLUMNS ], - params => scalar $params->Vars, - order => [ QUERY_ORDER ]); - my $data = $search->data; - - my @bugs; - foreach my $row (@$data) { - my $bug = {}; - foreach my $column (SELECT_COLUMNS) { - $bug->{$column} = shift @$row; - if ($column eq 'changeddate') { - my $datetime = datetime_from($bug->{$column}); - $datetime->set_time_zone($user->timezone); - $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z'); - $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now); - - # Provide a version for use by Bug.history and also for looking up last comment. - # We have to set to server's timezone and also subtract one second. - $datetime->set_time_zone(Bugzilla->local_timezone); - $datetime->subtract(seconds => 1); - $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T'); - } - } - push(@bugs, $bug); + my $qdef = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $datetime_now = DateTime->now(time_zone => $user->timezone); + + ## HACK to remove POST + delete $ENV{REQUEST_METHOD}; + + my $params = new Bugzilla::CGI($qdef->{params}); + + my $search = new Bugzilla::Search( + fields => [SELECT_COLUMNS], + params => scalar $params->Vars, + order => [QUERY_ORDER] + ); + my $data = $search->data; + + my @bugs; + foreach my $row (@$data) { + my $bug = {}; + foreach my $column (SELECT_COLUMNS) { + $bug->{$column} = shift @$row; + if ($column eq 'changeddate') { + my $datetime = datetime_from($bug->{$column}); + $datetime->set_time_zone($user->timezone); + $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z'); + $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now); + + # Provide a version for use by Bug.history and also for looking up last comment. + # We have to set to server's timezone and also subtract one second. + $datetime->set_time_zone(Bugzilla->local_timezone); + $datetime->subtract(seconds => 1); + $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T'); + } } + push(@bugs, $bug); + } - return (\@bugs, $params->canonicalise_query()); + return (\@bugs, $params->canonicalise_query()); } sub query_flags { - my ($type) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $datetime_now = DateTime->now(time_zone => $user->timezone); - - ($type ne 'requestee' || $type ne 'requester') - || ThrowCodeError('param_required', { param => 'type' }); - - my $match_params = { status => '?' }; - - if ($type eq 'requestee') { - $match_params->{'requestee_id'} = $user->id; - } - else { - $match_params->{'setter_id'} = $user->id; - } - - my $matched = Bugzilla::Flag->match($match_params); - - return [] if !@$matched; - - my @unfiltered_flags; - my %all_bugs; # Use hash to filter out duplicates - foreach my $flag (@$matched) { - next if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider); - - my $data = { - id => $flag->id, - type => $flag->type->name, - status => $flag->status, - attach_id => $flag->attach_id, - is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0, - bug_id => $flag->bug_id, - requester => $flag->setter->login, - requestee => $flag->requestee ? $flag->requestee->login : '', - updated => $flag->modification_date, - }; - push(@unfiltered_flags, $data); - - # Record bug id for later retrieval of status/summary - $all_bugs{$flag->{'bug_id'}}++; - } - - # Filter the bug list based on permission to see the bug - my %visible_bugs = map { $_ => 1 } @{ $user->visible_bugs([ keys %all_bugs ]) }; - - return [] if !scalar keys %visible_bugs; - - # Get all bug statuses and summaries in one query instead of loading - # many separate bug objects - my $bug_rows = $dbh->selectall_arrayref("SELECT bug_id, bug_status, short_desc + my ($type) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $datetime_now = DateTime->now(time_zone => $user->timezone); + + ($type ne 'requestee' || $type ne 'requester') + || ThrowCodeError('param_required', {param => 'type'}); + + my $match_params = {status => '?'}; + + if ($type eq 'requestee') { + $match_params->{'requestee_id'} = $user->id; + } + else { + $match_params->{'setter_id'} = $user->id; + } + + my $matched = Bugzilla::Flag->match($match_params); + + return [] if !@$matched; + + my @unfiltered_flags; + my %all_bugs; # Use hash to filter out duplicates + foreach my $flag (@$matched) { + next + if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider); + + my $data = { + id => $flag->id, + type => $flag->type->name, + status => $flag->status, + attach_id => $flag->attach_id, + is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0, + bug_id => $flag->bug_id, + requester => $flag->setter->login, + requestee => $flag->requestee ? $flag->requestee->login : '', + updated => $flag->modification_date, + }; + push(@unfiltered_flags, $data); + + # Record bug id for later retrieval of status/summary + $all_bugs{$flag->{'bug_id'}}++; + } + + # Filter the bug list based on permission to see the bug + my %visible_bugs = map { $_ => 1 } @{$user->visible_bugs([keys %all_bugs])}; + + return [] if !scalar keys %visible_bugs; + + # Get all bug statuses and summaries in one query instead of loading + # many separate bug objects + my $bug_rows = $dbh->selectall_arrayref( + "SELECT bug_id, bug_status, short_desc FROM bugs - WHERE " . $dbh->sql_in('bug_id', [ keys %visible_bugs ]), - { Slice => {} }); - foreach my $row (@$bug_rows) { - $visible_bugs{$row->{'bug_id'}} = { - bug_status => $row->{'bug_status'}, - short_desc => $row->{'short_desc'} - }; - } - - # Now drop out any flags for bugs the user cannot see - # or if the user did not want to see closed bugs - my @filtered_flags; - foreach my $flag (@unfiltered_flags) { - # Skip this flag if the bug is not visible to the user - next if !$visible_bugs{$flag->{'bug_id'}}; - - # Include bug status and summary with each flag - $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'}; - $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'}; - - # Format the updated date specific to the user's timezone - # and add the fancy human readable version - my $datetime = datetime_from($flag->{'updated'}); - $datetime->set_time_zone($user->timezone); - $flag->{'updated'} = $datetime->strftime('%Y-%m-%d %T %Z'); - $flag->{'updated_epoch'} = $datetime->epoch; - $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now); - - push(@filtered_flags, $flag); - } - - return [] if !@filtered_flags; - - # Sort by most recently updated - return [ sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } @filtered_flags ]; + WHERE " + . $dbh->sql_in('bug_id', [keys %visible_bugs]), {Slice => {}} + ); + foreach my $row (@$bug_rows) { + $visible_bugs{$row->{'bug_id'}} + = {bug_status => $row->{'bug_status'}, short_desc => $row->{'short_desc'}}; + } + + # Now drop out any flags for bugs the user cannot see + # or if the user did not want to see closed bugs + my @filtered_flags; + foreach my $flag (@unfiltered_flags) { + + # Skip this flag if the bug is not visible to the user + next if !$visible_bugs{$flag->{'bug_id'}}; + + # Include bug status and summary with each flag + $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'}; + $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'}; + + # Format the updated date specific to the user's timezone + # and add the fancy human readable version + my $datetime = datetime_from($flag->{'updated'}); + $datetime->set_time_zone($user->timezone); + $flag->{'updated'} = $datetime->strftime('%Y-%m-%d %T %Z'); + $flag->{'updated_epoch'} = $datetime->epoch; + $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now); + + push(@filtered_flags, $flag); + } + + return [] if !@filtered_flags; + + # Sort by most recently updated + return [sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } + @filtered_flags]; } 1; diff --git a/extensions/MyDashboard/lib/Util.pm b/extensions/MyDashboard/lib/Util.pm index 77d9505cb..f2e734b63 100644 --- a/extensions/MyDashboard/lib/Util.pm +++ b/extensions/MyDashboard/lib/Util.pm @@ -17,36 +17,40 @@ use Bugzilla::Status; use base qw(Exporter); @Bugzilla::Extension::MyDashboard::Util::EXPORT = qw( - open_states - closed_states - quoted_open_states - quoted_closed_states + open_states + closed_states + quoted_open_states + quoted_closed_states ); our $_open_states; + sub open_states { - $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 }); - return wantarray ? @$_open_states : $_open_states; + $_open_states ||= Bugzilla::Status->match({is_open => 1, isactive => 1}); + return wantarray ? @$_open_states : $_open_states; } our $_quoted_open_states; + sub quoted_open_states { - my $dbh = Bugzilla->dbh; - $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ]; - return wantarray ? @$_quoted_open_states : $_quoted_open_states; + my $dbh = Bugzilla->dbh; + $_quoted_open_states ||= [map { $dbh->quote($_->name) } open_states()]; + return wantarray ? @$_quoted_open_states : $_quoted_open_states; } our $_closed_states; + sub closed_states { - $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 }); - return wantarray ? @$_closed_states : $_closed_states; + $_closed_states ||= Bugzilla::Status->match({is_open => 0, isactive => 1}); + return wantarray ? @$_closed_states : $_closed_states; } our $_quoted_closed_states; + sub quoted_closed_states { - my $dbh = Bugzilla->dbh; - $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ]; - return wantarray ? @$_quoted_closed_states : $_quoted_closed_states; + my $dbh = Bugzilla->dbh; + $_quoted_closed_states ||= [map { $dbh->quote($_->name) } closed_states()]; + return wantarray ? @$_quoted_closed_states : $_quoted_closed_states; } 1; diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm index 5407c1d0b..6638bacf2 100644 --- a/extensions/MyDashboard/lib/WebService.pm +++ b/extensions/MyDashboard/lib/WebService.pm @@ -17,149 +17,149 @@ use Bugzilla::Error; use Bugzilla::Util qw(detaint_natural trick_taint template_var datetime_from); use Bugzilla::WebService::Util qw(validate); -use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS query_bugs query_flags); +use Bugzilla::Extension::MyDashboard::Queries + qw(QUERY_DEFS query_bugs query_flags); use Bugzilla::Extension::MyDashboard::BugInterest; use constant READ_ONLY => qw( - run_bug_query - run_flag_query + run_bug_query + run_flag_query ); use constant PUBLIC_METHODS => qw( - bug_interest_unmark - run_bug_query - run_flag_query - run_last_changes + bug_interest_unmark + run_bug_query + run_flag_query + run_last_changes ); sub run_last_changes { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); - trick_taint($params->{changeddate_api}); - trick_taint($params->{bug_id}); + trick_taint($params->{changeddate_api}); + trick_taint($params->{bug_id}); - my $last_comment_sql = " + my $last_comment_sql = " SELECT comment_id FROM longdescs WHERE bug_id = ? AND bug_when > ?"; - if (!$user->is_insider) { - $last_comment_sql .= " AND isprivate = 0"; + if (!$user->is_insider) { + $last_comment_sql .= " AND isprivate = 0"; + } + $last_comment_sql .= " LIMIT 1"; + my $last_comment_sth = $dbh->prepare($last_comment_sql); + + my $last_changes = {}; + my $activity + = $self->history({ + ids => [$params->{bug_id}], new_since => $params->{changeddate_api} + }); + if (@{$activity->{bugs}[0]{history}}) { + my $change_set = $activity->{bugs}[0]{history}[0]; + $last_changes->{activity} = $change_set->{changes}; + foreach my $change (@{$last_changes->{activity}}) { + $change->{field_desc} = template_var('field_descs')->{$change->{field_name}} + || $change->{field_name}; } - $last_comment_sql .= " LIMIT 1"; - my $last_comment_sth = $dbh->prepare($last_comment_sql); - - my $last_changes = {}; - my $activity = $self->history({ ids => [ $params->{bug_id} ], - new_since => $params->{changeddate_api} }); - if (@{$activity->{bugs}[0]{history}}) { - my $change_set = $activity->{bugs}[0]{history}[0]; - $last_changes->{activity} = $change_set->{changes}; - foreach my $change (@{ $last_changes->{activity} }) { - $change->{field_desc} - = template_var('field_descs')->{$change->{field_name}} || $change->{field_name}; - } - $last_changes->{email} = $change_set->{who}; - my $datetime = datetime_from($change_set->{when}); - $datetime->set_time_zone($user->timezone); - $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); - } - my $last_comment_id = $dbh->selectrow_array( - $last_comment_sth, undef, $params->{bug_id}, $params->{changeddate_api}); - if ($last_comment_id) { - my $comments = $self->comments({ comment_ids => [ $last_comment_id ] }); - my $comment = $comments->{comments}{$last_comment_id}; - $last_changes->{comment} = $comment->{text}; - $last_changes->{email} = $comment->{creator} if !$last_changes->{email}; - my $datetime = datetime_from($comment->{creation_time}); - $datetime->set_time_zone($user->timezone); - $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); - } - - return { results => [ {last_changes => $last_changes } ] }; + $last_changes->{email} = $change_set->{who}; + my $datetime = datetime_from($change_set->{when}); + $datetime->set_time_zone($user->timezone); + $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); + } + my $last_comment_id + = $dbh->selectrow_array($last_comment_sth, undef, $params->{bug_id}, + $params->{changeddate_api}); + if ($last_comment_id) { + my $comments = $self->comments({comment_ids => [$last_comment_id]}); + my $comment = $comments->{comments}{$last_comment_id}; + $last_changes->{comment} = $comment->{text}; + $last_changes->{email} = $comment->{creator} if !$last_changes->{email}; + my $datetime = datetime_from($comment->{creation_time}); + $datetime->set_time_zone($user->timezone); + $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); + } + + return {results => [{last_changes => $last_changes}]}; } sub run_bug_query { - my($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - defined $params->{query} - || ThrowCodeError('param_required', - { function => 'MyDashboard.run_bug_query', - param => 'query' }); - - my $result; - foreach my $qdef (QUERY_DEFS) { - next if $qdef->{name} ne $params->{query}; - my ($bugs, $query_string) = query_bugs($qdef); - - # Add last changes to each bug - foreach my $b (@$bugs) { - # Set the data type properly for webservice clients - # for non-string values. - $b->{bug_id} = $self->type('int', $b->{bug_id}); - } - - $query_string =~ s/^POSTDATA=&//; - $qdef->{bugs} = $bugs; - $qdef->{buffer} = $query_string; - $result = $qdef; - last; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + defined $params->{query} + || ThrowCodeError('param_required', + {function => 'MyDashboard.run_bug_query', param => 'query'}); + + my $result; + foreach my $qdef (QUERY_DEFS) { + next if $qdef->{name} ne $params->{query}; + my ($bugs, $query_string) = query_bugs($qdef); + + # Add last changes to each bug + foreach my $b (@$bugs) { + + # Set the data type properly for webservice clients + # for non-string values. + $b->{bug_id} = $self->type('int', $b->{bug_id}); } - return { result => $result }; + $query_string =~ s/^POSTDATA=&//; + $qdef->{bugs} = $bugs; + $qdef->{buffer} = $query_string; + $result = $qdef; + last; + } + + return {result => $result}; } sub run_flag_query { - my ($self, $params) =@_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - my $type = $params->{type}; - $type || ThrowCodeError('param_required', - { function => 'MyDashboard.run_flag_query', - param => 'type' }); - - my $results = query_flags($type); - - # Set the data type properly for webservice clients - # for non-string values. - foreach my $flag (@$results) { - $flag->{id} = $self->type('int', $flag->{id}); - $flag->{attach_id} = $self->type('int', $flag->{attach_id}); - $flag->{bug_id} = $self->type('int', $flag->{bug_id}); - $flag->{is_patch} = $self->type('boolean', $flag->{is_patch}); - } - - return { result => { $type => $results }}; + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + my $type = $params->{type}; + $type + || ThrowCodeError('param_required', + {function => 'MyDashboard.run_flag_query', param => 'type'}); + + my $results = query_flags($type); + + # Set the data type properly for webservice clients + # for non-string values. + foreach my $flag (@$results) { + $flag->{id} = $self->type('int', $flag->{id}); + $flag->{attach_id} = $self->type('int', $flag->{attach_id}); + $flag->{bug_id} = $self->type('int', $flag->{bug_id}); + $flag->{is_patch} = $self->type('boolean', $flag->{is_patch}); + } + + return {result => {$type => $results}}; } sub bug_interest_unmark { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - ThrowCodeError('param_required', { function => 'MyDashboard.bug_interest_unmark', param => 'bug_ids' }) - unless $params->{bug_ids}; + ThrowCodeError('param_required', + {function => 'MyDashboard.bug_interest_unmark', param => 'bug_ids'}) + unless $params->{bug_ids}; - my @bug_ids = ref($params->{bug_ids}) ? @{$params->{bug_ids}} : ( $params->{bug_ids} ); + my @bug_ids + = ref($params->{bug_ids}) ? @{$params->{bug_ids}} : ($params->{bug_ids}); - Bugzilla->dbh->bz_start_transaction(); - foreach my $bug_id (@bug_ids) { - Bugzilla::Extension::MyDashboard::BugInterest->unmark($user->id, $bug_id); - } - Bugzilla->dbh->bz_commit_transaction(); + Bugzilla->dbh->bz_start_transaction(); + foreach my $bug_id (@bug_ids) { + Bugzilla::Extension::MyDashboard::BugInterest->unmark($user->id, $bug_id); + } + Bugzilla->dbh->bz_commit_transaction(); } sub rest_resources { - return [ - qr{^/bug_interest_unmark$}, { - PUT => { - method => 'bug_interest_unmark' - } - } - ]; + return [qr{^/bug_interest_unmark$}, {PUT => {method => 'bug_interest_unmark'}}]; } 1; diff --git a/extensions/Needinfo/Config.pm b/extensions/Needinfo/Config.pm index d523d9d78..c930134d4 100644 --- a/extensions/Needinfo/Config.pm +++ b/extensions/Needinfo/Config.pm @@ -12,10 +12,8 @@ use warnings; use constant NAME => 'Needinfo'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/Needinfo/Extension.pm b/extensions/Needinfo/Extension.pm index f3f32439e..f9702a55f 100644 --- a/extensions/Needinfo/Extension.pm +++ b/extensions/Needinfo/Extension.pm @@ -21,256 +21,265 @@ use Bugzilla::User::Setting; our $VERSION = '0.01'; BEGIN { - *Bugzilla::User::needinfo_blocked = \&_user_needinfo_blocked; + *Bugzilla::User::needinfo_blocked = \&_user_needinfo_blocked; } sub _user_needinfo_blocked { - return $_[0]->settings->{block_needinfo}->{value} eq 'on'; + return $_[0]->settings->{block_needinfo}->{value} eq 'on'; } sub install_update_db { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - if (@{ Bugzilla::FlagType::match({ name => 'needinfo' }) }) { - return; - } - - print "Creating needinfo flag ... " . - "enable the Needinfo feature by editing the flag's properties.\n"; - - # inclusions 0:0 maps to __ANY__ : __ANY__ in the UI, - # meaning needinfo is enabled for all products and components by default - my $flagtype = Bugzilla::FlagType->create({ - name => 'needinfo', - description => "Set this flag when the bug is in need of additional information", - target_type => 'bug', - cc_list => '', - sortkey => 1, - is_active => 1, - is_requestable => 1, - is_requesteeble => 1, - is_multiplicable => 0, - request_group => '', - grant_group => '', - inclusions => ['0:0'], - exclusions => [], - }); + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + if (@{Bugzilla::FlagType::match({name => 'needinfo'})}) { + return; + } + + print "Creating needinfo flag ... " + . "enable the Needinfo feature by editing the flag's properties.\n"; + + # inclusions 0:0 maps to __ANY__ : __ANY__ in the UI, + # meaning needinfo is enabled for all products and components by default + my $flagtype = Bugzilla::FlagType->create({ + name => 'needinfo', + description => + "Set this flag when the bug is in need of additional information", + target_type => 'bug', + cc_list => '', + sortkey => 1, + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 0, + request_group => '', + grant_group => '', + inclusions => ['0:0'], + exclusions => [], + }); } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'block_needinfo', - options => ['on', 'off'], - default => 'off', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'block_needinfo', + options => ['on', 'off'], + default => 'off', + category => 'Reviews and Needinfo' + }); } # Clear the needinfo? flag if comment is being given by # requestee or someone used the override flag. sub bug_start_of_update { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $old_bug = $args->{old_bug}; - - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - my $params = Bugzilla->input_params; - - if ($params->{needinfo}) { - # do a match if applicable - Bugzilla::User::match_field({ - 'needinfo_from' => { 'type' => 'multi' } - }); + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $old_bug = $args->{old_bug}; + + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + my $params = Bugzilla->input_params; + + if ($params->{needinfo}) { + + # do a match if applicable + Bugzilla::User::match_field({'needinfo_from' => {'type' => 'multi'}}); + } + + # Set needinfo_done param to true so as to not loop back here + return if $params->{needinfo_done}; + $params->{needinfo_done} = 1; + Bugzilla->input_params($params); + + my $add_needinfo = delete $params->{needinfo}; + my $needinfo_type = delete $params->{needinfo_type} // ''; + my $needinfo_from = delete $params->{needinfo_from}; + my $needinfo_role = delete $params->{needinfo_role}; + my $is_redirect = $needinfo_type eq 'redirect_to' ? 1 : 0; + my $is_private = $params->{'comment_is_private'}; + + my @needinfo_overrides; + foreach my $key (grep(/^needinfo_override_/, keys %$params)) { + my ($id) = $key =~ /(\d+)$/; + + # Should always be true if key exists (checkbox) but better to be sure + push(@needinfo_overrides, $id) if $id && $params->{$key}; + } + + # Set the needinfo flag if user is requesting more information + my @new_flags; + my $needinfo_requestee; + + if ($add_needinfo) { + foreach my $type (@{$bug->flag_types}) { + next if $type->name ne 'needinfo'; + my %requestees; + + # Allow anyone to be the requestee + if (!$needinfo_role) { + $requestees{'anyone'} = 1; + } + + # Use assigned_to as requestee + elsif ($needinfo_role eq 'assigned_to') { + $requestees{$bug->assigned_to->login} = 1; + } + + # Use reporter as requestee + elsif ($needinfo_role eq 'reporter') { + $requestees{$bug->reporter->login} = 1; + } + + # Use qa_contact as requestee + elsif ($needinfo_role eq 'qa_contact') { + $requestees{$bug->qa_contact->login} = 1; + } + + # Use current user as requestee + elsif ($needinfo_role eq 'user') { + $requestees{$user->login} = 1; + } + elsif ($needinfo_role eq 'triage_owner') { + if ($bug->component_obj->triage_owner_id) { + $requestees{$bug->component_obj->triage_owner->login} = 1; + } + } + + # Use user specified requestee + elsif ($needinfo_role eq 'other' && $needinfo_from) { + my @needinfo_from_list + = ref $needinfo_from ? @$needinfo_from : ($needinfo_from); + foreach my $requestee (@needinfo_from_list) { + my $requestee_obj = Bugzilla::User->check($requestee); + $requestees{$requestee_obj->login} = 1; + } + } + + # Requestee is a mentor + elsif ($needinfo_role + && Bugzilla::User->check({name => $needinfo_role, cache => 1})) + { + $requestees{$needinfo_role} = 1; + } + + # Find out if the requestee has already been used and skip if so + my $requestee_found; + foreach my $flag (@{$type->{flags}}) { + if (!$flag->requestee && $requestees{'anyone'}) { + delete $requestees{'anyone'}; + } + if ($flag->requestee && $requestees{$flag->requestee->login}) { + delete $requestees{$flag->requestee->login}; + } + } + + foreach my $requestee (keys %requestees) { + my $needinfo_flag = {type_id => $type->id, status => '?'}; + if ($requestee ne 'anyone') { + _check_requestee($requestee); + $needinfo_flag->{requestee} = $requestee; + my $requestee_obj = Bugzilla::User->check($requestee); + if (!$requestee_obj->can_see_bug($bug->id)) { + $bug->add_cc($requestee_obj); + } + } + push(@new_flags, $needinfo_flag); + } } + } - # Set needinfo_done param to true so as to not loop back here - return if $params->{needinfo_done}; - $params->{needinfo_done} = 1; - Bugzilla->input_params($params); - - my $add_needinfo = delete $params->{needinfo}; - my $needinfo_type = delete $params->{needinfo_type} // ''; - my $needinfo_from = delete $params->{needinfo_from}; - my $needinfo_role = delete $params->{needinfo_role}; - my $is_redirect = $needinfo_type eq 'redirect_to' ? 1 : 0; - my $is_private = $params->{'comment_is_private'}; - - my @needinfo_overrides; - foreach my $key (grep(/^needinfo_override_/, keys %$params)) { - my ($id) = $key =~ /(\d+)$/; - # Should always be true if key exists (checkbox) but better to be sure - push(@needinfo_overrides, $id) if $id && $params->{$key}; - } + my @flags; + foreach my $flag (@{$bug->flags}) { + next if $flag->type->name ne 'needinfo'; - # Set the needinfo flag if user is requesting more information - my @new_flags; - my $needinfo_requestee; - - if ($add_needinfo) { - foreach my $type (@{ $bug->flag_types }) { - next if $type->name ne 'needinfo'; - my %requestees; - - # Allow anyone to be the requestee - if (!$needinfo_role) { - $requestees{'anyone'} = 1; - } - # Use assigned_to as requestee - elsif ($needinfo_role eq 'assigned_to') { - $requestees{$bug->assigned_to->login} = 1; - } - # Use reporter as requestee - elsif ($needinfo_role eq 'reporter') { - $requestees{$bug->reporter->login} = 1; - } - # Use qa_contact as requestee - elsif ($needinfo_role eq 'qa_contact') { - $requestees{$bug->qa_contact->login} = 1; - } - # Use current user as requestee - elsif ($needinfo_role eq 'user') { - $requestees{$user->login} = 1; - } - elsif ($needinfo_role eq 'triage_owner') { - if ($bug->component_obj->triage_owner_id) { - $requestees{$bug->component_obj->triage_owner->login} = 1; - } - } - # Use user specified requestee - elsif ($needinfo_role eq 'other' && $needinfo_from) { - my @needinfo_from_list = ref $needinfo_from - ? @$needinfo_from : - ($needinfo_from); - foreach my $requestee (@needinfo_from_list) { - my $requestee_obj = Bugzilla::User->check($requestee); - $requestees{$requestee_obj->login} = 1; - } - } - # Requestee is a mentor - elsif ($needinfo_role - && Bugzilla::User->check({ name => $needinfo_role, cache => 1 })) - { - $requestees{$needinfo_role} = 1; - } - - # Find out if the requestee has already been used and skip if so - my $requestee_found; - foreach my $flag (@{ $type->{flags} }) { - if (!$flag->requestee && $requestees{'anyone'}) { - delete $requestees{'anyone'}; - } - if ($flag->requestee && $requestees{$flag->requestee->login}) { - delete $requestees{$flag->requestee->login}; - } - } - - foreach my $requestee (keys %requestees) { - my $needinfo_flag = { type_id => $type->id, status => '?' }; - if ($requestee ne 'anyone') { - _check_requestee($requestee); - $needinfo_flag->{requestee} = $requestee; - my $requestee_obj = Bugzilla::User->check($requestee); - if (!$requestee_obj->can_see_bug($bug->id)) { - $bug->add_cc($requestee_obj); - } - } - push(@new_flags, $needinfo_flag); - } - } + # Clear if somehow the flag has been set to +/- + # or if the "clear needinfo" override checkbox is selected + if ($flag->status ne '?' or grep { $_ == $flag->id } @needinfo_overrides) { + push(@flags, {id => $flag->id, status => 'X'}); } + } - my @flags; - foreach my $flag (@{ $bug->flags }) { - next if $flag->type->name ne 'needinfo'; - # Clear if somehow the flag has been set to +/- - # or if the "clear needinfo" override checkbox is selected - if ($flag->status ne '?' - or grep { $_ == $flag->id } @needinfo_overrides) - { - push(@flags, { id => $flag->id, status => 'X' }); - } - } + if ($is_redirect && scalar(@new_flags) == 1) { - if ($is_redirect && scalar(@new_flags) == 1) { - # Find the current user's needinfo request - foreach my $flag (@{ $bug->flags }) { - next unless $flag->type->name eq 'needinfo' - && $flag->requestee - && $flag->requestee->id == $user->id; - # Setting the id on new_flag updates the existing flag instead of - # creating a new one. - $new_flags[0]->{id} = $flag->id; - last; - } - } + # Find the current user's needinfo request + foreach my $flag (@{$bug->flags}) { + next + unless $flag->type->name eq 'needinfo' + && $flag->requestee + && $flag->requestee->id == $user->id; - if (@flags || @new_flags) { - $bug->set_flags(\@flags, \@new_flags); + # Setting the id on new_flag updates the existing flag instead of + # creating a new one. + $new_flags[0]->{id} = $flag->id; + last; } + } + + if (@flags || @new_flags) { + $bug->set_flags(\@flags, \@new_flags); + } } sub _check_requestee { - my ($requestee) = @_; - my $user = ref($requestee) - ? $requestee - : Bugzilla::User->new({ name => $requestee, cache => 1 }); - if ($user->needinfo_blocked) { - ThrowUserError('needinfo_blocked', { requestee => $user }); - } + my ($requestee) = @_; + my $user + = ref($requestee) + ? $requestee + : Bugzilla::User->new({name => $requestee, cache => 1}); + if ($user->needinfo_blocked) { + ThrowUserError('needinfo_blocked', {requestee => $user}); + } } sub object_end_of_create { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo' - && $object->requestee; - _check_requestee($object->requestee); + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless $object->isa('Bugzilla::Flag') + && $object->type->name eq 'needinfo' + && $object->requestee; + _check_requestee($object->requestee); } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless exists $args->{changes}->{requestee_id} - && $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo' - && $object->requestee; - _check_requestee($object->requestee); + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless exists $args->{changes}->{requestee_id} + && $object->isa('Bugzilla::Flag') + && $object->type->name eq 'needinfo' + && $object->requestee; + _check_requestee($object->requestee); } sub object_before_delete { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo'; - my $user = Bugzilla->user; - - # Require canconfirm to clear requests targetted at someone else - if ($object->setter_id != $user->id - && $object->requestee - && $object->requestee->id != $user->id - && !$user->in_group('canconfirm')) - { - ThrowUserError('needinfo_illegal_change'); - } + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless $object->isa('Bugzilla::Flag') && $object->type->name eq 'needinfo'; + my $user = Bugzilla->user; + + # Require canconfirm to clear requests targetted at someone else + if ( $object->setter_id != $user->id + && $object->requestee + && $object->requestee->id != $user->id + && !$user->in_group('canconfirm')) + { + ThrowUserError('needinfo_illegal_change'); + } } sub user_preferences { - my ($self, $args) = @_; - return unless - $args->{current_tab} eq 'account' - && $args->{save_changes}; - - my $input = Bugzilla->input_params; - my $settings = Bugzilla->user->settings; - - my $value = $input->{block_needinfo} ? 'on' : 'off'; - $settings->{block_needinfo}->validate_value($value); - $settings->{block_needinfo}->set($value); - clear_settings_cache(Bugzilla->user->id); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'account' && $args->{save_changes}; + + my $input = Bugzilla->input_params; + my $settings = Bugzilla->user->settings; + + my $value = $input->{block_needinfo} ? 'on' : 'off'; + $settings->{block_needinfo}->validate_value($value); + $settings->{block_needinfo}->set($value); + clear_settings_cache(Bugzilla->user->id); } __PACKAGE__->NAME; diff --git a/extensions/OldBugMove/Extension.pm b/extensions/OldBugMove/Extension.pm index 1fa96c1ef..aa3d1aab0 100644 --- a/extensions/OldBugMove/Extension.pm +++ b/extensions/OldBugMove/Extension.pm @@ -39,172 +39,172 @@ use constant VERSION => BUGZILLA_VERSION; use constant CMT_MOVED_TO => 4; sub install_update_db { - my $reso_type = Bugzilla::Field::Choice->type('resolution'); - my $moved_reso = $reso_type->new({ name => 'MOVED' }); - # We make the MOVED resolution inactive, so that it doesn't show up - # as a valid drop-down option. - if ($moved_reso) { - $moved_reso->set_is_active(0); - $moved_reso->update(); - } - else { - print "Creating the MOVED resolution...\n"; - $reso_type->create( - { value => 'MOVED', sortkey => '30000', isactive => 0 }); - } + my $reso_type = Bugzilla::Field::Choice->type('resolution'); + my $moved_reso = $reso_type->new({name => 'MOVED'}); + + # We make the MOVED resolution inactive, so that it doesn't show up + # as a valid drop-down option. + if ($moved_reso) { + $moved_reso->set_is_active(0); + $moved_reso->update(); + } + else { + print "Creating the MOVED resolution...\n"; + $reso_type->create({value => 'MOVED', sortkey => '30000', isactive => 0}); + } } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{'panel_modules'}; - $modules->{'OldBugMove'} = 'Bugzilla::Extension::OldBugMove::Params'; + my ($self, $args) = @_; + my $modules = $args->{'panel_modules'}; + $modules->{'OldBugMove'} = 'Bugzilla::Extension::OldBugMove::Params'; } sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; + my ($self, $args) = @_; + my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{CMT_MOVED_TO} = CMT_MOVED_TO; + my $constants = $config->{VARIABLES}{constants}; + $constants->{CMT_MOVED_TO} = CMT_MOVED_TO; - my $vars = $config->{VARIABLES}; - $vars->{oldbugmove_user_is_mover} = \&_user_is_mover; + my $vars = $config->{VARIABLES}; + $vars->{oldbugmove_user_is_mover} = \&_user_is_mover; } sub object_before_delete { - my ($self, $args) = @_; - my $object = $args->{'object'}; - if ($object->isa('Bugzilla::Field::Choice::resolution')) { - if ($object->name eq 'MOVED') { - ThrowUserError('oldbugmove_no_delete_moved'); - } + my ($self, $args) = @_; + my $object = $args->{'object'}; + if ($object->isa('Bugzilla::Field::Choice::resolution')) { + if ($object->name eq 'MOVED') { + ThrowUserError('oldbugmove_no_delete_moved'); } + } } sub object_before_set { - my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; - if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { - # Store the old value so that end_of_set can check it. - $object->{'_oldbugmove_old_resolution'} = $object->resolution; - } + my ($self, $args) = @_; + my ($object, $field) = @$args{qw(object field)}; + if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { + + # Store the old value so that end_of_set can check it. + $object->{'_oldbugmove_old_resolution'} = $object->resolution; + } } sub object_end_of_set { - my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; - if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { - my $old_value = delete $object->{'_oldbugmove_old_resolution'}; - return if $old_value eq $object->resolution; - if ($object->resolution eq 'MOVED') { - $object->add_comment('', { type => CMT_MOVED_TO, - extra_data => Bugzilla->user->login }); - } + my ($self, $args) = @_; + my ($object, $field) = @$args{qw(object field)}; + if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { + my $old_value = delete $object->{'_oldbugmove_old_resolution'}; + return if $old_value eq $object->resolution; + if ($object->resolution eq 'MOVED') { + $object->add_comment('', + {type => CMT_MOVED_TO, extra_data => Bugzilla->user->login}); } + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{'object'}; + my ($self, $args) = @_; + my $object = $args->{'object'}; - if ($object->isa('Bugzilla::Bug') and Bugzilla->input_params->{'oldbugmove'}) { - my $new_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; - $object->set_bug_status($new_status, { resolution => 'MOVED' }); - } + if ($object->isa('Bugzilla::Bug') and Bugzilla->input_params->{'oldbugmove'}) { + my $new_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; + $object->set_bug_status($new_status, {resolution => 'MOVED'}); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - if ($class->isa('Bugzilla::Comment')) { - my $extra_data_validator = $validators->{extra_data}; - $validators->{extra_data} = - sub { _check_comment_extra_data($extra_data_validator, @_) }; - } - elsif ($class->isa('Bugzilla::Bug')) { - my $reso_validator = $validators->{resolution}; - $validators->{resolution} = - sub { _check_bug_resolution($reso_validator, @_) }; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + if ($class->isa('Bugzilla::Comment')) { + my $extra_data_validator = $validators->{extra_data}; + $validators->{extra_data} + = sub { _check_comment_extra_data($extra_data_validator, @_) }; + } + elsif ($class->isa('Bugzilla::Bug')) { + my $reso_validator = $validators->{resolution}; + $validators->{resolution} = sub { _check_bug_resolution($reso_validator, @_) }; + } } sub _check_bug_resolution { - my $original_validator = shift; - my ($invocant, $resolution) = @_; - - if ($resolution eq 'MOVED' && $invocant->resolution ne 'MOVED' - && !Bugzilla->input_params->{'oldbugmove'}) - { - # MOVED has a special meaning and can only be used when - # really moving bugs to another installation. - ThrowUserError('oldbugmove_no_manual_move'); - } - - return $original_validator->(@_); + my $original_validator = shift; + my ($invocant, $resolution) = @_; + + if ( $resolution eq 'MOVED' + && $invocant->resolution ne 'MOVED' + && !Bugzilla->input_params->{'oldbugmove'}) + { + # MOVED has a special meaning and can only be used when + # really moving bugs to another installation. + ThrowUserError('oldbugmove_no_manual_move'); + } + + return $original_validator->(@_); } sub _check_comment_extra_data { - my $original_validator = shift; - my ($invocant, $extra_data, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - - if ($type == CMT_MOVED_TO) { - return Bugzilla::User->check($extra_data)->login; - } - return $original_validator->(@_); + my $original_validator = shift; + my ($invocant, $extra_data, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + + if ($type == CMT_MOVED_TO) { + return Bugzilla::User->check($extra_data)->login; + } + return $original_validator->(@_); } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $changes) = @$args{qw(bug old_bug changes)}; - if (defined $changes->{'resolution'} - and $changes->{'resolution'}->[1] eq 'MOVED') - { - $self->_move_bug($bug, $old_bug); - } + my ($self, $args) = @_; + my ($bug, $old_bug, $changes) = @$args{qw(bug old_bug changes)}; + if (defined $changes->{'resolution'} + and $changes->{'resolution'}->[1] eq 'MOVED') + { + $self->_move_bug($bug, $old_bug); + } } sub _move_bug { - my ($self, $bug, $old_bug) = @_; - - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - - _user_is_mover(Bugzilla->user) - or ThrowUserError("auth_failure", { action => 'move', - object => 'bugs' }); - - # Don't export the new status and resolution. We want the current - # ones. - local $Storable::forgive_me = 1; - my $export_me = dclone($bug); - $export_me->{bug_status} = $old_bug->bug_status; - delete $export_me->{status}; - $export_me->{resolution} = $old_bug->resolution; - - # Prepare and send all data about these bugs to the new database - my $to = Bugzilla->params->{'move-to-address'}; - $to =~ s/@/\@/; - my $from = Bugzilla->params->{'mailfrom'}; - $from =~ s/@/\@/; - my $msg = "To: $to\n"; - $msg .= "From: Bugzilla <" . $from . ">\n"; - $msg .= "Subject: Moving bug " . $bug->id . "\n\n"; - my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc', - 'attachment', 'attachmentdata'); - my %displayfields = map { $_ => 1 } @fieldlist; - my $vars = { bugs => [$export_me], displayfields => \%displayfields }; - $template->process("bug/show.xml.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); - $msg .= "\n"; - MessageToMTA($msg); + my ($self, $bug, $old_bug) = @_; + + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + + _user_is_mover(Bugzilla->user) + or ThrowUserError("auth_failure", {action => 'move', object => 'bugs'}); + + # Don't export the new status and resolution. We want the current + # ones. + local $Storable::forgive_me = 1; + my $export_me = dclone($bug); + $export_me->{bug_status} = $old_bug->bug_status; + delete $export_me->{status}; + $export_me->{resolution} = $old_bug->resolution; + + # Prepare and send all data about these bugs to the new database + my $to = Bugzilla->params->{'move-to-address'}; + $to =~ s/@/\@/; + my $from = Bugzilla->params->{'mailfrom'}; + $from =~ s/@/\@/; + my $msg = "To: $to\n"; + $msg .= "From: Bugzilla <" . $from . ">\n"; + $msg .= "Subject: Moving bug " . $bug->id . "\n\n"; + my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc', 'attachment', + 'attachmentdata'); + my %displayfields = map { $_ => 1 } @fieldlist; + my $vars = {bugs => [$export_me], displayfields => \%displayfields}; + $template->process("bug/show.xml.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); + $msg .= "\n"; + MessageToMTA($msg); } sub _user_is_mover { - my $user = shift; + my $user = shift; - my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'}); - return ($user->id and grep($_ eq $user->login, @movers)) ? 1 : 0; + my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'}); + return ($user->id and grep($_ eq $user->login, @movers)) ? 1 : 0; } __PACKAGE__->NAME; diff --git a/extensions/OldBugMove/lib/Params.pm b/extensions/OldBugMove/lib/Params.pm index a8617e347..cea3fecf6 100644 --- a/extensions/OldBugMove/lib/Params.pm +++ b/extensions/OldBugMove/lib/Params.pm @@ -38,23 +38,11 @@ use Bugzilla::Config::Common; our $sortkey = 700; use constant get_param_list => ( - { - name => 'move-to-url', - type => 't', - default => '' - }, - - { - name => 'move-to-address', - type => 't', - default => 'bugzilla-import' - }, - - { - name => 'movers', - type => 't', - default => '' - }, + {name => 'move-to-url', type => 't', default => ''}, + + {name => 'move-to-address', type => 't', default => 'bugzilla-import'}, + + {name => 'movers', type => 't', default => ''}, ); 1; diff --git a/extensions/OpenGraph/Config.pm b/extensions/OpenGraph/Config.pm index 92d0a89bc..419d5d9d3 100644 --- a/extensions/OpenGraph/Config.pm +++ b/extensions/OpenGraph/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'OpenGraph'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'OpenGraph'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/OrangeFactor/Extension.pm b/extensions/OrangeFactor/Extension.pm index 56dd5dc6e..14d9da6df 100644 --- a/extensions/OrangeFactor/Extension.pm +++ b/extensions/OrangeFactor/Extension.pm @@ -22,37 +22,40 @@ use DateTime; our $VERSION = '1.0'; sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - my $user = Bugzilla->user; - - return unless ($file eq 'bug/show-header.html.tmpl' - || $file eq 'bug/edit.html.tmpl' - || $file eq 'bug_modal/header.html.tmpl' - || $file eq 'bug_modal/edit.html.tmpl'); - return unless ($user->id - && $user->settings->{'orange_factor'}->{'value'} eq 'on'); - - # in the header we just need to set the var, - # to ensure the css and javascript get included - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - if ($bug && grep($_->name eq 'intermittent-failure', @{ $bug->keyword_objects })) { - $vars->{'orange_factor'} = 1; - $vars->{'date_start'} = ( DateTime->now() - DateTime::Duration->new( days => 7 ) )->ymd(); - $vars->{'date_end'} = DateTime->now->ymd(); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + my $user = Bugzilla->user; + + return + unless ($file eq 'bug/show-header.html.tmpl' + || $file eq 'bug/edit.html.tmpl' + || $file eq 'bug_modal/header.html.tmpl' + || $file eq 'bug_modal/edit.html.tmpl'); + return + unless ($user->id && $user->settings->{'orange_factor'}->{'value'} eq 'on'); + + # in the header we just need to set the var, + # to ensure the css and javascript get included + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + if ($bug && grep($_->name eq 'intermittent-failure', @{$bug->keyword_objects})) + { + $vars->{'orange_factor'} = 1; + $vars->{'date_start'} + = (DateTime->now() - DateTime::Duration->new(days => 7))->ymd(); + $vars->{'date_end'} = DateTime->now->ymd(); + } } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'orange_factor', - options => ['on', 'off'], - default => 'off', - category => 'User Interface' - }); + my ($self, $args) = @_; + add_setting({ + name => 'orange_factor', + options => ['on', 'off'], + default => 'off', + category => 'User Interface' + }); } __PACKAGE__->NAME; diff --git a/extensions/PhabBugz/Extension.pm b/extensions/PhabBugz/Extension.pm index c857c60ab..5622f5050 100644 --- a/extensions/PhabBugz/Extension.pm +++ b/extensions/PhabBugz/Extension.pm @@ -19,29 +19,31 @@ use Bugzilla::Extension::PhabBugz::Feed; our $VERSION = '0.01'; sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{PhabBugz} = "Bugzilla::Extension::PhabBugz::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{PhabBugz} = "Bugzilla::Extension::PhabBugz::Config"; } sub auth_delegation_confirm { - my ($self, $args) = @_; - my $phab_enabled = Bugzilla->params->{phabricator_enabled}; - my $phab_callback_url = Bugzilla->params->{phabricator_auth_callback_url}; - my $phab_app_id = Bugzilla->params->{phabricator_app_id}; - - return unless $phab_enabled; - return unless $phab_callback_url; - return unless $phab_app_id; - - if (index($args->{callback}, $phab_callback_url) == 0 && $args->{app_id} eq $phab_app_id) { - ${$args->{skip_confirmation}} = 1; - } + my ($self, $args) = @_; + my $phab_enabled = Bugzilla->params->{phabricator_enabled}; + my $phab_callback_url = Bugzilla->params->{phabricator_auth_callback_url}; + my $phab_app_id = Bugzilla->params->{phabricator_app_id}; + + return unless $phab_enabled; + return unless $phab_callback_url; + return unless $phab_app_id; + + if (index($args->{callback}, $phab_callback_url) == 0 + && $args->{app_id} eq $phab_app_id) + { + ${$args->{skip_confirmation}} = 1; + } } sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{PhabBugz} = "Bugzilla::Extension::PhabBugz::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{PhabBugz} = "Bugzilla::Extension::PhabBugz::WebService"; } # @@ -49,42 +51,25 @@ sub webservice { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'phabbugz'} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - name => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - value => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1 - } - ], - INDEXES => [ - phabbugz_idx => { - FIELDS => ['name'], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'phabbugz'} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + name => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + value => {TYPE => 'MEDIUMTEXT', NOTNULL => 1} + ], + INDEXES => [phabbugz_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugz_feed.pl"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugz_feed.pl"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } __PACKAGE__->NAME; diff --git a/extensions/PhabBugz/bin/phabbugz_feed.pl b/extensions/PhabBugz/bin/phabbugz_feed.pl index 9db491bd0..2720c9104 100755 --- a/extensions/PhabBugz/bin/phabbugz_feed.pl +++ b/extensions/PhabBugz/bin/phabbugz_feed.pl @@ -14,8 +14,8 @@ use warnings; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Extension::PhabBugz::Daemon; diff --git a/extensions/PhabBugz/lib/Config.pm b/extensions/PhabBugz/lib/Config.pm index d4b71430b..d808b607a 100644 --- a/extensions/PhabBugz/lib/Config.pm +++ b/extensions/PhabBugz/lib/Config.pm @@ -16,48 +16,40 @@ use Bugzilla::Config::Common; our $sortkey = 1300; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'phabricator_enabled', - type => 'b', - default => 0 - }, - { - name => 'phabricator_base_uri', - type => 't', - default => '', - checker => \&check_urlbase - }, - { - name => 'phabricator_api_key', - type => 't', - default => '', - }, - { - name => 'phabricator_auth_callback_url', - type => 't', - default => '', - checker => sub { - my ($url) = (@_); - return 'must be an HTTP/HTTPS absolute URL' unless $url =~ m{^https?://}; - return ''; - } - }, - { - name => 'phabricator_app_id', - type => 't', - default => '', - checker => sub { - my ($app_id) = (@_); - return 'must be a hex number' unless $app_id =~ /^[[:xdigit:]]+$/; - return ''; - } - } - ); - - return @params; + my ($class) = @_; + + my @params = ( + {name => 'phabricator_enabled', type => 'b', default => 0}, + { + name => 'phabricator_base_uri', + type => 't', + default => '', + checker => \&check_urlbase + }, + {name => 'phabricator_api_key', type => 't', default => '',}, + { + name => 'phabricator_auth_callback_url', + type => 't', + default => '', + checker => sub { + my ($url) = (@_); + return 'must be an HTTP/HTTPS absolute URL' unless $url =~ m{^https?://}; + return ''; + } + }, + { + name => 'phabricator_app_id', + type => 't', + default => '', + checker => sub { + my ($app_id) = (@_); + return 'must be a hex number' unless $app_id =~ /^[[:xdigit:]]+$/; + return ''; + } + } + ); + + return @params; } 1; diff --git a/extensions/PhabBugz/lib/Constants.pm b/extensions/PhabBugz/lib/Constants.pm index 19987de25..642c1962b 100644 --- a/extensions/PhabBugz/lib/Constants.pm +++ b/extensions/PhabBugz/lib/Constants.pm @@ -13,13 +13,13 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - PHAB_AUTOMATION_USER - PHAB_ATTACHMENT_PATTERN - PHAB_CONTENT_TYPE - PHAB_FEED_POLL_SECONDS - PHAB_USER_POLL_SECONDS - PHAB_GROUP_POLL_SECONDS - PHAB_TIMEOUT + PHAB_AUTOMATION_USER + PHAB_ATTACHMENT_PATTERN + PHAB_CONTENT_TYPE + PHAB_FEED_POLL_SECONDS + PHAB_USER_POLL_SECONDS + PHAB_GROUP_POLL_SECONDS + PHAB_TIMEOUT ); use constant PHAB_ATTACHMENT_PATTERN => qr/^phabricator-D(\d+)/; diff --git a/extensions/PhabBugz/lib/Daemon.pm b/extensions/PhabBugz/lib/Daemon.pm index ef4a00534..9f995553f 100644 --- a/extensions/PhabBugz/lib/Daemon.pm +++ b/extensions/PhabBugz/lib/Daemon.pm @@ -21,7 +21,7 @@ use File::Spec; use Pod::Usage; sub start { - newdaemon(); + newdaemon(); } # @@ -29,69 +29,72 @@ sub start { # sub gd_preconfig { - my $self = shift; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = File::Spec->catfile(bz_locations()->{datadir}, $self->{gd_progname} . ".pid"); - } - return (pidfile => $pidfile); + my $self = shift; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = File::Spec->catfile(bz_locations()->{datadir}, + $self->{gd_progname} . ".pid"); + } + return (pidfile => $pidfile); } sub gd_getopt { - my $self = shift; - $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } else { - $self->{gd_progname} = basename($0); - } - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + my $self = shift; + $self->SUPER::gd_getopt(); + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - $0 = delete $self->{_original_zero}; + my $self = shift; + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0; -}; + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; +} sub gd_redirect_output { - my $self = shift; - - my $filename = File::Spec->catfile(bz_locations()->{datadir}, $self->{gd_progname} . ".log"); + my $self = shift; + + my $filename = File::Spec->catfile(bz_locations()->{datadir}, + $self->{gd_progname} . ".log"); + open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); + close(STDOUT); + open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; + $SIG{HUP} = sub { + close(STDERR); open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - close(STDOUT); - open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; - $SIG{HUP} = sub { - close(STDERR); - open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - }; + }; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_run { - my $self = shift; - $SIG{__DIE__} = \&Carp::confess if $self->{debug}; - my $phabbugz = Bugzilla::Extension::PhabBugz::Feed->new(); - $phabbugz->is_daemon(1); - $phabbugz->start(); + my $self = shift; + $SIG{__DIE__} = \&Carp::confess if $self->{debug}; + my $phabbugz = Bugzilla::Extension::PhabBugz::Feed->new(); + $phabbugz->is_daemon(1); + $phabbugz->start(); } 1; 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-' - # 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-' + # 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; diff --git a/extensions/PhabBugz/lib/Policy.pm b/extensions/PhabBugz/lib/Policy.pm index 415ea20fb..658d0ddde 100644 --- a/extensions/PhabBugz/lib/Policy.pm +++ b/extensions/PhabBugz/lib/Policy.pm @@ -21,30 +21,22 @@ use Types::Standard -all; use Type::Utils; use Type::Params qw( compile ); -has 'phid' => ( is => 'ro', isa => Str ); -has 'type' => ( is => 'ro', isa => Str ); -has 'name' => ( is => 'ro', isa => Str ); -has 'shortName' => ( is => 'ro', isa => Str ); -has 'fullName' => ( is => 'ro', isa => Str ); -has 'href' => ( is => 'ro', isa => Maybe[Str] ); -has 'workflow' => ( is => 'ro', isa => Maybe[Str] ); -has 'icon' => ( is => 'ro', isa => Str ); -has 'default' => ( is => 'ro', isa => Str ); +has 'phid' => (is => 'ro', isa => Str); +has 'type' => (is => 'ro', isa => Str); +has 'name' => (is => 'ro', isa => Str); +has 'shortName' => (is => 'ro', isa => Str); +has 'fullName' => (is => 'ro', isa => Str); +has 'href' => (is => 'ro', isa => Maybe [Str]); +has 'workflow' => (is => 'ro', isa => Maybe [Str]); +has 'icon' => (is => 'ro', isa => Str); +has 'default' => (is => 'ro', isa => Str); has 'rules' => ( - is => 'ro', - isa => ArrayRef[ - Dict[ - action => Str, - rule => Str, - value => Maybe[ArrayRef[Str]] - ] - ] + is => 'ro', + isa => + ArrayRef [Dict [action => Str, rule => Str, value => Maybe [ArrayRef [Str]]]] ); -has 'rule_projects' => ( - is => 'lazy', - isa => ArrayRef[Project], -); +has 'rule_projects' => (is => 'lazy', isa => ArrayRef [Project],); # { # "data": [ @@ -81,64 +73,61 @@ has 'rule_projects' => ( # } # } -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub new_from_query { - state $check = compile($Invocant | ClassName, Dict[phids => ArrayRef[Str]]); - my ($class, $params) = $check->(@_); - my $result = request('policy.query', $params); - if (exists $result->{result}{data} && @{ $result->{result}{data} }) { - return $class->new($result->{result}->{data}->[0]); - } + state $check = compile($Invocant | ClassName, Dict [phids => ArrayRef [Str]]); + my ($class, $params) = $check->(@_); + my $result = request('policy.query', $params); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + return $class->new($result->{result}->{data}->[0]); + } } sub create { - state $check = compile($Invocant | ClassName, ArrayRef[Project]); - my ($class, $projects) = $check->(@_); - - my $data = { - objectType => 'DREV', - default => 'deny', - policy => [ - { - action => 'allow', - rule => 'PhabricatorSubscriptionsSubscribersPolicyRule', - }, - { - action => 'allow', - rule => 'PhabricatorDifferentialReviewersPolicyRule' - } - ] - }; - - if (@$projects) { - push @{ $data->{policy} }, { - action => 'allow', - rule => 'PhabricatorProjectsAllPolicyRule', - value => [ map { $_->phid } @$projects ], - }; - } - else { - my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({ - name => 'secure-revision' - }); - push @{ $data->{policy} }, { action => 'allow', value => $secure_revision->phid }; - } - - my $result = request('policy.create', $data); - return $class->new_from_query({ phids => [ $result->{result}{phid} ] }); + state $check = compile($Invocant | ClassName, ArrayRef [Project]); + my ($class, $projects) = $check->(@_); + + my $data = { + objectType => 'DREV', + default => 'deny', + policy => [ + {action => 'allow', rule => 'PhabricatorSubscriptionsSubscribersPolicyRule',}, + {action => 'allow', rule => 'PhabricatorDifferentialReviewersPolicyRule'} + ] + }; + + if (@$projects) { + push @{$data->{policy}}, + { + action => 'allow', + rule => 'PhabricatorProjectsAllPolicyRule', + value => [map { $_->phid } @$projects], + }; + } + else { + my $secure_revision + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'secure-revision' + }); + push @{$data->{policy}}, {action => 'allow', value => $secure_revision->phid}; + } + + my $result = request('policy.create', $data); + return $class->new_from_query({phids => [$result->{result}{phid}]}); } sub _build_rule_projects { - my ($self) = @_; - - return [] unless $self->rules; - my $rule = first { $_->{rule} =~ /PhabricatorProjects(?:All)?PolicyRule/ } @{ $self->rules }; - return [] unless $rule; - return [ - map { Bugzilla::Extension::PhabBugz::Project->new_from_query( { phids => [$_] } ) } - @{ $rule->{value} } - ]; + my ($self) = @_; + + return [] unless $self->rules; + my $rule = first { $_->{rule} =~ /PhabricatorProjects(?:All)?PolicyRule/ } + @{$self->rules}; + return [] unless $rule; + return [ + map { Bugzilla::Extension::PhabBugz::Project->new_from_query({phids => [$_]}) } + @{$rule->{value}} + ]; } -1; \ No newline at end of file +1; diff --git a/extensions/PhabBugz/lib/Project.pm b/extensions/PhabBugz/lib/Project.pm index c18708887..8af01f74e 100644 --- a/extensions/PhabBugz/lib/Project.pm +++ b/extensions/PhabBugz/lib/Project.pm @@ -24,63 +24,61 @@ use Bugzilla::Extension::PhabBugz::Util qw(request); # Initialization # ######################### -has id => ( is => 'ro', isa => Int ); -has phid => ( is => 'ro', isa => Str ); -has type => ( is => 'ro', isa => Str ); -has name => ( is => 'ro', isa => Str ); -has description => ( is => 'ro', isa => Maybe[Str] ); -has creation_ts => ( is => 'ro', isa => Str ); -has modification_ts => ( is => 'ro', isa => Str ); -has view_policy => ( is => 'ro', isa => Str ); -has edit_policy => ( is => 'ro', isa => Str ); -has join_policy => ( is => 'ro', isa => Str ); -has members_raw => ( is => 'ro', isa => ArrayRef [ Dict [ phid => Str ] ] ); -has members => ( is => 'lazy', isa => ArrayRef[PhabUser] ); - -my $Invocant = class_type { class => __PACKAGE__ }; +has id => (is => 'ro', isa => Int); +has phid => (is => 'ro', isa => Str); +has type => (is => 'ro', isa => Str); +has name => (is => 'ro', isa => Str); +has description => (is => 'ro', isa => Maybe [Str]); +has creation_ts => (is => 'ro', isa => Str); +has modification_ts => (is => 'ro', isa => Str); +has view_policy => (is => 'ro', isa => Str); +has edit_policy => (is => 'ro', isa => Str); +has join_policy => (is => 'ro', isa => Str); +has members_raw => (is => 'ro', isa => ArrayRef [Dict [phid => Str]]); +has members => (is => 'lazy', isa => ArrayRef [PhabUser]); + +my $Invocant = class_type {class => __PACKAGE__}; sub new_from_query { - my ( $class, $params ) = @_; - - my $data = { - queryKey => 'all', - attachments => { members => 1 }, - constraints => $params - }; - - my $result = request( 'project.search', $data ); - if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) { - # If name is used as a query param, we need to loop through and look - # for exact match as Conduit will tokenize the name instead of doing - # exact string match :( If name is not used, then return first one. - if ( exists $params->{name} ) { - foreach my $item ( @{ $result->{result}{data} } ) { - next if $item->{fields}{name} ne $params->{name}; - return $class->new($item); - } - } - else { - return $class->new( $result->{result}{data}[0] ); - } + my ($class, $params) = @_; + + my $data + = {queryKey => 'all', attachments => {members => 1}, constraints => $params}; + + my $result = request('project.search', $data); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + + # If name is used as a query param, we need to loop through and look + # for exact match as Conduit will tokenize the name instead of doing + # exact string match :( If name is not used, then return first one. + if (exists $params->{name}) { + foreach my $item (@{$result->{result}{data}}) { + next if $item->{fields}{name} ne $params->{name}; + return $class->new($item); + } + } + else { + return $class->new($result->{result}{data}[0]); } + } } sub BUILDARGS { - my ( $class, $params ) = @_; + my ($class, $params) = @_; - $params->{name} = $params->{fields}->{name}; - $params->{description} = $params->{fields}->{description}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - $params->{join_policy} = $params->{fields}->{policy}->{join}; - $params->{members_raw} = $params->{attachments}->{members}->{members}; + $params->{name} = $params->{fields}->{name}; + $params->{description} = $params->{fields}->{description}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + $params->{join_policy} = $params->{fields}->{policy}->{join}; + $params->{members_raw} = $params->{attachments}->{members}->{members}; - delete $params->{fields}; - delete $params->{attachments}; + delete $params->{fields}; + delete $params->{attachments}; - return $params; + return $params; } # { @@ -146,131 +144,106 @@ sub BUILDARGS { ######################### sub create { - state $check = compile( - $Invocant | ClassName, - Dict[ - name => Str, - description => Str, - view_policy => Str, - edit_policy => Str, - join_policy => Str, - ] - ); - my ( $class, $params ) = $check->(@_); - - my $name = trim($params->{name}); - my $description = $params->{description}; - my $view_policy = $params->{view_policy}; - my $edit_policy = $params->{edit_policy}; - my $join_policy = $params->{join_policy}; - - my $data = { - transactions => [ - { type => 'name', value => $name }, - { type => 'description', value => $description }, - { type => 'edit', value => $edit_policy }, - { type => 'join', value => $join_policy }, - { type => 'view', value => $view_policy }, - { type => 'icon', value => 'group' }, - { type => 'color', value => 'red' } - ] - }; - - my $result = request( 'project.edit', $data ); - - return $class->new_from_query( - { phids => [ $result->{result}{object}{phid} ] } ); + state $check = compile( + $Invocant | ClassName, + Dict [ + name => Str, + description => Str, + view_policy => Str, + edit_policy => Str, + join_policy => Str, + ] + ); + my ($class, $params) = $check->(@_); + + my $name = trim($params->{name}); + my $description = $params->{description}; + my $view_policy = $params->{view_policy}; + my $edit_policy = $params->{edit_policy}; + my $join_policy = $params->{join_policy}; + + my $data = { + transactions => [ + {type => 'name', value => $name}, + {type => 'description', value => $description}, + {type => 'edit', value => $edit_policy}, + {type => 'join', value => $join_policy}, + {type => 'view', value => $view_policy}, + {type => 'icon', value => 'group'}, + {type => 'color', value => 'red'} + ] + }; + + my $result = request('project.edit', $data); + + return $class->new_from_query({phids => [$result->{result}{object}{phid}]}); } sub update { - my ($self) = @_; - - my $data = { - objectIdentifier => $self->phid, - transactions => [] - }; - - if ( $self->{set_name} ) { - push( - @{ $data->{transactions} }, - { - type => 'name', - value => $self->{set_name} - } - ); - } + my ($self) = @_; - if ( $self->{set_description} ) { - push( - @{ $data->{transactions} }, - { - type => 'description', - value => $self->{set_description} - } - ); - } + my $data = {objectIdentifier => $self->phid, transactions => []}; - if ( $self->{set_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.set', - value => $self->{set_members} - } - ); - } - else { - if ( $self->{add_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.add', - value => $self->{add_members} - } - ); - } - - if ( $self->{remove_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.remove', - value => $self->{remove_members} - } - ); - } - } + if ($self->{set_name}) { + push(@{$data->{transactions}}, {type => 'name', value => $self->{set_name}}); + } - if ( $self->{set_policy} ) { - foreach my $name ( "view", "edit" ) { - next unless $self->{set_policy}->{$name}; - push( - @{ $data->{transactions} }, - { - type => $name, - value => $self->{set_policy}->{$name} - } - ); - } - } + if ($self->{set_description}) { + push( + @{$data->{transactions}}, + {type => 'description', value => $self->{set_description}} + ); + } - if ($self->{add_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.add', - value => $self->{add_projects} - }); + if ($self->{set_members}) { + push( + @{$data->{transactions}}, + {type => 'members.set', value => $self->{set_members}} + ); + } + else { + if ($self->{add_members}) { + push( + @{$data->{transactions}}, + {type => 'members.add', value => $self->{add_members}} + ); } - if ($self->{remove_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.remove', - value => $self->{remove_projects} - }); + if ($self->{remove_members}) { + push( + @{$data->{transactions}}, + {type => 'members.remove', value => $self->{remove_members}} + ); } + } + + if ($self->{set_policy}) { + foreach my $name ("view", "edit") { + next unless $self->{set_policy}->{$name}; + push( + @{$data->{transactions}}, + {type => $name, value => $self->{set_policy}->{$name}} + ); + } + } - my $result = request( 'project.edit', $data ); + if ($self->{add_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.add', value => $self->{add_projects}} + ); + } - return $result; + if ($self->{remove_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.remove', value => $self->{remove_projects}} + ); + } + + my $result = request('project.edit', $data); + + return $result; } ######################### @@ -278,40 +251,40 @@ sub update { ######################### sub set_name { - my ( $self, $name ) = @_; - $name = trim($name); - $self->{set_name} = $name; + my ($self, $name) = @_; + $name = trim($name); + $self->{set_name} = $name; } sub set_description { - my ( $self, $description ) = @_; - $description = trim($description); - $self->{set_description} = $description; + my ($self, $description) = @_; + $description = trim($description); + $self->{set_description} = $description; } sub add_member { - my ( $self, $member ) = @_; - $self->{add_members} ||= []; - my $member_phid = blessed $member ? $member->phid : $member; - push( @{ $self->{add_members} }, $member_phid ); + my ($self, $member) = @_; + $self->{add_members} ||= []; + my $member_phid = blessed $member ? $member->phid : $member; + push(@{$self->{add_members}}, $member_phid); } sub remove_member { - my ( $self, $member ) = @_; - $self->{remove_members} ||= []; - my $member_phid = blessed $member ? $member->phid : $member; - push( @{ $self->{remove_members} }, $member_phid ); + my ($self, $member) = @_; + $self->{remove_members} ||= []; + my $member_phid = blessed $member ? $member->phid : $member; + push(@{$self->{remove_members}}, $member_phid); } sub set_members { - my ( $self, $members ) = @_; - $self->{set_members} = [ map { blessed $_ ? $_->phid : $_ } @$members ]; + my ($self, $members) = @_; + $self->{set_members} = [map { blessed $_ ? $_->phid : $_ } @$members]; } sub set_policy { - my ( $self, $name, $policy ) = @_; - $self->{set_policy} ||= {}; - $self->{set_policy}->{$name} = $policy; + my ($self, $name, $policy) = @_; + $self->{set_policy} ||= {}; + $self->{set_policy}->{$name} = $policy; } ############ @@ -319,21 +292,17 @@ sub set_policy { ############ sub _build_members { - my ( $self ) = @_; - return [] unless $self->members_raw; + my ($self) = @_; + return [] unless $self->members_raw; - my @phids; - foreach my $member ( @{ $self->members_raw } ) { - push( @phids, $member->{phid} ); - } + my @phids; + foreach my $member (@{$self->members_raw}) { + push(@phids, $member->{phid}); + } - return [] if !@phids; + return [] if !@phids; - return Bugzilla::Extension::PhabBugz::User->match( - { - phids => \@phids - } - ); + return Bugzilla::Extension::PhabBugz::User->match({phids => \@phids}); } -1; \ No newline at end of file +1; diff --git a/extensions/PhabBugz/lib/Revision.pm b/extensions/PhabBugz/lib/Revision.pm index 6ad906829..d529c581d 100644 --- a/extensions/PhabBugz/lib/Revision.pm +++ b/extensions/PhabBugz/lib/Revision.pm @@ -27,100 +27,93 @@ use Bugzilla::Extension::PhabBugz::Util qw(request); # Initialization # ######################### -has id => ( is => 'ro', isa => Int ); -has phid => ( is => 'ro', isa => Str ); -has title => ( is => 'ro', isa => Str ); -has summary => ( is => 'ro', isa => Str ); -has status => ( is => 'ro', isa => Str ); -has creation_ts => ( is => 'ro', isa => Str ); -has modification_ts => ( is => 'ro', isa => Str ); -has author_phid => ( is => 'ro', isa => Str ); -has bug_id => ( is => 'ro', isa => Str ); -has view_policy => ( is => 'ro', isa => Str ); -has edit_policy => ( is => 'ro', isa => Str ); -has subscriber_count => ( is => 'ro', isa => Int ); -has bug => ( is => 'lazy', isa => Object ); -has author => ( is => 'lazy', isa => Object ); -has reviews => ( is => 'lazy', isa => ArrayRef [ Dict [ user => PhabUser, status => Str ] ] ); -has subscribers => ( is => 'lazy', isa => ArrayRef [PhabUser] ); -has projects => ( is => 'lazy', isa => ArrayRef [Project] ); +has id => (is => 'ro', isa => Int); +has phid => (is => 'ro', isa => Str); +has title => (is => 'ro', isa => Str); +has summary => (is => 'ro', isa => Str); +has status => (is => 'ro', isa => Str); +has creation_ts => (is => 'ro', isa => Str); +has modification_ts => (is => 'ro', isa => Str); +has author_phid => (is => 'ro', isa => Str); +has bug_id => (is => 'ro', isa => Str); +has view_policy => (is => 'ro', isa => Str); +has edit_policy => (is => 'ro', isa => Str); +has subscriber_count => (is => 'ro', isa => Int); +has bug => (is => 'lazy', isa => Object); +has author => (is => 'lazy', isa => Object); +has reviews => + (is => 'lazy', isa => ArrayRef [Dict [user => PhabUser, status => Str]]); +has subscribers => (is => 'lazy', isa => ArrayRef [PhabUser]); +has projects => (is => 'lazy', isa => ArrayRef [Project]); has reviewers_raw => ( - is => 'ro', - isa => ArrayRef [ - Dict [ - reviewerPHID => Str, - status => Str, - isBlocking => Bool | JSONBool, - actorPHID => Maybe [Str], - ], - ] + is => 'ro', + isa => ArrayRef [ + Dict [ + reviewerPHID => Str, + status => Str, + isBlocking => Bool | JSONBool, + actorPHID => Maybe [Str], + ], + ] ); has subscribers_raw => ( - is => 'ro', - isa => Dict [ - subscriberPHIDs => ArrayRef [Str], - subscriberCount => Int, - viewerIsSubscribed => Bool | JSONBool, - ] -); -has projects_raw => ( - is => 'ro', - isa => Dict [ - projectPHIDs => ArrayRef [Str] - ] + is => 'ro', + isa => Dict [ + subscriberPHIDs => ArrayRef [Str], + subscriberCount => Int, + viewerIsSubscribed => Bool | JSONBool, + ] ); +has projects_raw => (is => 'ro', isa => Dict [projectPHIDs => ArrayRef [Str]]); sub new_from_query { - my ( $class, $params ) = @_; - - my $data = { - queryKey => 'all', - attachments => { - projects => 1, - reviewers => 1, - subscribers => 1 - }, - constraints => $params - }; - - my $result = request( 'differential.revision.search', $data ); - if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) { - $result = $result->{result}{data}[0]; - - # Some values in Phabricator for bug ids may have been saved - # white whitespace so we remove any here just in case. - $result->{fields}->{'bugzilla.bug-id'} = - $result->{fields}->{'bugzilla.bug-id'} - ? trim( $result->{fields}->{'bugzilla.bug-id'} ) - : ""; - return $class->new($result); - } - - return undef; + my ($class, $params) = @_; + + my $data = { + queryKey => 'all', + attachments => {projects => 1, reviewers => 1, subscribers => 1}, + constraints => $params + }; + + my $result = request('differential.revision.search', $data); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + $result = $result->{result}{data}[0]; + + # Some values in Phabricator for bug ids may have been saved + # white whitespace so we remove any here just in case. + $result->{fields}->{'bugzilla.bug-id'} + = $result->{fields}->{'bugzilla.bug-id'} + ? trim($result->{fields}->{'bugzilla.bug-id'}) + : ""; + return $class->new($result); + } + + return undef; } sub BUILDARGS { - my ( $class, $params ) = @_; - - $params->{title} = $params->{fields}->{title}; - $params->{summary} = $params->{fields}->{summary}; - $params->{status} = $params->{fields}->{status}->{value}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{author_phid} = $params->{fields}->{authorPHID}; - $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers} // []; - $params->{subscribers_raw} = $params->{attachments}->{subscribers}; - $params->{projects_raw} = $params->{attachments}->{projects}; - $params->{subscriber_count} = - $params->{attachments}->{subscribers}->{subscriberCount}; - - delete $params->{fields}; - delete $params->{attachments}; - - return $params; + my ($class, $params) = @_; + + $params->{title} = $params->{fields}->{title}; + $params->{summary} = $params->{fields}->{summary}; + $params->{status} = $params->{fields}->{status}->{value}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{author_phid} = $params->{fields}->{authorPHID}; + $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers} + // []; + $params->{subscribers_raw} = $params->{attachments}->{subscribers}; + $params->{projects_raw} = $params->{attachments}->{projects}; + $params->{subscriber_count} + = $params->{attachments}->{subscribers}->{subscriberCount}; + + delete $params->{fields}; + delete $params->{attachments}; + + return $params; } # { @@ -185,99 +178,71 @@ sub BUILDARGS { ######################### sub update { - my ($self) = @_; - - my $data = { - objectIdentifier => $self->phid, - transactions => [] - }; - - if ( $self->{added_comments} ) { - foreach my $comment ( @{ $self->{added_comments} } ) { - push @{ $data->{transactions} }, - { - type => 'comment', - value => $comment - }; - } - } - - if ( $self->{set_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.set', - value => $self->{set_subscribers} - }; - } - - if ( $self->{add_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.add', - value => $self->{add_subscribers} - }; - } + my ($self) = @_; - if ( $self->{remove_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.remove', - value => $self->{remove_subscribers} - }; - } - - if ( $self->{set_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.set', - value => $self->{set_reviewers} - }; - } - - if ( $self->{add_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.add', - value => $self->{add_reviewers} - }; - } + my $data = {objectIdentifier => $self->phid, transactions => []}; - if ( $self->{remove_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.remove', - value => $self->{remove_reviewers} - }; + if ($self->{added_comments}) { + foreach my $comment (@{$self->{added_comments}}) { + push @{$data->{transactions}}, {type => 'comment', value => $comment}; } - - if ( $self->{set_policy} ) { - foreach my $name ( "view", "edit" ) { - next unless $self->{set_policy}->{$name}; - push @{ $data->{transactions} }, - { - type => $name, - value => $self->{set_policy}->{$name} - }; - } + } + + if ($self->{set_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.set', value => $self->{set_subscribers}}; + } + + if ($self->{add_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.add', value => $self->{add_subscribers}}; + } + + if ($self->{remove_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.remove', value => $self->{remove_subscribers}}; + } + + if ($self->{set_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.set', value => $self->{set_reviewers}}; + } + + if ($self->{add_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.add', value => $self->{add_reviewers}}; + } + + if ($self->{remove_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.remove', value => $self->{remove_reviewers}}; + } + + if ($self->{set_policy}) { + foreach my $name ("view", "edit") { + next unless $self->{set_policy}->{$name}; + push @{$data->{transactions}}, + {type => $name, value => $self->{set_policy}->{$name}}; } + } - if ($self->{add_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.add', - value => $self->{add_projects} - }); - } + if ($self->{add_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.add', value => $self->{add_projects}} + ); + } - if ($self->{remove_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.remove', - value => $self->{remove_projects} - }); - } + if ($self->{remove_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.remove', value => $self->{remove_projects}} + ); + } - my $result = request( 'differential.revision.edit', $data ); + my $result = request('differential.revision.edit', $data); - return $result; + return $result; } ######################### @@ -285,80 +250,61 @@ sub update { ######################### sub _build_bug { - my ($self) = @_; - return $self->{bug} ||= - Bugzilla::Bug->new( { id => $self->bug_id, cache => 1 } ); + my ($self) = @_; + return $self->{bug} ||= Bugzilla::Bug->new({id => $self->bug_id, cache => 1}); } sub _build_author { - my ($self) = @_; - return $self->{author} if $self->{author}; - my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query( - { - phids => [ $self->author_phid ] - } - ); - if ($phab_user) { - return $self->{author} = $phab_user; - } + my ($self) = @_; + return $self->{author} if $self->{author}; + my $phab_user + = Bugzilla::Extension::PhabBugz::User->new_from_query({ + phids => [$self->author_phid] + }); + if ($phab_user) { + return $self->{author} = $phab_user; + } } sub _build_reviews { - my ($self) = @_; + my ($self) = @_; - my %by_phid = map { $_->{reviewerPHID} => $_ } @{ $self->reviewers_raw }; - my $users = Bugzilla::Extension::PhabBugz::User->match( - { - phids => [keys %by_phid] - } - ); + my %by_phid = map { $_->{reviewerPHID} => $_ } @{$self->reviewers_raw}; + my $users + = Bugzilla::Extension::PhabBugz::User->match({phids => [keys %by_phid]}); - return [ - map { - { - user => $_, - status => $by_phid{ $_->phid }{status}, - } - } @$users - ]; + return [map { {user => $_, status => $by_phid{$_->phid}{status},} } @$users]; } sub _build_subscribers { - my ($self) = @_; + my ($self) = @_; - return $self->{subscribers} if $self->{subscribers}; - return [] unless $self->subscribers_raw->{subscriberPHIDs}; + return $self->{subscribers} if $self->{subscribers}; + return [] unless $self->subscribers_raw->{subscriberPHIDs}; - my @phids; - foreach my $phid ( @{ $self->subscribers_raw->{subscriberPHIDs} } ) { - push @phids, $phid; - } + my @phids; + foreach my $phid (@{$self->subscribers_raw->{subscriberPHIDs}}) { + push @phids, $phid; + } - my $users = Bugzilla::Extension::PhabBugz::User->match( - { - phids => \@phids - } - ); + my $users = Bugzilla::Extension::PhabBugz::User->match({phids => \@phids}); - return $self->{subscribers} = $users; + return $self->{subscribers} = $users; } sub _build_projects { - my ($self) = @_; - - return $self->{projects} if $self->{projects}; - return [] unless $self->projects_raw->{projectPHIDs}; - - my @projects; - foreach my $phid ( @{ $self->projects_raw->{projectPHIDs} } ) { - push @projects, Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - phids => [ $phid ] - } - ); - } + my ($self) = @_; - return $self->{projects} = \@projects; + return $self->{projects} if $self->{projects}; + return [] unless $self->projects_raw->{projectPHIDs}; + + my @projects; + foreach my $phid (@{$self->projects_raw->{projectPHIDs}}) { + push @projects, + Bugzilla::Extension::PhabBugz::Project->new_from_query({phids => [$phid]}); + } + + return $self->{projects} = \@projects; } ######################### @@ -366,124 +312,116 @@ sub _build_projects { ######################### sub add_comment { - my ( $self, $comment ) = @_; - $comment = trim($comment); - $self->{added_comments} ||= []; - push @{ $self->{added_comments} }, $comment; + my ($self, $comment) = @_; + $comment = trim($comment); + $self->{added_comments} ||= []; + push @{$self->{added_comments}}, $comment; } sub add_reviewer { - my ( $self, $reviewer ) = @_; - $self->{add_reviewers} ||= []; - my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; - push @{ $self->{add_reviewers} }, $reviewer_phid; + my ($self, $reviewer) = @_; + $self->{add_reviewers} ||= []; + my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; + push @{$self->{add_reviewers}}, $reviewer_phid; } sub remove_reviewer { - my ( $self, $reviewer ) = @_; - $self->{remove_reviewers} ||= []; - my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; - push @{ $self->{remove_reviewers} }, $reviewer_phid; + my ($self, $reviewer) = @_; + $self->{remove_reviewers} ||= []; + my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; + push @{$self->{remove_reviewers}}, $reviewer_phid; } sub set_reviewers { - my ( $self, $reviewers ) = @_; - $self->{set_reviewers} = [ map { $_->phid } @$reviewers ]; + my ($self, $reviewers) = @_; + $self->{set_reviewers} = [map { $_->phid } @$reviewers]; } sub add_subscriber { - my ( $self, $subscriber ) = @_; - $self->{add_subscribers} ||= []; - my $subscriber_phid = - blessed $subscriber ? $subscriber->phid : $subscriber; - push @{ $self->{add_subscribers} }, $subscriber_phid; + my ($self, $subscriber) = @_; + $self->{add_subscribers} ||= []; + my $subscriber_phid = blessed $subscriber ? $subscriber->phid : $subscriber; + push @{$self->{add_subscribers}}, $subscriber_phid; } sub remove_subscriber { - my ( $self, $subscriber ) = @_; - $self->{remove_subscribers} ||= []; - my $subscriber_phid = - blessed $subscriber ? $subscriber->phid : $subscriber; - push @{ $self->{remove_subscribers} }, $subscriber_phid; + my ($self, $subscriber) = @_; + $self->{remove_subscribers} ||= []; + my $subscriber_phid = blessed $subscriber ? $subscriber->phid : $subscriber; + push @{$self->{remove_subscribers}}, $subscriber_phid; } sub set_subscribers { - my ( $self, $subscribers ) = @_; - $self->{set_subscribers} = $subscribers; + my ($self, $subscribers) = @_; + $self->{set_subscribers} = $subscribers; } sub set_policy { - my ( $self, $name, $policy ) = @_; - $self->{set_policy} ||= {}; - $self->{set_policy}->{$name} = $policy; + my ($self, $name, $policy) = @_; + $self->{set_policy} ||= {}; + $self->{set_policy}->{$name} = $policy; } sub add_project { - my ( $self, $project ) = @_; - $self->{add_projects} ||= []; - my $project_phid = blessed $project ? $project->phid : $project; - return undef unless $project_phid; - push @{ $self->{add_projects} }, $project_phid; + my ($self, $project) = @_; + $self->{add_projects} ||= []; + my $project_phid = blessed $project ? $project->phid : $project; + return undef unless $project_phid; + push @{$self->{add_projects}}, $project_phid; } sub remove_project { - my ( $self, $project ) = @_; - $self->{remove_projects} ||= []; - my $project_phid = blessed $project ? $project->phid : $project; - return undef unless $project_phid; - push @{ $self->{remove_projects} }, $project_phid; + my ($self, $project) = @_; + $self->{remove_projects} ||= []; + my $project_phid = blessed $project ? $project->phid : $project; + return undef unless $project_phid; + push @{$self->{remove_projects}}, $project_phid; } sub make_private { - my ( $self, $project_names ) = @_; - - my $secure_revision_project = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => 'secure-revision' - } - ); - - my @set_projects; - foreach my $name (@$project_names) { - my $set_project = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => $name - } - ); - push @set_projects, $set_project; - } + my ($self, $project_names) = @_; - my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects); - $self->set_policy('view', $new_policy->phid); - $self->set_policy('edit', $new_policy->phid); + my $secure_revision_project + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'secure-revision' + }); - foreach my $project ($secure_revision_project, @set_projects) { - $self->add_project($project->phid); - } + my @set_projects; + foreach my $name (@$project_names) { + my $set_project + = Bugzilla::Extension::PhabBugz::Project->new_from_query({name => $name}); + push @set_projects, $set_project; + } - return $self; + my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects); + $self->set_policy('view', $new_policy->phid); + $self->set_policy('edit', $new_policy->phid); + + foreach my $project ($secure_revision_project, @set_projects) { + $self->add_project($project->phid); + } + + return $self; } sub make_public { - my ( $self ) = @_; + my ($self) = @_; - my $editbugs = Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => 'bmo-editbugs-team' - } - ); + my $editbugs + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'bmo-editbugs-team' + }); - $self->set_policy( 'view', 'public' ); - $self->set_policy( 'edit', ( $editbugs ? $editbugs->phid : 'users' ) ); + $self->set_policy('view', 'public'); + $self->set_policy('edit', ($editbugs ? $editbugs->phid : 'users')); - my @current_group_projects = grep { $_->name =~ /^(bmo-.*|secure-revision)$/ } @{ $self->projects }; - foreach my $project (@current_group_projects) { - $self->remove_project($project->phid); - } + my @current_group_projects + = grep { $_->name =~ /^(bmo-.*|secure-revision)$/ } @{$self->projects}; + foreach my $project (@current_group_projects) { + $self->remove_project($project->phid); + } - return $self; + return $self; } 1; diff --git a/extensions/PhabBugz/lib/Types.pm b/extensions/PhabBugz/lib/Types.pm index 493e97fbc..267b8c26a 100644 --- a/extensions/PhabBugz/lib/Types.pm +++ b/extensions/PhabBugz/lib/Types.pm @@ -11,18 +11,15 @@ use 5.10.1; use strict; use warnings; -use Type::Library - -base, - -declare => qw( Revision LinkedPhabUser PhabUser Policy Project ); +use Type::Library -base, + -declare => qw( Revision LinkedPhabUser PhabUser Policy Project ); use Type::Utils -all; use Types::Standard -all; -class_type Revision, { class => 'Bugzilla::Extension::PhabBugz::Revision' }; -class_type Policy, { class => 'Bugzilla::Extension::PhabBugz::Policy' }; -class_type Project, { class => 'Bugzilla::Extension::PhabBugz::Project' }; -class_type PhabUser, { class => 'Bugzilla::Extension::PhabBugz::User' }; -declare LinkedPhabUser, - as PhabUser, - where { is_Int($_->bugzilla_id) }; +class_type Revision, {class => 'Bugzilla::Extension::PhabBugz::Revision'}; +class_type Policy, {class => 'Bugzilla::Extension::PhabBugz::Policy'}; +class_type Project, {class => 'Bugzilla::Extension::PhabBugz::Project'}; +class_type PhabUser, {class => 'Bugzilla::Extension::PhabBugz::User'}; +declare LinkedPhabUser, as PhabUser, where { is_Int($_->bugzilla_id) }; 1; diff --git a/extensions/PhabBugz/lib/User.pm b/extensions/PhabBugz/lib/User.pm index 209425bdf..2bc2ed7dc 100644 --- a/extensions/PhabBugz/lib/User.pm +++ b/extensions/PhabBugz/lib/User.pm @@ -23,44 +23,44 @@ use Type::Params qw(compile); # Initialization # ######################### -has 'id' => ( is => 'ro', isa => Int ); -has 'type' => ( is => 'ro', isa => Str ); -has 'phid' => ( is => 'ro', isa => Str ); -has 'name' => ( is => 'ro', isa => Str ); -has 'realname' => ( is => 'ro', isa => Str ); -has 'creation_ts' => ( is => 'ro', isa => Int ); -has 'modification_ts' => ( is => 'ro', isa => Int ); -has 'roles' => ( is => 'ro', isa => ArrayRef [Str] ); -has 'view_policy' => ( is => 'ro', isa => Str ); -has 'edit_policy' => ( is => 'ro', isa => Str ); -has 'bugzilla_id' => ( is => 'ro', isa => Maybe [Int] ); -has 'bugzilla_user' => ( is => 'lazy', isa => Maybe [User] ); - -my $Invocant = class_type { class => __PACKAGE__ }; +has 'id' => (is => 'ro', isa => Int); +has 'type' => (is => 'ro', isa => Str); +has 'phid' => (is => 'ro', isa => Str); +has 'name' => (is => 'ro', isa => Str); +has 'realname' => (is => 'ro', isa => Str); +has 'creation_ts' => (is => 'ro', isa => Int); +has 'modification_ts' => (is => 'ro', isa => Int); +has 'roles' => (is => 'ro', isa => ArrayRef [Str]); +has 'view_policy' => (is => 'ro', isa => Str); +has 'edit_policy' => (is => 'ro', isa => Str); +has 'bugzilla_id' => (is => 'ro', isa => Maybe [Int]); +has 'bugzilla_user' => (is => 'lazy', isa => Maybe [User]); + +my $Invocant = class_type {class => __PACKAGE__}; sub BUILDARGS { - my ( $class, $params ) = @_; - - $params->{name} = $params->{fields}->{username}; - $params->{realname} = $params->{fields}->{realName}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{roles} = $params->{fields}->{roles}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - - delete $params->{fields}; - - my $external_accounts = - $params->{attachments}{'external-accounts'}{'external-accounts'}; - if ($external_accounts) { - my $bug_user = first { $_->{type} eq 'bmo' } @$external_accounts; - $params->{bugzilla_id} = $bug_user->{id}; - } + my ($class, $params) = @_; + + $params->{name} = $params->{fields}->{username}; + $params->{realname} = $params->{fields}->{realName}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{roles} = $params->{fields}->{roles}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + + delete $params->{fields}; - delete $params->{attachments}; + my $external_accounts + = $params->{attachments}{'external-accounts'}{'external-accounts'}; + if ($external_accounts) { + my $bug_user = first { $_->{type} eq 'bmo' } @$external_accounts; + $params->{bugzilla_id} = $bug_user->{id}; + } - return $params; + delete $params->{attachments}; + + return $params; } # { @@ -110,45 +110,45 @@ sub BUILDARGS { # } sub new_from_query { - my ( $class, $params ) = @_; - my $matches = $class->match($params); - return $matches->[0]; + my ($class, $params) = @_; + my $matches = $class->match($params); + return $matches->[0]; } sub match { - state $check = compile( $Invocant | ClassName, Dict[ ids => ArrayRef[Int] ] | Dict[ phids => ArrayRef[Str] ] ); - my ( $class, $params ) = $check->(@_); - - # BMO id search takes precedence if bugzilla_ids is used. - my $bugzilla_ids = delete $params->{ids}; - if ($bugzilla_ids) { - my $bugzilla_data = - $class->get_phab_bugzilla_ids( { ids => $bugzilla_ids } ); - $params->{phids} = [ map { $_->{phid} } @$bugzilla_data ]; + state $check = compile($Invocant | ClassName, + Dict [ids => ArrayRef [Int]] | Dict [phids => ArrayRef [Str]]); + my ($class, $params) = $check->(@_); + + # BMO id search takes precedence if bugzilla_ids is used. + my $bugzilla_ids = delete $params->{ids}; + if ($bugzilla_ids) { + my $bugzilla_data = $class->get_phab_bugzilla_ids({ids => $bugzilla_ids}); + $params->{phids} = [map { $_->{phid} } @$bugzilla_data]; + } + + return [] if !@{$params->{phids}}; + + # Look for BMO external user id in external-accounts attachment + my $data = { + constraints => {phids => $params->{phids}}, + attachments => {'external-accounts' => 1} + }; + + # We can only fetch 100 users at a time so we need to do this in lumps + my $phab_users = []; + my $result; + do { + $result = request('user.search', $data)->{result}; + if (exists $result->{data} && @{$result->{data}}) { + foreach my $user (@{$result->{data}}) { + push @$phab_users, $class->new($user); + } } + $data->{after} = $result->{cursor}->{after}; + } while ($result->{cursor}->{after}); - return [] if !@{ $params->{phids} }; - - # Look for BMO external user id in external-accounts attachment - my $data = { - constraints => { phids => $params->{phids} }, - attachments => { 'external-accounts' => 1 } - }; - - # We can only fetch 100 users at a time so we need to do this in lumps - my $phab_users = []; - my $result; - do { - $result = request( 'user.search', $data )->{result}; - if ( exists $result->{data} && @{ $result->{data} } ) { - foreach my $user ( @{ $result->{data} } ) { - push @$phab_users, $class->new($user); - } - } - $data->{after} = $result->{cursor}->{after}; - } while ($result->{cursor}->{after}); - - return $phab_users; + return $phab_users; } ################# @@ -156,48 +156,44 @@ sub match { ################# sub _build_bugzilla_user { - my ($self) = @_; - return undef unless $self->bugzilla_id; - return Bugzilla::User->new( { id => $self->bugzilla_id, cache => 1 } ); + my ($self) = @_; + return undef unless $self->bugzilla_id; + return Bugzilla::User->new({id => $self->bugzilla_id, cache => 1}); } sub get_phab_bugzilla_ids { - state $check = compile($Invocant | ClassName, Dict[ids => ArrayRef[Int]]); - my ( $class, $params ) = $check->(@_); - - my $memcache = Bugzilla->memcached; - - # Try to find the values in memcache first - my @results; - my %bugzilla_ids = map { $_ => 1 } @{ $params->{ids} }; - foreach my $bugzilla_id ( keys %bugzilla_ids ) { - my $phid = - $memcache->get( { key => "phab_user_bugzilla_id_" . $bugzilla_id } ); - if ($phid) { - push @results, { id => $bugzilla_id, phid => $phid }; - delete $bugzilla_ids{$bugzilla_id}; - } + state $check = compile($Invocant | ClassName, Dict [ids => ArrayRef [Int]]); + my ($class, $params) = $check->(@_); + + my $memcache = Bugzilla->memcached; + + # Try to find the values in memcache first + my @results; + my %bugzilla_ids = map { $_ => 1 } @{$params->{ids}}; + foreach my $bugzilla_id (keys %bugzilla_ids) { + my $phid = $memcache->get({key => "phab_user_bugzilla_id_" . $bugzilla_id}); + if ($phid) { + push @results, {id => $bugzilla_id, phid => $phid}; + delete $bugzilla_ids{$bugzilla_id}; } + } + + if (%bugzilla_ids) { + $params->{ids} = [keys %bugzilla_ids]; + + my $result = request('bugzilla.account.search', $params); - if (%bugzilla_ids) { - $params->{ids} = [ keys %bugzilla_ids ]; - - my $result = request( 'bugzilla.account.search', $params ); - - # Store new values in memcache for later retrieval - foreach my $user ( @{ $result->{result} } ) { - next if !$user->{phid}; - $memcache->set( - { - key => "phab_user_bugzilla_id_" . $user->{id}, - value => $user->{phid} - } - ); - push @results, $user; - } + # Store new values in memcache for later retrieval + foreach my $user (@{$result->{result}}) { + next if !$user->{phid}; + $memcache->set({ + key => "phab_user_bugzilla_id_" . $user->{id}, value => $user->{phid} + }); + push @results, $user; } + } - return \@results; + return \@results; } 1; diff --git a/extensions/PhabBugz/lib/Util.pm b/extensions/PhabBugz/lib/Util.pm index 32f860413..613fd3466 100644 --- a/extensions/PhabBugz/lib/Util.pm +++ b/extensions/PhabBugz/lib/Util.pm @@ -32,167 +32,166 @@ use Mojo::JSON qw(encode_json); use base qw(Exporter); our @EXPORT = qw( - create_revision_attachment - get_attachment_revisions - get_bug_role_phids - intersect - is_attachment_phab_revision - request - set_phab_user + create_revision_attachment + get_attachment_revisions + get_bug_role_phids + intersect + is_attachment_phab_revision + request + set_phab_user ); sub create_revision_attachment { - state $check = compile(Bug, Revision, Str, User); - my ( $bug, $revision, $timestamp, $submitter ) = $check->(@_); - - my $phab_base_uri = Bugzilla->params->{phabricator_base_uri}; - ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; - - my $revision_uri = $phab_base_uri . "D" . $revision->id; - - # Check for previous attachment with same revision id. - # If one matches then return it instead. This is fine as - # BMO does not contain actual diff content. - my @review_attachments = grep { is_attachment_phab_revision($_) } @{ $bug->attachments }; - my $review_attachment = first { trim($_->data) eq $revision_uri } @review_attachments; - return $review_attachment if defined $review_attachment; - - # No attachment is present, so we can now create new one - - if (!$timestamp) { - ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - } - - # If submitter, then switch to that user when creating attachment - local $submitter->{groups} = [ Bugzilla::Group->get_all ]; # We need to always be able to add attachment - my $restore_prev_user = Bugzilla->set_user($submitter, scope_guard => 1); - - my $attachment = Bugzilla::Attachment->create( - { - bug => $bug, - creation_ts => $timestamp, - data => $revision_uri, - description => $revision->title, - filename => 'phabricator-D' . $revision->id . '-url.txt', - ispatch => 0, - isprivate => 0, - mimetype => PHAB_CONTENT_TYPE, - } - ); - - # Insert a comment about the new attachment into the database. - $bug->add_comment($revision->summary, { type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - - delete $bug->{attachments}; - - return $attachment; + state $check = compile(Bug, Revision, Str, User); + my ($bug, $revision, $timestamp, $submitter) = $check->(@_); + + my $phab_base_uri = Bugzilla->params->{phabricator_base_uri}; + ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; + + my $revision_uri = $phab_base_uri . "D" . $revision->id; + + # Check for previous attachment with same revision id. + # If one matches then return it instead. This is fine as + # BMO does not contain actual diff content. + my @review_attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments}; + my $review_attachment + = first { trim($_->data) eq $revision_uri } @review_attachments; + return $review_attachment if defined $review_attachment; + + # No attachment is present, so we can now create new one + + if (!$timestamp) { + ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + } + + # If submitter, then switch to that user when creating attachment + local $submitter->{groups} = [Bugzilla::Group->get_all]; # We need to always be able to add attachment + my $restore_prev_user = Bugzilla->set_user($submitter, scope_guard => 1); + + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $revision_uri, + description => $revision->title, + filename => 'phabricator-D' . $revision->id . '-url.txt', + ispatch => 0, + isprivate => 0, + mimetype => PHAB_CONTENT_TYPE, + }); + + # Insert a comment about the new attachment into the database. + $bug->add_comment($revision->summary, + {type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id}); + + delete $bug->{attachments}; + + return $attachment; } sub intersect { - my ($list1, $list2) = @_; - my %e = map { $_ => undef } @{$list1}; - return grep { exists( $e{$_} ) } @{$list2}; + my ($list1, $list2) = @_; + my %e = map { $_ => undef } @{$list1}; + return grep { exists($e{$_}) } @{$list2}; } sub get_bug_role_phids { - state $check = compile(Bug); - my ($bug) = $check->(@_); - - my @bug_users = ( $bug->reporter ); - push(@bug_users, $bug->assigned_to) - if $bug->assigned_to->email != Bugzilla->params->{'nobody_user'}; - push(@bug_users, $bug->qa_contact) if $bug->qa_contact; - push(@bug_users, @{ $bug->cc_users }) if @{ $bug->cc_users }; - - my $phab_users = - Bugzilla::Extension::PhabBugz::User->match( - { - ids => [ map { $_->id } @bug_users ] - } - ); - - return [ map { $_->phid } @{ $phab_users } ]; + state $check = compile(Bug); + my ($bug) = $check->(@_); + + my @bug_users = ($bug->reporter); + push(@bug_users, $bug->assigned_to) + if $bug->assigned_to->email != Bugzilla->params->{'nobody_user'}; + push(@bug_users, $bug->qa_contact) if $bug->qa_contact; + push(@bug_users, @{$bug->cc_users}) if @{$bug->cc_users}; + + my $phab_users + = Bugzilla::Extension::PhabBugz::User->match({ + ids => [map { $_->id } @bug_users] + }); + + return [map { $_->phid } @{$phab_users}]; } sub is_attachment_phab_revision { - state $check = compile(Attachment); - my ($attachment) = $check->(@_); - return $attachment->contenttype eq PHAB_CONTENT_TYPE; + state $check = compile(Attachment); + my ($attachment) = $check->(@_); + return $attachment->contenttype eq PHAB_CONTENT_TYPE; } sub get_attachment_revisions { - state $check = compile(Bug); - my ($bug) = $check->(@_); + state $check = compile(Bug); + my ($bug) = $check->(@_); - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - return unless @attachments; + return unless @attachments; - my @revision_ids; - foreach my $attachment (@attachments) { - my ($revision_id) = - ( $attachment->filename =~ PHAB_ATTACHMENT_PATTERN ); - next if !$revision_id; - push( @revision_ids, int($revision_id) ); - } + my @revision_ids; + foreach my $attachment (@attachments) { + my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + next if !$revision_id; + push(@revision_ids, int($revision_id)); + } - return unless @revision_ids; + return unless @revision_ids; - my @revisions; - foreach my $revision_id (@revision_ids) { - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ - ids => [ $revision_id ] - }); - push @revisions, $revision if $revision; - } + my @revisions; + foreach my $revision_id (@revision_ids) { + my $revision + = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ + ids => [$revision_id] + }); + push @revisions, $revision if $revision; + } - return \@revisions; + return \@revisions; } sub request { - state $check = compile(Str, HashRef); - my ($method, $data) = $check->(@_); - my $request_cache = Bugzilla->request_cache; - my $params = Bugzilla->params; - - my $ua = $request_cache->{phabricator_ua}; - unless ($ua) { - $ua = $request_cache->{phabricator_ua} = Mojo::UserAgent->new; - if ($params->{proxy_url}) { - $ua->proxy($params->{proxy_url}); - } + state $check = compile(Str, HashRef); + my ($method, $data) = $check->(@_); + my $request_cache = Bugzilla->request_cache; + my $params = Bugzilla->params; + + my $ua = $request_cache->{phabricator_ua}; + unless ($ua) { + $ua = $request_cache->{phabricator_ua} = Mojo::UserAgent->new; + if ($params->{proxy_url}) { + $ua->proxy($params->{proxy_url}); } + } - my $phab_api_key = $params->{phabricator_api_key}; - ThrowUserError('invalid_phabricator_api_key') unless $phab_api_key; - my $phab_base_uri = $params->{phabricator_base_uri}; - ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; + my $phab_api_key = $params->{phabricator_api_key}; + ThrowUserError('invalid_phabricator_api_key') unless $phab_api_key; + my $phab_base_uri = $params->{phabricator_base_uri}; + ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; - my $full_uri = $phab_base_uri . '/api/' . $method; + my $full_uri = $phab_base_uri . '/api/' . $method; - $data->{__conduit__} = { token => $phab_api_key }; + $data->{__conduit__} = {token => $phab_api_key}; - my $response = $ua->post($full_uri => form => { params => encode_json($data) })->result; - ThrowCodeError('phabricator_api_error', { reason => $response->message }) - if $response->is_error; + my $response + = $ua->post($full_uri => form => {params => encode_json($data)})->result; + ThrowCodeError('phabricator_api_error', {reason => $response->message}) + if $response->is_error; - my $result = $response->json; - ThrowCodeError('phabricator_api_error', - { reason => 'JSON decode failure' }) if !defined($result); - ThrowCodeError('phabricator_api_error', - { code => $result->{error_code}, - reason => $result->{error_info} }) if $result->{error_code}; + my $result = $response->json; + ThrowCodeError('phabricator_api_error', {reason => 'JSON decode failure'}) + if !defined($result); + ThrowCodeError('phabricator_api_error', + {code => $result->{error_code}, reason => $result->{error_info}}) + if $result->{error_code}; - return $result; + return $result; } sub set_phab_user { - my $user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER } ); - $user->{groups} = [ Bugzilla::Group->get_all ]; + my $user = Bugzilla::User->new({name => PHAB_AUTOMATION_USER}); + $user->{groups} = [Bugzilla::Group->get_all]; - return Bugzilla->set_user($user, scope_guard => 1); + return Bugzilla->set_user($user, scope_guard => 1); } 1; diff --git a/extensions/PhabBugz/lib/WebService.pm b/extensions/PhabBugz/lib/WebService.pm index 19a758a70..a9115263a 100644 --- a/extensions/PhabBugz/lib/WebService.pm +++ b/extensions/PhabBugz/lib/WebService.pm @@ -26,143 +26,140 @@ use List::MoreUtils qw(any); use MIME::Base64 qw(decode_base64); use constant READ_ONLY => qw( - check_user_enter_bug_permission - check_user_permission_for_bug + check_user_enter_bug_permission + check_user_permission_for_bug ); use constant PUBLIC_METHODS => qw( - check_user_enter_bug_permission - check_user_permission_for_bug - set_build_target + check_user_enter_bug_permission + check_user_permission_for_bug + set_build_target ); sub _check_phabricator { - # Ensure PhabBugz is on - ThrowUserError('phabricator_not_enabled') - unless Bugzilla->params->{phabricator_enabled}; + + # Ensure PhabBugz is on + ThrowUserError('phabricator_not_enabled') + unless Bugzilla->params->{phabricator_enabled}; } sub _validate_phab_user { - my ($self, $user) = @_; + my ($self, $user) = @_; - $self->_check_phabricator(); + $self->_check_phabricator(); - # Validate that the requesting user's email matches phab-bot - ThrowUserError('phabricator_unauthorized_user') - unless $user->login eq PHAB_AUTOMATION_USER; + # Validate that the requesting user's email matches phab-bot + ThrowUserError('phabricator_unauthorized_user') + unless $user->login eq PHAB_AUTOMATION_USER; } sub check_user_permission_for_bug { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - # Validate that a bug id and user id are provided - ThrowUserError('phabricator_invalid_request_params') - unless ($params->{bug_id} && $params->{user_id}); + # Validate that a bug id and user id are provided + ThrowUserError('phabricator_invalid_request_params') + unless ($params->{bug_id} && $params->{user_id}); - # Validate that the user exists - my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 }); + # Validate that the user exists + my $target_user = Bugzilla::User->check({id => $params->{user_id}, cache => 1}); - # Send back an object which says { "result": 1|0 } - return { - result => $target_user->can_see_bug($params->{bug_id}) - }; + # Send back an object which says { "result": 1|0 } + return {result => $target_user->can_see_bug($params->{bug_id})}; } sub check_user_enter_bug_permission { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - # Validate that a product name and user id are provided - ThrowUserError('phabricator_invalid_request_params') - unless ($params->{product} && $params->{user_id}); + # Validate that a product name and user id are provided + ThrowUserError('phabricator_invalid_request_params') + unless ($params->{product} && $params->{user_id}); - # Validate that the user exists - my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 }); + # Validate that the user exists + my $target_user = Bugzilla::User->check({id => $params->{user_id}, cache => 1}); - # Send back an object with the attribute "result" set to 1 if the user - # can enter bugs into the given product, or 0 if not. - return { - result => $target_user->can_enter_product($params->{product}) ? 1 : 0 - }; + # Send back an object with the attribute "result" set to 1 if the user + # can enter bugs into the given product, or 0 if not. + return {result => $target_user->can_enter_product($params->{product}) ? 1 : 0}; } sub set_build_target { - my ( $self, $params ) = @_; + my ($self, $params) = @_; - # Phabricator only supports sending credentials via HTTP Basic Auth - # so we exploit that function to pass in an API key as the password - # of basic auth. BMO does not support basic auth but does support - # use of API keys. - my $http_auth = Bugzilla->cgi->http('Authorization'); - $http_auth =~ s/^Basic\s+//; - $http_auth = decode_base64($http_auth); - my ($login, $api_key) = split(':', $http_auth); - $params->{'Bugzilla_login'} = $login; - $params->{'Bugzilla_api_key'} = $api_key; + # Phabricator only supports sending credentials via HTTP Basic Auth + # so we exploit that function to pass in an API key as the password + # of basic auth. BMO does not support basic auth but does support + # use of API keys. + my $http_auth = Bugzilla->cgi->http('Authorization'); + $http_auth =~ s/^Basic\s+//; + $http_auth = decode_base64($http_auth); + my ($login, $api_key) = split(':', $http_auth); + $params->{'Bugzilla_login'} = $login; + $params->{'Bugzilla_api_key'} = $api_key; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - my $revision_id = $params->{revision_id}; - my $build_target = $params->{build_target}; + my $revision_id = $params->{revision_id}; + my $build_target = $params->{build_target}; - ThrowUserError('invalid_phabricator_revision_id') - unless detaint_natural($revision_id); + ThrowUserError('invalid_phabricator_revision_id') + unless detaint_natural($revision_id); - ThrowUserError('invalid_phabricator_build_target') - unless $build_target =~ /^PHID-HMBT-[a-zA-Z0-9]+$/; - trick_taint($build_target); + ThrowUserError('invalid_phabricator_build_target') + unless $build_target =~ /^PHID-HMBT-[a-zA-Z0-9]+$/; + trick_taint($build_target); - Bugzilla->dbh->do( - "INSERT INTO phabbugz (name, value) VALUES (?, ?)", - undef, - 'build_target_' . $revision_id, - $build_target - ); + Bugzilla->dbh->do( + "INSERT INTO phabbugz (name, value) VALUES (?, ?)", + undef, 'build_target_' . $revision_id, + $build_target + ); - return { result => 1 }; + return {result => 1}; } sub rest_resources { - return [ - # Set build target in Phabricator - qr{^/phabbugz/build_target/(\d+)/(PHID-HMBT-.*)$}, { - POST => { - method => 'set_build_target', - params => sub { - return { - revision_id => $_[0], - build_target => $_[1] - }; - } - } - }, - # Bug permission checks - qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, { - GET => { - method => 'check_user_permission_for_bug', - params => sub { - return { bug_id => $_[0], user_id => $_[1] }; - } - } - }, - qr{^/phabbugz/check_enter_bug/([^/]+)/(\d+)$}, { - GET => { - method => 'check_user_enter_bug_permission', - params => sub { - return { product => $_[0], user_id => $_[1] }; - }, - }, + return [ + # Set build target in Phabricator + qr{^/phabbugz/build_target/(\d+)/(PHID-HMBT-.*)$}, + { + POST => { + method => 'set_build_target', + params => sub { + return {revision_id => $_[0], build_target => $_[1]}; + } + } + }, + + # Bug permission checks + qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, + { + GET => { + method => 'check_user_permission_for_bug', + params => sub { + return {bug_id => $_[0], user_id => $_[1]}; + } + } + }, + qr{^/phabbugz/check_enter_bug/([^/]+)/(\d+)$}, + { + GET => { + method => 'check_user_enter_bug_permission', + params => sub { + return {product => $_[0], user_id => $_[1]}; }, - ]; + }, + }, + ]; } 1; diff --git a/extensions/PhabBugz/t/basic.t b/extensions/PhabBugz/t/basic.t index af92dc64f..d0083c275 100644 --- a/extensions/PhabBugz/t/basic.t +++ b/extensions/PhabBugz/t/basic.t @@ -11,7 +11,7 @@ use 5.10.1; use lib qw( . lib local/lib/perl5 ); use Bugzilla; -BEGIN { Bugzilla->extensions }; +BEGIN { Bugzilla->extensions } use Test::More; use Test2::Tools::Mock; @@ -30,68 +30,65 @@ our @project_members; my $User = mock 'Bugzilla::Extension::PhabBugz::User' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'match' => sub { [ mock() ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => ['match' => sub { [mock()] },], ); my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => ( - override => [ - get_group_members => sub { - return [ map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } @group_members ]; - } - ] + override => [ + get_group_members => sub { + return [map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } + @group_members]; + } + ] ); my $Project = mock 'Bugzilla::Extension::PhabBugz::Project' => ( - override_constructor => [ - new_from_query => 'ref_copy', - ], - override => [ - 'members' => sub { - return [ map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } @project_members ]; - } - ] + override_constructor => [new_from_query => 'ref_copy',], + override => [ + 'members' => sub { + return [map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } + @project_members]; + } + ] ); -local Bugzilla->params->{phabricator_enabled} = 1; -local Bugzilla->params->{phabricator_api_key} = 'FAKE-API-KEY'; +local Bugzilla->params->{phabricator_enabled} = 1; +local Bugzilla->params->{phabricator_api_key} = 'FAKE-API-KEY'; local Bugzilla->params->{phabricator_base_uri} = 'http://fake.fabricator.tld'; my $Bugzilla = mock 'Bugzilla' => ( - override => [ - 'dbh' => sub { mock() }, - 'user' => sub { Bugzilla::User->new({ name => 'phab-bot@bmo.tld' }) }, - ], + override => [ + 'dbh' => sub { mock() }, + 'user' => sub { Bugzilla::User->new({name => 'phab-bot@bmo.tld'}) }, + ], ); my $BugzillaGroup = mock 'Bugzilla::Group' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'match' => sub { [ Bugzilla::Group->fake_new(id => 1, name => 'firefox-security' ) ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => [ + 'match' => + sub { [Bugzilla::Group->fake_new(id => 1, name => 'firefox-security')] }, + ], ); my $BugzillaUser = mock 'Bugzilla::User' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'new' => sub { - my ($class, $hash) = @_; - if ($hash->{name} eq 'phab-bot@bmo.tld') { - return $class->fake_new( id => 8_675_309, login_name => 'phab-bot@bmo.tld', realname => 'Fake PhabBot' ); - } - else { - } - }, - 'match' => sub { [ mock() ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => [ + 'new' => sub { + my ($class, $hash) = @_; + if ($hash->{name} eq 'phab-bot@bmo.tld') { + return $class->fake_new( + id => 8_675_309, + login_name => 'phab-bot@bmo.tld', + realname => 'Fake PhabBot' + ); + } + else { + } + }, + 'match' => sub { [mock()] }, + ], ); @@ -99,78 +96,66 @@ my $feed = Bugzilla::Extension::PhabBugz::Feed->new; # Same members in both do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - is_deeply($data->{transactions}, [], 'no-op'); - return mock_useragent_tx('{}'); - }, - ], - ); - local @group_members = ( - { phid => 'foo' }, - ); - local @project_members = ( - { phid => 'foo' }, - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + is_deeply($data->{transactions}, [], 'no-op'); + return mock_useragent_tx('{}'); + }, + ], + ); + local @group_members = ({phid => 'foo'},); + local @project_members = ({phid => 'foo'},); + $feed->group_query; }; # Project has members not in group do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - my $expected = [ { type => 'members.remove', value => ['foo'] } ]; - is_deeply($data->{transactions}, $expected, 'remove foo'); - return mock_useragent_tx('{}'); - }, - ] - ); - local @group_members = (); - local @project_members = ( - { phid => 'foo' }, - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + my $expected = [{type => 'members.remove', value => ['foo']}]; + is_deeply($data->{transactions}, $expected, 'remove foo'); + return mock_useragent_tx('{}'); + }, + ] + ); + local @group_members = (); + local @project_members = ({phid => 'foo'},); + $feed->group_query; }; # Group has members not in project do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - my $expected = [ { type => 'members.add', value => ['foo'] } ]; - is_deeply($data->{transactions}, $expected, 'add foo'); - return mock_useragent_tx('{}'); - }, - ] - ); - local @group_members = ( - { phid => 'foo' }, - ); - local @project_members = ( - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + my $expected = [{type => 'members.add', value => ['foo']}]; + is_deeply($data->{transactions}, $expected, 'add foo'); + return mock_useragent_tx('{}'); + }, + ] + ); + local @group_members = ({phid => 'foo'},); + local @project_members = (); + $feed->group_query; }; do { - my $Revision = mock 'Bugzilla::Extension::PhabBugz::Revision' => ( - override => [ - 'update' => sub { 1 }, - ], - ); - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - if ($url =~ /differential\.revision\.search/) { - my $content = < + (override => ['update' => sub {1},],); + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + if ($url =~ /differential\.revision\.search/) { + my $content = < ( - add_constructor => [ fake_new => 'hash' ], - ); - my $Bug = mock 'Bugzilla::Bug' => ( - add_constructor => [ fake_new => 'hash' ], - ); - my $bug = Bugzilla::Bug->fake_new( - bug_id => 23, - attachments => [ - Bugzilla::Attachment->fake_new( - mimetype => 'text/x-phabricator-request', - filename => 'phabricator-D9999-url.txt', - ), - ] - ); + return mock_useragent_tx($content); + } + else { + return mock_useragent_tx("bad request"); + } + }, + ], + ); + my $Attachment + = mock 'Bugzilla::Attachment' => (add_constructor => [fake_new => 'hash'],); + my $Bug = mock 'Bugzilla::Bug' => (add_constructor => [fake_new => 'hash'],); + my $bug = Bugzilla::Bug->fake_new( + bug_id => 23, + attachments => [ + Bugzilla::Attachment->fake_new( + mimetype => 'text/x-phabricator-request', + filename => 'phabricator-D9999-url.txt', + ), + ] + ); - my $revisions = get_attachment_revisions($bug); - is(ref($revisions), 'ARRAY', 'it is an array ref'); - isa_ok($revisions->[0], 'Bugzilla::Extension::PhabBugz::Revision'); - is($revisions->[0]->bug_id, 23, 'Bugzila ID is 23'); - ok( try { $revisions->[0]->update }, 'update revision'); + my $revisions = get_attachment_revisions($bug); + is(ref($revisions), 'ARRAY', 'it is an array ref'); + isa_ok($revisions->[0], 'Bugzilla::Extension::PhabBugz::Revision'); + is($revisions->[0]->bug_id, 23, 'Bugzila ID is 23'); + ok(try { $revisions->[0]->update }, 'update revision'); }; diff --git a/extensions/PhabBugz/t/feed-daemon-guts.t b/extensions/PhabBugz/t/feed-daemon-guts.t index 0c508be98..44d65eab4 100644 --- a/extensions/PhabBugz/t/feed-daemon-guts.t +++ b/extensions/PhabBugz/t/feed-daemon-guts.t @@ -24,8 +24,11 @@ use Digest::SHA qw(sha1_hex); use ok 'Bugzilla::Extension::PhabBugz::Feed'; use ok 'Bugzilla::Extension::PhabBugz::Constants', 'PHAB_AUTOMATION_USER'; -use ok 'Bugzilla::Config', 'SetParam'; -can_ok('Bugzilla::Extension::PhabBugz::Feed', qw( group_query feed_query user_query )); +use ok 'Bugzilla::Config', 'SetParam'; +can_ok( + 'Bugzilla::Extension::PhabBugz::Feed', + qw( group_query feed_query user_query ) +); Bugzilla->error_mode(ERROR_MODE_TEST); @@ -34,127 +37,117 @@ my $phab_bot = create_user(PHAB_AUTOMATION_USER, '*'); my $UserAgent = mock 'Mojo::UserAgent' => (); { - SetParam('phabricator_enabled', 0); - my $feed = Bugzilla::Extension::PhabBugz::Feed->new; - my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => ( - override => [ - get_last_id => sub { die "get_last_id" }, - ], - ); - - foreach my $method (qw( feed_query user_query group_query )) { - try { - $feed->$method; - pass "disabling the phabricator sync: $method"; - } - catch { - fail "disabling the phabricator sync: $method"; - } + SetParam('phabricator_enabled', 0); + my $feed = Bugzilla::Extension::PhabBugz::Feed->new; + my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => + (override => [get_last_id => sub { die "get_last_id" },],); + + foreach my $method (qw( feed_query user_query group_query )) { + try { + $feed->$method; + pass "disabling the phabricator sync: $method"; + } + catch { + fail "disabling the phabricator sync: $method"; } + } } my @bad_response = ( - ['http error', mock_useragent_tx("doesn't matter", sub { $_->code(500) }) ], - ['invalid json', mock_useragent_tx('foo') ], - ['json containing error code', mock_useragent_tx(encode_json({error_code => 1234 }))], + ['http error', mock_useragent_tx("doesn't matter", sub { $_->code(500) })], + ['invalid json', mock_useragent_tx('foo')], + [ + 'json containing error code', + mock_useragent_tx(encode_json({error_code => 1234})) + ], ); -SetParam(phabricator_enabled => 1); -SetParam(phabricator_api_key => 'FAKE-API-KEY'); +SetParam(phabricator_enabled => 1); +SetParam(phabricator_api_key => 'FAKE-API-KEY'); SetParam(phabricator_base_uri => 'http://fake.fabricator.tld/'); foreach my $bad_response (@bad_response) { - my $feed = Bugzilla::Extension::PhabBugz::Feed->new; - $UserAgent->override( - post => sub { - my ( $self, $url, undef, $params ) = @_; - return $bad_response->[1]; - } - ); - - foreach my $method (qw( feed_query user_query group_query )) { - try { - # This is a hack to get reasonable exception objects. - local $Bugzilla::Template::is_processing = 1; - $feed->$method; - fail "$method - $bad_response->[0]"; - } - catch { - is( $_->type, 'bugzilla.code.phabricator_api_error', "$method - $bad_response->[0]" ); - }; + my $feed = Bugzilla::Extension::PhabBugz::Feed->new; + $UserAgent->override( + post => sub { + my ($self, $url, undef, $params) = @_; + return $bad_response->[1]; } - $UserAgent->reset('post'); + ); + + foreach my $method (qw( feed_query user_query group_query )) { + try { + # This is a hack to get reasonable exception objects. + local $Bugzilla::Template::is_processing = 1; + $feed->$method; + fail "$method - $bad_response->[0]"; + } + catch { + is( + $_->type, + 'bugzilla.code.phabricator_api_error', + "$method - $bad_response->[0]" + ); + }; + } + $UserAgent->reset('post'); } -my $feed = Bugzilla::Extension::PhabBugz::Feed->new; -my $json = JSON::MaybeXS->new( canonical => 1, pretty => 1 ); -my $dylan = create_user( 'dylan@mozilla.com', '*', realname => 'Dylan Hardison :dylan' ); -my $evildylan = create_user( 'dylan@gmail.com', '*', realname => 'Evil Dylan :dylan' ); -my $myk = create_user( 'myk@mozilla.com', '*', realname => 'Myk Melez :myk' ); +my $feed = Bugzilla::Extension::PhabBugz::Feed->new; +my $json = JSON::MaybeXS->new(canonical => 1, pretty => 1); +my $dylan + = create_user('dylan@mozilla.com', '*', realname => 'Dylan Hardison :dylan'); +my $evildylan + = create_user('dylan@gmail.com', '*', realname => 'Evil Dylan :dylan'); +my $myk = create_user('myk@mozilla.com', '*', realname => 'Myk Melez :myk'); my $phab_bot_phid = next_phid('PHID-USER'); done_testing; sub user_search { - my (%conf) = @_; - - return { - error_info => undef, - error_code => undef, - result => { - cursor => { - after => $conf{after}, - order => undef, - limit => 100, - before => undef + my (%conf) = @_; + + return { + error_info => undef, + error_code => undef, + result => { + cursor => + {after => $conf{after}, order => undef, limit => 100, before => undef}, + query => {queryKey => undef}, + maps => {}, + data => [ + map { + +{ + attachments => { + $_->{bmo_id} + ? ("external-accounts" => + {"external-accounts" => [{type => 'bmo', id => $_->{bmo_id},}]}) + : (), }, - query => { - queryKey => undef + fields => { + roles => ["verified", "approved", "activated"], + realName => $_->{realname}, + dateModified => time, + policy => {view => "public", edit => "no-one"}, + dateCreated => time, + username => $_->{username}, }, - maps => {}, - data => [ - map { - +{ - attachments => { - $_->{bmo_id} - ? ( "external-accounts" => { - "external-accounts" => [ - { - type => 'bmo', - id => $_->{bmo_id}, - } - ] - } - ) - : (), - }, - fields => { - roles => [ "verified", "approved", "activated" ], - realName => $_->{realname}, - dateModified => time, - policy => { - view => "public", - edit => "no-one" - }, - dateCreated => time, - username => $_->{username}, - }, - phid => next_phid("PHID-USER"), - type => "USER", - id => $_->{phab_id}, - }, - } @{ $conf{users} }, - ] - } - }; + phid => next_phid("PHID-USER"), + type => "USER", + id => $_->{phab_id}, + }, + } @{$conf{users}}, + ] + } + }; } sub next_phid { - my ($prefix) = @_; - state $number = 'a' x 20; - return $prefix . '-' . ($number++); + my ($prefix) = @_; + state $number = 'a' x 20; + return $prefix . '-' . ($number++); } diff --git a/extensions/PhabBugz/t/review-flags.t b/extensions/PhabBugz/t/review-flags.t index 610c46dca..b23a55eec 100644 --- a/extensions/PhabBugz/t/review-flags.t +++ b/extensions/PhabBugz/t/review-flags.t @@ -15,11 +15,11 @@ use Test2::V0; our @EMAILS; BEGIN { - require Bugzilla::Mailer; - no warnings 'redefine'; - *Bugzilla::Mailer::MessageToMTA = sub { - push @EMAILS, [@_]; - }; + require Bugzilla::Mailer; + no warnings 'redefine'; + *Bugzilla::Mailer::MessageToMTA = sub { + push @EMAILS, [@_]; + }; } use Bugzilla::Test::MockDB; use Bugzilla::Test::MockParams; @@ -35,138 +35,121 @@ use Data::Dumper; use ok 'Bugzilla::Extension::PhabBugz::Feed'; use ok 'Bugzilla::Extension::PhabBugz::Constants', 'PHAB_AUTOMATION_USER'; -use ok 'Bugzilla::Config', 'SetParam'; -can_ok('Bugzilla::Extension::PhabBugz::Feed', qw( group_query feed_query user_query )); +use ok 'Bugzilla::Config', 'SetParam'; +can_ok( + 'Bugzilla::Extension::PhabBugz::Feed', + qw( group_query feed_query user_query ) +); SetParam(phabricator_base_uri => 'http://fake.phabricator.tld/'); -SetParam(mailfrom => 'bugzilla-daemon'); +SetParam(mailfrom => 'bugzilla-daemon'); Bugzilla->error_mode(ERROR_MODE_TEST); -my $nobody = create_user('nobody@mozilla.org', '*'); +my $nobody = create_user('nobody@mozilla.org', '*'); my $phab_bot = create_user(PHAB_AUTOMATION_USER, '*'); # Steve Rogers is the revision author -my $steve = create_user('steverogers@avengers.org', '*', realname => 'Steve Rogers :steve'); +my $steve = create_user('steverogers@avengers.org', '*', + realname => 'Steve Rogers :steve'); # Bucky Barns is the reviewer -my $bucky = create_user('bucky@avengers.org', '*', realname => 'Bucky Barns :bucky'); +my $bucky + = create_user('bucky@avengers.org', '*', realname => 'Bucky Barns :bucky'); my $firefox = Bugzilla::Product->create( - { - name => 'Firefox', - description => 'Fake firefox product', - version => 'Unspecified', - }, + { + name => 'Firefox', + description => 'Fake firefox product', + version => 'Unspecified', + }, ); -my $general = Bugzilla::Component->create( - { - product =>$firefox, - name => 'General', - description => 'The most general description', - initialowner => { id => $nobody->id }, - } -); +my $general = Bugzilla::Component->create({ + product => $firefox, + name => 'General', + description => 'The most general description', + initialowner => {id => $nobody->id}, +}); Bugzilla->set_user($steve); -my $bug = Bugzilla::Bug->create( - { - short_desc => 'test bug', - product => $firefox, - component => $general->name, - bug_severity => 'normal', - op_sys => 'Unspecified', - rep_platform => 'Unspecified', - version => 'Unspecified', - comment => 'first post', - priority => 'P1', - } -); - -my $recipients = { changer => $steve }; +my $bug = Bugzilla::Bug->create({ + short_desc => 'test bug', + product => $firefox, + component => $general->name, + bug_severity => 'normal', + op_sys => 'Unspecified', + rep_platform => 'Unspecified', + version => 'Unspecified', + comment => 'first post', + priority => 'P1', +}); + +my $recipients = {changer => $steve}; Bugzilla::BugMail::Send($bug->bug_id, $recipients); @EMAILS = (); -my $revision = Bugzilla::Extension::PhabBugz::Revision->new( - { - id => 1, - phid => 'PHID-DREV-uozm3ggfp7e7uoqegmc3', - type => 'DREV', - fields => { - title => "title", - summary => "the summary of the revision", - status => { value => "not sure" }, - dateCreated => time() - (60 * 60), - dateModified => time() - (60 * 5), - authorPHID => 'authorPHID', - policy => { - view => 'policy.view', - edit => 'policy.edit', - }, - 'bugzilla.bug-id' => $bug->id, - }, - attachments => { - projects => { projectPHIDs => [] }, - reviewers => { - reviewers => [ ], - }, - subscribers => { - subscriberPHIDs => [], - subscriberCount => 1, - viewerIsSubscribed => 1, - } - }, - reviews => [ - { - user => new_phab_user($bucky), - status => 'accepted', - } - ] - } -); -my $PhabRevisionMock = mock 'Bugzilla::Extension::PhabBugz::Revision' => ( - override => [ - make_public => sub { }, - update => sub { }, - ] -); +my $revision = Bugzilla::Extension::PhabBugz::Revision->new({ + id => 1, + phid => 'PHID-DREV-uozm3ggfp7e7uoqegmc3', + type => 'DREV', + fields => { + title => "title", + summary => "the summary of the revision", + status => {value => "not sure"}, + dateCreated => time() - (60 * 60), + dateModified => time() - (60 * 5), + authorPHID => 'authorPHID', + policy => {view => 'policy.view', edit => 'policy.edit',}, + 'bugzilla.bug-id' => $bug->id, + }, + attachments => { + projects => {projectPHIDs => []}, + reviewers => {reviewers => [],}, + subscribers => + {subscriberPHIDs => [], subscriberCount => 1, viewerIsSubscribed => 1,} + }, + reviews => [{user => new_phab_user($bucky), status => 'accepted',}] +}); +my $PhabRevisionMock = mock 'Bugzilla::Extension::PhabBugz::Revision' => + (override => [make_public => sub { }, update => sub { },]); my $PhabUserMock = mock 'Bugzilla::Extension::PhabBugz::User' => ( - override => [ - match => sub { - my ($class, $query) = @_; - if ($query && $query->{phids} && $query->{phids}[0]) { - my $phid = $query->{phids}[0]; - if ($phid eq 'authorPHID') { - return [ new_phab_user($steve, $phid) ]; - } - } - }, - ] + override => [ + match => sub { + my ($class, $query) = @_; + if ($query && $query->{phids} && $query->{phids}[0]) { + my $phid = $query->{phids}[0]; + if ($phid eq 'authorPHID') { + return [new_phab_user($steve, $phid)]; + } + } + }, + ] ); my $feed = Bugzilla::Extension::PhabBugz::Feed->new; my $changer = new_phab_user($bucky); @EMAILS = (); -$feed->process_revision_change( - $revision, $changer, "story text" -); +$feed->process_revision_change($revision, $changer, "story text"); # The first comment, and the comment made when the attachment is attached # are made by Steve. # The review comment is made by Bucky. -my $sth = Bugzilla->dbh->prepare("select profiles.login_name, thetext from longdescs join profiles on who = userid"); +my $sth + = Bugzilla->dbh->prepare( + "select profiles.login_name, thetext from longdescs join profiles on who = userid" + ); $sth->execute; while (my $row = $sth->fetchrow_hashref) { - if ($row->{thetext} =~ /first post/i) { - is($row->{login_name}, $steve->login, 'first post author'); - } - elsif ($row->{thetext} =~ /the summary of the revision/i) { - is($row->{login_name}, $steve->login, 'the first attachment comment'); - } - elsif ($row->{thetext} =~ /has approved the revision/i) { - is($row->{login_name}, $bucky->login); - } + if ($row->{thetext} =~ /first post/i) { + is($row->{login_name}, $steve->login, 'first post author'); + } + elsif ($row->{thetext} =~ /the summary of the revision/i) { + is($row->{login_name}, $steve->login, 'the first attachment comment'); + } + elsif ($row->{thetext} =~ /has approved the revision/i) { + is($row->{login_name}, $bucky->login); + } } diag Dumper(\@EMAILS); @@ -174,36 +157,25 @@ diag Dumper(\@EMAILS); done_testing; sub new_phab_user { - my ($bug_user, $phid) = @_; - - return Bugzilla::Extension::PhabBugz::User->new( - { - id => $bug_user->id * 1000, - type => "USER", - phid => $phid // "PHID-USER-" . ( $bug_user->id * 1000 ), - fields => { - username => $bug_user->nick, - realName => $bug_user->name, - dateCreated => time() - 60 * 60 * 24, - dateModified => time(), - roles => [], - policy => { - view => 'view', - edit => 'edit', - }, - }, - attachments => { - 'external-accounts' => { - 'external-accounts' => [ - { - type => 'bmo', - id => $bug_user->id, - } - ] - } - } - } - ); + my ($bug_user, $phid) = @_; + + return Bugzilla::Extension::PhabBugz::User->new({ + id => $bug_user->id * 1000, + type => "USER", + phid => $phid // "PHID-USER-" . ($bug_user->id * 1000), + fields => { + username => $bug_user->nick, + realName => $bug_user->name, + dateCreated => time() - 60 * 60 * 24, + dateModified => time(), + roles => [], + policy => {view => 'view', edit => 'edit',}, + }, + attachments => { + 'external-accounts' => + {'external-accounts' => [{type => 'bmo', id => $bug_user->id,}]} + } + }); -} \ No newline at end of file +} diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm index 9631de570..240530f00 100644 --- a/extensions/ProdCompSearch/Config.pm +++ b/extensions/ProdCompSearch/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'ProdCompSearch'; +use constant NAME => 'ProdCompSearch'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ProdCompSearch/Extension.pm b/extensions/ProdCompSearch/Extension.pm index ae507a7d6..6647eb08d 100644 --- a/extensions/ProdCompSearch/Extension.pm +++ b/extensions/ProdCompSearch/Extension.pm @@ -16,9 +16,9 @@ use base qw(Bugzilla::Extension); our $VERSION = '1'; sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService"; } diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm index b173137dd..b47b4a402 100644 --- a/extensions/ProdCompSearch/lib/WebService.pm +++ b/extensions/ProdCompSearch/lib/WebService.pm @@ -21,20 +21,21 @@ use Bugzilla::Util qw(detaint_natural trick_taint trim); ############# use constant PUBLIC_METHODS => qw( - prod_comp_search + prod_comp_search ); sub rest_resources { - return [ - qr{^/prod_comp_search/(.*)$}, { - GET => { - method => 'prod_comp_search', - params => sub { - return { search => $_[0] } - } - } + return [ + qr{^/prod_comp_search/(.*)$}, + { + GET => { + method => 'prod_comp_search', + params => sub { + return {search => $_[0]}; } - ] + } + } + ]; } ################## @@ -42,79 +43,91 @@ sub rest_resources { ################## sub prod_comp_search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->switch_to_shadow_db(); - - my $search = trim($params->{'search'} || ''); - $search || ThrowCodeError('param_required', - { function => 'PCS.prod_comp_search', param => 'search' }); - - my $limit = detaint_natural($params->{'limit'}) - ? $dbh->sql_limit($params->{'limit'}) - : ''; - - # We do this in the DB directly as we want it to be fast and - # not have the overhead of loading full product objects - - # All products which the user has "Entry" access to. - my $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT products.id FROM products + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my $search = trim($params->{'search'} || ''); + $search + || ThrowCodeError('param_required', + {function => 'PCS.prod_comp_search', param => 'search'}); + + my $limit + = detaint_natural($params->{'limit'}) + ? $dbh->sql_limit($params->{'limit'}) + : ''; + + # We do this in the DB directly as we want it to be fast and + # not have the overhead of loading full product objects + + # All products which the user has "Entry" access to. + my $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT products.id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0 AND group_id NOT IN (' . $user->groups_as_string . ') WHERE group_id IS NULL - AND products.isactive = 1'); - - if (scalar @$enterable_ids) { - # And all of these products must have at least one component - # and one version. - $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT products.id FROM products - WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . - ' AND products.id IN (SELECT DISTINCT components.product_id + AND products.isactive = 1' + ); + + if (scalar @$enterable_ids) { + + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' + . $dbh->sql_in('products.id', $enterable_ids) + . ' AND products.id IN (SELECT DISTINCT components.product_id FROM components WHERE components.isactive = 1) AND products.id IN (SELECT DISTINCT versions.product_id FROM versions - WHERE versions.isactive = 1)'); - } - - return { products => [] } if !scalar @$enterable_ids; - - trick_taint($search); - my @terms; - my @order; - - if ($search =~ /^(.*?)::(.*)$/) { - my ($product, $component) = (trim($1), trim($2)); - push @terms, _build_terms($product, 1, 0); - push @terms, _build_terms($component, 0, 1); - push @order, "products.name != " . $dbh->quote($product) if $product ne ''; - push @order, "components.name != " . $dbh->quote($component) if $component ne ''; - push @order, _build_like_order($product . ' ' . $component); - push @order, "products.name"; - push @order, "components.name"; - } else { - push @terms, _build_terms($search, 1, 1); - push @order, "products.name != " . $dbh->quote($search); - push @order, "components.name != " . $dbh->quote($search); - push @order, _build_like_order($search); - push @order, "products.name"; - push @order, "components.name"; - } - return { products => [] } if !scalar @terms; - - # To help mozilla staff file bmo administration bugs into the right - # component, sort bmo first when searching for 'bugzilla' - if ($search =~ /bugzilla/i && $search !~ /^bugzilla\s*::/i - && ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation'))) - { - unshift @order, "products.name != 'bugzilla.mozilla.org'"; - } - - my $components = $dbh->selectall_arrayref(" + WHERE versions.isactive = 1)' + ); + } + + return {products => []} if !scalar @$enterable_ids; + + trick_taint($search); + my @terms; + my @order; + + if ($search =~ /^(.*?)::(.*)$/) { + my ($product, $component) = (trim($1), trim($2)); + push @terms, _build_terms($product, 1, 0); + push @terms, _build_terms($component, 0, 1); + push @order, "products.name != " . $dbh->quote($product) if $product ne ''; + push @order, "components.name != " . $dbh->quote($component) + if $component ne ''; + push @order, _build_like_order($product . ' ' . $component); + push @order, "products.name"; + push @order, "components.name"; + } + else { + push @terms, _build_terms($search, 1, 1); + push @order, "products.name != " . $dbh->quote($search); + push @order, "components.name != " . $dbh->quote($search); + push @order, _build_like_order($search); + push @order, "products.name"; + push @order, "components.name"; + } + return {products => []} if !scalar @terms; + + # To help mozilla staff file bmo administration bugs into the right + # component, sort bmo first when searching for 'bugzilla' + if ( + $search =~ /bugzilla/i + && $search !~ /^bugzilla\s*::/i + && ( $user->in_group('mozilla-corporation') + || $user->in_group('mozilla-foundation')) + ) + { + unshift @order, "products.name != 'bugzilla.mozilla.org'"; + } + + my $components = $dbh->selectall_arrayref(" SELECT products.name AS product, components.name AS component FROM products @@ -122,19 +135,18 @@ sub prod_comp_search { WHERE (" . join(" AND ", @terms) . ") AND products.id IN (" . join(",", @$enterable_ids) . ") AND components.isactive = 1 - ORDER BY " . join(", ", @order) . " $limit", - { Slice => {} }); - - my $products = []; - my $current_product; - foreach my $component (@$components) { - if (!$current_product || $component->{product} ne $current_product) { - $current_product = $component->{product}; - push @$products, { product => $current_product }; - } - push @$products, $component; + ORDER BY " . join(", ", @order) . " $limit", {Slice => {}}); + + my $products = []; + my $current_product; + foreach my $component (@$components) { + if (!$current_product || $component->{product} ne $current_product) { + $current_product = $component->{product}; + push @$products, {product => $current_product}; } - return { products => $products }; + push @$products, $component; + } + return {products => $products}; } ################### @@ -142,34 +154,37 @@ sub prod_comp_search { ################### sub _build_terms { - my ($query, $product, $component) = @_; - my $dbh = Bugzilla->dbh(); - - my @fields; - push @fields, 'products.name', 'products.description' if $product; - push @fields, 'components.name', 'components.description' if $component; - # note: CONCAT_WS is MySQL specific - my $field = "CONCAT_WS(' ', ". join(',', @fields) . ")"; - - my @terms; - foreach my $word (split(/[\s,]+/, $query)) { - push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0") - if $word ne ''; - } - return @terms; + my ($query, $product, $component) = @_; + my $dbh = Bugzilla->dbh(); + + my @fields; + push @fields, 'products.name', 'products.description' if $product; + push @fields, 'components.name', 'components.description' if $component; + + # note: CONCAT_WS is MySQL specific + my $field = "CONCAT_WS(' ', " . join(',', @fields) . ")"; + + my @terms; + foreach my $word (split(/[\s,]+/, $query)) { + push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0") + if $word ne ''; + } + return @terms; } sub _build_like_order { - my ($query) = @_; - my $dbh = Bugzilla->dbh; - - my @terms; - foreach my $word (split(/[\s,]+/, $query)) { - push @terms, "CONCAT(products.name, components.name) LIKE " . $dbh->quote('%' . $word . '%') - if $word ne ''; - } - - return 'NOT(' . join(' AND ', @terms) . ')'; + my ($query) = @_; + my $dbh = Bugzilla->dbh; + + my @terms; + foreach my $word (split(/[\s,]+/, $query)) { + push @terms, + "CONCAT(products.name, components.name) LIKE " + . $dbh->quote('%' . $word . '%') + if $word ne ''; + } + + return 'NOT(' . join(' AND ', @terms) . ')'; } 1; diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm index 311400d16..4acaf4fa2 100644 --- a/extensions/Profanivore/Config.pm +++ b/extensions/Profanivore/Config.pm @@ -28,16 +28,8 @@ use warnings; use constant NAME => 'Profanivore'; use constant REQUIRED_MODULES => [ - { - package => 'Regexp-Common', - module => 'Regexp::Common', - version => 0 - }, - { - package => 'HTML-Tree', - module => 'HTML::Tree', - version => 0, - } + {package => 'Regexp-Common', module => 'Regexp::Common', version => 0}, + {package => 'HTML-Tree', module => 'HTML::Tree', version => 0,} ]; __PACKAGE__->NAME; diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm index 013f92fee..4682b3d1d 100644 --- a/extensions/Profanivore/Extension.pm +++ b/extensions/Profanivore/Extension.pm @@ -35,153 +35,150 @@ use Bugzilla::Util qw(is_7bit_clean); our $VERSION = '0.01'; sub bug_format_comment { - my ($self, $args) = @_; - my $regexes = $args->{'regexes'}; - my $comment = $args->{'comment'}; - - # Censor profanities if the comment author is not reasonably trusted. - # However, allow people to see their own profanities, which might stop - # them immediately noticing and trying to go around the filter. (I.e. - # it tries to stop an arms race starting.) - if ($comment && - !$comment->author->in_group('editbugs') && - $comment->author->id != Bugzilla->user->id) - { - push (@$regexes, { - match => RE_profanity('-i'), - replace => \&_replace_profanity - }); - } + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; + my $comment = $args->{'comment'}; + + # Censor profanities if the comment author is not reasonably trusted. + # However, allow people to see their own profanities, which might stop + # them immediately noticing and trying to go around the filter. (I.e. + # it tries to stop an arms race starting.) + if ( $comment + && !$comment->author->in_group('editbugs') + && $comment->author->id != Bugzilla->user->id) + { + push(@$regexes, {match => RE_profanity('-i'), replace => \&_replace_profanity}); + } } sub _replace_profanity { - # We don't have access to the actual profanity. - return "****"; + + # We don't have access to the actual profanity. + return "****"; } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{'email'}; - - my $author = $email->header("X-Bugzilla-Who"); - my $recipient = $email->header("To"); - - if ($author && $recipient && lc($author) ne lc($recipient)) { - my $email_suffix = Bugzilla->params->{'emailsuffix'}; - if ($email_suffix ne '') { - $recipient =~ s/\Q$email_suffix\E$//; - $author =~ s/\Q$email_suffix\E$//; - } - - $author = new Bugzilla::User({ name => $author }); - - if ($author && - $author->id && - !$author->in_group('editbugs')) - { - # Multipart emails - if (scalar $email->parts > 1) { - $email->walk_parts(sub { - my ($part) = @_; - return if $part->parts > 1; # Top-level - # do not filter attachments such as patches, etc. - if ($part->header('Content-Disposition') - && $part->header('Content-Disposition') =~ /attachment/) - { - return; - } - _fix_encoding($part); - my $body = $part->body_str; - my $new_body; - if ($part->content_type =~ /^text\/html/) { - $new_body = _filter_html($body); - if ($new_body ne $body) { - # HTML::Tree removes unnecessary whitespace, - # resulting in very long lines. We need to use - # quoted-printable encoding to avoid exceeding - # email's maximum line length. - $part->encoding_set('quoted-printable'); - } - } - elsif ($part->content_type =~ /^text\/plain/) { - $new_body = _filter_text($body); - } - if ($new_body && $new_body ne $body) { - $part->body_str_set($new_body); - } - }); - } - # Single part email - else { - _fix_encoding($email); - $email->body_str_set(_filter_text($email->body_str)); + my ($self, $args) = @_; + my $email = $args->{'email'}; + + my $author = $email->header("X-Bugzilla-Who"); + my $recipient = $email->header("To"); + + if ($author && $recipient && lc($author) ne lc($recipient)) { + my $email_suffix = Bugzilla->params->{'emailsuffix'}; + if ($email_suffix ne '') { + $recipient =~ s/\Q$email_suffix\E$//; + $author =~ s/\Q$email_suffix\E$//; + } + + $author = new Bugzilla::User({name => $author}); + + if ($author && $author->id && !$author->in_group('editbugs')) { + + # Multipart emails + if (scalar $email->parts > 1) { + $email->walk_parts(sub { + my ($part) = @_; + return if $part->parts > 1; # Top-level + # do not filter attachments such as patches, etc. + if ( $part->header('Content-Disposition') + && $part->header('Content-Disposition') =~ /attachment/) + { + return; + } + _fix_encoding($part); + my $body = $part->body_str; + my $new_body; + if ($part->content_type =~ /^text\/html/) { + $new_body = _filter_html($body); + if ($new_body ne $body) { + + # HTML::Tree removes unnecessary whitespace, + # resulting in very long lines. We need to use + # quoted-printable encoding to avoid exceeding + # email's maximum line length. + $part->encoding_set('quoted-printable'); } - } + } + elsif ($part->content_type =~ /^text\/plain/) { + $new_body = _filter_text($body); + } + if ($new_body && $new_body ne $body) { + $part->body_str_set($new_body); + } + }); + } + + # Single part email + else { + _fix_encoding($email); + $email->body_str_set(_filter_text($email->body_str)); + } } + } } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _filter_text { - my $text = shift; - my $offensive = RE_profanity('-i'); - $text =~ s/$offensive/****/g; - return $text; + my $text = shift; + my $offensive = RE_profanity('-i'); + $text =~ s/$offensive/****/g; + return $text; } sub _filter_html { - my $html = shift; - my $tree = HTML::Tree->new->parse_content($html); - my $comments_div = $tree->look_down( _tag => 'div', id => 'comments' ); - return $html if !$comments_div; - my @comments = $comments_div->look_down( _tag => 'pre' ); - my $dirty = 0; - foreach my $comment (@comments) { - _filter_html_node($comment, \$dirty); - } - if ($dirty) { - $html = $tree->as_HTML; - $tree->delete; - } - return $html; + my $html = shift; + my $tree = HTML::Tree->new->parse_content($html); + my $comments_div = $tree->look_down(_tag => 'div', id => 'comments'); + return $html if !$comments_div; + my @comments = $comments_div->look_down(_tag => 'pre'); + my $dirty = 0; + foreach my $comment (@comments) { + _filter_html_node($comment, \$dirty); + } + if ($dirty) { + $html = $tree->as_HTML; + $tree->delete; + } + return $html; } sub _filter_html_node { - my ($node, $dirty) = @_; - my $content = [ $node->content_list ]; - foreach my $item_r ($node->content_refs_list) { - if (ref $$item_r) { - _filter_html_node($$item_r); - } else { - my $new_text = _filter_text($$item_r); - if ($new_text ne $$item_r) { - $$item_r = $new_text; - $$dirty = 1; - } - } + my ($node, $dirty) = @_; + my $content = [$node->content_list]; + foreach my $item_r ($node->content_refs_list) { + if (ref $$item_r) { + _filter_html_node($$item_r); + } + else { + my $new_text = _filter_text($$item_r); + if ($new_text ne $$item_r) { + $$item_r = $new_text; + $$dirty = 1; + } } + } } __PACKAGE__->NAME; diff --git a/extensions/Push/Config.pm b/extensions/Push/Config.pm index 59b78d5a2..9ca73815a 100644 --- a/extensions/Push/Config.pm +++ b/extensions/Push/Config.pm @@ -14,39 +14,14 @@ use warnings; use constant NAME => 'Push'; use constant REQUIRED_MODULES => [ - { - package => 'Daemon-Generic', - module => 'Daemon::Generic', - version => '0' - }, - { - package => 'JSON-XS', - module => 'JSON::XS', - version => '2.0' - }, - { - package => 'Crypt-CBC', - module => 'Crypt::CBC', - version => '0' - }, - { - package => 'Crypt-DES', - module => 'Crypt::DES', - version => '0' - }, - { - package => 'Crypt-DES_EDE3', - module => 'Crypt::DES_EDE3', - version => '0' - }, + {package => 'Daemon-Generic', module => 'Daemon::Generic', version => '0'}, + {package => 'JSON-XS', module => 'JSON::XS', version => '2.0'}, + {package => 'Crypt-CBC', module => 'Crypt::CBC', version => '0'}, + {package => 'Crypt-DES', module => 'Crypt::DES', version => '0'}, + {package => 'Crypt-DES_EDE3', module => 'Crypt::DES_EDE3', version => '0'}, ]; -use constant OPTIONAL_MODULES => [ - { - package => 'XML-Simple', - module => 'XML::Simple', - version => '0' - }, -]; +use constant OPTIONAL_MODULES => + [{package => 'XML-Simple', module => 'XML::Simple', version => '0'},]; __PACKAGE__->NAME; diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm index f682dea35..4b60dcb73 100644 --- a/extensions/Push/Extension.pm +++ b/extensions/Push/Extension.pm @@ -38,18 +38,18 @@ $Carp::CarpInternal{'CGI::Carp'} = 1; # BEGIN { - *Bugzilla::push_ext = \&_get_instance; + *Bugzilla::push_ext = \&_get_instance; } sub _get_instance { - my $cache = Bugzilla->request_cache; - if (!$cache->{'push.instance'}) { - my $instance = Bugzilla::Extension::Push::Push->new(); - $cache->{'push.instance'} = $instance; - $instance->logger(Bugzilla::Extension::Push::Logger->new()); - $instance->connectors(Bugzilla::Extension::Push::Connectors->new()); - } - return $cache->{'push.instance'}; + my $cache = Bugzilla->request_cache; + if (!$cache->{'push.instance'}) { + my $instance = Bugzilla::Extension::Push::Push->new(); + $cache->{'push.instance'} = $instance; + $instance->logger(Bugzilla::Extension::Push::Logger->new()); + $instance->connectors(Bugzilla::Extension::Push::Connectors->new()); + } + return $cache->{'push.instance'}; } # @@ -57,22 +57,23 @@ sub _get_instance { # sub _enabled { - my ($self) = @_; - if (!exists $self->{'enabled'}) { - my $push = Bugzilla->push_ext; - $self->{'enabled'} = $push->config->{enabled} eq 'Enabled'; - if ($self->{'enabled'}) { - # if no connectors are enabled, no need to push anything - $self->{'enabled'} = 0; - foreach my $connector (Bugzilla->push_ext->connectors->list) { - if ($connector->enabled) { - $self->{'enabled'} = 1; - last; - } - } + my ($self) = @_; + if (!exists $self->{'enabled'}) { + my $push = Bugzilla->push_ext; + $self->{'enabled'} = $push->config->{enabled} eq 'Enabled'; + if ($self->{'enabled'}) { + + # if no connectors are enabled, no need to push anything + $self->{'enabled'} = 0; + foreach my $connector (Bugzilla->push_ext->connectors->list) { + if ($connector->enabled) { + $self->{'enabled'} = 1; + last; } + } } - return $self->{'enabled'}; + } + return $self->{'enabled'}; } # @@ -80,191 +81,186 @@ sub _enabled { # sub _object_created { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = _get_object_from_args($args); - return unless $object; - return unless _should_push($object); + my $object = _get_object_from_args($args); + return unless $object; + return unless _should_push($object); - $self->_push_object('create', $object, change_set_id(), { timestamp => $args->{'timestamp'} }); + $self->_push_object('create', $object, change_set_id(), + {timestamp => $args->{'timestamp'}}); } sub _object_modified { - my ($self, $args) = @_; - - my $object = _get_object_from_args($args); - return unless $object; - return unless _should_push($object); - - my $changes = $args->{'changes'} || {}; - return unless scalar keys %$changes; - - my $change_set = change_set_id(); - - # detect when a bug changes from public to private (or back), so connectors - # can remove now-private bugs if required. - if ($object->isa('Bugzilla::Bug')) { - # we can't use user->can_see_bug(old_bug) as that works on IDs, and the - # bug has already been updated, so for now assume that a bug without - # groups is public. - my $old_bug = $args->{'old_bug'}; - my $is_public = is_public($object); - my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public; - - if (!$is_public && $was_public) { - # bug is changing from public to private - # push a fake update with the just is_private change - my $private_changes = { - timestamp => $args->{'timestamp'}, - changes => [ - { - field => 'is_private', - removed => '0', - added => '1', - }, - ], - }; - # note we're sending the old bug object so we don't leak any - # security sensitive information. - $self->_push_object('modify', $old_bug, $change_set, $private_changes); - } elsif ($is_public && !$was_public) { - # bug is changing from private to public - # push a fake update with the just is_private change - my $private_changes = { - timestamp => $args->{'timestamp'}, - changes => [ - { - field => 'is_private', - removed => '1', - added => '0', - }, - ], - }; - # it's ok to send the new bug state here - $self->_push_object('modify', $object, $change_set, $private_changes); - } - } + my ($self, $args) = @_; - # make flagtypes changes easier to process - if (exists $changes->{'flagtypes.name'}) { - _split_flagtypes($changes); - } + my $object = _get_object_from_args($args); + return unless $object; + return unless _should_push($object); + + my $changes = $args->{'changes'} || {}; + return unless scalar keys %$changes; - # TODO split group changes? + my $change_set = change_set_id(); - # restructure the changes hash - my $changes_data = { + # detect when a bug changes from public to private (or back), so connectors + # can remove now-private bugs if required. + if ($object->isa('Bugzilla::Bug')) { + + # we can't use user->can_see_bug(old_bug) as that works on IDs, and the + # bug has already been updated, so for now assume that a bug without + # groups is public. + my $old_bug = $args->{'old_bug'}; + my $is_public = is_public($object); + my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public; + + if (!$is_public && $was_public) { + + # bug is changing from public to private + # push a fake update with the just is_private change + my $private_changes = { timestamp => $args->{'timestamp'}, - changes => [], - }; - foreach my $field_name (sort keys %$changes) { - my $new_field_name = $field_name; - $new_field_name =~ s/isprivate/is_private/; - - push @{$changes_data->{'changes'}}, { - field => $new_field_name, - removed => $changes->{$field_name}[0], - added => $changes->{$field_name}[1], - }; + changes => [{field => 'is_private', removed => '0', added => '1',},], + }; + + # note we're sending the old bug object so we don't leak any + # security sensitive information. + $self->_push_object('modify', $old_bug, $change_set, $private_changes); } + elsif ($is_public && !$was_public) { + + # bug is changing from private to public + # push a fake update with the just is_private change + my $private_changes = { + timestamp => $args->{'timestamp'}, + changes => [{field => 'is_private', removed => '1', added => '0',},], + }; - $self->_push_object('modify', $object, $change_set, $changes_data); + # it's ok to send the new bug state here + $self->_push_object('modify', $object, $change_set, $private_changes); + } + } + + # make flagtypes changes easier to process + if (exists $changes->{'flagtypes.name'}) { + _split_flagtypes($changes); + } + + # TODO split group changes? + + # restructure the changes hash + my $changes_data = {timestamp => $args->{'timestamp'}, changes => [],}; + foreach my $field_name (sort keys %$changes) { + my $new_field_name = $field_name; + $new_field_name =~ s/isprivate/is_private/; + + push @{$changes_data->{'changes'}}, + { + field => $new_field_name, + removed => $changes->{$field_name}[0], + added => $changes->{$field_name}[1], + }; + } + + $self->_push_object('modify', $object, $change_set, $changes_data); } sub _get_object_from_args { - my ($args) = @_; - return get_first_value($args, qw(object bug flag group)); + my ($args) = @_; + return get_first_value($args, qw(object bug flag group)); } sub _should_push { - my ($object_or_class) = @_; - my $class = blessed($object_or_class) || $object_or_class; - return grep { $_ eq $class } qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment); + my ($object_or_class) = @_; + my $class = blessed($object_or_class) || $object_or_class; + return + grep { $_ eq $class } + qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment); } # changes to bug flags are presented in a single field 'flagtypes.name' split # into individual fields sub _split_flagtypes { - my ($changes) = @_; - - my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]); - my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]); - delete $changes->{'flagtypes.name'}; - - foreach my $ra (@removed, @added) { - $changes->{$ra->[0]} = ['', '']; - } - foreach my $ra (@removed) { - my ($name, $value) = @$ra; - $changes->{$name}->[0] = $value; - } - foreach my $ra (@added) { - my ($name, $value) = @$ra; - $changes->{$name}->[1] = $value; - } + my ($changes) = @_; + + my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]); + my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]); + delete $changes->{'flagtypes.name'}; + + foreach my $ra (@removed, @added) { + $changes->{$ra->[0]} = ['', '']; + } + foreach my $ra (@removed) { + my ($name, $value) = @$ra; + $changes->{$name}->[0] = $value; + } + foreach my $ra (@added) { + my ($name, $value) = @$ra; + $changes->{$name}->[1] = $value; + } } sub _split_flagtype { - my ($value) = @_; - my @result; - foreach my $change (split(/, /, $value)) { - my $requestee = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $requestee = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - $value .= " ($requestee)" if $requestee; - push @result, [ "flag.$name", $value ]; + my ($value) = @_; + my @result; + foreach my $change (split(/, /, $value)) { + my $requestee = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $requestee = $1; } - return @result; + my ($name, $value) = $change =~ /^(.+)(.)$/; + $value .= " ($requestee)" if $requestee; + push @result, ["flag.$name", $value]; + } + return @result; } # changes to attachment flags come in via flag_end_of_update which has a # completely different structure for reporting changes than # object_end_of_update. this morphs flag to object updates. sub _morph_flag_updates { - my ($args) = @_; - - my @removed = _morph_flag_update($args->{'old_flags'}); - my @added = _morph_flag_update($args->{'new_flags'}); - - my $changes = {}; - foreach my $ra (@removed, @added) { - $changes->{$ra->[0]} = ['', '']; - } - foreach my $ra (@removed) { - my ($name, $value) = @$ra; - $changes->{$name}->[0] = $value; - } - foreach my $ra (@added) { - my ($name, $value) = @$ra; - $changes->{$name}->[1] = $value; - } - - foreach my $flag (keys %$changes) { - if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) { - delete $changes->{$flag}; - } + my ($args) = @_; + + my @removed = _morph_flag_update($args->{'old_flags'}); + my @added = _morph_flag_update($args->{'new_flags'}); + + my $changes = {}; + foreach my $ra (@removed, @added) { + $changes->{$ra->[0]} = ['', '']; + } + foreach my $ra (@removed) { + my ($name, $value) = @$ra; + $changes->{$name}->[0] = $value; + } + foreach my $ra (@added) { + my ($name, $value) = @$ra; + $changes->{$name}->[1] = $value; + } + + foreach my $flag (keys %$changes) { + if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) { + delete $changes->{$flag}; } + } - $args->{'changes'} = $changes; + $args->{'changes'} = $changes; } sub _morph_flag_update { - my ($values) = @_; - my @result; - foreach my $orig_change (@$values) { - my $change = $orig_change; # work on a copy - $change =~ s/^[^:]+://; - my $requestee = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $requestee = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - $value .= " ($requestee)" if $requestee; - push @result, [ "flag.$name", $value ]; + my ($values) = @_; + my @result; + foreach my $orig_change (@$values) { + my $change = $orig_change; # work on a copy + $change =~ s/^[^:]+://; + my $requestee = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $requestee = $1; } - return @result; + my ($name, $value) = $change =~ /^(.+)(.)$/; + $value .= " ($requestee)" if $requestee; + push @result, ["flag.$name", $value]; + } + return @result; } # @@ -272,49 +268,52 @@ sub _morph_flag_update { # sub _push_object { - my ($self, $message_type, $object, $change_set, $changes) = @_; - my $rh; - - # serialise the object - my ($rh_object, $name) = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object); - - if (!$rh_object) { - warn "empty hash from serialiser ($message_type $object)\n"; - return; - } - $rh->{$name} = $rh_object; - - # add in the events hash - my $rh_event = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes); - return unless $rh_event; - $rh_event->{'action'} = $message_type; - $rh_event->{'target'} = $name; - $rh_event->{'change_set'} = $change_set; - $rh_event->{'routing_key'} = "$name.$message_type"; - if (exists $rh_event->{'changes'}) { - $rh_event->{'routing_key'} .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}}); - } - $rh->{'event'} = $rh_event; - - # create message object - my $message = Bugzilla::Extension::Push::Message->new_transient({ - payload => to_json($rh), - change_set => $change_set, - routing_key => $rh_event->{'routing_key'}, - }); - - # don't hit the database unless there are interested connectors - my $should_push = 0; - foreach my $connector (Bugzilla->push_ext->connectors->list) { - next unless $connector->enabled; - next unless $connector->should_send($message); - $should_push = 1; - last; - } - return unless $should_push; - - # insert into push table - $message->create_from_transient(); + my ($self, $message_type, $object, $change_set, $changes) = @_; + my $rh; + + # serialise the object + my ($rh_object, $name) + = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object); + + if (!$rh_object) { + warn "empty hash from serialiser ($message_type $object)\n"; + return; + } + $rh->{$name} = $rh_object; + + # add in the events hash + my $rh_event + = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes); + return unless $rh_event; + $rh_event->{'action'} = $message_type; + $rh_event->{'target'} = $name; + $rh_event->{'change_set'} = $change_set; + $rh_event->{'routing_key'} = "$name.$message_type"; + if (exists $rh_event->{'changes'}) { + $rh_event->{'routing_key'} + .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}}); + } + $rh->{'event'} = $rh_event; + + # create message object + my $message = Bugzilla::Extension::Push::Message->new_transient({ + payload => to_json($rh), + change_set => $change_set, + routing_key => $rh_event->{'routing_key'}, + }); + + # don't hit the database unless there are interested connectors + my $should_push = 0; + foreach my $connector (Bugzilla->push_ext->connectors->list) { + next unless $connector->enabled; + next unless $connector->should_send($message); + $should_push = 1; + last; + } + return unless $should_push; + + # insert into push table + $message->create_from_transient(); } # @@ -322,104 +321,113 @@ sub _push_object { # sub object_end_of_create { - my ($self, $args) = @_; - return unless $self->_enabled; - - # it's better to process objects from a non-generic end_of_create where - # possible; don't process them here to avoid duplicate messages - my $object = _get_object_from_args($args); - return if !$object || - $object->isa('Bugzilla::Bug') || - blessed($object) =~ /^Bugzilla::Extension/; - - $self->_object_created($args); + my ($self, $args) = @_; + return unless $self->_enabled; + + # it's better to process objects from a non-generic end_of_create where + # possible; don't process them here to avoid duplicate messages + my $object = _get_object_from_args($args); + return + if !$object + || $object->isa('Bugzilla::Bug') + || blessed($object) =~ /^Bugzilla::Extension/; + + $self->_object_created($args); } sub object_end_of_update { - my ($self, $args) = @_; + my ($self, $args) = @_; - # User objects are updated with every page load (to touch the session - # token). Because we ignore user objects, there's no need to create an - # instance of Push to check if we're enabled. - my $object = _get_object_from_args($args); - return if !$object || $object->isa('Bugzilla::User'); + # User objects are updated with every page load (to touch the session + # token). Because we ignore user objects, there's no need to create an + # instance of Push to check if we're enabled. + my $object = _get_object_from_args($args); + return if !$object || $object->isa('Bugzilla::User'); - return unless $self->_enabled; + return unless $self->_enabled; - # it's better to process objects from a non-generic end_of_update where - # possible; don't process them here to avoid duplicate messages - return if $object->isa('Bugzilla::Bug') || - $object->isa('Bugzilla::Flag') || - blessed($object) =~ /^Bugzilla::Extension/; + # it's better to process objects from a non-generic end_of_update where + # possible; don't process them here to avoid duplicate messages + return + if $object->isa('Bugzilla::Bug') + || $object->isa('Bugzilla::Flag') + || blessed($object) =~ /^Bugzilla::Extension/; - $self->_object_modified($args); + $self->_object_modified($args); } # process bugs once they are fully formed # object_end_of_update is triggered while a bug is being created sub bug_end_of_create { - my ($self, $args) = @_; - return unless $self->_enabled; - $self->_object_created($args); + my ($self, $args) = @_; + return unless $self->_enabled; + $self->_object_created($args); } sub bug_end_of_update { - my ($self, $args) = @_; - return unless $self->_enabled; - $self->_object_modified($args); + my ($self, $args) = @_; + return unless $self->_enabled; + $self->_object_modified($args); } sub flag_end_of_update { - my ($self, $args) = @_; - return unless $self->_enabled; - _morph_flag_updates($args); - $self->_object_modified($args); - delete $args->{changes}; + my ($self, $args) = @_; + return unless $self->_enabled; + _morph_flag_updates($args); + $self->_object_modified($args); + delete $args->{changes}; } # comments in bugzilla 4.0 doesn't aren't included in the bug_end_of_* hooks, # this code uses custom hooks to trigger sub bug_comment_create { - my ($self, $args) = @_; - return unless $self->_enabled; + my ($self, $args) = @_; + return unless $self->_enabled; - return unless _should_push('Bugzilla::Comment'); - my $bug = $args->{'bug'} or return; - my $timestamp = $args->{'timestamp'} or return; + return unless _should_push('Bugzilla::Comment'); + my $bug = $args->{'bug'} or return; + my $timestamp = $args->{'timestamp'} or return; - my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp }); + my $comments + = Bugzilla::Comment->match({bug_id => $bug->id, bug_when => $timestamp}); - foreach my $comment (@$comments) { - if ($comment->body ne '') { - $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp }); - } + foreach my $comment (@$comments) { + if ($comment->body ne '') { + $self->_push_object('create', $comment, change_set_id(), + {timestamp => $timestamp}); } + } } sub bug_comment_update { - my ($self, $args) = @_; - return unless $self->_enabled; - - return unless _should_push('Bugzilla::Comment'); - my $bug = $args->{'bug'} or return; - my $timestamp = $args->{'timestamp'} or return; - - my $comment_id = $args->{'comment_id'}; - if ($comment_id) { - # XXX this should set changes. only is_private changes will trigger this event - my $comment = Bugzilla::Comment->new($comment_id); - $self->_push_object('update', $comment, change_set_id(), { timestamp => $timestamp }); - - } else { - # when a bug is created, an update is also triggered; we don't want to sent - # update messages for the initial comment, or for empty comments - my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp }); - foreach my $comment (@$comments) { - if ($comment->body ne '' && $comment->count) { - $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp }); - } - } + my ($self, $args) = @_; + return unless $self->_enabled; + + return unless _should_push('Bugzilla::Comment'); + my $bug = $args->{'bug'} or return; + my $timestamp = $args->{'timestamp'} or return; + + my $comment_id = $args->{'comment_id'}; + if ($comment_id) { + + # XXX this should set changes. only is_private changes will trigger this event + my $comment = Bugzilla::Comment->new($comment_id); + $self->_push_object('update', $comment, change_set_id(), + {timestamp => $timestamp}); + + } + else { + # when a bug is created, an update is also triggered; we don't want to sent + # update messages for the initial comment, or for empty comments + my $comments + = Bugzilla::Comment->match({bug_id => $bug->id, bug_when => $timestamp}); + foreach my $comment (@$comments) { + if ($comment->body ne '' && $comment->count) { + $self->_push_object('create', $comment, change_set_id(), + {timestamp => $timestamp}); + } } + } } # @@ -427,36 +435,30 @@ sub bug_comment_update { # sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - - if ($page eq 'push_config.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_config($vars); - - } elsif ($page eq 'push_queues.html' - || $page eq 'push_queues_view.html' - ) { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_queues($vars, $page); - - } elsif ($page eq 'push_log.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_log($vars); - } + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'push_config.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_config($vars); + + } + elsif ($page eq 'push_queues.html' || $page eq 'push_queues_view.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_queues($vars, $page); + + } + elsif ($page eq 'push_log.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_log($vars); + } } # @@ -464,196 +466,86 @@ sub page_before_template { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'push'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - payload => { - TYPE => 'LONGTEXT', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - ], - }; - $args->{'schema'}->{'push_backlog'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - message_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - payload => { - TYPE => 'LONGTEXT', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - attempt_ts => { - TYPE => 'DATETIME', - }, - attempts => { - TYPE => 'INT2', - NOTNULL => 1, - }, - last_error => { - TYPE => 'MEDIUMTEXT', - }, - ], - INDEXES => [ - push_backlog_idx => { - FIELDS => ['message_id', 'connector'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_backoff'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - next_attempt_ts => { - TYPE => 'DATETIME', - }, - attempts => { - TYPE => 'INT2', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_backoff_idx => { - FIELDS => ['connector'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_options'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - option_name => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - option_value => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_options_idx => { - FIELDS => ['connector', 'option_name'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_log'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - message_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - processed_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - result => { - TYPE => 'INT1', - NOTNULL => 1, - }, - data => { - TYPE => 'MEDIUMTEXT', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'push'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + payload => {TYPE => 'LONGTEXT', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + ], + }; + $args->{'schema'}->{'push_backlog'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + message_id => {TYPE => 'INT3', NOTNULL => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + payload => {TYPE => 'LONGTEXT', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + attempt_ts => {TYPE => 'DATETIME',}, + attempts => {TYPE => 'INT2', NOTNULL => 1,}, + last_error => {TYPE => 'MEDIUMTEXT',}, + ], + INDEXES => [ + push_backlog_idx => {FIELDS => ['message_id', 'connector'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'push_backoff'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + next_attempt_ts => {TYPE => 'DATETIME',}, + attempts => {TYPE => 'INT2', NOTNULL => 1,}, + ], + INDEXES => [push_backoff_idx => {FIELDS => ['connector'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'push_options'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + option_name => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + option_value => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => [ + push_options_idx => {FIELDS => ['connector', 'option_name'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'push_log'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + message_id => {TYPE => 'INT3', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + processed_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + result => {TYPE => 'INT1', NOTNULL => 1,}, + data => {TYPE => 'MEDIUMTEXT',}, + ], + }; } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting push extension logs and messages...\n"; - $dbh->do("DELETE FROM push"); - $dbh->do("DELETE FROM push_backlog"); - $dbh->do("DELETE FROM push_backoff"); - $dbh->do("DELETE FROM push_log"); - $dbh->do("DELETE FROM push_options"); + my $dbh = Bugzilla->dbh; + print "Deleting push extension logs and messages...\n"; + $dbh->do("DELETE FROM push"); + $dbh->do("DELETE FROM push_backlog"); + $dbh->do("DELETE FROM push_backoff"); + $dbh->do("DELETE FROM push_log"); + $dbh->do("DELETE FROM push_options"); } __PACKAGE__->NAME; diff --git a/extensions/Push/bin/bugzilla-pushd.pl b/extensions/Push/bin/bugzilla-pushd.pl index 47c905558..cc509aa45 100755 --- a/extensions/Push/bin/bugzilla-pushd.pl +++ b/extensions/Push/bin/bugzilla-pushd.pl @@ -14,8 +14,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Extension::Push::Daemon; diff --git a/extensions/Push/bin/nagios_push_checker.pl b/extensions/Push/bin/nagios_push_checker.pl index b578c33d2..4e6e94167 100755 --- a/extensions/Push/bin/nagios_push_checker.pl +++ b/extensions/Push/bin/nagios_push_checker.pl @@ -20,15 +20,14 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); # Number of jobs required in the queue before we alert -use constant WARN_COUNT => 500; -use constant ALARM_COUNT => 750; +use constant WARN_COUNT => 500; +use constant ALARM_COUNT => 750; -use constant NAGIOS_OK => 0; -use constant NAGIOS_WARNING => 1; -use constant NAGIOS_CRITICAL => 2; +use constant NAGIOS_OK => 0; +use constant NAGIOS_WARNING => 1; +use constant NAGIOS_CRITICAL => 2; -my $connector = shift - || die "Syntax: $0 connector\neg. $0 TCL\n"; +my $connector = shift || die "Syntax: $0 connector\neg. $0 TCL\n"; $connector = uc($connector); my $sql = <switch_to_shadow_db; -my ($count) = @{ $dbh->selectcol_arrayref($sql, undef, $connector) }; +my ($count) = @{$dbh->selectcol_arrayref($sql, undef, $connector)}; if ($count < WARN_COUNT) { - print "push $connector OK: $count messages found.\n"; - exit NAGIOS_OK; -} elsif ($count < ALARM_COUNT) { - print "push $connector WARNING: $count messages found.\n"; - exit NAGIOS_WARNING; -} else { - print "push $connector CRITICAL: $count messages found.\n"; - exit NAGIOS_CRITICAL; + print "push $connector OK: $count messages found.\n"; + exit NAGIOS_OK; +} +elsif ($count < ALARM_COUNT) { + print "push $connector WARNING: $count messages found.\n"; + exit NAGIOS_WARNING; +} +else { + print "push $connector CRITICAL: $count messages found.\n"; + exit NAGIOS_CRITICAL; } diff --git a/extensions/Push/lib/Admin.pm b/extensions/Push/lib/Admin.pm index 9df2bddcb..d86d30a62 100644 --- a/extensions/Push/lib/Admin.pm +++ b/extensions/Push/lib/Admin.pm @@ -19,110 +19,109 @@ use Bugzilla::Util qw(trim detaint_natural trick_taint); use base qw(Exporter); our @EXPORT = qw( - admin_config - admin_queues - admin_log + admin_config + admin_queues + admin_log ); sub admin_config { - my ($vars) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; - - if ($input->{save}) { - my $token = $input->{token}; - check_hash_token($token, ['push_config']); - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - _update_config_from_form('global', $push->config); - foreach my $connector ($push->connectors->list) { - _update_config_from_form($connector->name, $connector->config); - } - $push->set_config_last_modified(); - $dbh->bz_commit_transaction(); - $vars->{message} = 'push_config_updated'; + my ($vars) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; + + if ($input->{save}) { + my $token = $input->{token}; + check_hash_token($token, ['push_config']); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + _update_config_from_form('global', $push->config); + foreach my $connector ($push->connectors->list) { + _update_config_from_form($connector->name, $connector->config); } + $push->set_config_last_modified(); + $dbh->bz_commit_transaction(); + $vars->{message} = 'push_config_updated'; + } - $vars->{push} = $push; - $vars->{connectors} = $push->connectors; + $vars->{push} = $push; + $vars->{connectors} = $push->connectors; } sub _update_config_from_form { - my ($name, $config) = @_; - my $input = Bugzilla->input_params; - - # read values from form - my $values = {}; - foreach my $option ($config->options) { - my $option_name = $option->{name}; - $values->{$option_name} = trim($input->{$name . ".$option_name"}); + my ($name, $config) = @_; + my $input = Bugzilla->input_params; + + # read values from form + my $values = {}; + foreach my $option ($config->options) { + my $option_name = $option->{name}; + $values->{$option_name} = trim($input->{$name . ".$option_name"}); + } + + # validate + if ($values->{enabled} eq 'Enabled') { + eval { $config->validate($values); }; + if ($@) { + ThrowUserError('push_error', {error_message => clean_error($@)}); } + } + + # update + foreach my $option ($config->options) { + my $option_name = $option->{name}; + trick_taint($values->{$option_name}); + $config->{$option_name} = $values->{$option_name}; + } + $config->update(); +} - # validate - if ($values->{enabled} eq 'Enabled') { - eval { - $config->validate($values); - }; - if ($@) { - ThrowUserError('push_error', { error_message => clean_error($@) }); - } - } +sub admin_queues { + my ($vars, $page) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; - # update - foreach my $option ($config->options) { - my $option_name = $option->{name}; - trick_taint($values->{$option_name}); - $config->{$option_name} = $values->{$option_name}; + if ($page eq 'push_queues.html') { + $vars->{push} = $push; + + } + elsif ($page eq 'push_queues_view.html') { + my $queue; + if ($input->{connector}) { + my $connector = $push->connectors->by_name($input->{connector}) + || ThrowUserError('push_error', {error_message => 'Invalid connector'}); + $queue = $connector->backlog; } - $config->update(); -} + else { + $queue = $push->queue; + } + $vars->{queue} = $queue; -sub admin_queues { - my ($vars, $page) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; - - if ($page eq 'push_queues.html') { - $vars->{push} = $push; - - } elsif ($page eq 'push_queues_view.html') { - my $queue; - if ($input->{connector}) { - my $connector = $push->connectors->by_name($input->{connector}) - || ThrowUserError('push_error', { error_message => 'Invalid connector' }); - $queue = $connector->backlog; - } else { - $queue = $push->queue; - } - $vars->{queue} = $queue; - - my $id = $input->{message} || 0; - detaint_natural($id) - || ThrowUserError('push_error', { error_message => 'Invalid message ID' }); - my $message = $queue->by_id($id) - || ThrowUserError('push_error', { error_message => 'Invalid message ID' }); - - if ($input->{delete}) { - my $token = $input->{token}; - check_hash_token($token, ['deleteMessage']); - $message->remove_from_db(); - $vars->{message} = 'push_message_deleted'; - - } else { - $vars->{message_obj} = $message; - eval { - $vars->{json} = to_json($message->payload_decoded, 1); - }; - } + my $id = $input->{message} || 0; + detaint_natural($id) + || ThrowUserError('push_error', {error_message => 'Invalid message ID'}); + my $message = $queue->by_id($id) + || ThrowUserError('push_error', {error_message => 'Invalid message ID'}); + + if ($input->{delete}) { + my $token = $input->{token}; + check_hash_token($token, ['deleteMessage']); + $message->remove_from_db(); + $vars->{message} = 'push_message_deleted'; + + } + else { + $vars->{message_obj} = $message; + eval { $vars->{json} = to_json($message->payload_decoded, 1); }; } + } } sub admin_log { - my ($vars) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; - $vars->{push} = $push; + $vars->{push} = $push; } 1; diff --git a/extensions/Push/lib/BacklogMessage.pm b/extensions/Push/lib/BacklogMessage.pm index 28b17bae3..942eb77eb 100644 --- a/extensions/Push/lib/BacklogMessage.pm +++ b/extensions/Push/lib/BacklogMessage.pm @@ -28,31 +28,31 @@ use Encode; # initialisation # -use constant DB_TABLE => 'push_backlog'; +use constant DB_TABLE => 'push_backlog'; use constant DB_COLUMNS => qw( - id - message_id - push_ts - payload - change_set - routing_key - connector - attempt_ts - attempts - last_error + id + message_id + push_ts + payload + change_set + routing_key + connector + attempt_ts + attempts + last_error ); use constant UPDATE_COLUMNS => qw( - attempt_ts - attempts - last_error + attempt_ts + attempts + last_error ); use constant LIST_ORDER => 'push_ts'; use constant VALIDATORS => { - payload => \&_check_payload, - change_set => \&_check_change_set, - routing_key => \&_check_routing_key, - connector => \&_check_connector, - attempts => \&_check_attempts, + payload => \&_check_payload, + change_set => \&_check_change_set, + routing_key => \&_check_routing_key, + connector => \&_check_connector, + attempts => \&_check_attempts, }; # @@ -60,46 +60,46 @@ use constant VALIDATORS => { # sub create_from_message { - my ($class, $message, $connector) = @_; - my $self = $class->create({ - message_id => $message->id, - push_ts => $message->push_ts, - payload => $message->payload, - change_set => $message->change_set, - routing_key => $message->routing_key, - connector => $connector->name, - attempt_ts => undef, - attempts => 0, - last_error => undef, - }); - return $self; + my ($class, $message, $connector) = @_; + my $self = $class->create({ + message_id => $message->id, + push_ts => $message->push_ts, + payload => $message->payload, + change_set => $message->change_set, + routing_key => $message->routing_key, + connector => $connector->name, + attempt_ts => undef, + attempts => 0, + last_error => undef, + }); + return $self; } # # accessors # -sub message_id { return $_[0]->{'message_id'} } -sub push_ts { return $_[0]->{'push_ts'}; } -sub payload { return $_[0]->{'payload'}; } -sub change_set { return $_[0]->{'change_set'}; } +sub message_id { return $_[0]->{'message_id'} } +sub push_ts { return $_[0]->{'push_ts'}; } +sub payload { return $_[0]->{'payload'}; } +sub change_set { return $_[0]->{'change_set'}; } sub routing_key { return $_[0]->{'routing_key'}; } -sub connector { return $_[0]->{'connector'}; } -sub attempt_ts { return $_[0]->{'attempt_ts'}; } -sub attempts { return $_[0]->{'attempts'}; } -sub last_error { return $_[0]->{'last_error'}; } +sub connector { return $_[0]->{'connector'}; } +sub attempt_ts { return $_[0]->{'attempt_ts'}; } +sub attempts { return $_[0]->{'attempts'}; } +sub last_error { return $_[0]->{'last_error'}; } sub payload_decoded { - my ($self) = @_; - return from_json($self->{'payload'}); + my ($self) = @_; + return from_json($self->{'payload'}); } sub attempt_time { - my ($self) = @_; - if (!exists $self->{'attempt_time'}) { - $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch; - } - return $self->{'attempt_time'}; + my ($self) = @_; + if (!exists $self->{'attempt_time'}) { + $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch; + } + return $self->{'attempt_time'}; } # @@ -107,11 +107,11 @@ sub attempt_time { # sub inc_attempts { - my ($self, $error) = @_; - $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); - $self->{attempts} = $self->{attempts} + 1; - $self->{last_error} = $error; - $self->update; + my ($self, $error) = @_; + $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); + $self->{attempts} = $self->{attempts} + 1; + $self->{last_error} = $error; + $self->update; } # @@ -119,32 +119,35 @@ sub inc_attempts { # sub _check_payload { - my ($invocant, $value) = @_; - length($value) || ThrowCodeError('push_invalid_payload'); - return $value; + my ($invocant, $value) = @_; + length($value) || ThrowCodeError('push_invalid_payload'); + return $value; } sub _check_change_set { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_change_set'); + return $value; } sub _check_routing_key { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_routing_key'); + return $value; } sub _check_connector { - my ($invocant, $value) = @_; - Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } sub _check_attempts { - my ($invocant, $value) = @_; - return $value || 0; + my ($invocant, $value) = @_; + return $value || 0; } 1; diff --git a/extensions/Push/lib/BacklogQueue.pm b/extensions/Push/lib/BacklogQueue.pm index a7200c688..17d0a188f 100644 --- a/extensions/Push/lib/BacklogQueue.pm +++ b/extensions/Push/lib/BacklogQueue.pm @@ -15,74 +15,67 @@ use Bugzilla; use Bugzilla::Extension::Push::BacklogMessage; sub new { - my ($class, $connector) = @_; - my $self = {}; - bless($self, $class); - $self->{connector} = $connector; - return $self; + my ($class, $connector) = @_; + my $self = {}; + bless($self, $class); + $self->{connector} = $connector; + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array(" + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array(" SELECT COUNT(*) FROM push_backlog - WHERE connector = ?", - undef, - $self->{connector}); + WHERE connector = ?", undef, $self->{connector}); } sub oldest { - my ($self) = @_; - my @messages = $self->list( - limit => 1, - filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))', - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self) = @_; + my @messages = $self->list( + limit => 1, + filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))', + ); + return scalar(@messages) ? $messages[0] : undef; } sub by_id { - my ($self, $id) = @_; - my @messages = $self->list( - limit => 1, - filter => "AND (log.id = $id)", - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self, $id) = @_; + my @messages = $self->list(limit => 1, filter => "AND (log.id = $id)",); + return scalar(@messages) ? $messages[0] : undef; } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $filter_sql = $args{filter} || ''; - my $sth = $dbh->prepare(" + my $filter_sql = $args{filter} || ''; + my $sth = $dbh->prepare(" SELECT log.id, message_id, push_ts, payload, change_set, routing_key, attempt_ts, log.attempts FROM push_backlog log LEFT JOIN push_backoff off ON off.connector = log.connector - WHERE log.connector = ? ". - $args{filter} . " - ORDER BY push_ts " . - $dbh->sql_limit($args{limit}) - ); - $sth->execute($self->{connector}); - while (my $row = $sth->fetchrow_hashref()) { - push @result, Bugzilla::Extension::Push::BacklogMessage->new({ - id => $row->{id}, - message_id => $row->{message_id}, - push_ts => $row->{push_ts}, - payload => $row->{payload}, - change_set => $row->{change_set}, - routing_key => $row->{routing_key}, - connector => $self->{connector}, - attempt_ts => $row->{attempt_ts}, - attempts => $row->{attempts}, - }); - } - return @result; + WHERE log.connector = ? " . $args{filter} . " + ORDER BY push_ts " . $dbh->sql_limit($args{limit})); + $sth->execute($self->{connector}); + while (my $row = $sth->fetchrow_hashref()) { + push @result, + Bugzilla::Extension::Push::BacklogMessage->new({ + id => $row->{id}, + message_id => $row->{message_id}, + push_ts => $row->{push_ts}, + payload => $row->{payload}, + change_set => $row->{change_set}, + routing_key => $row->{routing_key}, + connector => $self->{connector}, + attempt_ts => $row->{attempt_ts}, + attempts => $row->{attempts}, + }); + } + return @result; } # @@ -90,39 +83,40 @@ sub list { # sub backoff { - my ($self) = @_; - if (!$self->{backoff}) { - my $ra = Bugzilla::Extension::Push::Backoff->match({ - connector => $self->{connector} + my ($self) = @_; + if (!$self->{backoff}) { + my $ra + = Bugzilla::Extension::Push::Backoff->match({connector => $self->{connector} + }); + if (@$ra) { + $self->{backoff} = $ra->[0]; + } + else { + $self->{backoff} + = Bugzilla::Extension::Push::Backoff->create({connector => $self->{connector} }); - if (@$ra) { - $self->{backoff} = $ra->[0]; - } else { - $self->{backoff} = Bugzilla::Extension::Push::Backoff->create({ - connector => $self->{connector} - }); - } } - return $self->{backoff}; + } + return $self->{backoff}; } sub reset_backoff { - my ($self) = @_; - my $backoff = $self->backoff; - $backoff->reset(); - $backoff->update(); + my ($self) = @_; + my $backoff = $self->backoff; + $backoff->reset(); + $backoff->update(); } sub inc_backoff { - my ($self) = @_; - my $backoff = $self->backoff; - $backoff->inc(); - $backoff->update(); + my ($self) = @_; + my $backoff = $self->backoff; + $backoff->inc(); + $backoff->update(); } sub connector { - my ($self) = @_; - return $self->{connector}; + my ($self) = @_; + return $self->{connector}; } 1; diff --git a/extensions/Push/lib/Backoff.pm b/extensions/Push/lib/Backoff.pm index 0436cdf14..070adfc29 100644 --- a/extensions/Push/lib/Backoff.pm +++ b/extensions/Push/lib/Backoff.pm @@ -26,21 +26,21 @@ use Bugzilla::Util; # initialisation # -use constant DB_TABLE => 'push_backoff'; +use constant DB_TABLE => 'push_backoff'; use constant DB_COLUMNS => qw( - id - connector - next_attempt_ts - attempts + id + connector + next_attempt_ts + attempts ); use constant UPDATE_COLUMNS => qw( - next_attempt_ts - attempts + next_attempt_ts + attempts ); use constant VALIDATORS => { - connector => \&_check_connector, - next_attempt_ts => \&_check_next_attempt_ts, - attempts => \&_check_attempts, + connector => \&_check_connector, + next_attempt_ts => \&_check_next_attempt_ts, + attempts => \&_check_attempts, }; use constant LIST_ORDER => 'next_attempt_ts'; @@ -48,16 +48,16 @@ use constant LIST_ORDER => 'next_attempt_ts'; # accessors # -sub connector { return $_[0]->{'connector'}; } +sub connector { return $_[0]->{'connector'}; } sub next_attempt_ts { return $_[0]->{'next_attempt_ts'}; } -sub attempts { return $_[0]->{'attempts'}; } +sub attempts { return $_[0]->{'attempts'}; } sub next_attempt_time { - my ($self) = @_; - if (!exists $self->{'next_attempt_time'}) { - $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch; - } - return $self->{'next_attempt_time'}; + my ($self) = @_; + if (!exists $self->{'next_attempt_time'}) { + $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch; + } + return $self->{'next_attempt_time'}; } # @@ -65,25 +65,26 @@ sub next_attempt_time { # sub reset { - my ($self) = @_; - $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); - $self->{attempts} = 0; - INFO( sprintf 'resetting backoff for %s', $self->connector ); + my ($self) = @_; + $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); + $self->{attempts} = 0; + INFO(sprintf 'resetting backoff for %s', $self->connector); } sub inc { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - my $attempts = $self->attempts + 1; - my $seconds = $attempts <= 4 ? 5 ** $attempts : 15 * 60; - my ($date) = $dbh->selectrow_array("SELECT " . $dbh->sql_date_math('NOW()', '+', $seconds, 'SECOND')); - - $self->{next_attempt_ts} = $date; - $self->{attempts} = $attempts; - INFO( - sprintf 'setting next attempt for %s to %s (attempt %s)', $self->connector, $date, $attempts - ); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + my $attempts = $self->attempts + 1; + my $seconds = $attempts <= 4 ? 5**$attempts : 15 * 60; + my ($date) + = $dbh->selectrow_array( + "SELECT " . $dbh->sql_date_math('NOW()', '+', $seconds, 'SECOND')); + + $self->{next_attempt_ts} = $date; + $self->{attempts} = $attempts; + INFO(sprintf 'setting next attempt for %s to %s (attempt %s)', + $self->connector, $date, $attempts); } # @@ -91,19 +92,20 @@ sub inc { # sub _check_connector { - my ($invocant, $value) = @_; - Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } sub _check_next_attempt_ts { - my ($invocant, $value) = @_; - return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()'); + my ($invocant, $value) = @_; + return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()'); } sub _check_attempts { - my ($invocant, $value) = @_; - return $value || 0; + my ($invocant, $value) = @_; + return $value || 0; } 1; diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm index 2db95b972..bb0d523ad 100644 --- a/extensions/Push/lib/Config.pm +++ b/extensions/Push/lib/Config.pm @@ -17,199 +17,201 @@ use Bugzilla::Extension::Push::Option; use Crypt::CBC; sub new { - my ($class, $name, @options) = @_; - my $self = { - _name => $name - }; - bless($self, $class); - - $self->{_options} = [@options]; - unshift @{$self->{_options}}, { - name => 'enabled', - label => 'Status', - help => '', - type => 'select', - values => [ 'Enabled', 'Disabled' ], - default => 'Disabled', + my ($class, $name, @options) = @_; + my $self = {_name => $name}; + bless($self, $class); + + $self->{_options} = [@options]; + unshift @{$self->{_options}}, + { + name => 'enabled', + label => 'Status', + help => '', + type => 'select', + values => ['Enabled', 'Disabled'], + default => 'Disabled', }; - return $self; + return $self; } sub options { - my ($self) = @_; - return @{$self->{_options}}; + my ($self) = @_; + return @{$self->{_options}}; } sub option { - my ($self, $name) = @_; - foreach my $option ($self->options) { - return $option if $option->{name} eq $name; - } - return undef; + my ($self, $name) = @_; + foreach my $option ($self->options) { + return $option if $option->{name} eq $name; + } + return undef; } sub load { - my ($self) = @_; - my $config = {}; - - # prime $config with defaults - foreach my $rh ($self->options) { - $config->{$rh->{name}} = $rh->{default}; + my ($self) = @_; + my $config = {}; + + # prime $config with defaults + foreach my $rh ($self->options) { + $config->{$rh->{name}} = $rh->{default}; + } + + # override defaults with values from database + my $options + = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},}); + foreach my $option (@$options) { + my $option_config = $self->option($option->name) || next; + if ($option_config->{type} eq 'password') { + $config->{$option->name} = $self->_decrypt($option->value); } - - # override defaults with values from database - my $options = Bugzilla::Extension::Push::Option->match({ - connector => $self->{_name}, - }); - foreach my $option (@$options) { - my $option_config = $self->option($option->name) - || next; - if ($option_config->{type} eq 'password') { - $config->{$option->name} = $self->_decrypt($option->value); - } else { - $config->{$option->name} = $option->value; - } + else { + $config->{$option->name} = $option->value; } + } - # validate when running from the daemon - if (Bugzilla->push_ext->is_daemon) { - $self->_validate_config($config); - } - - # done, update self - foreach my $name (keys %$config) { - my $value = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name}; - TRACE( sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || '' ); - $self->{$name} = $config->{$name}; - } + # validate when running from the daemon + if (Bugzilla->push_ext->is_daemon) { + $self->_validate_config($config); + } + + # done, update self + foreach my $name (keys %$config) { + my $value + = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name}; + TRACE(sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || ''); + $self->{$name} = $config->{$name}; + } } sub validate { - my ($self, $config) = @_; - $self->_validate_mandatory($config); - $self->_validate_config($config); + my ($self, $config) = @_; + $self->_validate_mandatory($config); + $self->_validate_config($config); } sub update { - my ($self) = @_; - - my @valid_options = map { $_->{name} } $self->options; - - my %options; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => $self->{_name}, - }); - foreach my $option (@$options_list) { - $options{$option->name} = $option; - } - - # delete options which are no longer valid - foreach my $name (keys %options) { - if (!grep { $_ eq $name } @valid_options) { - $options{$name}->remove_from_db(); - delete $options{$name}; - } + my ($self) = @_; + + my @valid_options = map { $_->{name} } $self->options; + + my %options; + my $options_list + = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},}); + foreach my $option (@$options_list) { + $options{$option->name} = $option; + } + + # delete options which are no longer valid + foreach my $name (keys %options) { + if (!grep { $_ eq $name } @valid_options) { + $options{$name}->remove_from_db(); + delete $options{$name}; } + } - # update options - foreach my $name (keys %options) { - my $option = $options{$name}; - if ($self->option($name)->{type} eq 'password') { - $option->set_value($self->_encrypt($self->{$name})); - } else { - $option->set_value($self->{$name}); - } - $option->update(); + # update options + foreach my $name (keys %options) { + my $option = $options{$name}; + if ($self->option($name)->{type} eq 'password') { + $option->set_value($self->_encrypt($self->{$name})); } - - # add missing options - foreach my $name (@valid_options) { - next if exists $options{$name}; - Bugzilla::Extension::Push::Option->create({ - connector => $self->{_name}, - option_name => $name, - option_value => $self->{$name}, - }); + else { + $option->set_value($self->{$name}); } + $option->update(); + } + + # add missing options + foreach my $name (@valid_options) { + next if exists $options{$name}; + Bugzilla::Extension::Push::Option->create({ + connector => $self->{_name}, + option_name => $name, + option_value => $self->{$name}, + }); + } } sub _remove_invalid_options { - my ($self, $config) = @_; - my @names; - foreach my $rh ($self->options) { - push @names, $rh->{name}; - } - foreach my $name (keys %$config) { - if ($name =~ /^_/ || !grep { $_ eq $name } @names) { - delete $config->{$name}; - } + my ($self, $config) = @_; + my @names; + foreach my $rh ($self->options) { + push @names, $rh->{name}; + } + foreach my $name (keys %$config) { + if ($name =~ /^_/ || !grep { $_ eq $name } @names) { + delete $config->{$name}; } + } } sub _validate_mandatory { - my ($self, $config) = @_; - $self->_remove_invalid_options($config); - - my @missing; - foreach my $option ($self->options) { - next unless $option->{required}; - my $name = $option->{name}; - if (!exists $config->{$name} || !defined($config->{$name}) || $config->{$name} eq '') { - push @missing, $option; - } + my ($self, $config) = @_; + $self->_remove_invalid_options($config); + + my @missing; + foreach my $option ($self->options) { + next unless $option->{required}; + my $name = $option->{name}; + if ( !exists $config->{$name} + || !defined($config->{$name}) + || $config->{$name} eq '') + { + push @missing, $option; + } + } + if (@missing) { + my $connector = $self->{_name}; + @missing = map { $_->{label} } @missing; + if (scalar @missing == 1) { + die "The option '$missing[0]' for the connector '$connector' is mandatory\n"; } - if (@missing) { - my $connector = $self->{_name}; - @missing = map { $_->{label} } @missing; - if (scalar @missing == 1) { - die "The option '$missing[0]' for the connector '$connector' is mandatory\n"; - } else { - die "The following options for the connector '$connector' are mandatory:\n " - . join("\n ", @missing) . "\n"; - } + else { + die "The following options for the connector '$connector' are mandatory:\n " + . join("\n ", @missing) . "\n"; } + } } sub _validate_config { - my ($self, $config) = @_; - $self->_remove_invalid_options($config); - - my @errors; - foreach my $option ($self->options) { - my $name = $option->{name}; - next unless exists $config->{$name} && exists $option->{validate}; - eval { - $option->{validate}->($config->{$name}, $config); - }; - push @errors, $@ if $@; - } - die join("\n", @errors) if @errors; - - if ($self->{_name} ne 'global') { - my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name}; - $class->options_validate($config); - } + my ($self, $config) = @_; + $self->_remove_invalid_options($config); + + my @errors; + foreach my $option ($self->options) { + my $name = $option->{name}; + next unless exists $config->{$name} && exists $option->{validate}; + eval { $option->{validate}->($config->{$name}, $config); }; + push @errors, $@ if $@; + } + die join("\n", @errors) if @errors; + + if ($self->{_name} ne 'global') { + my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name}; + $class->options_validate($config); + } } sub _cipher { - my ($self) = @_; - $self->{_cipher} ||= Crypt::CBC->new( - -key => Bugzilla->localconfig->{'site_wide_secret'}, - -cipher => 'DES_EDE3'); - return $self->{_cipher}; + my ($self) = @_; + $self->{_cipher} ||= Crypt::CBC->new( + -key => Bugzilla->localconfig->{'site_wide_secret'}, + -cipher => 'DES_EDE3' + ); + return $self->{_cipher}; } sub _decrypt { - my ($self, $value) = @_; - my $result; - eval { $result = $self->_cipher->decrypt_hex($value) }; - return $@ ? '' : $result; + my ($self, $value) = @_; + my $result; + eval { $result = $self->_cipher->decrypt_hex($value) }; + return $@ ? '' : $result; } sub _encrypt { - my ($self, $value) = @_; - return $self->_cipher->encrypt_hex($value); + my ($self, $value) = @_; + return $self->_cipher->encrypt_hex($value); } 1; diff --git a/extensions/Push/lib/Connector.disabled/AMQP.pm b/extensions/Push/lib/Connector.disabled/AMQP.pm index 1ba365e21..dda73dade 100644 --- a/extensions/Push/lib/Connector.disabled/AMQP.pm +++ b/extensions/Push/lib/Connector.disabled/AMQP.pm @@ -20,211 +20,197 @@ use Bugzilla::Util qw(generate_random_password); use DateTime; sub init { - my ($self) = @_; - $self->{mq} = 0; - $self->{channel} = 1; - - if ($self->config->{queue}) { - $self->{queue_name} = $self->config->{queue}; - } else { - my $queue_name = Bugzilla->localconfig->{'urlbase'}; - $queue_name =~ s#^https?://##; - $queue_name =~ s#/$#|#; - $queue_name .= generate_random_password(16); - $self->{queue_name} = $queue_name; - } + my ($self) = @_; + $self->{mq} = 0; + $self->{channel} = 1; + + if ($self->config->{queue}) { + $self->{queue_name} = $self->config->{queue}; + } + else { + my $queue_name = Bugzilla->localconfig->{'urlbase'}; + $queue_name =~ s#^https?://##; + $queue_name =~ s#/$#|#; + $queue_name .= generate_random_password(16); + $self->{queue_name} = $queue_name; + } } sub options { - return ( - { - name => 'host', - label => 'AMQP Hostname', - type => 'string', - default => 'localhost', - required => 1, - }, - { - name => 'port', - label => 'AMQP Port', - type => 'string', - default => '5672', - required => 1, - validate => sub { - $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n"; - }, - }, - { - name => 'username', - label => 'Username', - type => 'string', - default => 'guest', - required => 1, - }, - { - name => 'password', - label => 'Password', - type => 'password', - default => 'guest', - required => 1, - }, - { - name => 'vhost', - label => 'Virtual Host', - type => 'string', - default => '/', - required => 1, - }, - { - name => 'exchange', - label => 'Exchange', - type => 'string', - default => '', - required => 1, - }, - { - name => 'queue', - label => 'Queue', - type => 'string', - }, - ); + return ( + { + name => 'host', + label => 'AMQP Hostname', + type => 'string', + default => 'localhost', + required => 1, + }, + { + name => 'port', + label => 'AMQP Port', + type => 'string', + default => '5672', + required => 1, + validate => sub { + $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n"; + }, + }, + { + name => 'username', + label => 'Username', + type => 'string', + default => 'guest', + required => 1, + }, + { + name => 'password', + label => 'Password', + type => 'password', + default => 'guest', + required => 1, + }, + { + name => 'vhost', + label => 'Virtual Host', + type => 'string', + default => '/', + required => 1, + }, + { + name => 'exchange', + label => 'Exchange', + type => 'string', + default => '', + required => 1, + }, + {name => 'queue', label => 'Queue', type => 'string',}, + ); } sub stop { - my ($self) = @_; - if ($self->{mq}) { - Bugzilla->push_ext->logger->debug('AMQP: disconnecting'); - $self->{mq}->disconnect(); - $self->{mq} = 0; - } + my ($self) = @_; + if ($self->{mq}) { + Bugzilla->push_ext->logger->debug('AMQP: disconnecting'); + $self->{mq}->disconnect(); + $self->{mq} = 0; + } } sub _connect { - my ($self) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - $self->stop(); - - $logger->debug('AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port}); - require Net::RabbitMQ; - my $mq = Net::RabbitMQ->new(); - $mq->connect( - $config->{host}, - { - port => $config->{port}, - user => $config->{username}, - password => $config->{password}, - } - ); - $self->{mq} = $mq; - - $logger->debug('AMQP: Opening channel ' . $self->{channel}); - $self->{mq}->channel_open($self->{channel}); - - $logger->debug('AMQP: Declaring queue ' . $self->{queue_name}); - $self->{mq}->queue_declare( - $self->{channel}, - $self->{queue_name}, - { - passive => 0, - durable => 1, - exclusive => 0, - auto_delete => 0, - }, - ); + my ($self) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + $self->stop(); + + $logger->debug( + 'AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port}); + require Net::RabbitMQ; + my $mq = Net::RabbitMQ->new(); + $mq->connect( + $config->{host}, + { + port => $config->{port}, + user => $config->{username}, + password => $config->{password}, + } + ); + $self->{mq} = $mq; + + $logger->debug('AMQP: Opening channel ' . $self->{channel}); + $self->{mq}->channel_open($self->{channel}); + + $logger->debug('AMQP: Declaring queue ' . $self->{queue_name}); + $self->{mq}->queue_declare($self->{channel}, $self->{queue_name}, + {passive => 0, durable => 1, exclusive => 0, auto_delete => 0,}, + ); } sub _bind { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # bind to queue (also acts to verify the connection is still valid) - if ($self->{mq}) { - eval { - $logger->debug('AMQP: binding queue(' . $self->{queue_name} . ') with exchange(' . $config->{exchange} . ')'); - $self->{mq}->queue_bind( - $self->{channel}, - $self->{queue_name}, - $config->{exchange}, - $message->routing_key, - ); - }; - if ($@) { - $logger->debug('AMQP: ' . clean_error($@)); - $self->{mq} = 0; - } + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + # bind to queue (also acts to verify the connection is still valid) + if ($self->{mq}) { + eval { + $logger->debug('AMQP: binding queue(' + . $self->{queue_name} + . ') with exchange(' + . $config->{exchange} + . ')'); + $self->{mq}->queue_bind( + $self->{channel}, $self->{queue_name}, + $config->{exchange}, $message->routing_key, + ); + }; + if ($@) { + $logger->debug('AMQP: ' . clean_error($@)); + $self->{mq} = 0; } + } } sub should_send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - - my $payload = $message->payload_decoded(); - my $target = $payload->{event}->{target}; - my $is_private = $payload->{$target}->{is_private} ? 1 : 0; - if (!$is_private && exists $payload->{$target}->{bug}) { - $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0; - } - - if ($is_private) { - # we only want to push the is_private message from the change_set, as - # this is guaranteed to contain public information only - if ($message->routing_key !~ /\.modify:is_private$/) { - $logger->debug('AMQP: Ignoring private message'); - return 0; - } - $logger->debug('AMQP: Sending change of message to is_private'); + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + + my $payload = $message->payload_decoded(); + my $target = $payload->{event}->{target}; + my $is_private = $payload->{$target}->{is_private} ? 1 : 0; + if (!$is_private && exists $payload->{$target}->{bug}) { + $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0; + } + + if ($is_private) { + + # we only want to push the is_private message from the change_set, as + # this is guaranteed to contain public information only + if ($message->routing_key !~ /\.modify:is_private$/) { + $logger->debug('AMQP: Ignoring private message'); + return 0; } - return 1; + $logger->debug('AMQP: Sending change of message to is_private'); + } + return 1; } sub send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # don't push comments to pulse - if ($message->routing_key =~ /^comment\./) { - $logger->debug('AMQP: Ignoring comment'); - return PUSH_RESULT_IGNORED; - } + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; - # don't push private data - $self->should_push($message) - || return PUSH_RESULT_IGNORED; + # don't push comments to pulse + if ($message->routing_key =~ /^comment\./) { + $logger->debug('AMQP: Ignoring comment'); + return PUSH_RESULT_IGNORED; + } - $self->_bind($message); + # don't push private data + $self->should_push($message) || return PUSH_RESULT_IGNORED; - eval { - # reconnect if required - if (!$self->{mq}) { - $self->_connect(); - } - - # send message - $logger->debug('AMQP: Publishing message'); - $self->{mq}->publish( - $self->{channel}, - $message->routing_key, - $message->payload, - { - exchange => $config->{exchange}, - }, - { - content_type => 'text/plain', - content_encoding => '8bit', - }, - ); - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); + $self->_bind($message); + + eval { + # reconnect if required + if (!$self->{mq}) { + $self->_connect(); } - return PUSH_RESULT_OK; + # send message + $logger->debug('AMQP: Publishing message'); + $self->{mq}->publish( + $self->{channel}, $message->routing_key, $message->payload, + {exchange => $config->{exchange},}, + {content_type => 'text/plain', content_encoding => '8bit',}, + ); + }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } + + return PUSH_RESULT_OK; } 1; diff --git a/extensions/Push/lib/Connector.disabled/ServiceNow.pm b/extensions/Push/lib/Connector.disabled/ServiceNow.pm index d0ebdcf10..032e47dde 100644 --- a/extensions/Push/lib/Connector.disabled/ServiceNow.pm +++ b/extensions/Push/lib/Connector.disabled/ServiceNow.pm @@ -32,403 +32,411 @@ use MIME::Base64; use Net::LDAP; use constant SEND_COMPONENTS => ( - { - product => 'mozilla.org', - component => 'Server Operations: Desktop Issues', - }, + {product => 'mozilla.org', component => 'Server Operations: Desktop Issues',}, ); sub options { - return ( - { - name => 'bugzilla_user', - label => 'Bugzilla Service-Now User', - type => 'string', - default => 'service.now@bugzilla.tld', - required => 1, - validate => sub { - Bugzilla::User->new({ name => $_[0] }) - || die "Invalid Bugzilla user ($_[0])\n"; - }, - }, - { - name => 'ldap_scheme', - label => 'Mozilla LDAP Scheme', - type => 'select', - values => [ 'LDAP', 'LDAPS' ], - default => 'LDAPS', - required => 1, - }, - { - name => 'ldap_host', - label => 'Mozilla LDAP Host', - type => 'string', - default => '', - required => 1, - }, - { - name => 'ldap_user', - label => 'Mozilla LDAP Bind Username', - type => 'string', - default => '', - required => 1, - }, - { - name => 'ldap_pass', - label => 'Mozilla LDAP Password', - type => 'password', - default => '', - required => 1, - }, - { - name => 'ldap_poll', - label => 'Mozilla LDAP Poll Frequency', - type => 'string', - default => '3', - required => 1, - help => 'minutes', - validate => sub { - $_[0] =~ /\D/ - && die "LDAP Poll Frequency must be an integer\n"; - $_[0] == 0 - && die "LDAP Poll Frequency cannot be less than one minute\n"; - }, - }, - { - name => 'service_now_url', - label => 'Service Now JSON URL', - type => 'string', - default => 'https://mozilladev.service-now.com', - required => 1, - help => "Must start with https:// and end with ?JSON", - validate => sub { - $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/# - || die "Invalid Service Now JSON URL\n"; - $_[0] =~ m#\?JSON$# - || die "Invalid Service Now JSON URL (must end with ?JSON)\n"; - }, - }, - { - name => 'service_now_user', - label => 'Service Now JSON Username', - type => 'string', - default => '', - required => 1, - }, - { - name => 'service_now_pass', - label => 'Service Now JSON Password', - type => 'password', - default => '', - required => 1, - }, - ); + return ( + { + name => 'bugzilla_user', + label => 'Bugzilla Service-Now User', + type => 'string', + default => 'service.now@bugzilla.tld', + required => 1, + validate => sub { + Bugzilla::User->new({name => $_[0]}) || die "Invalid Bugzilla user ($_[0])\n"; + }, + }, + { + name => 'ldap_scheme', + label => 'Mozilla LDAP Scheme', + type => 'select', + values => ['LDAP', 'LDAPS'], + default => 'LDAPS', + required => 1, + }, + { + name => 'ldap_host', + label => 'Mozilla LDAP Host', + type => 'string', + default => '', + required => 1, + }, + { + name => 'ldap_user', + label => 'Mozilla LDAP Bind Username', + type => 'string', + default => '', + required => 1, + }, + { + name => 'ldap_pass', + label => 'Mozilla LDAP Password', + type => 'password', + default => '', + required => 1, + }, + { + name => 'ldap_poll', + label => 'Mozilla LDAP Poll Frequency', + type => 'string', + default => '3', + required => 1, + help => 'minutes', + validate => sub { + $_[0] =~ /\D/ && die "LDAP Poll Frequency must be an integer\n"; + $_[0] == 0 && die "LDAP Poll Frequency cannot be less than one minute\n"; + }, + }, + { + name => 'service_now_url', + label => 'Service Now JSON URL', + type => 'string', + default => 'https://mozilladev.service-now.com', + required => 1, + help => "Must start with https:// and end with ?JSON", + validate => sub { + $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/# + || die "Invalid Service Now JSON URL\n"; + $_[0] =~ m#\?JSON$# + || die "Invalid Service Now JSON URL (must end with ?JSON)\n"; + }, + }, + { + name => 'service_now_user', + label => 'Service Now JSON Username', + type => 'string', + default => '', + required => 1, + }, + { + name => 'service_now_pass', + label => 'Service Now JSON Password', + type => 'password', + default => '', + required => 1, + }, + ); } sub options_validate { - my ($self, $config) = @_; - my $host = $config->{ldap_host}; - trick_taint($host); - my $scheme = lc($config->{ldap_scheme}); - eval { - my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5) - or die $!; - $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); - }; - if ($@) { - die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@); - } + my ($self, $config) = @_; + my $host = $config->{ldap_host}; + trick_taint($host); + my $scheme = lc($config->{ldap_scheme}); + eval { + my $ldap + = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5) + or die $!; + $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); + }; + if ($@) { + die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@); + } } my $_instance; sub init { - my ($self) = @_; - $_instance = $self; + my ($self) = @_; + $_instance = $self; } sub load_config { - my ($self) = @_; - $self->SUPER::load_config(@_); - $self->{bugzilla_user} ||= Bugzilla::User->new({ name => $self->config->{bugzilla_user} }); + my ($self) = @_; + $self->SUPER::load_config(@_); + $self->{bugzilla_user} + ||= Bugzilla::User->new({name => $self->config->{bugzilla_user}}); } sub should_send { - my ($self, $message) = @_; - - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data) - || return 0; - - # we don't want to send the initial comment in a separate message - # because we inject it into the inital message - if (exists $data->{comment} && $data->{comment}->{number} == 0) { - return 0; - } - - my $target = $data->{event}->{target}; - unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') { - return 0; - } - - # ensure the service-now user can see the bug - if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) { - return 0; - } - $self->{bugzilla_user}->can_see_bug($bug_data->{id}) - || return 0; - - # don't push changes made by the service-now account - $data->{event}->{user}->{id} == $self->{bugzilla_user}->id - && return 0; - - # filter based on the component - my $bug = Bugzilla::Bug->new($bug_data->{id}); - my $send = 0; - foreach my $rh (SEND_COMPONENTS) { - if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) { - $send = 1; - last; - } + my ($self, $message) = @_; + + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data) || return 0; + + # we don't want to send the initial comment in a separate message + # because we inject it into the inital message + if (exists $data->{comment} && $data->{comment}->{number} == 0) { + return 0; + } + + my $target = $data->{event}->{target}; + unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') { + return 0; + } + + # ensure the service-now user can see the bug + if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) { + return 0; + } + $self->{bugzilla_user}->can_see_bug($bug_data->{id}) || return 0; + + # don't push changes made by the service-now account + $data->{event}->{user}->{id} == $self->{bugzilla_user}->id && return 0; + + # filter based on the component + my $bug = Bugzilla::Bug->new($bug_data->{id}); + my $send = 0; + foreach my $rh (SEND_COMPONENTS) { + if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) { + $send = 1; + last; } - return $send; + } + return $send; } sub send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # should_send intiailises bugzilla_user; make sure we return a useful error message - if (!$self->{bugzilla_user}) { - return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")"); - } - - # load the bug - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data); - my $bug = Bugzilla::Bug->new($bug_data->{id}); - - if ($message->routing_key eq 'bug.create') { - # inject the comment into the data for new bugs - my $comment = shift @{ $bug->comments }; - if ($comment->body ne '') { - $bug_data->{comment} = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1); - } - - } elsif ($message->routing_key eq 'attachment.create') { - # inject the attachment payload - my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id}); - $data->{attachment}->{data} = encode_base64($attachment->data); - } - - # map bmo login to ldap login and insert into json payload - $self->_add_ldap_logins($data, {}); - - # flatten json data - $self->_flatten($data); - - # add sysparm_action - $data->{sysparm_action} = 'insert'; - - if ($logger->debugging) { - $logger->debug(to_json(ref($data) ? $data : from_json($data), 1)); - } - - # send to service-now - my $request = HTTP::Request->new(POST => $self->config->{service_now_url}); - $request->content_type('application/json'); - $request->content(to_json($data)); - $request->authorization_basic($self->config->{service_now_user}, $self->config->{service_now_pass}); - - $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->localconfig->{urlbase}); - my $result = $self->{lwp}->request($request); - - # http level errors - if (!$result->is_success) { - # treat these as transient - return (PUSH_RESULT_TRANSIENT, $result->status_line); - } - - # empty response - if (length($result->content) == 0) { - # malformed request, treat as transient to allow code to fix - # may also be misconfiguration on servicenow, also transient - return (PUSH_RESULT_TRANSIENT, "Empty response"); + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + +# should_send intiailises bugzilla_user; make sure we return a useful error message + if (!$self->{bugzilla_user}) { + return (PUSH_RESULT_TRANSIENT, + "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")"); + } + + # load the bug + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data); + my $bug = Bugzilla::Bug->new($bug_data->{id}); + + if ($message->routing_key eq 'bug.create') { + + # inject the comment into the data for new bugs + my $comment = shift @{$bug->comments}; + if ($comment->body ne '') { + $bug_data->{comment} + = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1); } - # json errors - my $result_data; - eval { - $result_data = from_json($result->content); - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); - } - if ($logger->debugging) { - $logger->debug(to_json($result_data, 1)); - } - if (exists $result_data->{error}) { - return (PUSH_RESULT_ERROR, $result_data->{error}); - }; - - # malformed/unexpected json response - if (!exists $result_data->{records} - || ref($result_data->{records}) ne 'ARRAY' - || scalar(@{$result_data->{records}}) == 0 - ) { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: missing or empty 'records' array"); - } - - my $record = $result_data->{records}->[0]; - if (ref($record) ne 'HASH') { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records' array does not contain an object"); - } + } + elsif ($message->routing_key eq 'attachment.create') { + + # inject the attachment payload + my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id}); + $data->{attachment}->{data} = encode_base64($attachment->data); + } + + # map bmo login to ldap login and insert into json payload + $self->_add_ldap_logins($data, {}); + + # flatten json data + $self->_flatten($data); + + # add sysparm_action + $data->{sysparm_action} = 'insert'; + + if ($logger->debugging) { + $logger->debug(to_json(ref($data) ? $data : from_json($data), 1)); + } + + # send to service-now + my $request = HTTP::Request->new(POST => $self->config->{service_now_url}); + $request->content_type('application/json'); + $request->content(to_json($data)); + $request->authorization_basic($self->config->{service_now_user}, + $self->config->{service_now_pass}); + + $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->localconfig->{urlbase}); + my $result = $self->{lwp}->request($request); + + # http level errors + if (!$result->is_success) { + + # treat these as transient + return (PUSH_RESULT_TRANSIENT, $result->status_line); + } + + # empty response + if (length($result->content) == 0) { + + # malformed request, treat as transient to allow code to fix + # may also be misconfiguration on servicenow, also transient + return (PUSH_RESULT_TRANSIENT, "Empty response"); + } + + # json errors + my $result_data; + eval { $result_data = from_json($result->content); }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } + if ($logger->debugging) { + $logger->debug(to_json($result_data, 1)); + } + if (exists $result_data->{error}) { + return (PUSH_RESULT_ERROR, $result_data->{error}); + } + + # malformed/unexpected json response + if (!exists $result_data->{records} + || ref($result_data->{records}) ne 'ARRAY' + || scalar(@{$result_data->{records}}) == 0) + { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: missing or empty 'records' array"); + } + + my $record = $result_data->{records}->[0]; + if (ref($record) ne 'HASH') { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: 'records' array does not contain an object" + ); + } - # sys_id is the unique identifier for this action - if (!exists $record->{sys_id} || $record->{sys_id} eq '') { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id"); - } + # sys_id is the unique identifier for this action + if (!exists $record->{sys_id} || $record->{sys_id} eq '') { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id" + ); + } - # success - return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id}); + # success + return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id}); } sub _get_bug_data { - my ($self, $data) = @_; - my $target = $data->{event}->{target}; - if ($target eq 'bug') { - return $data->{bug}; - } elsif (exists $data->{$target}->{bug}) { - return $data->{$target}->{bug}; - } else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } sub _flatten { - # service-now expects a flat json object - my ($self, $data) = @_; - my $target = $data->{event}->{target}; + # service-now expects a flat json object + my ($self, $data) = @_; - # delete unnecessary deep objects - if ($target eq 'comment' || $target eq 'attachment') { - $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id}; - delete $data->{$target}->{bug}; - } - delete $data->{event}->{changes}; + my $target = $data->{event}->{target}; - $self->_flatten_hash($data, $data, 'u'); + # delete unnecessary deep objects + if ($target eq 'comment' || $target eq 'attachment') { + $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id}; + delete $data->{$target}->{bug}; + } + delete $data->{event}->{changes}; + + $self->_flatten_hash($data, $data, 'u'); } sub _flatten_hash { - my ($self, $base_hash, $hash, $prefix) = @_; - foreach my $key (keys %$hash) { - if (ref($hash->{$key}) eq 'HASH') { - $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key"); - } elsif (ref($hash->{$key}) ne 'ARRAY') { - $base_hash->{$prefix . "_$key"} = $hash->{$key}; - } - delete $hash->{$key}; + my ($self, $base_hash, $hash, $prefix) = @_; + foreach my $key (keys %$hash) { + if (ref($hash->{$key}) eq 'HASH') { + $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key"); + } + elsif (ref($hash->{$key}) ne 'ARRAY') { + $base_hash->{$prefix . "_$key"} = $hash->{$key}; } + delete $hash->{$key}; + } } sub _add_ldap_logins { - my ($self, $rh, $cache) = @_; - if (exists $rh->{login}) { - my $login = $rh->{login}; - $cache->{$login} ||= $self->_bmo_to_ldap($login); - Bugzilla->push_ext->logger->debug("BMO($login) --> LDAP(" . $cache->{$login} . ")"); - $rh->{ldap} = $cache->{$login}; - } - foreach my $key (keys %$rh) { - next unless ref($rh->{$key}) eq 'HASH'; - $self->_add_ldap_logins($rh->{$key}, $cache); - } + my ($self, $rh, $cache) = @_; + if (exists $rh->{login}) { + my $login = $rh->{login}; + $cache->{$login} ||= $self->_bmo_to_ldap($login); + Bugzilla->push_ext->logger->debug( + "BMO($login) --> LDAP(" . $cache->{$login} . ")"); + $rh->{ldap} = $cache->{$login}; + } + foreach my $key (keys %$rh) { + next unless ref($rh->{$key}) eq 'HASH'; + $self->_add_ldap_logins($rh->{$key}, $cache); + } } sub _bmo_to_ldap { - my ($self, $login) = @_; - my $ldap = $self->_ldap_cache(); + my ($self, $login) = @_; + my $ldap = $self->_ldap_cache(); - return '' unless $login =~ /\@mozilla\.(?:com|org)$/; + return '' unless $login =~ /\@mozilla\.(?:com|org)$/; - foreach my $check ($login, canon_email($login)) { - # check for matching bugmail entry - foreach my $mail (keys %$ldap) { - next unless $ldap->{$mail}{bugmail_canon} eq $check; - return $mail; - } + foreach my $check ($login, canon_email($login)) { - # check for matching mail - if (exists $ldap->{$check}) { - return $check; - } + # check for matching bugmail entry + foreach my $mail (keys %$ldap) { + next unless $ldap->{$mail}{bugmail_canon} eq $check; + return $mail; + } - # check for matching email alias - foreach my $mail (sort keys %$ldap) { - next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}}; - return $mail; - } + # check for matching mail + if (exists $ldap->{$check}) { + return $check; + } + + # check for matching email alias + foreach my $mail (sort keys %$ldap) { + next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}}; + return $mail; } + } - return ''; + return ''; } sub _ldap_cache { - my ($self) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # cache of all ldap entries; updated infrequently - if (!$self->{ldap_cache_time} || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) { - $logger->debug('refreshing LDAP cache'); - - my $cache = {}; - - my $host = $config->{ldap_host}; - trick_taint($host); - my $scheme = lc($config->{ldap_scheme}); - my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die') - or die $!; - $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); - foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') { - my $result = $ldap->search( - base => $ldap_base, - scope => 'sub', - filter => '(mail=*)', - attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'], - ); - foreach my $entry ($result->entries) { - my ($name, $bugMail, $mail, $type) = - map { $entry->get_value($_) || '' } - qw(cn bugzillaEmail mail employeeType); - next if $type eq 'DISABLED'; - $mail = lc $mail; - $bugMail = '' if $bugMail !~ /\@/; - $bugMail = trim($bugMail); - if ($bugMail =~ / /) { - $bugMail = (grep { /\@/ } split / /, $bugMail)[0]; - } - $name =~ s/\s+/ /g; - $cache->{$mail}{name} = trim($name); - $cache->{$mail}{bugmail} = $bugMail; - $cache->{$mail}{bugmail_canon} = canon_email($bugMail); - $cache->{$mail}{aliases} = []; - foreach my $alias ( - @{$entry->get_value('emailAlias', asref => 1) || []} - ) { - push @{$cache->{$mail}{aliases}}, canon_email($alias); - } - } - } + my ($self) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + # cache of all ldap entries; updated infrequently + if (!$self->{ldap_cache_time} + || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) + { + $logger->debug('refreshing LDAP cache'); - $self->{ldap_cache} = $cache; - $self->{ldap_cache_time} = (time); + my $cache = {}; + + my $host = $config->{ldap_host}; + trick_taint($host); + my $scheme = lc($config->{ldap_scheme}); + my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die') or die $!; + $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); + foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') { + my $result = $ldap->search( + base => $ldap_base, + scope => 'sub', + filter => '(mail=*)', + attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'], + ); + foreach my $entry ($result->entries) { + my ($name, $bugMail, $mail, $type) + = map { $entry->get_value($_) || '' } qw(cn bugzillaEmail mail employeeType); + next if $type eq 'DISABLED'; + $mail = lc $mail; + $bugMail = '' if $bugMail !~ /\@/; + $bugMail = trim($bugMail); + if ($bugMail =~ / /) { + $bugMail = (grep {/\@/} split / /, $bugMail)[0]; + } + $name =~ s/\s+/ /g; + $cache->{$mail}{name} = trim($name); + $cache->{$mail}{bugmail} = $bugMail; + $cache->{$mail}{bugmail_canon} = canon_email($bugMail); + $cache->{$mail}{aliases} = []; + foreach my $alias (@{$entry->get_value('emailAlias', asref => 1) || []}) { + push @{$cache->{$mail}{aliases}}, canon_email($alias); + } + } } - return $self->{ldap_cache}; + $self->{ldap_cache} = $cache; + $self->{ldap_cache_time} = (time); + } + + return $self->{ldap_cache}; } 1; diff --git a/extensions/Push/lib/Connector/Base.pm b/extensions/Push/lib/Connector/Base.pm index ee41bd160..bd46fe6b4 100644 --- a/extensions/Push/lib/Connector/Base.pm +++ b/extensions/Push/lib/Connector/Base.pm @@ -18,59 +18,65 @@ use Bugzilla::Extension::Push::BacklogQueue; use Bugzilla::Extension::Push::Backoff; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - ($self->{name}) = $class =~ /^.+:(.+)$/; - $self->init(); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + ($self->{name}) = $class =~ /^.+:(.+)$/; + $self->init(); + return $self; } sub name { - my $self = shift; - return $self->{name}; + my $self = shift; + return $self->{name}; } sub init { - my ($self) = @_; - # abstract - # perform any initialisation here - # will be run when created by the web pages or by the daemon - # and also when the configuration needs to be reloaded + my ($self) = @_; + + # abstract + # perform any initialisation here + # will be run when created by the web pages or by the daemon + # and also when the configuration needs to be reloaded } sub stop { - my ($self) = @_; - # abstract - # run from the daemon only; disconnect from remote hosts, etc + my ($self) = @_; + + # abstract + # run from the daemon only; disconnect from remote hosts, etc } sub should_send { - my ($self, $message) = @_; - # abstract - # return boolean indicating if the connector will be sending the message. - # this will be called each message, and should be a very quick simple test. - # the connector can perform a more exhaustive test in the send() method. - return 0; + my ($self, $message) = @_; + + # abstract + # return boolean indicating if the connector will be sending the message. + # this will be called each message, and should be a very quick simple test. + # the connector can perform a more exhaustive test in the send() method. + return 0; } sub send { - my ($self, $message) = @_; - # abstract - # deliver the message, daemon only + my ($self, $message) = @_; + + # abstract + # deliver the message, daemon only } sub options { - my ($self) = @_; - # abstract - # return an array of configuration variables - return (); + my ($self) = @_; + + # abstract + # return an array of configuration variables + return (); } sub options_validate { - my ($class, $config) = @_; - # abstract, static - # die if a combination of options in $config is invalid + my ($class, $config) = @_; + + # abstract, static + # die if a combination of options in $config is invalid } # @@ -78,29 +84,30 @@ sub options_validate { # sub config { - my ($self) = @_; - if (!$self->{config}) { - $self->load_config(); - } - return $self->{config}; + my ($self) = @_; + if (!$self->{config}) { + $self->load_config(); + } + return $self->{config}; } sub load_config { - my ($self) = @_; - my $config = Bugzilla::Extension::Push::Config->new($self->name, $self->options); - $config->load(); - $self->{config} = $config; + my ($self) = @_; + my $config + = Bugzilla::Extension::Push::Config->new($self->name, $self->options); + $config->load(); + $self->{config} = $config; } sub enabled { - my ($self) = @_; - return $self->config->{enabled} eq 'Enabled'; + my ($self) = @_; + return $self->config->{enabled} eq 'Enabled'; } sub backlog { - my ($self) = @_; - $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name); - return $self->{backlog}; + my ($self) = @_; + $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name); + return $self->{backlog}; } 1; diff --git a/extensions/Push/lib/Connector/File.pm b/extensions/Push/lib/Connector/File.pm index ae249ffe5..7d86953f8 100644 --- a/extensions/Push/lib/Connector/File.pm +++ b/extensions/Push/lib/Connector/File.pm @@ -20,51 +20,46 @@ use Encode; use FileHandle; sub init { - my ($self) = @_; + my ($self) = @_; } sub options { - return ( - { - name => 'filename', - label => 'Filename', - type => 'string', - default => 'push.log', - required => 1, - validate => sub { - my $filename = shift; - $filename =~ m#^/# - && die "Absolute paths are not permitted\n"; - $filename =~ m#\.\.# - && die "Relative paths are not permitted\n"; - }, - }, - ); + return ( + { + name => 'filename', + label => 'Filename', + type => 'string', + default => 'push.log', + required => 1, + validate => sub { + my $filename = shift; + $filename =~ m#^/# && die "Absolute paths are not permitted\n"; + $filename =~ m#\.\.# && die "Relative paths are not permitted\n"; + }, + }, + ); } sub should_send { - my ($self, $message) = @_; - return 1; + my ($self, $message) = @_; + return 1; } sub send { - my ($self, $message) = @_; + my ($self, $message) = @_; - # pretty-format json payload - my $payload = $message->payload_decoded; - $payload = to_json($payload, 1); + # pretty-format json payload + my $payload = $message->payload_decoded; + $payload = to_json($payload, 1); - my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename}; - Bugzilla->push_ext->logger->debug("File: Appending to $filename"); - my $fh = FileHandle->new(">>$filename"); - $fh->binmode(':utf8'); - $fh->print( - "[" . scalar(localtime) . "]\n" . - $payload . "\n\n" - ); - $fh->close; + my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename}; + Bugzilla->push_ext->logger->debug("File: Appending to $filename"); + my $fh = FileHandle->new(">>$filename"); + $fh->binmode(':utf8'); + $fh->print("[" . scalar(localtime) . "]\n" . $payload . "\n\n"); + $fh->close; - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } 1; diff --git a/extensions/Push/lib/Connector/Phabricator.pm b/extensions/Push/lib/Connector/Phabricator.pm index 33e2bb6ad..61a39e32b 100644 --- a/extensions/Push/lib/Connector/Phabricator.pm +++ b/extensions/Push/lib/Connector/Phabricator.pm @@ -29,105 +29,96 @@ use Bugzilla::Extension::Push::Constants; use Bugzilla::Extension::Push::Util qw(is_public); sub options { - return ( - { - name => 'phabricator_url', - label => 'Phabricator URL', - type => 'string', - default => '', - required => 1, - } - ); + return ({ + name => 'phabricator_url', + label => 'Phabricator URL', + type => 'string', + default => '', + required => 1, + }); } sub should_send { - my ( $self, $message ) = @_; + my ($self, $message) = @_; - return 0 unless Bugzilla->params->{phabricator_enabled}; + return 0 unless Bugzilla->params->{phabricator_enabled}; - # We are only interested currently in bug group, assignee, qa-contact, or cc changes. - return 0 - unless $message->routing_key =~ - /^(?:attachment|bug)\.modify:.*\b(bug_group|assigned_to|qa_contact|cc)\b/; +# We are only interested currently in bug group, assignee, qa-contact, or cc changes. + return 0 + unless $message->routing_key + =~ /^(?:attachment|bug)\.modify:.*\b(bug_group|assigned_to|qa_contact|cc)\b/; - my $bug = $self->_get_bug_by_data( $message->payload_decoded ) || return 0; + my $bug = $self->_get_bug_by_data($message->payload_decoded) || return 0; - return $bug->has_attachment_with_mimetype(PHAB_CONTENT_TYPE); + return $bug->has_attachment_with_mimetype(PHAB_CONTENT_TYPE); } sub send { - my ( $self, $message ) = @_; - - my $logger = Bugzilla->push_ext->logger; - - my $data = $message->payload_decoded; - - my $bug = $self->_get_bug_by_data($data) || return PUSH_RESULT_OK; - - my $is_public = is_public($bug); - - my $revisions = get_attachment_revisions($bug); - - my $group_change = - ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bbug_group\b/) - ? 1 - : 0; - - foreach my $revision (@$revisions) { - if ( $is_public && $group_change ) { - Bugzilla->audit(sprintf( - 'Making revision %s public for bug %s', - $revision->id, - $bug->id - )); - $revision->make_public(); - } - elsif ( !$is_public && $group_change ) { - Bugzilla->audit(sprintf( - 'Giving revision %s a custom policy for bug %s', - $revision->id, - $bug->id - )); - my $set_project_names = [ map { "bmo-" . $_->name } @{ $bug->groups_in } ]; - $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. - if (!$is_public) { - Bugzilla->audit(sprintf( - 'Updating subscribers for %s for bug %s', - $revision->id, - $bug->id - )); - my $subscribers = get_bug_role_phids($bug); - $revision->set_subscribers($subscribers) if $subscribers; - } - - $revision->update(); + my ($self, $message) = @_; + + my $logger = Bugzilla->push_ext->logger; + + my $data = $message->payload_decoded; + + my $bug = $self->_get_bug_by_data($data) || return PUSH_RESULT_OK; + + my $is_public = is_public($bug); + + my $revisions = get_attachment_revisions($bug); + + my $group_change + = ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bbug_group\b/) + ? 1 + : 0; + + foreach my $revision (@$revisions) { + if ($is_public && $group_change) { + Bugzilla->audit( + sprintf('Making revision %s public for bug %s', $revision->id, $bug->id)); + $revision->make_public(); } + elsif (!$is_public && $group_change) { + Bugzilla->audit(sprintf( + 'Giving revision %s a custom policy for bug %s', + $revision->id, $bug->id + )); + my $set_project_names = [map { "bmo-" . $_->name } @{$bug->groups_in}]; + $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. + if (!$is_public) { + Bugzilla->audit( + sprintf('Updating subscribers for %s for bug %s', $revision->id, $bug->id)); + my $subscribers = get_bug_role_phids($bug); + $revision->set_subscribers($subscribers) if $subscribers; + } + + $revision->update(); + } - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } sub _get_bug_by_data { - my ( $self, $data ) = @_; - my $bug_data = $self->_get_bug_data($data) || return 0; - my $bug = Bugzilla::Bug->new( { id => $bug_data->{id} } ); + my ($self, $data) = @_; + my $bug_data = $self->_get_bug_data($data) || return 0; + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}}); } sub _get_bug_data { - my ( $self, $data ) = @_; - my $target = $data->{event}->{target}; - if ( $target eq 'bug' ) { - return $data->{bug}; - } - elsif ( exists $data->{$target}->{bug} ) { - return $data->{$target}->{bug}; - } - else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } 1; diff --git a/extensions/Push/lib/Connector/Spark.pm b/extensions/Push/lib/Connector/Spark.pm index e58ddfbe4..1eb6f22c6 100644 --- a/extensions/Push/lib/Connector/Spark.pm +++ b/extensions/Push/lib/Connector/Spark.pm @@ -25,150 +25,154 @@ use LWP::UserAgent; use List::MoreUtils qw(any); sub options { - return ( - { - name => 'spark_endpoint', - label => 'Spark API Endpoint', - type => 'string', - default => 'https://api.ciscospark.com/v1', - required => 1, - }, - { - name => 'spark_room_id', - label => 'Spark Room ID', - type => 'string', - default => 'bugzilla', - required => 1, - }, - { - name => 'spark_api_key', - label => 'Spark API Key', - type => 'string', - default => '', - required => 1, - }, - ); + return ( + { + name => 'spark_endpoint', + label => 'Spark API Endpoint', + type => 'string', + default => 'https://api.ciscospark.com/v1', + required => 1, + }, + { + name => 'spark_room_id', + label => 'Spark Room ID', + type => 'string', + default => 'bugzilla', + required => 1, + }, + { + name => 'spark_api_key', + label => 'Spark API Key', + type => 'string', + default => '', + required => 1, + }, + ); } sub stop { - my ($self) = @_; + my ($self) = @_; } sub should_send { - my ($self, $message) = @_; - - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data) - || return 0; - - # Send if bug has cisco-spark keyword - my $bug = Bugzilla::Bug->new({ id => $bug_data->{id}, cache => 1 }); - return 0 unless $bug->has_keyword('cisco-spark'); - - if ($message->routing_key eq 'bug.create') { - return 1; - } - else { - foreach my $change (@{ $data->{event}->{changes} }) { - # send status and resolution updates - return 1 if $change->{field} eq 'bug_status' || $change->{field} eq 'resolution'; - # also send if the right keyword has been added to this bug - if ($change->{field} eq 'keywords' && $change->{added}) { - my @added = split(/, /, $change->{added}); - return 1 if any { $_ eq 'cisco-spark' } @added; - } - } + my ($self, $message) = @_; + + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data) || return 0; + + # Send if bug has cisco-spark keyword + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}, cache => 1}); + return 0 unless $bug->has_keyword('cisco-spark'); + + if ($message->routing_key eq 'bug.create') { + return 1; + } + else { + foreach my $change (@{$data->{event}->{changes}}) { + + # send status and resolution updates + return 1 + if $change->{field} eq 'bug_status' || $change->{field} eq 'resolution'; + + # also send if the right keyword has been added to this bug + if ($change->{field} eq 'keywords' && $change->{added}) { + my @added = split(/, /, $change->{added}); + return 1 if any { $_ eq 'cisco-spark' } @added; + } } + } - # and nothing else - return 0; + # and nothing else + return 0; } sub send { - my ($self, $message) = @_; + my ($self, $message) = @_; - eval { - my $data = $message->payload_decoded(); - my $bug_data = $self->_get_bug_data($data); - my $bug = Bugzilla::Bug->new({ id => $bug_data->{id}, cache => 1 }); + eval { + my $data = $message->payload_decoded(); + my $bug_data = $self->_get_bug_data($data); + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}, cache => 1}); - my $text = "Bug " . $bug->id . " - " . $bug->short_desc . "\n"; - if ($message->routing_key eq 'bug.create') { - $text = "New " . $text; + my $text = "Bug " . $bug->id . " - " . $bug->short_desc . "\n"; + if ($message->routing_key eq 'bug.create') { + $text = "New " . $text; + } + else { + foreach my $change (@{$data->{event}->{changes}}) { + if ($change->{field} eq 'bug_status') { + $text + .= "Status changed: " . $change->{removed} . " -> " . $change->{added} . "\n"; } - else { - foreach my $change (@{ $data->{event}->{changes} }) { - if ($change->{field} eq 'bug_status') { - $text .= "Status changed: " . - $change->{removed} . " -> " . $change->{added} . "\n"; - } - if ($change->{field} eq 'resolution') { - $text .= "Resolution changed: " . - ($change->{removed} ? $change->{removed} . " -> " : "") . $change->{added} . "\n"; - } - } + if ($change->{field} eq 'resolution') { + $text + .= "Resolution changed: " + . ($change->{removed} ? $change->{removed} . " -> " : "") + . $change->{added} . "\n"; } - $text .= Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=" . $bug->id; + } + } + $text .= Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=" . $bug->id; - my $room_id = $self->config->{spark_room_id}; - my $message_uri = $self->_spark_uri('messages'); + my $room_id = $self->config->{spark_room_id}; + my $message_uri = $self->_spark_uri('messages'); - my $json_data = { roomId => $room_id, text => $text }; + my $json_data = {roomId => $room_id, text => $text}; - my $headers = HTTP::Headers->new( - Content_Type => 'application/json' - ); - my $request = HTTP::Request->new('POST', $message_uri, $headers, encode_json($json_data)); - my $resp = $self->_user_agent->request($request); + my $headers = HTTP::Headers->new(Content_Type => 'application/json'); + my $request + = HTTP::Request->new('POST', $message_uri, $headers, encode_json($json_data)); + my $resp = $self->_user_agent->request($request); - if ($resp->code != 200) { - die "Expected HTTP 200 response, got " . $resp->code; - } - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); + if ($resp->code != 200) { + die "Expected HTTP 200 response, got " . $resp->code; } + }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } # Private methods sub _get_bug_data { - my ($self, $data) = @_; - my $target = $data->{event}->{target}; - if ($target eq 'bug') { - return $data->{bug}; - } elsif (exists $data->{$target}->{bug}) { - return $data->{$target}->{bug}; - } else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } sub _user_agent { - my ($self) = @_; - my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); - $ua->timeout(10); - $ua->protocols_allowed(['http', 'https']); - - if (my $proxy_url = Bugzilla->params->{proxy_url}) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy(); - } - - $ua->default_header( - 'Authorization' => 'Bearer ' . $self->config->{spark_api_key} - ); - - return $ua; + my ($self) = @_; + my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + + if (my $proxy_url = Bugzilla->params->{proxy_url}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy(); + } + + $ua->default_header( + 'Authorization' => 'Bearer ' . $self->config->{spark_api_key}); + + return $ua; } sub _spark_uri { - my ($self, $path) = @_; - return URI->new($self->config->{spark_endpoint} . "/" . $path); + my ($self, $path) = @_; + return URI->new($self->config->{spark_endpoint} . "/" . $path); } 1; diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm index d3c55d3ca..9a3856c02 100644 --- a/extensions/Push/lib/Connectors.pm +++ b/extensions/Push/lib/Connectors.pm @@ -19,94 +19,97 @@ use File::Basename; use Try::Tiny; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - - $self->{names} = []; - $self->{objects} = {}; - $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector'; - - foreach my $file (glob($self->{path} . '/*.pm')) { - my $name = basename($file); - $name =~ s/\.pm$//; - next if $name eq 'Base'; - if (length($name) > 32) { - WARN("Ignoring connector '$name': Name longer than 32 characters"); - } - push @{$self->{names}}, $name; - TRACE("Found connector '$name'"); + my ($class) = @_; + my $self = {}; + bless($self, $class); + + $self->{names} = []; + $self->{objects} = {}; + $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector'; + + foreach my $file (glob($self->{path} . '/*.pm')) { + my $name = basename($file); + $name =~ s/\.pm$//; + next if $name eq 'Base'; + if (length($name) > 32) { + WARN("Ignoring connector '$name': Name longer than 32 characters"); } + push @{$self->{names}}, $name; + TRACE("Found connector '$name'"); + } - return $self; + return $self; } sub _load { - my ($self) = @_; - return if scalar keys %{$self->{objects}}; - - foreach my $name (@{$self->{names}}) { - next if exists $self->{objects}->{$name}; - my $file = $self->{path} . "/$name.pm"; - trick_taint($file); - require $file; - my $package = "Bugzilla::Extension::Push::Connector::$name"; - - TRACE("Loading connector '$name'"); - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - try { - my $connector = $package->new(); - $connector->load_config(); - $self->{objects}->{$name} = $connector; - } catch { - ERROR("Connector '$name' failed to load: " . clean_error($_)); - }; - Bugzilla->error_mode($old_error_mode); + my ($self) = @_; + return if scalar keys %{$self->{objects}}; + + foreach my $name (@{$self->{names}}) { + next if exists $self->{objects}->{$name}; + my $file = $self->{path} . "/$name.pm"; + trick_taint($file); + require $file; + my $package = "Bugzilla::Extension::Push::Connector::$name"; + + TRACE("Loading connector '$name'"); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + try { + my $connector = $package->new(); + $connector->load_config(); + $self->{objects}->{$name} = $connector; } + catch { + ERROR("Connector '$name' failed to load: " . clean_error($_)); + }; + Bugzilla->error_mode($old_error_mode); + } } sub stop { - my ($self) = @_; - foreach my $connector ($self->list) { - next unless $connector->enabled; - TRACE("Stopping '" . $connector->name . "'"); - try { - $connector->stop(); - } catch { - ERROR("Connector '" . $connector->name . "' failed to stop: " . clean_error($_)); - }; + my ($self) = @_; + foreach my $connector ($self->list) { + next unless $connector->enabled; + TRACE("Stopping '" . $connector->name . "'"); + try { + $connector->stop(); } + catch { + ERROR( + "Connector '" . $connector->name . "' failed to stop: " . clean_error($_)); + }; + } } sub reload { - my ($self) = @_; - $self->stop(); - $self->{objects} = {}; - $self->_load(); + my ($self) = @_; + $self->stop(); + $self->{objects} = {}; + $self->_load(); } sub names { - my ($self) = @_; - return @{$self->{names}}; + my ($self) = @_; + return @{$self->{names}}; } sub list { - my ($self) = @_; - $self->_load(); - return sort { $a->name cmp $b->name } values %{$self->{objects}}; + my ($self) = @_; + $self->_load(); + return sort { $a->name cmp $b->name } values %{$self->{objects}}; } sub exists { - my ($self, $name) = @_; - $self->by_name($name) ? 1 : 0; + my ($self, $name) = @_; + $self->by_name($name) ? 1 : 0; } sub by_name { - my ($self, $name) = @_; - $self->_load(); - return unless exists $self->{objects}->{$name}; - return $self->{objects}->{$name}; + my ($self, $name) = @_; + $self->_load(); + return unless exists $self->{objects}->{$name}; + return $self->{objects}->{$name}; } 1; diff --git a/extensions/Push/lib/Constants.pm b/extensions/Push/lib/Constants.pm index 09c5e464c..c021b32f4 100644 --- a/extensions/Push/lib/Constants.pm +++ b/extensions/Push/lib/Constants.pm @@ -14,14 +14,14 @@ use warnings; use base 'Exporter'; our @EXPORT = qw( - PUSH_RESULT_OK - PUSH_RESULT_IGNORED - PUSH_RESULT_TRANSIENT - PUSH_RESULT_ERROR - PUSH_RESULT_UNKNOWN - push_result_to_string - - POLL_INTERVAL_SECONDS + PUSH_RESULT_OK + PUSH_RESULT_IGNORED + PUSH_RESULT_TRANSIENT + PUSH_RESULT_ERROR + PUSH_RESULT_UNKNOWN + push_result_to_string + + POLL_INTERVAL_SECONDS ); use constant PUSH_RESULT_OK => 1; @@ -31,12 +31,12 @@ use constant PUSH_RESULT_ERROR => 4; use constant PUSH_RESULT_UNKNOWN => 5; sub push_result_to_string { - my ($result) = @_; - return 'OK' if $result == PUSH_RESULT_OK; - return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED; - return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT; - return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR; - return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN; + my ($result) = @_; + return 'OK' if $result == PUSH_RESULT_OK; + return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED; + return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT; + return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR; + return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN; } use constant POLL_INTERVAL_SECONDS => 30; diff --git a/extensions/Push/lib/Daemon.pm b/extensions/Push/lib/Daemon.pm index 7f2459a95..7fb5352ca 100644 --- a/extensions/Push/lib/Daemon.pm +++ b/extensions/Push/lib/Daemon.pm @@ -20,7 +20,7 @@ use File::Basename; use Pod::Usage; sub start { - newdaemon(); + newdaemon(); } # @@ -28,70 +28,71 @@ sub start { # sub gd_preconfig { - my $self = shift; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; - } - return (pidfile => $pidfile); + my $self = shift; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; + } + return (pidfile => $pidfile); } sub gd_getopt { - my $self = shift; - $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } else { - $self->{gd_progname} = basename($0); - } - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + my $self = shift; + $self->SUPER::gd_getopt(); + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - $0 = delete $self->{_original_zero}; + my $self = shift; + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0; -}; + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; +} sub gd_redirect_output { - my $self = shift; + my $self = shift; - my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log"; + my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log"; + open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); + close(STDOUT); + open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; + $SIG{HUP} = sub { + close(STDERR); open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - close(STDOUT); - open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; - $SIG{HUP} = sub { - close(STDERR); - open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - }; + }; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_run { - my $self = shift; - $::SIG{__DIE__} = \&Carp::confess if $self->{debug}; - my $push = Bugzilla->push_ext; - $push->logger->{debug} = $self->{debug}; - $push->is_daemon(1); - $push->start(); + my $self = shift; + $::SIG{__DIE__} = \&Carp::confess if $self->{debug}; + my $push = Bugzilla->push_ext; + $push->logger->{debug} = $self->{debug}; + $push->is_daemon(1); + $push->start(); } 1; diff --git a/extensions/Push/lib/Log.pm b/extensions/Push/lib/Log.pm index 8a35a6cf5..7358477ed 100644 --- a/extensions/Push/lib/Log.pm +++ b/extensions/Push/lib/Log.pm @@ -15,32 +15,30 @@ use Bugzilla; use Bugzilla::Extension::Push::Message; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log"); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log"); } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $ids = $dbh->selectcol_arrayref(" + my $ids = $dbh->selectcol_arrayref(" SELECT id FROM push_log - ORDER BY processed_ts DESC " . - $dbh->sql_limit(100) - ); - return Bugzilla::Extension::Push::LogEntry->new_from_list($ids); + ORDER BY processed_ts DESC " . $dbh->sql_limit(100)); + return Bugzilla::Extension::Push::LogEntry->new_from_list($ids); } 1; diff --git a/extensions/Push/lib/LogEntry.pm b/extensions/Push/lib/LogEntry.pm index f4e894b94..0d9770a8a 100644 --- a/extensions/Push/lib/LogEntry.pm +++ b/extensions/Push/lib/LogEntry.pm @@ -26,21 +26,19 @@ use Bugzilla::Extension::Push::Constants; # initialisation # -use constant DB_TABLE => 'push_log'; +use constant DB_TABLE => 'push_log'; use constant DB_COLUMNS => qw( - id - message_id - change_set - routing_key - connector - push_ts - processed_ts - result - data + id + message_id + change_set + routing_key + connector + push_ts + processed_ts + result + data ); -use constant VALIDATORS => { - data => \&_check_data, -}; +use constant VALIDATORS => {data => \&_check_data,}; use constant NAME_FIELD => ''; use constant LIST_ORDER => 'processed_ts DESC'; @@ -48,14 +46,14 @@ use constant LIST_ORDER => 'processed_ts DESC'; # accessors # -sub message_id { return $_[0]->{'message_id'}; } -sub change_set { return $_[0]->{'change_set'}; } -sub routing_key { return $_[0]->{'routing_key'}; } -sub connector { return $_[0]->{'connector'}; } -sub push_ts { return $_[0]->{'push_ts'}; } +sub message_id { return $_[0]->{'message_id'}; } +sub change_set { return $_[0]->{'change_set'}; } +sub routing_key { return $_[0]->{'routing_key'}; } +sub connector { return $_[0]->{'connector'}; } +sub push_ts { return $_[0]->{'push_ts'}; } sub processed_ts { return $_[0]->{'processed_ts'}; } -sub result { return $_[0]->{'result'}; } -sub data { return $_[0]->{'data'}; } +sub result { return $_[0]->{'result'}; } +sub data { return $_[0]->{'data'}; } sub result_string { return push_result_to_string($_[0]->result) } @@ -64,8 +62,8 @@ sub result_string { return push_result_to_string($_[0]->result) } # sub _check_data { - my ($invocant, $value) = @_; - return $value eq '' ? undef : $value; + my ($invocant, $value) = @_; + return $value eq '' ? undef : $value; } 1; diff --git a/extensions/Push/lib/Logger.pm b/extensions/Push/lib/Logger.pm index 5d92010ee..ec6dbe497 100644 --- a/extensions/Push/lib/Logger.pm +++ b/extensions/Push/lib/Logger.pm @@ -20,42 +20,38 @@ use Bugzilla::Extension::Push::LogEntry; Log::Log4perl->wrapper_register(__PACKAGE__); sub info { - my ($this, $message) = @_; - INFO($message); + my ($this, $message) = @_; + INFO($message); } sub error { - my ($this, $message) = @_; - ERROR($message); + my ($this, $message) = @_; + ERROR($message); } sub debug { - my ($this, $message) = @_; - DEBUG($message); + my ($this, $message) = @_; + DEBUG($message); } sub result { - my ($self, $connector, $message, $result, $data) = @_; - $data ||= ''; - - my $log_msg = sprintf - '%s: Message #%s: %s %s', - $connector->name, - $message->message_id, - push_result_to_string($result), - $data; - $self->info($log_msg); - - Bugzilla::Extension::Push::LogEntry->create({ - message_id => $message->message_id, - change_set => $message->change_set, - routing_key => $message->routing_key, - connector => $connector->name, - push_ts => $message->push_ts, - processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'), - result => $result, - data => $data, - }); + my ($self, $connector, $message, $result, $data) = @_; + $data ||= ''; + + my $log_msg = sprintf '%s: Message #%s: %s %s', $connector->name, + $message->message_id, push_result_to_string($result), $data; + $self->info($log_msg); + + Bugzilla::Extension::Push::LogEntry->create({ + message_id => $message->message_id, + change_set => $message->change_set, + routing_key => $message->routing_key, + connector => $connector->name, + push_ts => $message->push_ts, + processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'), + result => $result, + data => $data, + }); } sub _build_logger { Log::Log4perl->get_logger(__PACKAGE__); } diff --git a/extensions/Push/lib/Message.pm b/extensions/Push/lib/Message.pm index 1beb18ef0..3587de1d9 100644 --- a/extensions/Push/lib/Message.pm +++ b/extensions/Push/lib/Message.pm @@ -27,50 +27,50 @@ use Encode; # initialisation # -use constant DB_TABLE => 'push'; +use constant DB_TABLE => 'push'; use constant DB_COLUMNS => qw( - id - push_ts - payload - change_set - routing_key + id + push_ts + payload + change_set + routing_key ); use constant LIST_ORDER => 'push_ts'; use constant VALIDATORS => { - push_ts => \&_check_push_ts, - payload => \&_check_payload, - change_set => \&_check_change_set, - routing_key => \&_check_routing_key, + push_ts => \&_check_push_ts, + payload => \&_check_payload, + change_set => \&_check_change_set, + routing_key => \&_check_routing_key, }; # this creates an object which doesn't exist on the database sub new_transient { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $object = shift; - bless($object, $class) if $object; - return $object; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $object = shift; + bless($object, $class) if $object; + return $object; } # take a transient object and commit sub create_from_transient { - my ($self) = @_; - return $self->create($self); + my ($self) = @_; + return $self->create($self); } # # accessors # -sub push_ts { return $_[0]->{'push_ts'}; } -sub payload { return $_[0]->{'payload'}; } -sub change_set { return $_[0]->{'change_set'}; } +sub push_ts { return $_[0]->{'push_ts'}; } +sub payload { return $_[0]->{'payload'}; } +sub change_set { return $_[0]->{'change_set'}; } sub routing_key { return $_[0]->{'routing_key'}; } -sub message_id { return $_[0]->id; } +sub message_id { return $_[0]->id; } sub payload_decoded { - my ($self) = @_; - return from_json($self->{'payload'}); + my ($self) = @_; + return from_json($self->{'payload'}); } # @@ -78,27 +78,29 @@ sub payload_decoded { # sub _check_push_ts { - my ($invocant, $value) = @_; - $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()'); - return $value; + my ($invocant, $value) = @_; + $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()'); + return $value; } sub _check_payload { - my ($invocant, $value) = @_; - length($value) || ThrowCodeError('push_invalid_payload'); - return $value; + my ($invocant, $value) = @_; + length($value) || ThrowCodeError('push_invalid_payload'); + return $value; } sub _check_change_set { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_change_set'); + return $value; } sub _check_routing_key { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_routing_key'); + return $value; } 1; diff --git a/extensions/Push/lib/Option.pm b/extensions/Push/lib/Option.pm index a08e4c11d..a8e67714c 100644 --- a/extensions/Push/lib/Option.pm +++ b/extensions/Push/lib/Option.pm @@ -21,27 +21,25 @@ use Bugzilla::Util; # initialisation # -use constant DB_TABLE => 'push_options'; +use constant DB_TABLE => 'push_options'; use constant DB_COLUMNS => qw( - id - connector - option_name - option_value + id + connector + option_name + option_value ); use constant UPDATE_COLUMNS => qw( - option_value + option_value ); -use constant VALIDATORS => { - connector => \&_check_connector, -}; +use constant VALIDATORS => {connector => \&_check_connector,}; use constant LIST_ORDER => 'connector'; # # accessors # -sub connector { return $_[0]->{'connector'}; } -sub name { return $_[0]->{'option_name'}; } +sub connector { return $_[0]->{'connector'}; } +sub name { return $_[0]->{'option_name'}; } sub value { return $_[0]->{'option_value'}; } # @@ -55,12 +53,12 @@ sub set_value { $_[0]->{'option_value'} = $_[1]; } # sub _check_connector { - my ($invocant, $value) = @_; - $value eq '*' - || $value eq 'global' - || Bugzilla->push_ext->connectors->exists($value) - || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + $value eq '*' + || $value eq 'global' + || Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } 1; diff --git a/extensions/Push/lib/Push.pm b/extensions/Push/lib/Push.pm index ab640da81..97bac942b 100644 --- a/extensions/Push/lib/Push.pm +++ b/extensions/Push/lib/Push.pm @@ -24,269 +24,273 @@ use Bugzilla::Extension::Push::Util; use DateTime; use Try::Tiny; -has 'is_daemon' => ( - is => 'rw', - default => 0, -); +has 'is_daemon' => (is => 'rw', default => 0,); sub start { - my ($self) = @_; - my $connectors = $self->connectors; - $self->{config_last_modified} = $self->get_config_last_modified(); - $self->{config_last_checked} = (time); - - foreach my $connector ($connectors->list) { - $connector->backlog->reset_backoff(); - } - - my $pushd_loop = IO::Async::Loop->new; - my $main_timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => POLL_INTERVAL_SECONDS, - reschedule => 'drift', - on_tick => sub { - if ( $self->_dbh_check() ) { - $self->_reload(); - try { - $self->push(); - } - catch { - FATAL($_); - }; - } - }, + my ($self) = @_; + my $connectors = $self->connectors; + $self->{config_last_modified} = $self->get_config_last_modified(); + $self->{config_last_checked} = (time); + + foreach my $connector ($connectors->list) { + $connector->backlog->reset_backoff(); + } + + my $pushd_loop = IO::Async::Loop->new; + my $main_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => POLL_INTERVAL_SECONDS, + reschedule => 'drift', + on_tick => sub { + if ($self->_dbh_check()) { + $self->_reload(); + try { + $self->push(); + } + catch { + FATAL($_); + }; + } + }, + ); + if (Bugzilla->datadog) { + my $dog_timer = IO::Async::Timer::Periodic->new( + interval => 120, + reschedule => 'drift', + on_tick => sub { $self->heartbeat }, ); - if ( Bugzilla->datadog ) { - my $dog_timer = IO::Async::Timer::Periodic->new( - interval => 120, - reschedule => 'drift', - on_tick => sub { $self->heartbeat }, - ); - $pushd_loop->add($dog_timer); - $dog_timer->start; - } + $pushd_loop->add($dog_timer); + $dog_timer->start; + } - $pushd_loop->add($main_timer); - $main_timer->start; - $pushd_loop->run; + $pushd_loop->add($main_timer); + $main_timer->start; + $pushd_loop->run; } sub heartbeat { - my ($self) = @_; - my $dd = Bugzilla->datadog('bugzilla.pushd'); - - $dd->gauge('scheduled_jobs', Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push')); - - foreach my $connector ($self->connectors->list) { - if ($connector->enabled) { - my $lcname = lc $connector->name; - $dd->gauge("${lcname}.backlog", Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push_backlog WHERE connector = ?', undef, $connector->name)); - } + my ($self) = @_; + my $dd = Bugzilla->datadog('bugzilla.pushd'); + + $dd->gauge('scheduled_jobs', + Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push')); + + foreach my $connector ($self->connectors->list) { + if ($connector->enabled) { + my $lcname = lc $connector->name; + $dd->gauge( + "${lcname}.backlog", + Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM push_backlog WHERE connector = ?', undef, + $connector->name + ) + ); } + } } sub push { - my ($self) = @_; - my $logger = $self->logger; - my $connectors = $self->connectors; + my ($self) = @_; + my $logger = $self->logger; + my $connectors = $self->connectors; + + my $enabled = 0; + foreach my $connector ($connectors->list) { + if ($connector->enabled) { + $enabled = 1; + last; + } + } + return unless $enabled; - my $enabled = 0; + $logger->debug("polling"); + + # process each message + while (my $message = $self->queue->oldest) { foreach my $connector ($connectors->list) { - if ($connector->enabled) { - $enabled = 1; - last; + next unless $connector->enabled; + next unless $connector->should_send($message); + $logger->debug("pushing to " . $connector->name); + + my $is_backlogged = $connector->backlog->count; + + if (!$is_backlogged) { + + # connector isn't backlogged, immediate send + $logger->debug("immediate send"); + my ($result, $data); + eval { ($result, $data) = $connector->send($message); }; + if ($@) { + $result = PUSH_RESULT_TRANSIENT; + $data = clean_error($@); } - } - return unless $enabled; - - $logger->debug("polling"); - - # process each message - while(my $message = $self->queue->oldest) { - foreach my $connector ($connectors->list) { - next unless $connector->enabled; - next unless $connector->should_send($message); - $logger->debug("pushing to " . $connector->name); - - my $is_backlogged = $connector->backlog->count; - - if (!$is_backlogged) { - # connector isn't backlogged, immediate send - $logger->debug("immediate send"); - my ($result, $data); - eval { - ($result, $data) = $connector->send($message); - }; - if ($@) { - $result = PUSH_RESULT_TRANSIENT; - $data = clean_error($@); - } - if (!$result) { - $logger->error($connector->name . " failed to return a result code"); - $result = PUSH_RESULT_UNKNOWN; - } - $logger->result($connector, $message, $result, $data); - - if ($result == PUSH_RESULT_TRANSIENT) { - $is_backlogged = 1; - } - } - - # if the connector is backlogged, push to the backlog queue - if ($is_backlogged) { - INFO('connector is backlogged'); - my $backlog = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, $connector); - } + if (!$result) { + $logger->error($connector->name . " failed to return a result code"); + $result = PUSH_RESULT_UNKNOWN; } + $logger->result($connector, $message, $result, $data); - # message processed - $message->remove_from_db(); + if ($result == PUSH_RESULT_TRANSIENT) { + $is_backlogged = 1; + } + } + + # if the connector is backlogged, push to the backlog queue + if ($is_backlogged) { + INFO('connector is backlogged'); + my $backlog + = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, + $connector); + } } - # process backlog - foreach my $connector ($connectors->list) { - next unless $connector->enabled; - my $message = $connector->backlog->oldest(); - next unless $message; - - $logger->debug("processing backlog for " . $connector->name); - while ($message) { - my ($result, $data); - eval { - ($result, $data) = $connector->send($message); - }; - if ($@) { - $result = PUSH_RESULT_TRANSIENT; - $data = $@; - } - $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data); - if (!$result) { - $logger->error($connector->name . " failed to return a result code"); - $result = PUSH_RESULT_UNKNOWN; - } - $logger->result($connector, $message, $result, $data); - - if ($result == PUSH_RESULT_TRANSIENT) { - # connector is still down, stop trying - $connector->backlog->inc_backoff(); - last; - } - - # message was processed - $message->remove_from_db(); - - $message = $connector->backlog->oldest(); - } + # message processed + $message->remove_from_db(); + } + + # process backlog + foreach my $connector ($connectors->list) { + next unless $connector->enabled; + my $message = $connector->backlog->oldest(); + next unless $message; + + $logger->debug("processing backlog for " . $connector->name); + while ($message) { + my ($result, $data); + eval { ($result, $data) = $connector->send($message); }; + if ($@) { + $result = PUSH_RESULT_TRANSIENT; + $data = $@; + } + $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data); + if (!$result) { + $logger->error($connector->name . " failed to return a result code"); + $result = PUSH_RESULT_UNKNOWN; + } + $logger->result($connector, $message, $result, $data); + + if ($result == PUSH_RESULT_TRANSIENT) { + + # connector is still down, stop trying + $connector->backlog->inc_backoff(); + last; + } + + # message was processed + $message->remove_from_db(); + + $message = $connector->backlog->oldest(); } + } } sub _reload { - my ($self) = @_; - - # check for updated config every 60 seconds - my $now = (time); - if ($now - $self->{config_last_checked} < 60) { - return; - } - $self->{config_last_checked} = $now; - - $self->logger->debug('Checking for updated configuration'); - if ($self->get_config_last_modified eq $self->{config_last_modified}) { - return; - } - $self->{config_last_modified} = $self->get_config_last_modified(); - - $self->logger->debug('Configuration has been updated'); - $self->connectors->reload(); + my ($self) = @_; + + # check for updated config every 60 seconds + my $now = (time); + if ($now - $self->{config_last_checked} < 60) { + return; + } + $self->{config_last_checked} = $now; + + $self->logger->debug('Checking for updated configuration'); + if ($self->get_config_last_modified eq $self->{config_last_modified}) { + return; + } + $self->{config_last_modified} = $self->get_config_last_modified(); + + $self->logger->debug('Configuration has been updated'); + $self->connectors->reload(); } sub get_config_last_modified { - my ($self) = @_; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => '*', - option_name => 'last-modified', + my ($self) = @_; + my $options_list + = Bugzilla::Extension::Push::Option->match({ + connector => '*', option_name => 'last-modified', }); - if (@$options_list) { - return $options_list->[0]->value; - } else { - return $self->set_config_last_modified(); - } + if (@$options_list) { + return $options_list->[0]->value; + } + else { + return $self->set_config_last_modified(); + } } sub set_config_last_modified { - my ($self) = @_; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => '*', - option_name => 'last-modified', + my ($self) = @_; + my $options_list + = Bugzilla::Extension::Push::Option->match({ + connector => '*', option_name => 'last-modified', }); - my $now = DateTime->now->datetime(); - if (@$options_list) { - $options_list->[0]->set_value($now); - $options_list->[0]->update(); - } else { - Bugzilla::Extension::Push::Option->create({ - connector => '*', - option_name => 'last-modified', - option_value => $now, - }); - } - return $now; + my $now = DateTime->now->datetime(); + if (@$options_list) { + $options_list->[0]->set_value($now); + $options_list->[0]->update(); + } + else { + Bugzilla::Extension::Push::Option->create({ + connector => '*', option_name => 'last-modified', option_value => $now, + }); + } + return $now; } sub config { - my ($self) = @_; - if (!$self->{config}) { - $self->{config} = Bugzilla::Extension::Push::Config->new( - 'global', - { - name => 'log_purge', - label => 'Purge logs older than (days)', - type => 'string', - default => '7', - required => '1', - validate => sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; }, - }, - ); - $self->{config}->load(); - } - return $self->{config}; + my ($self) = @_; + if (!$self->{config}) { + $self->{config} = Bugzilla::Extension::Push::Config->new( + 'global', + { + name => 'log_purge', + label => 'Purge logs older than (days)', + type => 'string', + default => '7', + required => '1', + validate => + sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; }, + }, + ); + $self->{config}->load(); + } + return $self->{config}; } sub logger { - my ($self, $value) = @_; - $self->{logger} = $value if $value; - return $self->{logger}; + my ($self, $value) = @_; + $self->{logger} = $value if $value; + return $self->{logger}; } sub connectors { - my ($self, $value) = @_; - $self->{connectors} = $value if $value; - return $self->{connectors}; + my ($self, $value) = @_; + $self->{connectors} = $value if $value; + return $self->{connectors}; } sub queue { - my ($self) = @_; - $self->{queue} ||= Bugzilla::Extension::Push::Queue->new(); - return $self->{queue}; + my ($self) = @_; + $self->{queue} ||= Bugzilla::Extension::Push::Queue->new(); + return $self->{queue}; } sub log { - my ($self) = @_; - $self->{log} ||= Bugzilla::Extension::Push::Log->new(); - return $self->{log}; + my ($self) = @_; + $self->{log} ||= Bugzilla::Extension::Push::Log->new(); + return $self->{log}; } sub _dbh_check { - my ($self) = @_; - eval { - Bugzilla->dbh->selectrow_array("SELECT 1 FROM push"); - }; - if ($@) { - $self->logger->error(clean_error($@)); - return 0; - } else { - return 1; - } + my ($self) = @_; + eval { Bugzilla->dbh->selectrow_array("SELECT 1 FROM push"); }; + if ($@) { + $self->logger->error(clean_error($@)); + return 0; + } + else { + return 1; + } } 1; diff --git a/extensions/Push/lib/Queue.pm b/extensions/Push/lib/Queue.pm index 3ee0321d9..f59423e6a 100644 --- a/extensions/Push/lib/Queue.pm +++ b/extensions/Push/lib/Queue.pm @@ -15,59 +15,54 @@ use Bugzilla; use Bugzilla::Extension::Push::Message; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array("SELECT COUNT(*) FROM push"); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array("SELECT COUNT(*) FROM push"); } sub oldest { - my ($self) = @_; - my @messages = $self->list(limit => 1); - return scalar(@messages) ? $messages[0] : undef; + my ($self) = @_; + my @messages = $self->list(limit => 1); + return scalar(@messages) ? $messages[0] : undef; } sub by_id { - my ($self, $id) = @_; - my @messages = $self->list( - limit => 1, - filter => "AND (push.id = $id)", - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self, $id) = @_; + my @messages = $self->list(limit => 1, filter => "AND (push.id = $id)",); + return scalar(@messages) ? $messages[0] : undef; } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT id, push_ts, payload, change_set, routing_key FROM push - WHERE (1 = 1) " . - $args{filter} . " - ORDER BY push_ts " . - $dbh->sql_limit($args{limit}) - ); - $sth->execute(); - while (my $row = $sth->fetchrow_hashref()) { - push @result, Bugzilla::Extension::Push::Message->new({ - id => $row->{id}, - push_ts => $row->{push_ts}, - payload => $row->{payload}, - change_set => $row->{change_set}, - routing_key => $row->{routing_key}, - }); - } - return @result; + WHERE (1 = 1) " . $args{filter} . " + ORDER BY push_ts " . $dbh->sql_limit($args{limit})); + $sth->execute(); + while (my $row = $sth->fetchrow_hashref()) { + push @result, + Bugzilla::Extension::Push::Message->new({ + id => $row->{id}, + push_ts => $row->{push_ts}, + payload => $row->{payload}, + change_set => $row->{change_set}, + routing_key => $row->{routing_key}, + }); + } + return @result; } 1; diff --git a/extensions/Push/lib/Serialise.pm b/extensions/Push/lib/Serialise.pm index bb6834c13..c878ff4d9 100644 --- a/extensions/Push/lib/Serialise.pm +++ b/extensions/Push/lib/Serialise.pm @@ -19,140 +19,145 @@ use Scalar::Util 'blessed'; use JSON (); my $_instance; + sub instance { - $_instance ||= Bugzilla::Extension::Push::Serialise->_new(); - return $_instance; + $_instance ||= Bugzilla::Extension::Push::Serialise->_new(); + return $_instance; } sub _new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } # given an object, serliase to a hash sub object_to_hash { - my ($self, $object, $is_shallow) = @_; - - my $method = lc(blessed($object)); - $method =~ s/::/_/g; - $method =~ s/^bugzilla//; - return unless $self->can($method); - (my $name = $method) =~ s/^_//; - - # check for a cached hash - my $cache = Bugzilla->request_cache; - my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object; - if (exists($cache->{$cache_id})) { - return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id}; - } - - # call the right method to serialise to a hash - my $rh = $self->$method($object, $is_shallow); - - # store in cache - if ($cache_id) { - $cache->{$cache_id} = $rh; - } - - return wantarray ? ($rh, $name) : $rh; + my ($self, $object, $is_shallow) = @_; + + my $method = lc(blessed($object)); + $method =~ s/::/_/g; + $method =~ s/^bugzilla//; + return unless $self->can($method); + (my $name = $method) =~ s/^_//; + + # check for a cached hash + my $cache = Bugzilla->request_cache; + my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object; + if (exists($cache->{$cache_id})) { + return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id}; + } + + # call the right method to serialise to a hash + my $rh = $self->$method($object, $is_shallow); + + # store in cache + if ($cache_id) { + $cache->{$cache_id} = $rh; + } + + return wantarray ? ($rh, $name) : $rh; } # given a changes hash, return an event hash sub changes_to_event { - my ($self, $changes) = @_; - - my $event = {}; - - # create common (created and modified) fields - $event->{'user'} = $self->object_to_hash(Bugzilla->user); - my $timestamp = - $changes->{'timestamp'} - || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $event->{'time'} = datetime_to_timestamp($timestamp); - - foreach my $change (@{$changes->{'changes'}}) { - if (exists $change->{'field'}) { - # map undef to emtpy - hash_undef_to_empty($change); - - # custom_fields change from undef to empty, ignore these changes - return if ($change->{'added'} || "") eq "" && - ($change->{'removed'} || "") eq ""; - - # use saner field serialisation - my $field = $change->{'field'}; - $change->{'field'} = $field; - - if ($field eq 'priority' || $field eq 'target_milestone') { - $change->{'added'} = _select($change->{'added'}); - $change->{'removed'} = _select($change->{'removed'}); - - } elsif ($field =~ /^cf_/) { - $change->{'added'} = _custom_field($field, $change->{'added'}); - $change->{'removed'} = _custom_field($field, $change->{'removed'}); - } - - $event->{'changes'} = [] unless exists $event->{'changes'}; - push @{$event->{'changes'}}, $change; - } + my ($self, $changes) = @_; + + my $event = {}; + + # create common (created and modified) fields + $event->{'user'} = $self->object_to_hash(Bugzilla->user); + my $timestamp = $changes->{'timestamp'} + || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $event->{'time'} = datetime_to_timestamp($timestamp); + + foreach my $change (@{$changes->{'changes'}}) { + if (exists $change->{'field'}) { + + # map undef to emtpy + hash_undef_to_empty($change); + + # custom_fields change from undef to empty, ignore these changes + return + if ($change->{'added'} || "") eq "" && ($change->{'removed'} || "") eq ""; + + # use saner field serialisation + my $field = $change->{'field'}; + $change->{'field'} = $field; + + if ($field eq 'priority' || $field eq 'target_milestone') { + $change->{'added'} = _select($change->{'added'}); + $change->{'removed'} = _select($change->{'removed'}); + + } + elsif ($field =~ /^cf_/) { + $change->{'added'} = _custom_field($field, $change->{'added'}); + $change->{'removed'} = _custom_field($field, $change->{'removed'}); + } + + $event->{'changes'} = [] unless exists $event->{'changes'}; + push @{$event->{'changes'}}, $change; } + } - return $event; + return $event; } # bugzilla returns '---' or '--' for single-select fields that have no value # selected. it makes more sense to return an empty string. sub _select { - my ($value) = @_; - return '' if $value eq '---' or $value eq '--'; - return $value; + my ($value) = @_; + return '' if $value eq '---' or $value eq '--'; + return $value; } # return an object which serialises to a json boolean, but still acts as a perl # boolean sub _boolean { - my ($value) = @_; - return $value ? JSON::true : JSON::false; + my ($value) = @_; + return $value ? JSON::true : JSON::false; } sub _string { - my ($value) = @_; - return defined($value) ? $value : ''; + my ($value) = @_; + return defined($value) ? $value : ''; } sub _time { - my ($value) = @_; - return defined($value) ? datetime_to_timestamp($value) : undef; + my ($value) = @_; + return defined($value) ? datetime_to_timestamp($value) : undef; } sub _integer { - my ($value) = @_; - return defined($value) ? $value + 0 : undef; + my ($value) = @_; + return defined($value) ? $value + 0 : undef; } sub _array { - my ($value) = @_; - return defined($value) ? $value : []; + my ($value) = @_; + return defined($value) ? $value : []; } sub _custom_field { - my ($field, $value) = @_; - $field = Bugzilla::Field->new({ name => $field }) unless blessed $field; + my ($field, $value) = @_; + $field = Bugzilla::Field->new({name => $field}) unless blessed $field; - if ($field->type == FIELD_TYPE_DATETIME) { - return _time($value); + if ($field->type == FIELD_TYPE_DATETIME) { + return _time($value); - } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { - return _select($value); + } + elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { + return _select($value); - } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - return _array($value); + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + return _array($value); - } else { - return _string($value); - } + } + else { + return _string($value); + } } # @@ -162,158 +167,148 @@ sub _custom_field { # sub _bug { - my ($self, $bug) = @_; - - my $version = $bug->can('version_obj') - ? $bug->version_obj - : Bugzilla::Version->new({ name => $bug->version, product => $bug->product_obj }); - - my $milestone; - if (_select($bug->target_milestone) ne '') { - $milestone = $bug->can('target_milestone_obj') - ? $bug->target_milestone_obj - : Bugzilla::Milestone->new({ name => $bug->target_milestone, product => $bug->product_obj }); - } - - my $status = $bug->can('status_obj') - ? $bug->status_obj - : Bugzilla::Status->new({ name => $bug->bug_status }); - - my $rh = { - id => _integer($bug->bug_id), - alias => _string($bug->alias), - assigned_to => $self->_user($bug->assigned_to), - classification => _string($bug->classification), - component => $self->_component($bug->component_obj), - creation_time => _time($bug->creation_ts || $bug->delta_ts), - flags => (mapr { $self->_flag($_) } $bug->flags), - is_private => _boolean(!is_public($bug)), - keywords => (mapr { _string($_->name) } $bug->keyword_objects), - last_change_time => _time($bug->delta_ts), - operating_system => _string($bug->op_sys), - platform => _string($bug->rep_platform), - priority => _select($bug->priority), - product => $self->_product($bug->product_obj), - qa_contact => $self->_user($bug->qa_contact), - reporter => $self->_user($bug->reporter), - resolution => _string($bug->resolution), - severity => _string($bug->bug_severity), - status => $self->_status($status), - summary => _string($bug->short_desc), - target_milestone => $self->_milestone($milestone), - url => _string($bug->bug_file_loc), - version => $self->_version($version), - whiteboard => _string($bug->status_whiteboard), - }; - - # add custom fields - my @custom_fields = Bugzilla->active_custom_fields( - { product => $bug->product_obj, component => $bug->component_obj }); - foreach my $field (@custom_fields) { - my $name = $field->name; - $rh->{$name} = _custom_field($field, $bug->$name); - } - - return $rh; + my ($self, $bug) = @_; + + my $version + = $bug->can('version_obj') + ? $bug->version_obj + : Bugzilla::Version->new( + {name => $bug->version, product => $bug->product_obj}); + + my $milestone; + if (_select($bug->target_milestone) ne '') { + $milestone + = $bug->can('target_milestone_obj') + ? $bug->target_milestone_obj + : Bugzilla::Milestone->new( + {name => $bug->target_milestone, product => $bug->product_obj}); + } + + my $status + = $bug->can('status_obj') + ? $bug->status_obj + : Bugzilla::Status->new({name => $bug->bug_status}); + + my $rh = { + id => _integer($bug->bug_id), + alias => _string($bug->alias), + assigned_to => $self->_user($bug->assigned_to), + classification => _string($bug->classification), + component => $self->_component($bug->component_obj), + creation_time => _time($bug->creation_ts || $bug->delta_ts), + flags => (mapr { $self->_flag($_) } $bug->flags), + is_private => _boolean(!is_public($bug)), + keywords => (mapr { _string($_->name) } $bug->keyword_objects), + last_change_time => _time($bug->delta_ts), + operating_system => _string($bug->op_sys), + platform => _string($bug->rep_platform), + priority => _select($bug->priority), + product => $self->_product($bug->product_obj), + qa_contact => $self->_user($bug->qa_contact), + reporter => $self->_user($bug->reporter), + resolution => _string($bug->resolution), + severity => _string($bug->bug_severity), + status => $self->_status($status), + summary => _string($bug->short_desc), + target_milestone => $self->_milestone($milestone), + url => _string($bug->bug_file_loc), + version => $self->_version($version), + whiteboard => _string($bug->status_whiteboard), + }; + + # add custom fields + my @custom_fields = Bugzilla->active_custom_fields( + {product => $bug->product_obj, component => $bug->component_obj}); + foreach my $field (@custom_fields) { + my $name = $field->name; + $rh->{$name} = _custom_field($field, $bug->$name); + } + + return $rh; } sub _user { - my ($self, $user) = @_; - return undef unless $user; - return { - id => _integer($user->id), - login => _string($user->login), - real_name => _string($user->name), - }; + my ($self, $user) = @_; + return undef unless $user; + return { + id => _integer($user->id), + login => _string($user->login), + real_name => _string($user->name), + }; } sub _component { - my ($self, $component) = @_; - return { - id => _integer($component->id), - name => _string($component->name), - }; + my ($self, $component) = @_; + return {id => _integer($component->id), name => _string($component->name),}; } sub _attachment { - my ($self, $attachment, $is_shallow) = @_; - my $rh = { - id => _integer($attachment->id), - content_type => _string($attachment->contenttype), - creation_time => _time($attachment->attached), - description => _string($attachment->description), - file_name => _string($attachment->filename), - flags => (mapr { $self->_flag($_) } $attachment->flags), - is_obsolete => _boolean($attachment->isobsolete), - is_patch => _boolean($attachment->ispatch), - is_private => _boolean(!is_public($attachment)), - last_change_time => _time($attachment->modification_time), - }; - if (!$is_shallow) { - $rh->{bug} = $self->_bug($attachment->bug); - } - return $rh; + my ($self, $attachment, $is_shallow) = @_; + my $rh = { + id => _integer($attachment->id), + content_type => _string($attachment->contenttype), + creation_time => _time($attachment->attached), + description => _string($attachment->description), + file_name => _string($attachment->filename), + flags => (mapr { $self->_flag($_) } $attachment->flags), + is_obsolete => _boolean($attachment->isobsolete), + is_patch => _boolean($attachment->ispatch), + is_private => _boolean(!is_public($attachment)), + last_change_time => _time($attachment->modification_time), + }; + if (!$is_shallow) { + $rh->{bug} = $self->_bug($attachment->bug); + } + return $rh; } sub _comment { - my ($self, $comment, $is_shallow) = @_; - my $rh = { - id => _integer($comment->bug_id), - body => _string($comment->body), - creation_time => _time($comment->creation_ts), - is_private => _boolean($comment->is_private), - number => _integer($comment->count), - }; - if (!$is_shallow) { - $rh->{bug} = $self->_bug($comment->bug); - } - return $rh; + my ($self, $comment, $is_shallow) = @_; + my $rh = { + id => _integer($comment->bug_id), + body => _string($comment->body), + creation_time => _time($comment->creation_ts), + is_private => _boolean($comment->is_private), + number => _integer($comment->count), + }; + if (!$is_shallow) { + $rh->{bug} = $self->_bug($comment->bug); + } + return $rh; } sub _product { - my ($self, $product) = @_; - return { - id => _integer($product->id), - name => _string($product->name), - }; + my ($self, $product) = @_; + return {id => _integer($product->id), name => _string($product->name),}; } sub _flag { - my ($self, $flag) = @_; - my $rh = { - id => _integer($flag->id), - name => _string($flag->type->name), - value => _string($flag->status), - }; - if ($flag->requestee) { - $rh->{'requestee'} = $self->_user($flag->requestee); - } - return $rh; + my ($self, $flag) = @_; + my $rh = { + id => _integer($flag->id), + name => _string($flag->type->name), + value => _string($flag->status), + }; + if ($flag->requestee) { + $rh->{'requestee'} = $self->_user($flag->requestee); + } + return $rh; } sub _version { - my ($self, $version) = @_; - return { - id => _integer($version->id), - name => _string($version->name), - }; + my ($self, $version) = @_; + return {id => _integer($version->id), name => _string($version->name),}; } sub _milestone { - my ($self, $milestone) = @_; - return undef unless $milestone; - return { - id => _integer($milestone->id), - name => _string($milestone->name), - }; + my ($self, $milestone) = @_; + return undef unless $milestone; + return {id => _integer($milestone->id), name => _string($milestone->name),}; } sub _status { - my ($self, $status) = @_; - return { - id => _integer($status->id), - name => _string($status->name), - }; + my ($self, $status) = @_; + return {id => _integer($status->id), name => _string($status->name),}; } 1; diff --git a/extensions/Push/lib/Util.pm b/extensions/Push/lib/Util.pm index bda6331bf..34a0879ea 100644 --- a/extensions/Push/lib/Util.pm +++ b/extensions/Push/lib/Util.pm @@ -22,142 +22,147 @@ use Time::HiRes; use base qw(Exporter); our @EXPORT = qw( - datetime_to_timestamp - debug_dump - get_first_value - hash_undef_to_empty - is_public - mapr - clean_error - change_set_id - canon_email - to_json from_json + datetime_to_timestamp + debug_dump + get_first_value + hash_undef_to_empty + is_public + mapr + clean_error + change_set_id + canon_email + to_json from_json ); # returns true if the specified object is public sub is_public { - my ($object) = @_; + my ($object) = @_; - my $default_user = Bugzilla::User->new(); + my $default_user = Bugzilla::User->new(); - if ($object->isa('Bugzilla::Bug')) { - return unless $default_user->can_see_bug($object->bug_id); - return 1; + if ($object->isa('Bugzilla::Bug')) { + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } elsif ($object->isa('Bugzilla::Comment')) { - return if $object->is_private; - return unless $default_user->can_see_bug($object->bug_id); - return 1; + } + elsif ($object->isa('Bugzilla::Comment')) { + return if $object->is_private; + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } elsif ($object->isa('Bugzilla::Attachment')) { - return if $object->isprivate; - return unless $default_user->can_see_bug($object->bug_id); - return 1; + } + elsif ($object->isa('Bugzilla::Attachment')) { + return if $object->isprivate; + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } else { - warn "Unsupported class " . blessed($object) . " passed to is_public()\n"; - } + } + else { + warn "Unsupported class " . blessed($object) . " passed to is_public()\n"; + } - return 1; + return 1; } # return the first existing value from the hashref for the given list of keys sub get_first_value { - my ($rh, @keys) = @_; - foreach my $field (@keys) { - return $rh->{$field} if exists $rh->{$field}; - } - return; + my ($rh, @keys) = @_; + foreach my $field (@keys) { + return $rh->{$field} if exists $rh->{$field}; + } + return; } # wrapper for map that works on array references sub mapr(&$) { - my ($filter, $ra) = @_; - my @result = map(&$filter, @$ra); - return \@result; + my ($filter, $ra) = @_; + my @result = map(&$filter, @$ra); + return \@result; } # convert datetime string (from db) to a UTC json friendly datetime sub datetime_to_timestamp { - my ($datetime_string) = @_; - return '' unless $datetime_string; - return datetime_from($datetime_string, 'UTC')->datetime(); + my ($datetime_string) = @_; + return '' unless $datetime_string; + return datetime_from($datetime_string, 'UTC')->datetime(); } # replaces all undef values in a hashref with an empty string (deep) sub hash_undef_to_empty { - my ($rh) = @_; - foreach my $key (keys %$rh) { - my $value = $rh->{$key}; - if (!defined($value)) { - $rh->{$key} = ''; - } elsif (ref($value) eq 'HASH') { - hash_undef_to_empty($value); - } + my ($rh) = @_; + foreach my $key (keys %$rh) { + my $value = $rh->{$key}; + if (!defined($value)) { + $rh->{$key} = ''; + } + elsif (ref($value) eq 'HASH') { + hash_undef_to_empty($value); } + } } # debugging methods sub debug_dump { - my ($object) = @_; - local $Data::Dumper::Sortkeys = 1; - my $output = Dumper($object); - $output =~ s/$output"; + my ($object) = @_; + local $Data::Dumper::Sortkeys = 1; + my $output = Dumper($object); + $output =~ s/$output"; } # removes stacktrace and "at /some/path ..." from errors sub clean_error { - my ($error) = @_; - my $path = bz_locations->{'extensionsdir'}; - $error = $1 if $error =~ /^(.+?) at \Q$path/s; - $path = '/loader/0x'; - $error = $1 if $error =~ /^(.+?) at \Q$path/s; - $error =~ s/(^\s+|\s+$)//g; - return $error; + my ($error) = @_; + my $path = bz_locations->{'extensionsdir'}; + $error = $1 if $error =~ /^(.+?) at \Q$path/s; + $path = '/loader/0x'; + $error = $1 if $error =~ /^(.+?) at \Q$path/s; + $error =~ s/(^\s+|\s+$)//g; + return $error; } # generate a new change_set id sub change_set_id { - return "$$." . Time::HiRes::time(); + return "$$." . Time::HiRes::time(); } # remove guff from email addresses sub clean_email { - my $email = shift; - $email = trim($email); - $email = $1 if $email =~ /^(\S+)/; - $email =~ s/@/@/; - $email = lc $email; - return $email; + my $email = shift; + $email = trim($email); + $email = $1 if $email =~ /^(\S+)/; + $email =~ s/@/@/; + $email = lc $email; + return $email; } # resolve to canonised email form # eg. glob+bmo@mozilla.com --> glob@mozilla.com sub canon_email { - my $email = shift; - $email = clean_email($email); - $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/; - return $email; + my $email = shift; + $email = clean_email($email); + $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/; + return $email; } # json helpers sub to_json { - my ($object, $pretty) = @_; - if ($pretty) { - return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object)); - } else { - return JSON->new->ascii(1)->shrink(1)->encode($object); - } + my ($object, $pretty) = @_; + if ($pretty) { + return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object)); + } + else { + return JSON->new->ascii(1)->shrink(1)->encode($object); + } } sub from_json { - my ($json) = @_; - if (utf8::is_utf8($json)) { - $json = encode('utf8', $json); - } - return JSON->new->utf8(1)->decode($json); + my ($json) = @_; + if (utf8::is_utf8($json)) { + $json = encode('utf8', $json); + } + return JSON->new->utf8(1)->decode($json); } 1; diff --git a/extensions/Push/template/en/default/setup/strings.txt.pl b/extensions/Push/template/en/default/setup/strings.txt.pl index 6f41f26d5..7bdab77bd 100644 --- a/extensions/Push/template/en/default/setup/strings.txt.pl +++ b/extensions/Push/template/en/default/setup/strings.txt.pl @@ -10,6 +10,6 @@ use warnings; use 5.10.1; %strings = ( - feature_push_amqp => 'Push: AMQP Support', - feature_push_stomp => 'Push: STOMP Support', + feature_push_amqp => 'Push: AMQP Support', + feature_push_stomp => 'Push: STOMP Support', ); diff --git a/extensions/REMO/Config.pm b/extensions/REMO/Config.pm index a679d64a0..a1d8327de 100644 --- a/extensions/REMO/Config.pm +++ b/extensions/REMO/Config.pm @@ -28,10 +28,8 @@ use warnings; use constant NAME => 'REMO'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/REMO/Extension.pm b/extensions/REMO/Extension.pm index df4e70c80..7ca74e081 100644 --- a/extensions/REMO/Extension.pm +++ b/extensions/REMO/Extension.pm @@ -37,304 +37,311 @@ use List::Util qw(first); our $VERSION = '0.01'; sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; - if ($page eq 'remo-form-payment.html') { - _remo_form_payment($vars); - } + if ($page eq 'remo-form-payment.html') { + _remo_form_payment($vars); + } } sub _remo_form_payment { - my ($vars) = @_; - my $input = Bugzilla->input_params; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - if ($input->{'action'} eq 'commit') { - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - - my $bug_id = $input->{'bug_id'}; - detaint_natural($bug_id); - my $bug = Bugzilla::Bug->check($bug_id); - - # Detect if the user already used the same form to submit again - my $token = trim($input->{'token'}); - if ($token) { - my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token); - if (!$creator_id - || $creator_id != $user->id - || $old_attach_id !~ "^remo_form_payment:") - { - # The token is invalid. - ThrowUserError('token_does_not_exist'); - } - - $old_attach_id =~ s/^remo_form_payment://; - if ($old_attach_id) { - ThrowUserError('remo_payment_cancel_dupe', - { bugid => $bug_id, attachid => $old_attach_id }); - } - } - - # Make sure the user can attach to this bug - if (!$bug->user->{'canedit'}) { - ThrowUserError("remo_payment_bug_edit_denied", - { bug_id => $bug->id }); - } - - # Make sure the bug is under the correct product/component - if ($bug->product ne 'Mozilla Reps' - || $bug->component ne 'Budget Requests') - { - ThrowUserError('remo_payment_invalid_product'); - } - - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); - - $dbh->bz_start_transaction; - - # Create the comment to be added based on the form fields from rep-payment-form - my $comment; - $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment) - || ThrowTemplateError($template->error()); - $bug->add_comment($comment, { isprivate => 0 }); - - # Attach expense report - # FIXME: Would be nice to be able to have the above prefilled comment and - # the following attachments all show up under a single comment. But the longdescs - # table can only handle one attach_id per comment currently. At least only one - # email is sent the way it is done below. - my $attachment; - if (defined $cgi->upload('expenseform')) { - # Determine content-type - my $content_type = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'}; - - $attachment = Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $timestamp, - data => $cgi->upload('expenseform'), - description => 'Expense Form', - filename => scalar $cgi->upload('expenseform'), - ispatch => 0, - isprivate => 0, - mimetype => $content_type, - }); - - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - - # Attach receipts file - if (defined $cgi->upload("receipts")) { - # Determine content-type - my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'}; - - $attachment = Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $timestamp, - data => $cgi->upload('receipts'), - description => "Receipts", - filename => scalar $cgi->upload("receipts"), - ispatch => 0, - isprivate => 0, - mimetype => $content_type, - }); - - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - - $bug->update($timestamp); - - if ($token) { - trick_taint($token); - $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef, - ("remo_form_payment:" . $attachment->id, $token)); - } - - $dbh->bz_commit_transaction; - - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - $vars->{'bugs'} = [ new Bugzilla::Bug($bug_id) ]; - $vars->{'header_done'} = 1; - $vars->{'contenttypemethod'} = 'autodetect'; - - my $recipients = { 'changer' => $user }; - $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients); - - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my ($vars) = @_; + my $input = Bugzilla->input_params; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + if ($input->{'action'} eq 'commit') { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + + my $bug_id = $input->{'bug_id'}; + detaint_natural($bug_id); + my $bug = Bugzilla::Bug->check($bug_id); + + # Detect if the user already used the same form to submit again + my $token = trim($input->{'token'}); + if ($token) { + my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token); + if (!$creator_id + || $creator_id != $user->id + || $old_attach_id !~ "^remo_form_payment:") + { + # The token is invalid. + ThrowUserError('token_does_not_exist'); + } + + $old_attach_id =~ s/^remo_form_payment://; + if ($old_attach_id) { + ThrowUserError('remo_payment_cancel_dupe', + {bugid => $bug_id, attachid => $old_attach_id}); + } } - else { - $vars->{'token'} = issue_session_token('remo_form_payment:'); + + # Make sure the user can attach to this bug + if (!$bug->user->{'canedit'}) { + ThrowUserError("remo_payment_bug_edit_denied", {bug_id => $bug->id}); + } + + # Make sure the bug is under the correct product/component + if ($bug->product ne 'Mozilla Reps' || $bug->component ne 'Budget Requests') { + ThrowUserError('remo_payment_invalid_product'); + } + + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + + $dbh->bz_start_transaction; + + # Create the comment to be added based on the form fields from rep-payment-form + my $comment; + $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment) + || ThrowTemplateError($template->error()); + $bug->add_comment($comment, {isprivate => 0}); + + # Attach expense report + # FIXME: Would be nice to be able to have the above prefilled comment and + # the following attachments all show up under a single comment. But the longdescs + # table can only handle one attach_id per comment currently. At least only one + # email is sent the way it is done below. + my $attachment; + if (defined $cgi->upload('expenseform')) { + + # Determine content-type + my $content_type + = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('expenseform'), + description => 'Expense Form', + filename => scalar $cgi->upload('expenseform'), + ispatch => 0, + isprivate => 0, + mimetype => $content_type, + }); + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + + # Attach receipts file + if (defined $cgi->upload("receipts")) { + + # Determine content-type + my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('receipts'), + description => "Receipts", + filename => scalar $cgi->upload("receipts"), + ispatch => 0, + isprivate => 0, + mimetype => $content_type, + }); + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + + $bug->update($timestamp); + + if ($token) { + trick_taint($token); + $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', + undef, ("remo_form_payment:" . $attachment->id, $token)); } + + $dbh->bz_commit_transaction; + + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; + $vars->{'bugs'} = [new Bugzilla::Bug($bug_id)]; + $vars->{'header_done'} = 1; + $vars->{'contenttypemethod'} = 'autodetect'; + + my $recipients = {'changer' => $user}; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/created.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + else { + $vars->{'token'} = issue_session_token('remo_form_payment:'); + } } my %CSV_COLUMNS = ( - "Date Required" => { pos => 1, value => '%cf_due_date' }, - "Requester" => { pos => 2, value => 'Rizki Kelimutu' }, - "Email 1" => { pos => 3, value => 'rkelimutu@mozilla.com' }, - "Mozilla Space" => { pos => 4, value => 'Remote' }, - "Team" => { pos => 5, value => 'Participation' }, - "Department Code" => { pos => 6, value => '1002' }, - "Purpose" => { pos => 7, value => 'Rep event: %eventpage' }, - "Item 1" => { pos => 8 }, - "Item 2" => { pos => 9 }, - "Item 3" => { pos => 10 }, - "Item 4" => { pos => 11 }, - "Item 5" => { pos => 12 }, - "Item 6" => { pos => 13 }, - "Item 7" => { pos => 14 }, - "Item 8" => { pos => 15 }, - "Item 9" => { pos => 16 }, - "Item 10" => { pos => 17 }, - "Item 11" => { pos => 18 }, - "Item 12" => { pos => 19 }, - "Item 13" => { pos => 20 }, - "Item 14" => { pos => 21 }, - "Recipient Name" => { pos => 22, value => '%shiptofirstname %shiptolastname' }, - "Email 2" => { pos => 23, value => sub { Bugzilla->user->email } }, - "Address 1" => { pos => 24, value => '%shiptoaddress1' }, - "Address 2" => { pos => 25, value => '%shiptoaddress2' }, - "City" => { pos => 26, value => '%shiptocity' }, - "State" => { pos => 27, value => '%shiptostate' }, - "Zip" => { pos => 28, value => '%shiptopcode' }, - "Country" => { pos => 29, value => '%shiptocountry' }, - "Phone number" => { pos => 30, value => '%shiptophone' }, - "Notes" => { pos => 31, value => '%shipadditional' }, + "Date Required" => {pos => 1, value => '%cf_due_date'}, + "Requester" => {pos => 2, value => 'Rizki Kelimutu'}, + "Email 1" => {pos => 3, value => 'rkelimutu@mozilla.com'}, + "Mozilla Space" => {pos => 4, value => 'Remote'}, + "Team" => {pos => 5, value => 'Participation'}, + "Department Code" => {pos => 6, value => '1002'}, + "Purpose" => {pos => 7, value => 'Rep event: %eventpage'}, + "Item 1" => {pos => 8}, + "Item 2" => {pos => 9}, + "Item 3" => {pos => 10}, + "Item 4" => {pos => 11}, + "Item 5" => {pos => 12}, + "Item 6" => {pos => 13}, + "Item 7" => {pos => 14}, + "Item 8" => {pos => 15}, + "Item 9" => {pos => 16}, + "Item 10" => {pos => 17}, + "Item 11" => {pos => 18}, + "Item 12" => {pos => 19}, + "Item 13" => {pos => 20}, + "Item 14" => {pos => 21}, + "Recipient Name" => {pos => 22, value => '%shiptofirstname %shiptolastname'}, + "Email 2" => { + pos => 23, + value => sub { Bugzilla->user->email } + }, + "Address 1" => {pos => 24, value => '%shiptoaddress1'}, + "Address 2" => {pos => 25, value => '%shiptoaddress2'}, + "City" => {pos => 26, value => '%shiptocity'}, + "State" => {pos => 27, value => '%shiptostate'}, + "Zip" => {pos => 28, value => '%shiptopcode'}, + "Country" => {pos => 29, value => '%shiptocountry'}, + "Phone number" => {pos => 30, value => '%shiptophone'}, + "Notes" => {pos => 31, value => '%shipadditional'}, ); sub _expand_value { - my $value = shift; - if (ref $value && ref $value eq 'CODE') { - return $value->(); - } - else { - my $cgi = Bugzilla->cgi; - $value =~ s/%(\w+)/$cgi->param($1)/ge; - return $value; - } + my $value = shift; + if (ref $value && ref $value eq 'CODE') { + return $value->(); + } + else { + my $cgi = Bugzilla->cgi; + $value =~ s/%(\w+)/$cgi->param($1)/ge; + return $value; + } } sub _csv_quote { - my $s = shift; - $s =~ s/"/""/g; - return qq{"$s"}; + my $s = shift; + $s =~ s/"/""/g; + return qq{"$s"}; } sub _csv_line { - return join(",", map { _csv_quote($_) } @_); + return join(",", map { _csv_quote($_) } @_); } sub _csv_encode { - return join("\r\n", map { _csv_line(@$_) } @_) . "\r\n"; + return join("\r\n", map { _csv_line(@$_) } @_) . "\r\n"; } sub post_bug_after_creation { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $template = Bugzilla->template; - - my $format = Bugzilla->input_params->{format}; - - return unless defined $format; - - if ($format eq 'remo-swag') { - # If the attachment cannot be successfully added to the bug, - # we notify the user, but we don't interrupt the bug creation process. - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my @attachments; - eval { - my $xml; - $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml) - || ThrowTemplateError($template->error()); - - push @attachments, Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $bug->creation_ts, - data => $xml, - description => 'Remo Swag Request (XML)', - filename => 'remo-swag.xml', - ispatch => 0, - isprivate => 0, - mimetype => 'text/xml', - }); - - my @columns_raw = sort { $CSV_COLUMNS{$a}{pos} <=> $CSV_COLUMNS{$b}{pos} } keys %CSV_COLUMNS; - my @data = map { _expand_value( $CSV_COLUMNS{$_}{value} ) } @columns_raw; - my @columns = map { s/^(Item|Email) \d+$/$1/g; $_ } @columns_raw; - my $csv = _csv_encode(\@columns, \@data); - - push @attachments, Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $bug->creation_ts, - data => $csv, - description => 'Remo Swag Request (CSV)', - filename => 'remo-swag.csv', - ispatch => 0, - isprivate => 0, - mimetype => 'text/csv', - }); - }; - if ($@) { - warn "$@"; - } - - if (@attachments) { - # Insert comment for attachment - foreach my $attachment (@attachments) { - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - $bug->update($bug->creation_ts); - delete $bug->{attachments}; - } - else { - $vars->{'message'} = 'attachment_creation_failed'; - } - - Bugzilla->error_mode($error_mode_cache); + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $template = Bugzilla->template; + + my $format = Bugzilla->input_params->{format}; + + return unless defined $format; + + if ($format eq 'remo-swag') { + + # If the attachment cannot be successfully added to the bug, + # we notify the user, but we don't interrupt the bug creation process. + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my @attachments; + eval { + my $xml; + $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml) + || ThrowTemplateError($template->error()); + + push @attachments, + Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $bug->creation_ts, + data => $xml, + description => 'Remo Swag Request (XML)', + filename => 'remo-swag.xml', + ispatch => 0, + isprivate => 0, + mimetype => 'text/xml', + }); + + my @columns_raw + = sort { $CSV_COLUMNS{$a}{pos} <=> $CSV_COLUMNS{$b}{pos} } keys %CSV_COLUMNS; + my @data = map { _expand_value($CSV_COLUMNS{$_}{value}) } @columns_raw; + my @columns = map { s/^(Item|Email) \d+$/$1/g; $_ } @columns_raw; + my $csv = _csv_encode(\@columns, \@data); + + push @attachments, + Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $bug->creation_ts, + data => $csv, + description => 'Remo Swag Request (CSV)', + filename => 'remo-swag.csv', + ispatch => 0, + isprivate => 0, + mimetype => 'text/csv', + }); + }; + if ($@) { + warn "$@"; } - elsif ($format eq 'mozreps') { - my $needinfo_type = first { $_->name eq 'needinfo' } @{$bug->flag_types}; - return unless $needinfo_type; - my %original_cc = map { $_ => 1 } Bugzilla->cgi->param('cc'); - my @cc_users = grep { $_->is_enabled && $original_cc{$_->login}} @{$bug->cc_users}; - my @new_flags = map { - { type_id => $needinfo_type->id, - status => '?', - requestee => $_->login } - } @cc_users; - ThrowUserError('remo_missing_voucher') unless @cc_users; - - $bug->set_flags(\@new_flags, []) if @new_flags; - $bug->add_comment( - join(", ", map { $_->name || $_->login } @cc_users) . - ": You have been added as a voucher to this Reps application.\n" . - "Please provide a comment describing why you endorse this application.\n" . - "Thanks!" - ); + if (@attachments) { - $bug->update($bug->creation_ts); - Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user }); + # Insert comment for attachment + foreach my $attachment (@attachments) { + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + $bug->update($bug->creation_ts); + delete $bug->{attachments}; } + else { + $vars->{'message'} = 'attachment_creation_failed'; + } + + Bugzilla->error_mode($error_mode_cache); + } + + elsif ($format eq 'mozreps') { + my $needinfo_type = first { $_->name eq 'needinfo' } @{$bug->flag_types}; + return unless $needinfo_type; + my %original_cc = map { $_ => 1 } Bugzilla->cgi->param('cc'); + my @cc_users + = grep { $_->is_enabled && $original_cc{$_->login} } @{$bug->cc_users}; + my @new_flags + = map { {type_id => $needinfo_type->id, status => '?', requestee => $_->login + } } @cc_users; + ThrowUserError('remo_missing_voucher') unless @cc_users; + + $bug->set_flags(\@new_flags, []) if @new_flags; + $bug->add_comment( + join(", ", map { $_->name || $_->login } @cc_users) + . ": You have been added as a voucher to this Reps application.\n" + . "Please provide a comment describing why you endorse this application.\n" + . "Thanks!"); + + $bug->update($bug->creation_ts); + Bugzilla::BugMail::Send($bug->id, {changer => Bugzilla->user}); + } } __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/Config.pm b/extensions/RequestNagger/Config.pm index a338cd441..7bcaf3013 100644 --- a/extensions/RequestNagger/Config.pm +++ b/extensions/RequestNagger/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'RequestNagger'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'RequestNagger'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/Extension.pm b/extensions/RequestNagger/Extension.pm index e0f97c9f7..54a11ff5b 100644 --- a/extensions/RequestNagger/Extension.pm +++ b/extensions/RequestNagger/Extension.pm @@ -25,241 +25,239 @@ use DateTime; our $VERSION = '1'; BEGIN { - *Bugzilla::Flag::age = \&_flag_age; - *Bugzilla::Flag::deferred = \&_flag_deferred; - *Bugzilla::Product::nag_interval = \&_product_nag_interval; + *Bugzilla::Flag::age = \&_flag_age; + *Bugzilla::Flag::deferred = \&_flag_deferred; + *Bugzilla::Product::nag_interval = \&_product_nag_interval; } sub _flag_age { - return time_ago(datetime_from($_[0]->modification_date)); + return time_ago(datetime_from($_[0]->modification_date)); } sub _flag_deferred { - my ($self) = @_; - if (!exists $self->{deferred}) { - my $dbh = Bugzilla->dbh; - my ($defer_until) = $dbh->selectrow_array( - "SELECT defer_until FROM nag_defer WHERE flag_id=?", - undef, - $self->id - ); - $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef; - } - return $self->{deferred}; + my ($self) = @_; + if (!exists $self->{deferred}) { + my $dbh = Bugzilla->dbh; + my ($defer_until) + = $dbh->selectrow_array("SELECT defer_until FROM nag_defer WHERE flag_id=?", + undef, $self->id); + $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef; + } + return $self->{deferred}; } sub _product_nag_interval { $_[0]->{nag_interval} } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Product')) { - push @$columns, 'nag_interval'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push @$columns, 'nag_interval'; - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - return unless $class->isa('Bugzilla::Product'); - my $input = Bugzilla->input_params; - if (exists $input->{nag_interval}) { - my $interval = _check_nag_interval($input->{nag_interval}); - $params->{nag_interval} = $interval; - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); + my $input = Bugzilla->input_params; + if (exists $input->{nag_interval}) { + my $interval = _check_nag_interval($input->{nag_interval}); + $params->{nag_interval} = $interval; + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - return unless $object->isa('Bugzilla::Product'); - my $input = Bugzilla->input_params; - if (exists $input->{nag_interval}) { - my $interval = _check_nag_interval($input->{nag_interval}); - $object->set('nag_interval', $interval); - } + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); + my $input = Bugzilla->input_params; + if (exists $input->{nag_interval}) { + my $interval = _check_nag_interval($input->{nag_interval}); + $object->set('nag_interval', $interval); + } } sub _check_nag_interval { - my ($value) = @_; - detaint_natural($value) - || ThrowUserError('invalid_parameter', { name => 'request reminding interval', err => 'must be numeric' }); - return $value < 0 ? 0 : $value * 24; + my ($value) = @_; + detaint_natural($value) + || ThrowUserError('invalid_parameter', + {name => 'request reminding interval', err => 'must be numeric'}); + return $value < 0 ? 0 : $value * 24; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'request_defer.html'; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $input = Bugzilla->input_params; - - # load flag - my $flag_id = scalar($input->{flag}) - || ThrowUserError('request_nagging_flag_invalid'); - detaint_natural($flag_id) - || ThrowUserError('request_nagging_flag_invalid'); - my $flag = Bugzilla::Flag->new({ id => $flag_id, cache => 1 }) - || ThrowUserError('request_nagging_flag_invalid'); - - # you can only defer flags directed at you - $user->can_see_bug($flag->bug->id) - || ThrowUserError("bug_access_denied", { bug_id => $flag->bug->id }); - $flag->status eq '?' - || ThrowUserError('request_nagging_flag_set'); - $flag->requestee - || ThrowUserError('request_nagging_flag_wind'); - $flag->requestee->id == $user->id - || ThrowUserError('request_nagging_flag_not_owned'); - - my $date = DateTime->now()->truncate(to => 'day'); - my $defer_until; - if ($input->{'defer-until'} - && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) - { - $defer_until = DateTime->new(year => $1, month => $2, day => $3); - if ($defer_until > $date->clone->add(days => 7)) { - $defer_until = undef; - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'request_defer.html'; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $input = Bugzilla->input_params; + + # load flag + my $flag_id + = scalar($input->{flag}) || ThrowUserError('request_nagging_flag_invalid'); + detaint_natural($flag_id) || ThrowUserError('request_nagging_flag_invalid'); + my $flag = Bugzilla::Flag->new({id => $flag_id, cache => 1}) + || ThrowUserError('request_nagging_flag_invalid'); + + # you can only defer flags directed at you + $user->can_see_bug($flag->bug->id) + || ThrowUserError("bug_access_denied", {bug_id => $flag->bug->id}); + $flag->status eq '?' || ThrowUserError('request_nagging_flag_set'); + $flag->requestee || ThrowUserError('request_nagging_flag_wind'); + $flag->requestee->id == $user->id + || ThrowUserError('request_nagging_flag_not_owned'); + + my $date = DateTime->now()->truncate(to => 'day'); + my $defer_until; + if ( $input->{'defer-until'} + && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) + { + $defer_until = DateTime->new(year => $1, month => $2, day => $3); + if ($defer_until > $date->clone->add(days => 7)) { + $defer_until = undef; } - - if ($input->{save} && $defer_until) { - $self->_defer_until($flag_id, $defer_until); - $vars->{saved} = "1"; - $vars->{defer_until} = $defer_until; - } - else { - my @dates; - foreach my $i (1..7) { - $date->add(days => 1); - unshift @dates, { days => $i, date => $date->clone }; - } - $vars->{defer_until} = \@dates; + } + + if ($input->{save} && $defer_until) { + $self->_defer_until($flag_id, $defer_until); + $vars->{saved} = "1"; + $vars->{defer_until} = $defer_until; + } + else { + my @dates; + foreach my $i (1 .. 7) { + $date->add(days => 1); + unshift @dates, {days => $i, date => $date->clone}; } + $vars->{defer_until} = \@dates; + } - $vars->{flag} = $flag; + $vars->{flag} = $flag; } sub _defer_until { - my ($self, $flag_id, $defer_until) = @_; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - my ($defer_id) = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", undef, $flag_id); - if ($defer_id) { - $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", undef, $defer_until->ymd, $flag_id); - } else { - $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", undef, $flag_id, $defer_until->ymd); - } - - $dbh->bz_commit_transaction(); + my ($self, $flag_id, $defer_until) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my ($defer_id) + = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", + undef, $flag_id); + if ($defer_id) { + $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", + undef, $defer_until->ymd, $flag_id); + } + else { + $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", + undef, $flag_id, $defer_until->ymd); + } + + $dbh->bz_commit_transaction(); } sub object_end_of_update { - my ($self, $args) = @_; - if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) { - # any change to the flag (setting, clearing, or retargetting) will clear the deferals - my $flag = $args->{object}; - Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id); - } + my ($self, $args) = @_; + if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) { + +# any change to the flag (setting, clearing, or retargetting) will clear the deferals + my $flag = $args->{object}; + Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id); + } } sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'request_nagging'; - - my $save = $args->{'save_changes'}; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - my %watching = - map { $_ => 1 } - @{ $dbh->selectcol_arrayref( - "SELECT profiles.login_name + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'request_nagging'; + + my $save = $args->{'save_changes'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + my %watching = map { $_ => 1 } @{ + $dbh->selectcol_arrayref( + "SELECT profiles.login_name FROM nag_watch INNER JOIN profiles ON nag_watch.nagged_id = profiles.userid WHERE nag_watch.watcher_id = ? - ORDER BY profiles.login_name", - undef, - $user->id - ) }; - - my $nag_settings = Bugzilla::Extension::RequestNagger::Settings->new($user->id); - - if ($save) { - my $input = Bugzilla->input_params; - Bugzilla::User::match_field({ 'add_watching' => {'type' => 'multi'} }); - - $dbh->bz_start_transaction(); - - # user preference - if (my $value = $input->{request_nagging}) { - my $settings = $user->settings; - my $setting = new Bugzilla::User::Setting('request_nagging'); - if ($value eq 'default') { - $settings->{request_nagging}->reset_to_default; - } - else { - $setting->validate_value($value); - $settings->{request_nagging}->set($value); - } - } - - # watching - if ($input->{remove_watched_users}) { - my $del_watching = ref($input->{del_watching}) ? $input->{del_watching} : [ $input->{del_watching} ]; - foreach my $login (@$del_watching) { - my $u = Bugzilla::User->new({ name => $login, cache => 1 }) - || next; - next unless exists $watching{$u->login}; - $dbh->do( - "DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?", - undef, - $user->id, $u->id - ); - delete $watching{$u->login}; - } - } - if ($input->{add_watching}) { - my $add_watching = ref($input->{add_watching}) ? $input->{add_watching} : [ $input->{add_watching} ]; - foreach my $login (@$add_watching) { - my $u = Bugzilla::User->new({ name => $login, cache => 1 }) - || next; - next if exists $watching{$u->login}; - $dbh->do( - "INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)", - undef, - $user->id, $u->id - ); - $watching{$u->login} = 1; - } - } - - # watching settings - foreach my $field (Bugzilla::Extension::RequestNagger::Settings::FIELDS()) { - $nag_settings->set($field, $input->{$field}); - } - - $dbh->bz_commit_transaction(); + ORDER BY profiles.login_name", undef, $user->id + ) + }; + + my $nag_settings = Bugzilla::Extension::RequestNagger::Settings->new($user->id); + + if ($save) { + my $input = Bugzilla->input_params; + Bugzilla::User::match_field({'add_watching' => {'type' => 'multi'}}); + + $dbh->bz_start_transaction(); + + # user preference + if (my $value = $input->{request_nagging}) { + my $settings = $user->settings; + my $setting = new Bugzilla::User::Setting('request_nagging'); + if ($value eq 'default') { + $settings->{request_nagging}->reset_to_default; + } + else { + $setting->validate_value($value); + $settings->{request_nagging}->set($value); + } } - $vars->{watching} = [ sort keys %watching ]; - $vars->{settings} = $nag_settings; + # watching + if ($input->{remove_watched_users}) { + my $del_watching + = ref($input->{del_watching}) + ? $input->{del_watching} + : [$input->{del_watching}]; + foreach my $login (@$del_watching) { + my $u = Bugzilla::User->new({name => $login, cache => 1}) || next; + next unless exists $watching{$u->login}; + $dbh->do("DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?", + undef, $user->id, $u->id); + delete $watching{$u->login}; + } + } + if ($input->{add_watching}) { + my $add_watching + = ref($input->{add_watching}) + ? $input->{add_watching} + : [$input->{add_watching}]; + foreach my $login (@$add_watching) { + my $u = Bugzilla::User->new({name => $login, cache => 1}) || next; + next if exists $watching{$u->login}; + $dbh->do("INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)", + undef, $user->id, $u->id); + $watching{$u->login} = 1; + } + } + + # watching settings + foreach my $field (Bugzilla::Extension::RequestNagger::Settings::FIELDS()) { + $nag_settings->set($field, $input->{$field}); + } - my $handled = $args->{'handled'}; - $$handled = 1; + $dbh->bz_commit_transaction(); + } + + $vars->{watching} = [sort keys %watching]; + $vars->{settings} = $nag_settings; + + my $handled = $args->{'handled'}; + $$handled = 1; } # @@ -267,125 +265,77 @@ sub user_preferences { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'nag_watch'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - nagged_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - watcher_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - nag_watch_idx => { - FIELDS => [ 'nagged_id', 'watcher_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'nag_defer'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flags', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - defer_until => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - nag_defer_idx => { - FIELDS => [ 'flag_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'nag_settings'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - setting_name => { - TYPE => 'VARCHAR(16)', - NOTNULL => 1, - }, - setting_value => { - TYPE => 'VARCHAR(16)', - NOTNULL => 1, - }, - ], - INDEXES => [ - nag_setting_idx => { - FIELDS => [ 'user_id', 'setting_name' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'nag_watch'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + nagged_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + watcher_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + nag_watch_idx => {FIELDS => ['nagged_id', 'watcher_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'nag_defer'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'flags', COLUMN => 'id', DELETE => 'CASCADE',} + }, + defer_until => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [nag_defer_idx => {FIELDS => ['flag_id'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'nag_settings'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + setting_name => {TYPE => 'VARCHAR(16)', NOTNULL => 1,}, + setting_value => {TYPE => 'VARCHAR(16)', NOTNULL => 1,}, + ], + INDEXES => [ + nag_setting_idx => {FIELDS => ['user_id', 'setting_name'], TYPE => 'UNIQUE',}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('products', 'nag_interval', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24 }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('products', 'nag_interval', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24}); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; - my $extensions_dir = bz_locations()->{'extensionsdir'}; - my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name + = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl"; + $files->{$script_name} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'request_nagging', - options => ['on', 'off'], - default => 'on', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'request_nagging', + options => ['on', 'off'], + default => 'on', + category => 'Reviews and Needinfo' + }); } __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/bin/send-request-nags.pl b/extensions/RequestNagger/bin/send-request-nags.pl index f823fc197..33c49d2b6 100755 --- a/extensions/RequestNagger/bin/send-request-nags.pl +++ b/extensions/RequestNagger/bin/send-request-nags.pl @@ -39,8 +39,8 @@ my $DO_NOT_NAG = grep { $_ eq '-d' } @ARGV; @ARGV = grep { !/^-/ } @ARGV; if (my $filename = shift @ARGV) { - _send_email(decode_json(scalar read_file($filename))); - exit; + _send_email(decode_json(scalar read_file($filename))); + exit; } my $dbh = Bugzilla->dbh; @@ -53,269 +53,272 @@ Bugzilla->switch_to_shadow_db(); # send nags to requestees send_nags( - reports => [ 'requestee' ], - requestee_sql => REQUESTEE_NAG_SQL, - setter_sql => SETTER_NAG_SQL, - template => 'user', - date => $date, + reports => ['requestee'], + requestee_sql => REQUESTEE_NAG_SQL, + setter_sql => SETTER_NAG_SQL, + template => 'user', + date => $date, ); # send nags to watchers send_nags( - reports => [ 'requestee', 'setter' ], - requestee_sql => WATCHING_REQUESTEE_NAG_SQL, - setter_sql => WATCHING_SETTER_NAG_SQL, - template => 'watching', - date => $date, + reports => ['requestee', 'setter'], + requestee_sql => WATCHING_REQUESTEE_NAG_SQL, + setter_sql => WATCHING_SETTER_NAG_SQL, + template => 'watching', + date => $date, ); sub send_nags { - my (%args) = @_; - my $requests = {}; - my $watching = $args{template} eq 'watching'; - - # get requests - - foreach my $report (@{ $args{reports} }) { - - # collate requests - my $rows = $dbh->selectall_arrayref($args{$report . '_sql'}, { Slice => {} }); - foreach my $request (@$rows) { - next unless _include_request($request, $report); - - my $target = Bugzilla::User->new({ id => $request->{target_id}, cache => 1 }); - push @{ - $requests - ->{$request->{recipient_id}} - ->{$target->login} - ->{$report} - ->{$request->{flag_type}} - }, $request; - push @{ - $requests - ->{$request->{recipient_id}} - ->{$target->login} - ->{bug_ids} - ->{$report} - }, $request->{bug_id}; - } + my (%args) = @_; + my $requests = {}; + my $watching = $args{template} eq 'watching'; - # process requests here to avoid doing it in the templates - foreach my $recipient_id (keys %$requests) { - foreach my $target_login (keys %{ $requests->{$recipient_id} }) { - my $rh = $requests->{$recipient_id}->{$target_login}; - - # build a list of valid types in the correct order - $rh->{types}->{$report} = []; - foreach my $type (map { $_->{type} } FLAG_TYPES) { - next unless exists $rh->{$report}->{$type}; - push @{ $rh->{types}->{$report} }, $type; - } - - # build a summary - $rh->{summary}->{$report} = join(', ', - map { scalar(@{ $rh->{$report}->{$_} }) . ' ' . $_ } - @{ $rh->{types}->{$report} } - ); - - if ($watching && $report eq 'setter') { - # remove links to reports with too many items to display - my $total = 0; - foreach my $type (@{ $rh->{types}->{$report} }) { - $total += scalar(@{ $rh->{$report}->{$type} }); - } - if ($total > MAX_SETTER_COUNT) { - $rh->{types}->{$report} = []; - } - } - } - } - } + # get requests - # send emails - - foreach my $recipient_id (sort keys %$requests) { - # send the email in a separate process to avoid excessive memory usage - my $params = { - recipient_id => $recipient_id, - template => $args{template}, - date => $args{date}, - reports => $args{reports}, - requests => $requests->{$recipient_id}, - }; - my ($fh, $filename) = tempfile(); - print $fh encode_json($params); - close($fh); - - my @command = ($0, $filename); - push @command, '-d' if $DO_NOT_NAG; - system(@command); - unlink($filename); - } -} - -sub _include_request { - my ($request, $report) = @_; - state $now = datetime_from($db_date, 'UTC')->truncate( to => 'day' ); - - my $recipient = Bugzilla::User->new({ id => $request->{recipient_id}, cache => 1 }); - - if ($report eq 'requestee') { - # check recipient group membership - my $group; - foreach my $type (FLAG_TYPES) { - next unless $type->{type} eq $request->{flag_type}; - $group = $type->{group}; - last; - } - return 0 unless $recipient->in_group($group); - } + foreach my $report (@{$args{reports}}) { - # check bug visibility - return 0 unless $recipient->can_see_bug($request->{bug_id}); + # collate requests + my $rows = $dbh->selectall_arrayref($args{$report . '_sql'}, {Slice => {}}); + foreach my $request (@$rows) { + next unless _include_request($request, $report); - # check attachment visibility - if ($request->{attach_id}) { - my $attachment = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 }); - return 0 if $attachment->isprivate && !$recipient->is_insider; + my $target = Bugzilla::User->new({id => $request->{target_id}, cache => 1}); + push @{$requests->{$request->{recipient_id}}->{$target->login}->{$report} + ->{$request->{flag_type}}}, $request; + push @{$requests->{$request->{recipient_id}}->{$target->login}->{bug_ids} + ->{$report}}, $request->{bug_id}; } - # exclude weekends and re-check nag-interval - my $date = datetime_from($request->{modification_date}, 'UTC'); - my $hours = 0; - $hours += 24 - $date->hour if $date->day_of_week <= 5; - $date->add( days => 1 )->truncate( to => 'day' ); - while ($date < $now) { - $hours += 24 if $date->day_of_week <= 5; - $date->add( days => 1 ); - } - return 0 if $hours < ($request->{extended_period} ? $request->{nag_interval} + 24 : $request->{nag_interval}); + # process requests here to avoid doing it in the templates + foreach my $recipient_id (keys %$requests) { + foreach my $target_login (keys %{$requests->{$recipient_id}}) { + my $rh = $requests->{$recipient_id}->{$target_login}; - return 1; -} + # build a list of valid types in the correct order + $rh->{types}->{$report} = []; + foreach my $type (map { $_->{type} } FLAG_TYPES) { + next unless exists $rh->{$report}->{$type}; + push @{$rh->{types}->{$report}}, $type; + } -sub _send_email { - my ($params) = @_; - - my @reports = @{ $params->{reports} }; - my $recipient_id = $params->{recipient_id}; - my $requests = $params->{requests}; - my $watching = $params->{template} eq 'watching'; - my $recipient = Bugzilla::User->new({ id => $recipient_id, cache => 1 }); - my $securemail = Bugzilla::User->can('public_key'); - my $has_key = $securemail && $recipient->public_key; - my $has_private_bug = 0; - - my $settings = Bugzilla::Extension::RequestNagger::Settings->new($recipient_id); - if ($watching && $settings->no_encryption) { - $has_key = 0; - } + # build a summary + $rh->{summary}->{$report} = join(', ', + map { scalar(@{$rh->{$report}->{$_}}) . ' ' . $_ } @{$rh->{types}->{$report}}); + + if ($watching && $report eq 'setter') { - foreach my $target_login (keys %$requests) { - my $rh = $requests->{$target_login}; - $rh->{target} = Bugzilla::User->new({ name => $target_login, cache => 1 }); - foreach my $report (@reports) { - foreach my $type (keys %{ $rh->{$report} }) { - foreach my $request (@{ $rh->{$report}->{$type} }) { - - _create_objects($request); - - # we need to encrypt or censor emails which contain - # non-public bugs - if ($request->{bug}->is_private) { - $has_private_bug = 1; - $request->{bug}->{sanitise_bug} = !$securemail || !$has_key; - } - else { - $request->{bug}->{sanitise_bug} = 0; - } - } - } + # remove links to reports with too many items to display + my $total = 0; + foreach my $type (@{$rh->{types}->{$report}}) { + $total += scalar(@{$rh->{$report}->{$type}}); + } + if ($total > MAX_SETTER_COUNT) { + $rh->{types}->{$report} = []; + } } + } } - my $encrypt = $securemail && $has_private_bug && $has_key; - - # generate email - my $template = Bugzilla->template_inner($recipient->setting('lang')); - my $template_file = $params->{template}; - my $vars = { - recipient => $recipient, - requests => $requests, - date => $params->{date}, - }; + } - my ($header, $text); - $template->process("email/request_nagging-$template_file-header.txt.tmpl", $vars, \$header) - || ThrowTemplateError($template->error()); - $header .= "\n"; - $template->process("email/request_nagging-$template_file.txt.tmpl", $vars, \$text) - || ThrowTemplateError($template->error()); - - my @parts = ( - Email::MIME->create( - attributes => { - content_type => 'text/plain', - charset => 'UTF-8', - encoding => 'quoted-printable', - }, - body_str => $text, - ) - ); - if ($recipient->setting('email_format') eq 'html') { - my $html; - $template->process("email/request_nagging-$template_file.html.tmpl", $vars, \$html) - || ThrowTemplateError($template->error()); - push @parts, Email::MIME->create( - attributes => { - content_type => 'text/html', - charset => 'UTF-8', - encoding => 'quoted-printable', - }, - body_str => $html, - ); - } + # send emails - my $email = Email::MIME->new($header); - $email->header_set('X-Generated-By' => hostname()); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } - else { - $email->content_type_set('multipart/alternative'); - } - $email->parts_set(\@parts); - if ($encrypt) { - $email->header_set('X-Bugzilla-Encrypt' => '1'); - } + foreach my $recipient_id (sort keys %$requests) { - # send - if ($DO_NOT_NAG) { - # uncomment the following line to enable other extensions to - # process this email, including encryption - # Bugzilla::Hook::process('mailer_before_send', { email => $email }); - print $email->as_string, "\n"; - } - else { - MessageToMTA($email); - } + # send the email in a separate process to avoid excessive memory usage + my $params = { + recipient_id => $recipient_id, + template => $args{template}, + date => $args{date}, + reports => $args{reports}, + requests => $requests->{$recipient_id}, + }; + my ($fh, $filename) = tempfile(); + print $fh encode_json($params); + close($fh); + + my @command = ($0, $filename); + push @command, '-d' if $DO_NOT_NAG; + system(@command); + unlink($filename); + } } -sub _create_objects { - my ($request) = @_; +sub _include_request { + my ($request, $report) = @_; + state $now = datetime_from($db_date, 'UTC')->truncate(to => 'day'); - $request->{recipient} = Bugzilla::User->new({ id => $request->{recipient_id}, cache => 1 }); - $request->{setter} = Bugzilla::User->new({ id => $request->{setter_id}, cache => 1 }); + my $recipient + = Bugzilla::User->new({id => $request->{recipient_id}, cache => 1}); - if (defined $request->{requestee_id}) { - $request->{requestee} = Bugzilla::User->new({ id => $request->{requestee_id}, cache => 1 }); - } - if (exists $request->{watcher_id}) { - $request->{watcher} = Bugzilla::User->new({ id => $request->{watcher_id}, cache => 1 }); + if ($report eq 'requestee') { + + # check recipient group membership + my $group; + foreach my $type (FLAG_TYPES) { + next unless $type->{type} eq $request->{flag_type}; + $group = $type->{group}; + last; } + return 0 unless $recipient->in_group($group); + } + + # check bug visibility + return 0 unless $recipient->can_see_bug($request->{bug_id}); + + # check attachment visibility + if ($request->{attach_id}) { + my $attachment + = Bugzilla::Attachment->new({id => $request->{attach_id}, cache => 1}); + return 0 if $attachment->isprivate && !$recipient->is_insider; + } + + # exclude weekends and re-check nag-interval + my $date = datetime_from($request->{modification_date}, 'UTC'); + my $hours = 0; + $hours += 24 - $date->hour if $date->day_of_week <= 5; + $date->add(days => 1)->truncate(to => 'day'); + while ($date < $now) { + $hours += 24 if $date->day_of_week <= 5; + $date->add(days => 1); + } + return 0 + if $hours < ($request->{extended_period} + ? $request->{nag_interval} + 24 + : $request->{nag_interval}); + + return 1; +} - $request->{bug} = Bugzilla::Extension::RequestNagger::Bug->new({ id => $request->{bug_id}, cache => 1 }); - $request->{flag} = Bugzilla::Flag->new({ id => $request->{flag_id}, cache => 1 }); - if (defined $request->{attach_id}) { - $request->{attachment} = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 }); +sub _send_email { + my ($params) = @_; + + my @reports = @{$params->{reports}}; + my $recipient_id = $params->{recipient_id}; + my $requests = $params->{requests}; + my $watching = $params->{template} eq 'watching'; + my $recipient = Bugzilla::User->new({id => $recipient_id, cache => 1}); + my $securemail = Bugzilla::User->can('public_key'); + my $has_key = $securemail && $recipient->public_key; + my $has_private_bug = 0; + + my $settings = Bugzilla::Extension::RequestNagger::Settings->new($recipient_id); + if ($watching && $settings->no_encryption) { + $has_key = 0; + } + + foreach my $target_login (keys %$requests) { + my $rh = $requests->{$target_login}; + $rh->{target} = Bugzilla::User->new({name => $target_login, cache => 1}); + foreach my $report (@reports) { + foreach my $type (keys %{$rh->{$report}}) { + foreach my $request (@{$rh->{$report}->{$type}}) { + + _create_objects($request); + + # we need to encrypt or censor emails which contain + # non-public bugs + if ($request->{bug}->is_private) { + $has_private_bug = 1; + $request->{bug}->{sanitise_bug} = !$securemail || !$has_key; + } + else { + $request->{bug}->{sanitise_bug} = 0; + } + } + } } + } + my $encrypt = $securemail && $has_private_bug && $has_key; + + # generate email + my $template = Bugzilla->template_inner($recipient->setting('lang')); + my $template_file = $params->{template}; + my $vars + = {recipient => $recipient, requests => $requests, date => $params->{date},}; + + my ($header, $text); + $template->process("email/request_nagging-$template_file-header.txt.tmpl", + $vars, \$header) + || ThrowTemplateError($template->error()); + $header .= "\n"; + $template->process("email/request_nagging-$template_file.txt.tmpl", + $vars, \$text) + || ThrowTemplateError($template->error()); + + my @parts = (Email::MIME->create( + attributes => { + content_type => 'text/plain', + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body_str => $text, + )); + + if ($recipient->setting('email_format') eq 'html') { + my $html; + $template->process("email/request_nagging-$template_file.html.tmpl", + $vars, \$html) + || ThrowTemplateError($template->error()); + push @parts, + Email::MIME->create( + attributes => { + content_type => 'text/html', + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body_str => $html, + ); + } + + my $email = Email::MIME->new($header); + $email->header_set('X-Generated-By' => hostname()); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + } + $email->parts_set(\@parts); + if ($encrypt) { + $email->header_set('X-Bugzilla-Encrypt' => '1'); + } + + # send + if ($DO_NOT_NAG) { + + # uncomment the following line to enable other extensions to + # process this email, including encryption + # Bugzilla::Hook::process('mailer_before_send', { email => $email }); + print $email->as_string, "\n"; + } + else { + MessageToMTA($email); + } +} + +sub _create_objects { + my ($request) = @_; + + $request->{recipient} + = Bugzilla::User->new({id => $request->{recipient_id}, cache => 1}); + $request->{setter} + = Bugzilla::User->new({id => $request->{setter_id}, cache => 1}); + + if (defined $request->{requestee_id}) { + $request->{requestee} + = Bugzilla::User->new({id => $request->{requestee_id}, cache => 1}); + } + if (exists $request->{watcher_id}) { + $request->{watcher} + = Bugzilla::User->new({id => $request->{watcher_id}, cache => 1}); + } + + $request->{bug} = Bugzilla::Extension::RequestNagger::Bug->new( + {id => $request->{bug_id}, cache => 1}); + $request->{flag} = Bugzilla::Flag->new({id => $request->{flag_id}, cache => 1}); + if (defined $request->{attach_id}) { + $request->{attachment} + = Bugzilla::Attachment->new({id => $request->{attach_id}, cache => 1}); + } } diff --git a/extensions/RequestNagger/lib/Bug.pm b/extensions/RequestNagger/lib/Bug.pm index 974a688ea..dc510d486 100644 --- a/extensions/RequestNagger/lib/Bug.pm +++ b/extensions/RequestNagger/lib/Bug.pm @@ -17,29 +17,29 @@ use feature 'state'; use Bugzilla::User; sub short_desc { - my ($self) = @_; - return $self->{sanitise_bug} ? '(Secure bug)' : $self->SUPER::short_desc; + my ($self) = @_; + return $self->{sanitise_bug} ? '(Secure bug)' : $self->SUPER::short_desc; } sub is_private { - my ($self) = @_; - if (!exists $self->{is_private}) { - state $default_user //= Bugzilla::User->new(); - $self->{is_private} = !$default_user->can_see_bug($self); - } - return $self->{is_private}; + my ($self) = @_; + if (!exists $self->{is_private}) { + state $default_user //= Bugzilla::User->new(); + $self->{is_private} = !$default_user->can_see_bug($self); + } + return $self->{is_private}; } sub tooltip { - my ($self) = @_; - my $tooltip = $self->bug_status; - if ($self->bug_status eq 'RESOLVED') { - $tooltip .= '/' . $self->resolution; - } - if (!$self->{sanitise_bug}) { - $tooltip .= ' ' . $self->product . ' :: ' . $self->component; - } - return $tooltip; + my ($self) = @_; + my $tooltip = $self->bug_status; + if ($self->bug_status eq 'RESOLVED') { + $tooltip .= '/' . $self->resolution; + } + if (!$self->{sanitise_bug}) { + $tooltip .= ' ' . $self->product . ' :: ' . $self->component; + } + return $tooltip; } 1; diff --git a/extensions/RequestNagger/lib/Constants.pm b/extensions/RequestNagger/lib/Constants.pm index bc6cf3371..1309e06c9 100644 --- a/extensions/RequestNagger/lib/Constants.pm +++ b/extensions/RequestNagger/lib/Constants.pm @@ -14,13 +14,13 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - MAX_SETTER_COUNT - MAX_REQUEST_AGE - FLAG_TYPES - REQUESTEE_NAG_SQL - SETTER_NAG_SQL - WATCHING_REQUESTEE_NAG_SQL - WATCHING_SETTER_NAG_SQL + MAX_SETTER_COUNT + MAX_REQUEST_AGE + FLAG_TYPES + REQUESTEE_NAG_SQL + SETTER_NAG_SQL + WATCHING_REQUESTEE_NAG_SQL + WATCHING_SETTER_NAG_SQL ); # if there are more than this many requests that a user is waiting on, show a @@ -29,33 +29,24 @@ use constant MAX_SETTER_COUNT => 7; # ignore any request older than this many days in the requestee emails # massively overdue requests will still be included in the 'watching' emails -use constant MAX_REQUEST_AGE => 90; # about three months +use constant MAX_REQUEST_AGE => 90; # about three months # the order of this array determines the order used in email use constant FLAG_TYPES => ( - { - type => 'review', # flag_type.name - group => 'everyone', # the user must be a member of this group to receive reminders - }, - { - type => 'superview', - group => 'everyone', - }, - { - type => 'feedback', - group => 'everyone', - }, - { - type => 'needinfo', - group => 'editbugs', - }, + { + type => 'review', # flag_type.name + group => 'everyone', # the user must be a member of this group to receive reminders + }, + {type => 'superview', group => 'everyone',}, + {type => 'feedback', group => 'everyone',}, + {type => 'needinfo', group => 'editbugs',}, ); sub REQUESTEE_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT flagtypes.name AS flag_type, flags.id AS flag_id, @@ -84,7 +75,8 @@ sub REQUESTEE_NAG_SQL { AND flags.status = '?' AND products.nag_interval != 0 AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval - AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " . MAX_REQUEST_AGE . " + AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " + . MAX_REQUEST_AGE . " AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on') AND requestee.disable_mail = 0 AND nag_defer.id IS NULL @@ -96,10 +88,10 @@ sub REQUESTEE_NAG_SQL { } sub SETTER_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT flagtypes.name AS flag_type, flags.id AS flag_id, @@ -128,7 +120,8 @@ sub SETTER_NAG_SQL { AND flags.status = '?' AND products.nag_interval != 0 AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval - AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " . MAX_REQUEST_AGE . " + AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " + . MAX_REQUEST_AGE . " AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on') AND setter.disable_mail = 0 AND nag_defer.id IS NULL @@ -140,10 +133,10 @@ sub SETTER_NAG_SQL { } sub WATCHING_REQUESTEE_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT nag_watch.watcher_id, flagtypes.name AS flag_type, @@ -192,10 +185,10 @@ sub WATCHING_REQUESTEE_NAG_SQL { } sub WATCHING_SETTER_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT nag_watch.watcher_id, flagtypes.name AS flag_type, diff --git a/extensions/RequestNagger/lib/Settings.pm b/extensions/RequestNagger/lib/Settings.pm index 393d224ba..839c37485 100644 --- a/extensions/RequestNagger/lib/Settings.pm +++ b/extensions/RequestNagger/lib/Settings.pm @@ -17,47 +17,48 @@ use List::MoreUtils qw( any ); use constant FIELDS => qw( reviews_only extended_period no_encryption ); sub new { - my ($class, $user_id) = @_; - - my $dbh = Bugzilla->dbh; - my $self = { user_id => $user_id }; - foreach my $row (@{ $dbh->selectall_arrayref( - "SELECT setting_name,setting_value FROM nag_settings WHERE user_id = ?", - { Slice => {} }, - $user_id - ) }) { - $self->{$row->{setting_name}} = $row->{setting_value}; - } - - return bless($self, $class); + my ($class, $user_id) = @_; + + my $dbh = Bugzilla->dbh; + my $self = {user_id => $user_id}; + foreach my $row (@{ + $dbh->selectall_arrayref( + "SELECT setting_name,setting_value FROM nag_settings WHERE user_id = ?", + {Slice => {}}, $user_id) + }) + { + $self->{$row->{setting_name}} = $row->{setting_value}; + } + + return bless($self, $class); } -sub reviews_only { exists $_[0]->{reviews_only} ? $_[0]->{reviews_only} : 0 } -sub extended_period { exists $_[0]->{extended_period} ? $_[0]->{extended_period} : 0 } -sub no_encryption { exists $_[0]->{no_encryption} ? $_[0]->{no_encryption} : 0 } +sub reviews_only { exists $_[0]->{reviews_only} ? $_[0]->{reviews_only} : 0 } + +sub extended_period { + exists $_[0]->{extended_period} ? $_[0]->{extended_period} : 0; +} +sub no_encryption { exists $_[0]->{no_encryption} ? $_[0]->{no_encryption} : 0 } sub set { - my ($self, $field, $value) = @_; - return unless any { $_ eq $field } FIELDS; - $value = $value ? 1 : 0; - - my $dbh = Bugzilla->dbh; - if (exists $self->{$field}) { - $dbh->do( - "UPDATE nag_settings SET setting_value=? WHERE user_id=? AND setting_name=?", - undef, - $value, $self->{user_id}, $field - ); - } - else { - $dbh->do( - "INSERT INTO nag_settings(user_id, setting_name, setting_value) VALUES (?, ?, ?)", - undef, - $self->{user_id}, $field, $value - ); - } - - $self->{$field} = $value; + my ($self, $field, $value) = @_; + return unless any { $_ eq $field } FIELDS; + $value = $value ? 1 : 0; + + my $dbh = Bugzilla->dbh; + if (exists $self->{$field}) { + $dbh->do( + "UPDATE nag_settings SET setting_value=? WHERE user_id=? AND setting_name=?", + undef, $value, $self->{user_id}, $field); + } + else { + $dbh->do( + "INSERT INTO nag_settings(user_id, setting_name, setting_value) VALUES (?, ?, ?)", + undef, $self->{user_id}, $field, $value + ); + } + + $self->{$field} = $value; } 1; diff --git a/extensions/RestrictComments/Config.pm b/extensions/RestrictComments/Config.pm index be703bed7..eb99e5a94 100644 --- a/extensions/RestrictComments/Config.pm +++ b/extensions/RestrictComments/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'RestrictComments'; +use constant NAME => 'RestrictComments'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/RestrictComments/Extension.pm b/extensions/RestrictComments/Extension.pm index e93540d5a..83075ac23 100644 --- a/extensions/RestrictComments/Extension.pm +++ b/extensions/RestrictComments/Extension.pm @@ -17,84 +17,86 @@ use Bugzilla::Constants; use Bugzilla::Util qw(i_am_webservice); BEGIN { - *Bugzilla::Bug::restrict_comments = \&_bug_restrict_comments; + *Bugzilla::Bug::restrict_comments = \&_bug_restrict_comments; } sub _bug_restrict_comments { - my ($self) = @_; - return $self->{restrict_comments}; + my ($self) = @_; + return $self->{restrict_comments}; } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $priv_results) = @$args{qw(bug priv_results)}; - my $user = Bugzilla->user; - - if ($user->id - && $bug->restrict_comments - && !$user->in_group(Bugzilla->params->{'restrict_comments_group'})) - { - push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - return; - } + my ($self, $args) = @_; + my ($bug, $priv_results) = @$args{qw(bug priv_results)}; + my $user = Bugzilla->user; + + if ( $user->id + && $bug->restrict_comments + && !$user->in_group(Bugzilla->params->{'restrict_comments_group'})) + { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + return; + } } sub _can_restrict_comments { - my ($self, $object) = @_; - return unless $object->isa('Bugzilla::Bug'); - $self->{setter_group} ||= Bugzilla->params->{'restrict_comments_enable_group'}; - return Bugzilla->user->in_group($self->{setter_group}); + my ($self, $object) = @_; + return unless $object->isa('Bugzilla::Bug'); + $self->{setter_group} ||= Bugzilla->params->{'restrict_comments_enable_group'}; + return Bugzilla->user->in_group($self->{setter_group}); } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $input = Bugzilla->input_params; - my $update_restrict_comments = !i_am_webservice() || exists $input->{restrict_comments}; - if ($update_restrict_comments && $self->_can_restrict_comments($object)) { - $object->set('restrict_comments', $input->{restrict_comments} ? 1 : undef); - } + my ($self, $args) = @_; + my $object = $args->{object}; + my $input = Bugzilla->input_params; + my $update_restrict_comments + = !i_am_webservice() || exists $input->{restrict_comments}; + if ($update_restrict_comments && $self->_can_restrict_comments($object)) { + $object->set('restrict_comments', $input->{restrict_comments} ? 1 : undef); + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($self->_can_restrict_comments($object)) { - push(@$columns, 'restrict_comments'); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($self->_can_restrict_comments($object)) { + push(@$columns, 'restrict_comments'); + } } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'restrict_comments')) { - push @$columns, 'restrict_comments'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'restrict_comments')) { + push @$columns, 'restrict_comments'; } + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, 'restrict_comments') + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + push(@$fields, 'restrict_comments'); } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{RestrictComments} = "Bugzilla::Extension::RestrictComments::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{RestrictComments} = "Bugzilla::Extension::RestrictComments::Config"; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $field = new Bugzilla::Field({ name => 'restrict_comments' }); - if (!$field) { - Bugzilla::Field->create({ name => 'restrict_comments', description => 'Restrict Comments' }); - } + my $field = new Bugzilla::Field({name => 'restrict_comments'}); + if (!$field) { + Bugzilla::Field->create( + {name => 'restrict_comments', description => 'Restrict Comments'}); + } - $dbh->bz_add_column('bugs', 'restrict_comments', { TYPE => 'BOOLEAN' }); + $dbh->bz_add_column('bugs', 'restrict_comments', {TYPE => 'BOOLEAN'}); } __PACKAGE__->NAME; diff --git a/extensions/RestrictComments/lib/Config.pm b/extensions/RestrictComments/lib/Config.pm index e5dbc518c..c1f9829f9 100644 --- a/extensions/RestrictComments/lib/Config.pm +++ b/extensions/RestrictComments/lib/Config.pm @@ -17,26 +17,26 @@ use Bugzilla::Group; our $sortkey = 510; sub get_param_list { - my ($class) = @_; - - my @param_list = ( - { - name => 'restrict_comments_group', - type => 's', - choices => \&get_all_group_names, - default => '', - checker => \&check_group - }, - { - name => 'restrict_comments_enable_group', - type => 's', - choices => \&get_all_group_names, - default => '', - checker => \&check_group - }, - ); - - return @param_list; + my ($class) = @_; + + my @param_list = ( + { + name => 'restrict_comments_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group + }, + { + name => 'restrict_comments_enable_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group + }, + ); + + return @param_list; } 1; diff --git a/extensions/Review/Config.pm b/extensions/Review/Config.pm index ea7e8a725..aa9a8c32d 100644 --- a/extensions/Review/Config.pm +++ b/extensions/Review/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'Review'; +use constant NAME => 'Review'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm index a918a5ca5..975857cf7 100644 --- a/extensions/Review/Extension.pm +++ b/extensions/Review/Extension.pm @@ -36,95 +36,96 @@ use constant MENTOR_LIMIT => 10; # BEGIN { - *Bugzilla::Product::reviewers = \&_product_reviewers; - *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs; - *Bugzilla::Product::reviewer_required = \&_product_reviewer_required; - *Bugzilla::Component::reviewers = \&_component_reviewers; - *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs; - *Bugzilla::Bug::mentors = \&_bug_mentors; - *Bugzilla::Bug::bug_mentors = \&_bug_mentors; - *Bugzilla::Bug::bug_mentor = \&_bug_mentors; - *Bugzilla::Bug::is_mentor = \&_bug_is_mentor; - *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors; - *Bugzilla::User::review_count = \&_user_review_count; - *Bugzilla::User::reviews_blocked = \&_user_reviews_blocked; - *Bugzilla::User::is_active = \&_user_is_active; + *Bugzilla::Product::reviewers = \&_product_reviewers; + *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs; + *Bugzilla::Product::reviewer_required = \&_product_reviewer_required; + *Bugzilla::Component::reviewers = \&_component_reviewers; + *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs; + *Bugzilla::Bug::mentors = \&_bug_mentors; + *Bugzilla::Bug::bug_mentors = \&_bug_mentors; + *Bugzilla::Bug::bug_mentor = \&_bug_mentors; + *Bugzilla::Bug::is_mentor = \&_bug_is_mentor; + *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors; + *Bugzilla::User::review_count = \&_user_review_count; + *Bugzilla::User::reviews_blocked = \&_user_reviews_blocked; + *Bugzilla::User::is_active = \&_user_is_active; } # # monkey-patched methods # -sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) } -sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) } -sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) } -sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) } +sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) } +sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) } +sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) } +sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) } sub _reviewers { - my ($object, $type, $unfiltered) = @_; - return join(', ', map { $_->login } @{ _reviewers_objs($object, $type, $unfiltered) }); + my ($object, $type, $unfiltered) = @_; + return + join(', ', map { $_->login } @{_reviewers_objs($object, $type, $unfiltered)}); } sub _reviewers_objs { - my ($object, $type, $unfiltered) = @_; - if (!$object->{reviewers}) { - my $dbh = Bugzilla->dbh; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey", - undef, - $object->id, - ); - # new_from_list always sorts according to the object's definition, - # so we have to reorder the list - my $users = Bugzilla::User->new_from_list($user_ids); - my %user_map = map { $_->id => $_ } @$users; - my @reviewers = map { $user_map{$_} } @$user_ids; - if (!$unfiltered) { - @reviewers = grep { - $_->is_enabled - && $_->is_active - && $_->name !~ UNAVAILABLE_RE - && !$_->reviews_blocked - } @reviewers; - } - $object->{reviewers} = \@reviewers; + my ($object, $type, $unfiltered) = @_; + if (!$object->{reviewers}) { + my $dbh = Bugzilla->dbh; + my $user_ids + = $dbh->selectcol_arrayref( + "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey", + undef, $object->id,); + + # new_from_list always sorts according to the object's definition, + # so we have to reorder the list + my $users = Bugzilla::User->new_from_list($user_ids); + my %user_map = map { $_->id => $_ } @$users; + my @reviewers = map { $user_map{$_} } @$user_ids; + if (!$unfiltered) { + @reviewers = grep { + $_->is_enabled + && $_->is_active + && $_->name !~ UNAVAILABLE_RE + && !$_->reviews_blocked + } @reviewers; } - return $object->{reviewers}; + $object->{reviewers} = \@reviewers; + } + return $object->{reviewers}; } sub _user_is_active { - my ($self) = @_; + my ($self) = @_; - # never consider .bugs or .tld addresses as inactive. - return 1 if $self->login =~ /\.(?:bugs|tld)$/; - return 1 unless Bugzilla->params->{max_reviewer_last_seen}; - return 0 if !defined($self->last_seen_date); + # never consider .bugs or .tld addresses as inactive. + return 1 if $self->login =~ /\.(?:bugs|tld)$/; + return 1 unless Bugzilla->params->{max_reviewer_last_seen}; + return 0 if !defined($self->last_seen_date); - my $dt = datetime_from($self->last_seen_date); - my $days_ago = $dt->delta_days(DateTime->now())->in_units('days'); + my $dt = datetime_from($self->last_seen_date); + my $days_ago = $dt->delta_days(DateTime->now())->in_units('days'); - return $days_ago <= Bugzilla->params->{max_reviewer_last_seen}; + return $days_ago <= Bugzilla->params->{max_reviewer_last_seen}; } sub _user_review_count { - my ($self) = @_; - if (!exists $self->{review_count}) { - my $dbh = Bugzilla->dbh; - ($self->{review_count}) = $dbh->selectrow_array( - "SELECT COUNT(*) + my ($self) = @_; + if (!exists $self->{review_count}) { + my $dbh = Bugzilla->dbh; + ($self->{review_count}) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM flags INNER JOIN flagtypes ON flagtypes.id = flags.type_id WHERE flags.requestee_id = ? - AND " . $dbh->sql_in('flagtypes.name', [ "'review'", "'feedback'" ]), - undef, - $self->id, - ); - } - return $self->{review_count}; + AND " + . $dbh->sql_in('flagtypes.name', ["'review'", "'feedback'"]), undef, + $self->id, + ); + } + return $self->{review_count}; } sub _user_reviews_blocked { - return $_[0]->settings->{block_reviews}->{value} eq 'on'; + return $_[0]->settings->{block_reviews}->{value} eq 'on'; } # @@ -132,148 +133,143 @@ sub _user_reviews_blocked { # sub _bug_mentors { - my ($self, $options) = @_; - $options //= {}; - my $dbh = Bugzilla->dbh; - if (!$self->{bug_mentors}) { - my $mentor_ids = $dbh->selectcol_arrayref(" - SELECT user_id FROM bug_mentors WHERE bug_id = ?", - undef, - $self->id); - $self->{bug_mentors} = []; - foreach my $mentor_id (@$mentor_ids) { - push(@{ $self->{bug_mentors} }, Bugzilla::User->new({ id => $mentor_id, cache => 1 })); - } - $self->{bug_mentors} = [ - sort { $a->login cmp $b->login } @{ $self->{bug_mentors} } - ]; - } - my @result = @{ $self->{bug_mentors} }; - if ($options->{exclude_needinfo_blocked}) { - @result = grep { !$_->needinfo_blocked } @result; - } - if ($options->{exclude_review_blocked}) { - @result = grep { !$_->reviews_blocked } @result; + my ($self, $options) = @_; + $options //= {}; + my $dbh = Bugzilla->dbh; + if (!$self->{bug_mentors}) { + my $mentor_ids = $dbh->selectcol_arrayref(" + SELECT user_id FROM bug_mentors WHERE bug_id = ?", undef, $self->id); + $self->{bug_mentors} = []; + foreach my $mentor_id (@$mentor_ids) { + push( + @{$self->{bug_mentors}}, + Bugzilla::User->new({id => $mentor_id, cache => 1}) + ); } - return \@result; + $self->{bug_mentors} + = [sort { $a->login cmp $b->login } @{$self->{bug_mentors}}]; + } + my @result = @{$self->{bug_mentors}}; + if ($options->{exclude_needinfo_blocked}) { + @result = grep { !$_->needinfo_blocked } @result; + } + if ($options->{exclude_review_blocked}) { + @result = grep { !$_->reviews_blocked } @result; + } + return \@result; } sub _bug_is_mentor { - my ($self, $user) = @_; - my $user_id = ($user || Bugzilla->user)->id; - return (grep { $_->id == $user_id} @{ $self->mentors }) ? 1 : 0; + my ($self, $user) = @_; + my $user_id = ($user || Bugzilla->user)->id; + return (grep { $_->id == $user_id } @{$self->mentors}) ? 1 : 0; } sub _bug_set_bug_mentors { - my ($self, $value) = @_; - $self->set('bug_mentors', $value); + my ($self, $value) = @_; + $self->set('bug_mentors', $value); } sub object_validators { - my ($self, $args) = @_; - return unless $args->{class} eq 'Bugzilla::Bug'; - $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors; + my ($self, $args) = @_; + return unless $args->{class} eq 'Bugzilla::Bug'; + $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors; } sub _bug_check_bug_mentors { - my ($self, $value) = @_; - my %seen; - my $mentors = [ - grep { !$seen{$_->id}++ } - map { Bugzilla::User->check({ name => $_, cache => 1 }) } - ref($value) ? @$value : ($value) - ]; - if (scalar(@$mentors) > MENTOR_LIMIT) { - ThrowUserError('mentor_limit_exceeded', { limit => MENTOR_LIMIT }); - } - return $mentors; + my ($self, $value) = @_; + my %seen; + my $mentors + = [grep { !$seen{$_->id}++ } + map { Bugzilla::User->check({name => $_, cache => 1}) } + ref($value) ? @$value : ($value)]; + if (scalar(@$mentors) > MENTOR_LIMIT) { + ThrowUserError('mentor_limit_exceeded', {limit => MENTOR_LIMIT}); + } + return $mentors; } sub bug_user_match_fields { - my ($self, $args) = @_; - $args->{fields}->{bug_mentors} = { type => 'multi' }; + my ($self, $args) = @_; + $args->{fields}->{bug_mentors} = {type => 'multi'}; } sub bug_before_create { - my ($self, $args) = @_; - my $params = $args->{params}; - my $stash = $args->{stash}; - $stash->{bug_mentors} = delete $params->{bug_mentors}; + my ($self, $args) = @_; + my $params = $args->{params}; + my $stash = $args->{stash}; + $stash->{bug_mentors} = delete $params->{bug_mentors}; } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $stash = $args->{stash}; - if (my $mentors = $stash->{bug_mentors}) { - $self->_update_user_table({ - object => $bug, - old_users => [], - new_users => $self->_bug_check_bug_mentors($mentors), - table => 'bug_mentors', - id_field => 'bug_id', - }); - } + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $stash = $args->{stash}; + if (my $mentors = $stash->{bug_mentors}) { + $self->_update_user_table({ + object => $bug, + old_users => [], + new_users => $self->_bug_check_bug_mentors($mentors), + table => 'bug_mentors', + id_field => 'bug_id', + }); + } } sub _update_user_table { - my ($self, $args) = @_; - my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) = - @$args{qw(object old_users new_users table id_field has_sortkey return)}; - my $dbh = Bugzilla->dbh; - my (@removed, @added); - - # remove deleted users - foreach my $old_user (@$old_users) { - if (!grep { $_->id == $old_user->id } @$new_users) { - $dbh->do( - "DELETE FROM $table WHERE $id_field = ? AND user_id = ?", - undef, - $object->id, $old_user->id, - ); - push @removed, $old_user; - } + my ($self, $args) = @_; + my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) + = @$args{qw(object old_users new_users table id_field has_sortkey return)}; + my $dbh = Bugzilla->dbh; + my (@removed, @added); + + # remove deleted users + foreach my $old_user (@$old_users) { + if (!grep { $_->id == $old_user->id } @$new_users) { + $dbh->do("DELETE FROM $table WHERE $id_field = ? AND user_id = ?", + undef, $object->id, $old_user->id,); + push @removed, $old_user; } - # add new users - foreach my $new_user (@$new_users) { - if (!grep { $_->id == $new_user->id } @$old_users) { - $dbh->do( - "INSERT INTO $table ($id_field, user_id) VALUES (?, ?)", - undef, - $object->id, $new_user->id, - ); - push @added, $new_user; - } + } + + # add new users + foreach my $new_user (@$new_users) { + if (!grep { $_->id == $new_user->id } @$old_users) { + $dbh->do("INSERT INTO $table ($id_field, user_id) VALUES (?, ?)", + undef, $object->id, $new_user->id,); + push @added, $new_user; } + } - return unless @removed || @added; - - if ($has_sortkey) { - # update the sortkey for all users - for (my $i = 0; $i < scalar(@$new_users); $i++) { - $dbh->do( - "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?", - undef, - ($i + 1) * 10, $object->id, $new_users->[$i]->id, - ); - } - } + return unless @removed || @added; - if (!$return) { - return undef; - } - elsif ($return eq 'diff') { - return [ - @removed ? join(', ', map { $_->login } @removed) : undef, - @added ? join(', ', map { $_->login } @added) : undef, - ]; - } - elsif ($return eq 'old-new') { - return [ - @$old_users ? join(', ', map { $_->login } @$old_users) : '', - @$new_users ? join(', ', map { $_->login } @$new_users) : '', - ]; + if ($has_sortkey) { + + # update the sortkey for all users + for (my $i = 0; $i < scalar(@$new_users); $i++) { + $dbh->do( + "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?", + undef, ($i + 1) * 10, + $object->id, $new_users->[$i]->id, + ); } + } + + if (!$return) { + return undef; + } + elsif ($return eq 'diff') { + return [ + @removed ? join(', ', map { $_->login } @removed) : undef, + @added ? join(', ', map { $_->login } @added) : undef, + ]; + } + elsif ($return eq 'old-new') { + return [ + @$old_users ? join(', ', map { $_->login } @$old_users) : '', + @$new_users ? join(', ', map { $_->login } @$new_users) : '', + ]; + } } # @@ -283,44 +279,46 @@ sub _update_user_table { sub _product_reviewer_required { $_[0]->{reviewer_required} } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Product')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(reviewer_required); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } - elsif ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(review_request_count feedback_request_count needinfo_request_count); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(reviewer_required); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } + elsif ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns + = qw(review_request_count feedback_request_count needinfo_request_count); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push @$columns, 'reviewer_required'; - } - elsif ($object->isa('Bugzilla::User')) { - push @$columns, qw(review_request_count feedback_request_count needinfo_request_count); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'reviewer_required'; + } + elsif ($object->isa('Bugzilla::User')) { + push @$columns, + qw(review_request_count feedback_request_count needinfo_request_count); + } } sub _new_users_from_input { - my ($field) = @_; - my $input_params = Bugzilla->input_params; - return undef unless exists $input_params->{$field}; - return [] unless $input_params->{$field}; - Bugzilla::User::match_field({ $field => {'type' => 'multi'} });; - my $value = $input_params->{$field}; - my %seen; - return [ - grep { !$seen{$_->id}++ } - map { Bugzilla::User->check({ name => $_, cache => 1 }) } - ref($value) ? @$value : ($value) - ]; + my ($field) = @_; + my $input_params = Bugzilla->input_params; + return undef unless exists $input_params->{$field}; + return [] unless $input_params->{$field}; + Bugzilla::User::match_field({$field => {'type' => 'multi'}}); + my $value = $input_params->{$field}; + my %seen; + return [grep { !$seen{$_->id}++ } + map { Bugzilla::User->check({name => $_, cache => 1}) } + ref($value) ? @$value : ($value)]; } # @@ -328,311 +326,329 @@ sub _new_users_from_input { # sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - return unless $class->isa('Bugzilla::Product'); + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); - $params->{reviewer_required} = Bugzilla->cgi->param('reviewer_required') ? 1 : 0; + $params->{reviewer_required} + = Bugzilla->cgi->param('reviewer_required') ? 1 : 0; } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - return unless $object->isa('Bugzilla::Product'); + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); - $object->set('reviewer_required', Bugzilla->cgi->param('reviewer_required') ? 1 : 0); + $object->set('reviewer_required', + Bugzilla->cgi->param('reviewer_required') ? 1 : 0); } sub object_end_of_create { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - - if ($object->isa('Bugzilla::Product')) { - $self->_update_user_table({ - object => $object, - old_users => [], - new_users => _new_users_from_input('reviewers'), - table => 'product_reviewers', - id_field => 'product_id', - has_sortkey => 1, - }); - } - elsif ($object->isa('Bugzilla::Component')) { - $self->_update_user_table({ - object => $object, - old_users => [], - new_users => _new_users_from_input('reviewers'), - table => 'component_reviewers', - id_field => 'component_id', - has_sortkey => 1, - }); - } - elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') { - _check_requestee($object); - _adjust_request_count($object, +1); - } - if (_is_countable_flag($object)) { - $self->_log_flag_state_activity($object, $object->status, $object->modification_date); - } + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + + if ($object->isa('Bugzilla::Product')) { + $self->_update_user_table({ + object => $object, + old_users => [], + new_users => _new_users_from_input('reviewers'), + table => 'product_reviewers', + id_field => 'product_id', + has_sortkey => 1, + }); + } + elsif ($object->isa('Bugzilla::Component')) { + $self->_update_user_table({ + object => $object, + old_users => [], + new_users => _new_users_from_input('reviewers'), + table => 'component_reviewers', + id_field => 'component_id', + has_sortkey => 1, + }); + } + elsif (_is_countable_flag($object) + && $object->requestee_id + && $object->status eq '?') + { + _check_requestee($object); + _adjust_request_count($object, +1); + } + if (_is_countable_flag($object)) { + $self->_log_flag_state_activity($object, $object->status, + $object->modification_date); + } } sub object_end_of_update { - my ($self, $args) = @_; - my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; - - if ($object->isa('Bugzilla::Product') && exists Bugzilla->input_params->{reviewers}) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->reviewers_objs(1), - new_users => _new_users_from_input('reviewers'), - table => 'product_reviewers', - id_field => 'product_id', - has_sortkey => 1, - return => 'old-new', - }); - $changes->{reviewers} = $diff if $diff; + my ($self, $args) = @_; + my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; + + if ($object->isa('Bugzilla::Product') + && exists Bugzilla->input_params->{reviewers}) + { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->reviewers_objs(1), + new_users => _new_users_from_input('reviewers'), + table => 'product_reviewers', + id_field => 'product_id', + has_sortkey => 1, + return => 'old-new', + }); + $changes->{reviewers} = $diff if $diff; + } + elsif ($object->isa('Bugzilla::Component')) { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->reviewers_objs(1), + new_users => _new_users_from_input('reviewers'), + table => 'component_reviewers', + id_field => 'component_id', + has_sortkey => 1, + return => 'old-new', + }); + $changes->{reviewers} = $diff if $diff; + } + elsif ($object->isa('Bugzilla::Bug')) { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->mentors, + new_users => $object->mentors, + table => 'bug_mentors', + id_field => 'bug_id', + return => 'diff', + }); + $changes->{bug_mentor} = $diff if $diff; + } + elsif (_is_countable_flag($object)) { + my ($old_status, $new_status) = ($old_object->status, $object->status); + if ($old_status ne '?' && $new_status eq '?') { + + # setting flag to ? + _adjust_request_count($object, +1); + if ($object->requestee_id) { + _check_requestee($object); + } } - elsif ($object->isa('Bugzilla::Component')) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->reviewers_objs(1), - new_users => _new_users_from_input('reviewers'), - table => 'component_reviewers', - id_field => 'component_id', - has_sortkey => 1, - return => 'old-new', - }); - $changes->{reviewers} = $diff if $diff; + elsif ($old_status eq '?' && $new_status ne '?') { + + # setting flag from ? + _adjust_request_count($old_object, -1); } - elsif ($object->isa('Bugzilla::Bug')) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->mentors, - new_users => $object->mentors, - table => 'bug_mentors', - id_field => 'bug_id', - return => 'diff', - }); - $changes->{bug_mentor} = $diff if $diff; + elsif ($old_object->requestee_id && !$object->requestee_id) { + + # removing requestee + _adjust_request_count($old_object, -1); } - elsif (_is_countable_flag($object)) { - my ($old_status, $new_status) = ($old_object->status, $object->status); - if ($old_status ne '?' && $new_status eq '?') { - # setting flag to ? - _adjust_request_count($object, +1); - if ($object->requestee_id) { - _check_requestee($object); - } - } - elsif ($old_status eq '?' && $new_status ne '?') { - # setting flag from ? - _adjust_request_count($old_object, -1); - } - elsif ($old_object->requestee_id && !$object->requestee_id) { - # removing requestee - _adjust_request_count($old_object, -1); - } - elsif (!$old_object->requestee_id && $object->requestee_id) { - # setting requestee - _check_requestee($object); - _adjust_request_count($object, +1); - } - elsif ($old_object->requestee_id && $object->requestee_id - && $old_object->requestee_id != $object->requestee_id) - { - # changing requestee - _check_requestee($object); - _adjust_request_count($old_object, -1); - _adjust_request_count($object, +1); - } + elsif (!$old_object->requestee_id && $object->requestee_id) { + + # setting requestee + _check_requestee($object); + _adjust_request_count($object, +1); } + elsif ($old_object->requestee_id + && $object->requestee_id + && $old_object->requestee_id != $object->requestee_id) + { + # changing requestee + _check_requestee($object); + _adjust_request_count($old_object, -1); + _adjust_request_count($object, +1); + } + } } sub flag_updated { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - my $changes = $args->{changes}; - - return unless scalar(keys %$changes); - if (_is_countable_flag($flag)) { - $self->_log_flag_state_activity($flag, $flag->status, $timestamp); - } + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + my $changes = $args->{changes}; + + return unless scalar(keys %$changes); + if (_is_countable_flag($flag)) { + $self->_log_flag_state_activity($flag, $flag->status, $timestamp); + } } sub flag_deleted { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; - if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') { - _adjust_request_count($flag, -1); - } + if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') { + _adjust_request_count($flag, -1); + } - if (_is_countable_flag($flag)) { - $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id); - } + if (_is_countable_flag($flag)) { + $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id); + } } sub _is_countable_flag { - my ($object) = @_; - return unless $object->isa('Bugzilla::Flag'); - my $type_name = $object->type->name; - return $type_name eq 'review' || $type_name eq 'feedback' || $type_name eq 'needinfo'; + my ($object) = @_; + return unless $object->isa('Bugzilla::Flag'); + my $type_name = $object->type->name; + return + $type_name eq 'review' + || $type_name eq 'feedback' + || $type_name eq 'needinfo'; } sub _check_requestee { - my ($flag) = @_; - return unless $flag->type->name eq 'review' || $flag->type->name eq 'feedback'; - if ($flag->requestee->reviews_blocked) { - ThrowUserError('reviews_blocked', - { requestee => $flag->requestee, flagtype => $flag->type->name }); - } + my ($flag) = @_; + return unless $flag->type->name eq 'review' || $flag->type->name eq 'feedback'; + if ($flag->requestee->reviews_blocked) { + ThrowUserError('reviews_blocked', + {requestee => $flag->requestee, flagtype => $flag->type->name}); + } } sub _log_flag_state_activity { - my ($self, $flag, $status, $timestamp, $setter_id) = @_; - - $setter_id //= $flag->setter_id; - - Bugzilla::Extension::Review::FlagStateActivity->create({ - flag_when => $timestamp, - setter_id => $setter_id, - status => $status, - type_id => $flag->type_id, - flag_id => $flag->id, - requestee_id => $flag->requestee_id, - bug_id => $flag->bug_id, - attachment_id => $flag->attach_id, - }); + my ($self, $flag, $status, $timestamp, $setter_id) = @_; + + $setter_id //= $flag->setter_id; + + Bugzilla::Extension::Review::FlagStateActivity->create({ + flag_when => $timestamp, + setter_id => $setter_id, + status => $status, + type_id => $flag->type_id, + flag_id => $flag->id, + requestee_id => $flag->requestee_id, + bug_id => $flag->bug_id, + attachment_id => $flag->attach_id, + }); } sub _adjust_request_count { - my ($flag, $add) = @_; - return unless my $requestee_id = $flag->requestee_id; - my $field = $flag->type->name . '_request_count'; - - # update the current user's object so things are display correctly on the - # post-processing page - my $user = Bugzilla->user; - if ($requestee_id == $user->id) { - $user->{$field} += $add; - } - - # update database directly to avoid creating audit_log entries - $add = $add == -1 ? ' - 1' : ' + 1'; - Bugzilla->dbh->do( - "UPDATE profiles SET $field = $field $add WHERE userid = ?", - undef, - $requestee_id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $requestee_id }); + my ($flag, $add) = @_; + return unless my $requestee_id = $flag->requestee_id; + my $field = $flag->type->name . '_request_count'; + + # update the current user's object so things are display correctly on the + # post-processing page + my $user = Bugzilla->user; + if ($requestee_id == $user->id) { + $user->{$field} += $add; + } + + # update database directly to avoid creating audit_log entries + $add = $add == -1 ? ' - 1' : ' + 1'; + Bugzilla->dbh->do("UPDATE profiles SET $field = $field $add WHERE userid = ?", + undef, $requestee_id); + Bugzilla->memcached->clear({table => 'profiles', id => $requestee_id}); } # bugzilla's handling of requestee matching when creating bugs is "if it's # wrong, or matches too many, default to empty", which breaks mandatory # reviewer requirements. instead we just throw an error. sub post_bug_attachment_flags { - my ($self, $args) = @_; - $self->_check_review_flag($args); + my ($self, $args) = @_; + $self->_check_review_flag($args); } sub create_attachment_flags { - my ($self, $args) = @_; - $self->_check_review_flag($args); + my ($self, $args) = @_; + $self->_check_review_flag($args); } sub _check_review_flag { - my ($self, $args) = @_; - my ($bug, $attachment) = @$args{qw( bug attachment )}; - my $cgi = Bugzilla->cgi; - - # extract the set flag-types - my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param(); - @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids; - return unless scalar(@flagtype_ids); - - # find valid review flagtypes - my $flag_types = Bugzilla::FlagType::match({ - product_id => $bug->product_id, - component_id => $bug->component_id, - is_active => 1 - }); - foreach my $flag_type (@$flag_types) { - next unless $flag_type->name eq 'review' - && $flag_type->target_type eq 'attachment'; - my $type_id = $flag_type->id; - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || ''); - if ($reviewers eq '' && $bug->product_obj->reviewer_required) { - ThrowUserError('reviewer_required'); - } - - foreach my $reviewer (split(/[,;]+/, $reviewers)) { - # search on the reviewer - my $users = Bugzilla::User::match($reviewer, 2, 1); - - # no matches - if (scalar(@$users) == 0) { - ThrowUserError('user_match_failed', { name => $reviewer }); - } - - # more than one match, throw error - if (scalar(@$users) > 1) { - ThrowUserError('user_match_too_many', { fields => [ 'review' ] }); - } - - # we want to throw an error if the requestee does not have access - # to the bug. bugzilla's default behaviour is to sliently drop the - # requestee, which results in a confusing 'reviewer required' - # error. - # fake it by creating a flag and try to set the requestee. - # bugzilla's flags don't have a normal constructor or property - # setters, so we have to bless it directly then call the internal - # check_requestee method. urgh. - my $flag = bless({ - type_id => $flag_type->id, - status => '?', - bug_id => $bug->id, - attach_id => $attachment->id - }, 'Bugzilla::Flag'); - $flag->_check_requestee($users->[0]->login, $bug, $attachment); - } + my ($self, $args) = @_; + my ($bug, $attachment) = @$args{qw( bug attachment )}; + my $cgi = Bugzilla->cgi; + + # extract the set flag-types + my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param(); + @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids; + return unless scalar(@flagtype_ids); + + # find valid review flagtypes + my $flag_types = Bugzilla::FlagType::match({ + product_id => $bug->product_id, + component_id => $bug->component_id, + is_active => 1 + }); + foreach my $flag_type (@$flag_types) { + next + unless $flag_type->name eq 'review' + && $flag_type->target_type eq 'attachment'; + my $type_id = $flag_type->id; + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || ''); + if ($reviewers eq '' && $bug->product_obj->reviewer_required) { + ThrowUserError('reviewer_required'); } + + foreach my $reviewer (split(/[,;]+/, $reviewers)) { + + # search on the reviewer + my $users = Bugzilla::User::match($reviewer, 2, 1); + + # no matches + if (scalar(@$users) == 0) { + ThrowUserError('user_match_failed', {name => $reviewer}); + } + + # more than one match, throw error + if (scalar(@$users) > 1) { + ThrowUserError('user_match_too_many', {fields => ['review']}); + } + + # we want to throw an error if the requestee does not have access + # to the bug. bugzilla's default behaviour is to sliently drop the + # requestee, which results in a confusing 'reviewer required' + # error. + # fake it by creating a flag and try to set the requestee. + # bugzilla's flags don't have a normal constructor or property + # setters, so we have to bless it directly then call the internal + # check_requestee method. urgh. + my $flag = bless( + { + type_id => $flag_type->id, + status => '?', + bug_id => $bug->id, + attach_id => $attachment->id + }, + 'Bugzilla::Flag' + ); + $flag->_check_requestee($users->[0]->login, $bug, $attachment); + } + } } sub flag_end_of_update { - my ($self, $args) = @_; - my ($object, $old_flags, $new_flags) = @$args{qw(object old_flags new_flags)}; - my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object; - - my (undef, $added) = diff_arrays($old_flags, $new_flags); - foreach my $change (@$added) { - $change =~ s/^[^:]+://; - my $reviewer = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $reviewer = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - - if ($name eq 'review' && $value eq '?') { - if ($reviewer eq '') { - ThrowUserError('reviewer_required') if $bug->product_obj->reviewer_required; - } - else { - my $reviewer_obj = Bugzilla::User->check({ - name => $reviewer, - cache => 1 - }); - - ThrowUserError('reviewer_inactive', { - reviewer => $reviewer_obj, - timeout => Bugzilla->params->{max_reviewer_last_seen} - }) unless $reviewer_obj->is_active; - } - } + my ($self, $args) = @_; + my ($object, $old_flags, $new_flags) = @$args{qw(object old_flags new_flags)}; + my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object; + + my (undef, $added) = diff_arrays($old_flags, $new_flags); + foreach my $change (@$added) { + $change =~ s/^[^:]+://; + my $reviewer = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $reviewer = $1; + } + my ($name, $value) = $change =~ /^(.+)(.)$/; + + if ($name eq 'review' && $value eq '?') { + if ($reviewer eq '') { + ThrowUserError('reviewer_required') if $bug->product_obj->reviewer_required; + } + else { + my $reviewer_obj = Bugzilla::User->check({name => $reviewer, cache => 1}); + + ThrowUserError( + 'reviewer_inactive', + { + reviewer => $reviewer_obj, + timeout => Bugzilla->params->{max_reviewer_last_seen} + } + ) unless $reviewer_obj->is_active; + } } + } } # @@ -640,44 +656,45 @@ sub flag_end_of_update { # sub buglist_columns { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $columns = $args->{columns}; - $columns->{bug_mentor} = { title => 'Mentor' }; - if (Bugzilla->user->id) { - $columns->{bug_mentor}->{name} - = $dbh->sql_group_concat('map_mentors_names.login_name'); - } - else { - $columns->{bug_mentor}->{name} - = $dbh->sql_group_concat('map_mentors_names.realname'); - - } + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + my $columns = $args->{columns}; + $columns->{bug_mentor} = {title => 'Mentor'}; + if (Bugzilla->user->id) { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.login_name'); + } + else { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.realname'); + + } } sub buglist_column_joins { - my ($self, $args) = @_; - my $column_joins = $args->{column_joins}; - $column_joins->{bug_mentor} = { - as => 'map_mentors', - table => 'bug_mentors', - then_to => { - as => 'map_mentors_names', - table => 'profiles', - from => 'map_mentors.user_id', - to => 'userid', - }, + my ($self, $args) = @_; + my $column_joins = $args->{column_joins}; + $column_joins->{bug_mentor} = { + as => 'map_mentors', + table => 'bug_mentors', + then_to => { + as => 'map_mentors_names', + table => 'profiles', + from => 'map_mentors.user_id', + to => 'userid', }, + }, + ; } sub search_operator_field_override { - my ($self, $args) = @_; - my $operators = $args->{operators}; - $operators->{bug_mentor} = { - _non_changed => sub { - Bugzilla::Search::_user_nonchanged(@_) - } - }; + my ($self, $args) = @_; + my $operators = $args->{operators}; + $operators->{bug_mentor} = { + _non_changed => sub { + Bugzilla::Search::_user_nonchanged(@_); + } + }; } # @@ -685,105 +702,103 @@ sub search_operator_field_override { # sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Review} = "Bugzilla::Extension::Review::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Review} = "Bugzilla::Extension::Review::WebService"; } sub user_preferences { - my ($self, $args) = @_; - return unless - $args->{current_tab} eq 'account' - && $args->{save_changes}; - - my $input = Bugzilla->input_params; - my $settings = Bugzilla->user->settings; - - my $value = $input->{block_reviews} ? 'on' : 'off'; - $settings->{block_reviews}->validate_value($value); - $settings->{block_reviews}->set($value); - clear_settings_cache(Bugzilla->user->id); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'account' && $args->{save_changes}; + + my $input = Bugzilla->input_params; + my $settings = Bugzilla->user->settings; + + my $value = $input->{block_reviews} ? 'on' : 'off'; + $settings->{block_reviews}->validate_value($value); + $settings->{block_reviews}->set($value); + clear_settings_cache(Bugzilla->user->id); } sub page_before_template { - my ($self, $args) = @_; - - if ($args->{page_id} eq 'review_suggestions.html') { - $self->review_suggestions_report($args); - } - elsif ($args->{page_id} eq 'review_requests_rebuild.html') { - $self->review_requests_rebuild($args); - } - elsif ($args->{page_id} eq 'review_history.html') { - $self->review_history($args); - } + my ($self, $args) = @_; + + if ($args->{page_id} eq 'review_suggestions.html') { + $self->review_suggestions_report($args); + } + elsif ($args->{page_id} eq 'review_requests_rebuild.html') { + $self->review_requests_rebuild($args); + } + elsif ($args->{page_id} eq 'review_history.html') { + $self->review_history($args); + } } sub review_suggestions_report { - my ($self, $args) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $products = []; - my @products = sort { lc($a->name) cmp lc($b->name) } - @{ Bugzilla->user->get_accessible_products }; - foreach my $product_obj (@products) { - my $has_reviewers = 0; - my $product = { - name => $product_obj->name, - components => [], - reviewers => $product_obj->reviewers_objs(1), - }; - $has_reviewers = scalar @{ $product->{reviewers} }; - - foreach my $component_obj (@{ $product_obj->components }) { - my $component = { - name => $component_obj->name, - reviewers => $component_obj->reviewers_objs(1), - }; - if (@{ $component->{reviewers} }) { - push @{ $product->{components} }, $component; - $has_reviewers = 1; - } - } - - if ($has_reviewers) { - push @$products, $product; - } + my ($self, $args) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $products = []; + my @products = sort { lc($a->name) cmp lc($b->name) } + @{Bugzilla->user->get_accessible_products}; + foreach my $product_obj (@products) { + my $has_reviewers = 0; + my $product = { + name => $product_obj->name, + components => [], + reviewers => $product_obj->reviewers_objs(1), + }; + $has_reviewers = scalar @{$product->{reviewers}}; + + foreach my $component_obj (@{$product_obj->components}) { + my $component = { + name => $component_obj->name, + reviewers => $component_obj->reviewers_objs(1), + }; + if (@{$component->{reviewers}}) { + push @{$product->{components}}, $component; + $has_reviewers = 1; + } + } + + if ($has_reviewers) { + push @$products, $product; } - $args->{vars}->{products} = $products; + } + $args->{vars}->{products} = $products; } sub review_requests_rebuild { - my ($self, $args) = @_; - - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', { group => 'admin', - action => 'run', - object => 'review_requests_rebuild' }); - if (Bugzilla->cgi->param('rebuild')) { - my $processed_users = 0; - rebuild_review_counters(sub { - my ($count, $total) = @_; - $processed_users = $total; - }); - $args->{vars}->{rebuild} = 1; - $args->{vars}->{total} = $processed_users; - } + my ($self, $args) = @_; + + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'run', object => 'review_requests_rebuild'}); + if (Bugzilla->cgi->param('rebuild')) { + my $processed_users = 0; + rebuild_review_counters(sub { + my ($count, $total) = @_; + $processed_users = $total; + }); + $args->{vars}->{rebuild} = 1; + $args->{vars}->{total} = $processed_users; + } } sub review_history { - my ($self, $args) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - Bugzilla::User::match_field({ 'requestee' => { 'type' => 'single' } }); - my $requestee = Bugzilla->input_params->{requestee}; - if ($requestee) { - $args->{vars}{requestee} = Bugzilla::User->check({ name => $requestee, cache => 1 }); - } - else { - $args->{vars}{requestee} = $user; - } + my ($self, $args) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + Bugzilla::User::match_field({'requestee' => {'type' => 'single'}}); + my $requestee = Bugzilla->input_params->{requestee}; + if ($requestee) { + $args->{vars}{requestee} + = Bugzilla::User->check({name => $requestee, cache => 1}); + } + else { + $args->{vars}{requestee} = $user; + } } # @@ -791,282 +806,178 @@ sub review_history { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'product_reviewers'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - display_name => { - TYPE => 'VARCHAR(64)', - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => 0, - }, - ], - INDEXES => [ - product_reviewers_idx => { - FIELDS => [ 'user_id', 'product_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'component_reviewers'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - display_name => { - TYPE => 'VARCHAR(64)', - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => 0, - }, - ], - INDEXES => [ - component_reviewers_idx => { - FIELDS => [ 'user_id', 'component_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - - $args->{'schema'}->{'flag_state_activity'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - - flag_when => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - - type_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - - flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - - setter_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - }, - }, - - requestee_id => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - }, - }, - - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' - } - }, - - attachment_id => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE' - } - }, - - status => { - TYPE => 'CHAR(1)', - NOTNULL => 1, - }, - ], - }; - - $args->{'schema'}->{'bug_mentors'} = { - FIELDS => [ - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - bug_mentors_idx => { - FIELDS => [ 'bug_id', 'user_id' ], - TYPE => 'UNIQUE', - }, - bug_mentors_bug_id_idx => [ 'bug_id' ], - ], - }; - - $args->{'schema'}->{'bug_mentors'} = { - FIELDS => [ - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - bug_mentors_idx => { - FIELDS => [ 'bug_id', 'user_id' ], - TYPE => 'UNIQUE', - }, - bug_mentors_bug_id_idx => [ 'bug_id' ], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'product_reviewers'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + display_name => {TYPE => 'VARCHAR(64)',}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',} + }, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0,}, + ], + INDEXES => [ + product_reviewers_idx => + {FIELDS => ['user_id', 'product_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'component_reviewers'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + display_name => {TYPE => 'VARCHAR(64)',}, + component_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',} + }, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0,}, + ], + INDEXES => [ + component_reviewers_idx => + {FIELDS => ['user_id', 'component_id'], TYPE => 'UNIQUE',}, + ], + }; + + $args->{'schema'}->{'flag_state_activity'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + + flag_when => {TYPE => 'DATETIME', NOTNULL => 1,}, + + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + flag_id => {TYPE => 'INT3', NOTNULL => 1,}, + + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid',}, + }, + + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid',},}, + + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + + attachment_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + + status => {TYPE => 'CHAR(1)', NOTNULL => 1,}, + ], + }; + + $args->{'schema'}->{'bug_mentors'} = { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + bug_mentors_idx => {FIELDS => ['bug_id', 'user_id'], TYPE => 'UNIQUE',}, + bug_mentors_bug_id_idx => ['bug_id'], + ], + }; + + $args->{'schema'}->{'bug_mentors'} = { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + bug_mentors_idx => {FIELDS => ['bug_id', 'user_id'], TYPE => 'UNIQUE',}, + bug_mentors_bug_id_idx => ['bug_id'], + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column( - 'products', - 'reviewer_required', { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' } - ); - $dbh->bz_add_column( - 'profiles', - 'review_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - $dbh->bz_add_column( - 'profiles', - 'feedback_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - $dbh->bz_add_column( - 'profiles', - 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - - my $field = Bugzilla::Field->new({ name => 'bug_mentor' }); - if (!$field) { - Bugzilla::Field->create({ - name => 'bug_mentor', - description => 'Mentor', - mailhead => 1 - }); - } - elsif (!$field->in_new_bugmail) { - $field->set_in_new_bugmail(1); - $field->update(); - } + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('products', 'reviewer_required', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('profiles', 'review_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'feedback_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'needinfo_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + my $field = Bugzilla::Field->new({name => 'bug_mentor'}); + if (!$field) { + Bugzilla::Field->create({ + name => 'bug_mentor', description => 'Mentor', mailhead => 1 + }); + } + elsif (!$field->in_new_bugmail) { + $field->set_in_new_bugmail(1); + $field->update(); + } } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'block_reviews', - options => ['on', 'off'], - default => 'off', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'block_reviews', + options => ['on', 'off'], + default => 'off', + category => 'Reviews and Needinfo' + }); } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'max_reviewer_last_seen', - type => 't', - default => '', - default => 0, - checker => \&check_numeric, + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + { + name => 'max_reviewer_last_seen', + type => 't', + default => '', + default => 0, + checker => \&check_numeric, }; } @@ -1075,46 +986,44 @@ sub config_modify_panels { # sub webservice_user_get { - my ($self, $args) = @_; - my ($webservice, $params, $users) = @$args{qw(webservice params users)}; + my ($self, $args) = @_; + my ($webservice, $params, $users) = @$args{qw(webservice params users)}; - return unless filter_wants($params, 'requests'); + return unless filter_wants($params, 'requests'); - my $ids = [ - map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } - grep { exists $_->{id} } - @$users - ]; + my $ids = [map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } + grep { exists $_->{id} } @$users]; - return unless @$ids; - - my %user_map = map { $_->id => $_ } @{ Bugzilla::User->new_from_list($ids) }; - - foreach my $user (@$users) { - my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; - my $user_obj = $user_map{$id}; - - $user->{requests} = { - review => { - blocked => $webservice->type('boolean', $user_obj->reviews_blocked), - pending => $webservice->type('int', $user_obj->{review_request_count}), - }, - feedback => { - # reviews_blocked includes feedback as well - blocked => $webservice->type('boolean', $user_obj->reviews_blocked), - pending => $webservice->type('int', $user_obj->{feedback_request_count}), - }, - needinfo => { - blocked => $webservice->type('boolean', $user_obj->needinfo_blocked), - pending => $webservice->type('int', $user_obj->{needinfo_request_count}), - }, - }; - } + return unless @$ids; + + my %user_map = map { $_->id => $_ } @{Bugzilla::User->new_from_list($ids)}; + + foreach my $user (@$users) { + my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; + my $user_obj = $user_map{$id}; + + $user->{requests} = { + review => { + blocked => $webservice->type('boolean', $user_obj->reviews_blocked), + pending => $webservice->type('int', $user_obj->{review_request_count}), + }, + feedback => { + + # reviews_blocked includes feedback as well + blocked => $webservice->type('boolean', $user_obj->reviews_blocked), + pending => $webservice->type('int', $user_obj->{feedback_request_count}), + }, + needinfo => { + blocked => $webservice->type('boolean', $user_obj->needinfo_blocked), + pending => $webservice->type('int', $user_obj->{needinfo_request_count}), + }, + }; + } } sub webservice_user_suggest { - my ($self, $args) = @_; - $self->webservice_user_get($args); + my ($self, $args) = @_; + $self->webservice_user_get($args); } __PACKAGE__->NAME; diff --git a/extensions/Review/bin/migrate_mentor_from_whiteboard.pl b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl index debf173a7..19837779e 100755 --- a/extensions/Review/bin/migrate_mentor_from_whiteboard.pl +++ b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl @@ -36,11 +36,11 @@ EOF <>; # we need to be logged in to do user searching and update bugs -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -$nobody->{groups} = [ Bugzilla::Group->get_all ]; +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +$nobody->{groups} = [Bugzilla::Group->get_all]; Bugzilla->set_user($nobody); -my $mentor_field = Bugzilla::Field->check({ name => 'bug_mentor' }); +my $mentor_field = Bugzilla::Field->check({name => 'bug_mentor'}); my $dbh = Bugzilla->dbh; # fix broken migration @@ -54,48 +54,40 @@ my $sth = $dbh->prepare(" $sth->execute($mentor_field->id); my %pair; while (my $row = $sth->fetchrow_hashref) { - if ($row->{added} && $row->{removed}) { - %pair = (); - next; - } - if ($row->{added}) { - $pair{bug_id} = $row->{bug_id}; - $pair{bug_when} = $row->{bug_when}; - $pair{who} = $row->{added}; - next; - } - if (!$pair{bug_id}) { - next; - } - if ($row->{removed}) { - if ($row->{bug_id} == $pair{bug_id} - && $row->{bug_when} eq $pair{bug_when} - && $row->{removed} eq $pair{who}) - { - print "Fixing mentor on bug $row->{bug_id}\n"; - my $user = Bugzilla::User->check({ name => $row->{removed} }); - $dbh->bz_start_transaction; - $dbh->do( - "DELETE FROM bugs_activity WHERE id = ?", - undef, - $row->{id} - ); - my ($exists) = $dbh->selectrow_array( - "SELECT 1 FROM bug_mentors WHERE bug_id = ? AND user_id = ?", - undef, - $row->{bug_id}, $user->id - ); - if (!$exists) { - $dbh->do( - "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", - undef, - $row->{bug_id}, $user->id, - ); - } - $dbh->bz_commit_transaction; - %pair = (); - } + if ($row->{added} && $row->{removed}) { + %pair = (); + next; + } + if ($row->{added}) { + $pair{bug_id} = $row->{bug_id}; + $pair{bug_when} = $row->{bug_when}; + $pair{who} = $row->{added}; + next; + } + if (!$pair{bug_id}) { + next; + } + if ($row->{removed}) { + if ( $row->{bug_id} == $pair{bug_id} + && $row->{bug_when} eq $pair{bug_when} + && $row->{removed} eq $pair{who}) + { + print "Fixing mentor on bug $row->{bug_id}\n"; + my $user = Bugzilla::User->check({name => $row->{removed}}); + $dbh->bz_start_transaction; + $dbh->do("DELETE FROM bugs_activity WHERE id = ?", undef, $row->{id}); + my ($exists) + = $dbh->selectrow_array( + "SELECT 1 FROM bug_mentors WHERE bug_id = ? AND user_id = ?", + undef, $row->{bug_id}, $user->id); + if (!$exists) { + $dbh->do("INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", + undef, $row->{bug_id}, $user->id,); + } + $dbh->bz_commit_transaction; + %pair = (); } + } } # migrate remaining bugs @@ -110,119 +102,95 @@ my $bug_ids = $dbh->selectcol_arrayref(" print "Bugs found: " . scalar(@$bug_ids) . "\n"; my $bugs = Bugzilla::Bug->new_from_list($bug_ids); foreach my $bug (@$bugs) { - my $whiteboard = $bug->status_whiteboard; - my $orig_whiteboard = $whiteboard; - my ($mentors, $errors) = extract_mentors($whiteboard); - - printf "%7s %s\n", $bug->id, $whiteboard; - foreach my $error (@$errors) { - print " $error\n"; + my $whiteboard = $bug->status_whiteboard; + my $orig_whiteboard = $whiteboard; + my ($mentors, $errors) = extract_mentors($whiteboard); + + printf "%7s %s\n", $bug->id, $whiteboard; + foreach my $error (@$errors) { + print " $error\n"; + } + foreach my $user (@$mentors) { + print " Mentor: " . $user->identity . "\n"; + } + next if @$errors; + $whiteboard =~ s/\[mentor=[^\]]+\]//g; + + my $migrated + = $dbh->selectcol_arrayref("SELECT user_id FROM bug_mentors WHERE bug_id = ?", + undef, $bug->id); + if (@$migrated) { + foreach my $migrated_id (@$migrated) { + $mentors = [grep { $_->id != $migrated_id } @$mentors]; } - foreach my $user (@$mentors) { - print " Mentor: " . $user->identity . "\n"; + if (!@$mentors) { + print " mentor(s) already migrated\n"; + next; } - next if @$errors; - $whiteboard =~ s/\[mentor=[^\]]+\]//g; - - my $migrated = $dbh->selectcol_arrayref( - "SELECT user_id FROM bug_mentors WHERE bug_id = ?", - undef, - $bug->id - ); - if (@$migrated) { - foreach my $migrated_id (@$migrated) { - $mentors = [ - grep { $_->id != $migrated_id } - @$mentors - ]; - } - if (!@$mentors) { - print " mentor(s) already migrated\n"; - next; - } - } - - my $delta_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $dbh->bz_start_transaction; - $dbh->do( - "UPDATE bugs SET status_whiteboard=? WHERE bug_id=?", - undef, - $whiteboard, $bug->id - ); - Bugzilla::Bug::LogActivityEntry( - $bug->id, - 'status_whiteboard', - $orig_whiteboard, - $whiteboard, - $nobody->id, - $delta_ts, - ); - foreach my $mentor (@$mentors) { - $dbh->do( - "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", - undef, - $bug->id, $mentor->id, - ); - Bugzilla::Bug::LogActivityEntry( - $bug->id, - 'bug_mentor', - '', - $mentor->login, - $nobody->id, - $delta_ts, - ); - } - $dbh->do( - "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", - undef, - $bug->id, - ); - $dbh->bz_commit_transaction; + } + + my $delta_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $dbh->bz_start_transaction; + $dbh->do("UPDATE bugs SET status_whiteboard=? WHERE bug_id=?", + undef, $whiteboard, $bug->id); + Bugzilla::Bug::LogActivityEntry($bug->id, 'status_whiteboard', + $orig_whiteboard, $whiteboard, $nobody->id, $delta_ts,); + foreach my $mentor (@$mentors) { + $dbh->do("INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", + undef, $bug->id, $mentor->id,); + Bugzilla::Bug::LogActivityEntry($bug->id, 'bug_mentor', '', $mentor->login, + $nobody->id, $delta_ts,); + } + $dbh->do("UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", + undef, $bug->id,); + $dbh->bz_commit_transaction; } sub extract_mentors { - my ($whiteboard) = @_; - - my (@mentors, @errors); - my $logout = 0; - while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) { - my $mentor_string = $1; - $mentor_string =~ s/(^\s+|\s+$)//g; - if ($mentor_string =~ /\@/) { - # assume it's a full username if it contains an @ - my $user = Bugzilla::User->new({ name => $mentor_string }); - if (!$user) { - push @errors, "'$mentor_string' failed to match any users"; - } else { - push @mentors, $user; - } - } else { - # otherwise assume it's a : prefixed nick - - $mentor_string =~ s/^://; - my $matches = find_users(":$mentor_string"); - if (!@$matches) { - $matches = find_users($mentor_string); - } - - if (!$matches || !@$matches) { - push @errors, "'$mentor_string' failed to match any users"; - } elsif (scalar(@$matches) > 1) { - push @errors, "'$mentor_string' matches more than one user: " . - join(', ', map { $_->identity } @$matches); - } else { - push @mentors, $matches->[0]; - } - } + my ($whiteboard) = @_; + + my (@mentors, @errors); + my $logout = 0; + while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) { + my $mentor_string = $1; + $mentor_string =~ s/(^\s+|\s+$)//g; + if ($mentor_string =~ /\@/) { + + # assume it's a full username if it contains an @ + my $user = Bugzilla::User->new({name => $mentor_string}); + if (!$user) { + push @errors, "'$mentor_string' failed to match any users"; + } + else { + push @mentors, $user; + } + } + else { + # otherwise assume it's a : prefixed nick + + $mentor_string =~ s/^://; + my $matches = find_users(":$mentor_string"); + if (!@$matches) { + $matches = find_users($mentor_string); + } + + if (!$matches || !@$matches) { + push @errors, "'$mentor_string' failed to match any users"; + } + elsif (scalar(@$matches) > 1) { + push @errors, "'$mentor_string' matches more than one user: " + . join(', ', map { $_->identity } @$matches); + } + else { + push @mentors, $matches->[0]; + } } - return (\@mentors, \@errors); + } + return (\@mentors, \@errors); } sub find_users { - my ($query) = @_; - my $matches = Bugzilla::User::match("*$query*", 2); - return [ - grep { $_->name =~ /:?\Q$query\E\b/i } - @$matches - ]; + my ($query) = @_; + my $matches = Bugzilla::User::match("*$query*", 2); + return [grep { $_->name =~ /:?\Q$query\E\b/i } @$matches]; } diff --git a/extensions/Review/bin/review_requests_rebuild.pl b/extensions/Review/bin/review_requests_rebuild.pl index 03d25d045..8bda4119c 100755 --- a/extensions/Review/bin/review_requests_rebuild.pl +++ b/extensions/Review/bin/review_requests_rebuild.pl @@ -24,7 +24,7 @@ use Bugzilla::Extension::Review::Util; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); -rebuild_review_counters(sub{ - my ($count, $total) = @_; - indicate_progress({ current => $count, total => $total, every => 5 }); +rebuild_review_counters(sub { + my ($count, $total) = @_; + indicate_progress({current => $count, total => $total, every => 5}); }); diff --git a/extensions/Review/lib/FlagStateActivity.pm b/extensions/Review/lib/FlagStateActivity.pm index 35da42351..92efb6c02 100644 --- a/extensions/Review/lib/FlagStateActivity.pm +++ b/extensions/Review/lib/FlagStateActivity.pm @@ -24,65 +24,58 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - flag_when - type_id - flag_id - setter_id - requestee_id - bug_id - attachment_id - status + id + flag_when + type_id + flag_id + setter_id + requestee_id + bug_id + attachment_id + status ); sub _check_param_required { - my ($param) = @_; - - return sub { - my ($invocant, $value) = @_; - $value = trim($value) - or ThrowCodeError('param_required', {param => $param}); - return $value; - }, + my ($param) = @_; + + return sub { + my ($invocant, $value) = @_; + $value = trim($value) or ThrowCodeError('param_required', {param => $param}); + return $value; + },; } sub _check_date { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - $date = trim($date); - datetime_from($date) - or ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD HH24:MI:SS' }); - return $date; + $date = trim($date); + datetime_from($date) + or ThrowUserError('illegal_date', + {date => $date, format => 'YYYY-MM-DD HH24:MI:SS'}); + return $date; } sub _check_status { - my ($self, $status) = @_; - - # - Make sure the status is valid. - # - Make sure the user didn't request the flag unless it's requestable. - # If the flag existed and was requested before it became unrequestable, - # leave it as is. - if (none { $status eq $_ } qw( X + - ? )) { - ThrowUserError( - 'flag_status_invalid', - { - id => $self->id, - status => $status - } - ); - } - return $status; + my ($self, $status) = @_; + + # - Make sure the status is valid. + # - Make sure the user didn't request the flag unless it's requestable. + # If the flag existed and was requested before it became unrequestable, + # leave it as is. + if (none { $status eq $_ } qw( X + - ? )) { + ThrowUserError('flag_status_invalid', {id => $self->id, status => $status}); + } + return $status; } use constant VALIDATORS => { - flag_when => \&_check_date, - type_id => _check_param_required('type_id'), - flag_id => _check_param_required('flag_id'), - setter_id => _check_param_required('setter_id'), - bug_id => _check_param_required('bug_id'), - status => \&_check_status, + flag_when => \&_check_date, + type_id => _check_param_required('type_id'), + flag_id => _check_param_required('flag_id'), + setter_id => _check_param_required('setter_id'), + bug_id => _check_param_required('bug_id'), + status => \&_check_status, }; sub flag_when { return $_[0]->{flag_when} } @@ -95,30 +88,33 @@ sub attachment_id { return $_[0]->{attachment_id} } sub status { return $_[0]->{status} } sub type { - my ($self) = @_; - return $self->{type} //= Bugzilla::FlagType->new({ id => $self->type_id, cache => 1 }); + my ($self) = @_; + return $self->{type} + //= Bugzilla::FlagType->new({id => $self->type_id, cache => 1}); } sub setter { - my ($self) = @_; - return $self->{setter} //= Bugzilla::User->new({ id => $self->setter_id, cache => 1 }); + my ($self) = @_; + return $self->{setter} + //= Bugzilla::User->new({id => $self->setter_id, cache => 1}); } sub requestee { - my ($self) = @_; - return undef unless defined $self->requestee_id; - return $self->{requestee} //= Bugzilla::User->new({ id => $self->requestee_id, cache => 1 }); + my ($self) = @_; + return undef unless defined $self->requestee_id; + return $self->{requestee} + //= Bugzilla::User->new({id => $self->requestee_id, cache => 1}); } sub bug { - my ($self) = @_; - return $self->{bug} //= Bugzilla::Bug->new({ id => $self->bug_id, cache => 1 }); + my ($self) = @_; + return $self->{bug} //= Bugzilla::Bug->new({id => $self->bug_id, cache => 1}); } sub attachment { - my ($self) = @_; - return $self->{attachment} //= - Bugzilla::Attachment->new({ id => $self->attachment_id, cache => 1 }); + my ($self) = @_; + return $self->{attachment} + //= Bugzilla::Attachment->new({id => $self->attachment_id, cache => 1}); } 1; diff --git a/extensions/Review/lib/Util.pm b/extensions/Review/lib/Util.pm index a8744079d..61d4e9117 100644 --- a/extensions/Review/lib/Util.pm +++ b/extensions/Review/lib/Util.pm @@ -17,12 +17,12 @@ use Bugzilla; our @EXPORT = qw( rebuild_review_counters ); sub rebuild_review_counters { - my ($callback) = @_; - my $dbh = Bugzilla->dbh; + my ($callback) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT flags.requestee_id AS user_id, flagtypes.name AS flagtype, COUNT(*) as count @@ -32,55 +32,48 @@ sub rebuild_review_counters { WHERE flags.status = '?' AND flagtypes.name IN ('review', 'feedback', 'needinfo') GROUP BY flags.requestee_id, flagtypes.name - ", { Slice => {} }); + ", {Slice => {}}); - my ($count, $total, $current) = (1, scalar(@$rows), { id => 0 }); - foreach my $row (@$rows) { - $callback->($count++, $total) if $callback; - if ($row->{user_id} != $current->{id}) { - _update_profile($dbh, $current) if $current->{id}; - $current = { id => $row->{user_id} }; - } - $current->{$row->{flagtype}} = $row->{count}; + my ($count, $total, $current) = (1, scalar(@$rows), {id => 0}); + foreach my $row (@$rows) { + $callback->($count++, $total) if $callback; + if ($row->{user_id} != $current->{id}) { + _update_profile($dbh, $current) if $current->{id}; + $current = {id => $row->{user_id}}; } - _update_profile($dbh, $current) if $current->{id}; + $current->{$row->{flagtype}} = $row->{count}; + } + _update_profile($dbh, $current) if $current->{id}; - foreach my $field (qw( review feedback needinfo )) { - _fix_negatives($dbh, $field); - } + foreach my $field (qw( review feedback needinfo )) { + _fix_negatives($dbh, $field); + } - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } sub _fix_negatives { - my ($dbh, $field) = @_; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT userid FROM profiles WHERE ${field}_request_count < 0" - ); - return unless @$user_ids; - $dbh->do( - "UPDATE profiles SET ${field}_request_count = 0 WHERE " . $dbh->sql_in('userid', $user_ids) - ); - foreach my $user_id (@$user_ids) { - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); - } + my ($dbh, $field) = @_; + my $user_ids = $dbh->selectcol_arrayref( + "SELECT userid FROM profiles WHERE ${field}_request_count < 0"); + return unless @$user_ids; + $dbh->do("UPDATE profiles SET ${field}_request_count = 0 WHERE " + . $dbh->sql_in('userid', $user_ids)); + foreach my $user_id (@$user_ids) { + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); + } } sub _update_profile { - my ($dbh, $data) = @_; - $dbh->do(" + my ($dbh, $data) = @_; + $dbh->do(" UPDATE profiles SET review_request_count = ?, feedback_request_count = ?, needinfo_request_count = ? - WHERE userid = ?", - undef, - $data->{review} || 0, - $data->{feedback} || 0, - $data->{needinfo} || 0, - $data->{id} - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $data->{id} }); + WHERE userid = ?", undef, $data->{review} || 0, $data->{feedback} || 0, + $data->{needinfo} || 0, $data->{id}); + Bugzilla->memcached->clear({table => 'profiles', id => $data->{id}}); } 1; diff --git a/extensions/Review/lib/WebService.pm b/extensions/Review/lib/WebService.pm index 0c54d725a..79843cf2c 100644 --- a/extensions/Review/lib/WebService.pm +++ b/extensions/Review/lib/WebService.pm @@ -20,277 +20,295 @@ use Bugzilla::Util qw(detaint_natural trick_taint); use Bugzilla::WebService::Util 'filter'; use constant PUBLIC_METHODS => qw( - flag_activity - suggestions + flag_activity + suggestions ); sub suggestions { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - - my ($bug, $product, $component); - if (exists $params->{bug_id}) { - $bug = Bugzilla::Bug->check($params->{bug_id}); - $product = $bug->product_obj; - $component = $bug->component_obj; - } - elsif (exists $params->{product}) { - $product = Bugzilla::Product->check($params->{product}); - if (exists $params->{component}) { - $component = Bugzilla::Component->check({ - product => $product, name => $params->{component} - }); - } - } - else { - ThrowUserError("reviewer_suggestions_param_required"); + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my ($bug, $product, $component); + if (exists $params->{bug_id}) { + $bug = Bugzilla::Bug->check($params->{bug_id}); + $product = $bug->product_obj; + $component = $bug->component_obj; + } + elsif (exists $params->{product}) { + $product = Bugzilla::Product->check($params->{product}); + if (exists $params->{component}) { + $component + = Bugzilla::Component->check({ + product => $product, name => $params->{component} + }); } + } + else { + ThrowUserError("reviewer_suggestions_param_required"); + } - my @reviewers; - if ($bug) { - # we always need to be authentiated to perform user matching - my $user = Bugzilla->user; - if (!$user->id) { - Bugzilla->set_user(Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} })); - push @reviewers, @{ $bug->mentors }; - Bugzilla->set_user($user); - } else { - push @reviewers, @{ $bug->mentors }; - } - } - if ($component) { - push @reviewers, @{ $component->reviewers_objs }; - } - if (!$component || !@{ $component->reviewers_objs }) { - push @reviewers, @{ $product->reviewers_objs }; - } + my @reviewers; + if ($bug) { - my @result; - foreach my $reviewer (@reviewers) { - push @result, { - id => $self->type('int', $reviewer->id), - email => $self->type('email', $reviewer->login), - name => $self->type('string', $reviewer->name), - review_count => $self->type('int', $reviewer->review_count), - }; + # we always need to be authentiated to perform user matching + my $user = Bugzilla->user; + if (!$user->id) { + Bugzilla->set_user(Bugzilla::User->check( + {name => Bugzilla->params->{'nobody_user'}})); + push @reviewers, @{$bug->mentors}; + Bugzilla->set_user($user); + } + else { + push @reviewers, @{$bug->mentors}; } - return \@result; + } + if ($component) { + push @reviewers, @{$component->reviewers_objs}; + } + if (!$component || !@{$component->reviewers_objs}) { + push @reviewers, @{$product->reviewers_objs}; + } + + my @result; + foreach my $reviewer (@reviewers) { + push @result, + { + id => $self->type('int', $reviewer->id), + email => $self->type('email', $reviewer->login), + name => $self->type('string', $reviewer->name), + review_count => $self->type('int', $reviewer->review_count), + }; + } + return \@result; } sub flag_activity { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my %match_criteria; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my %match_criteria; - if (my $flag_id = $params->{flag_id}) { - detaint_natural($flag_id) - or ThrowUserError('invalid_flag_id', { flag_id => $flag_id }); + if (my $flag_id = $params->{flag_id}) { + detaint_natural($flag_id) + or ThrowUserError('invalid_flag_id', {flag_id => $flag_id}); - $match_criteria{flag_id} = $flag_id; - } - - if (my $flag_ids = $params->{flag_ids}) { - foreach my $flag_id (@$flag_ids) { - detaint_natural($flag_id) - or ThrowUserError('invalid_flag_id', { flag_id => $flag_id }); - } + $match_criteria{flag_id} = $flag_id; + } - $match_criteria{flag_id} = $flag_ids; + if (my $flag_ids = $params->{flag_ids}) { + foreach my $flag_id (@$flag_ids) { + detaint_natural($flag_id) + or ThrowUserError('invalid_flag_id', {flag_id => $flag_id}); } - if (my $type_id = $params->{type_id}) { - detaint_natural($type_id) - or ThrowUserError('invalid_flag_type_id', { type_id => $type_id }); + $match_criteria{flag_id} = $flag_ids; + } - $match_criteria{type_id} = $type_id; - } + if (my $type_id = $params->{type_id}) { + detaint_natural($type_id) + or ThrowUserError('invalid_flag_type_id', {type_id => $type_id}); - if (my $type_name = $params->{type_name}) { - trick_taint($type_name); - my $flag_types = Bugzilla::FlagType::match({ name => $type_name }); - $match_criteria{type_id} = [map { $_->id } @$flag_types]; - } + $match_criteria{type_id} = $type_id; + } - foreach my $user_field (qw( requestee setter )) { - if (my $user_name = $params->{$user_field}) { - my $user = Bugzilla::User->check({ name => $user_name, cache => 1, _error => 'invalid_username' }); + if (my $type_name = $params->{type_name}) { + trick_taint($type_name); + my $flag_types = Bugzilla::FlagType::match({name => $type_name}); + $match_criteria{type_id} = [map { $_->id } @$flag_types]; + } - $match_criteria{ $user_field . "_id" } = $user->id; - } - } + foreach my $user_field (qw( requestee setter )) { + if (my $user_name = $params->{$user_field}) { + my $user = Bugzilla::User->check( + {name => $user_name, cache => 1, _error => 'invalid_username'}); - foreach my $field (qw( bug_id status )) { - if (exists $params->{$field}) { - $match_criteria{$field} = $params->{$field}; - } + $match_criteria{$user_field . "_id"} = $user->id; } + } - ThrowCodeError('param_required', { param => 'limit', function => 'Review.flag_activity()' }) - if defined $params->{offset} && !defined $params->{limit}; - - my $limit = delete $params->{limit}; - my $offset = delete $params->{offset}; - my $after = delete $params->{after}; - my $before = delete $params->{before}; - my $max_results = Bugzilla->params->{max_search_results}; - - if (!$limit || $limit > $max_results) { - $limit = $max_results; + foreach my $field (qw( bug_id status )) { + if (exists $params->{$field}) { + $match_criteria{$field} = $params->{$field}; } - - if ($after && $after =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { - $after = $1; - } - else { - my $now = DateTime->now; - $now->subtract(days => 30); - $after = $now->ymd('-'); - } - - if ($before && $before =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { - $before = $1; - } - else { - my $now = DateTime->now; - $before = $now->ymd('-'); - } - - $match_criteria{LIMIT} = $limit; - $match_criteria{OFFSET} = $offset if defined $offset; - $match_criteria{WHERE} = { 'date(flag_when) BETWEEN ? AND ?' => [$after, $before] }; - - # Throw error if no other parameters have been passed other than limit and offset - if (!grep(!/^(LIMIT|OFFSET)$/, keys %match_criteria)) { - ThrowUserError('flag_activity_parameters_required'); - } - - my $matches = Bugzilla::Extension::Review::FlagStateActivity->match(\%match_criteria); - my $user = Bugzilla->user; - $user->visible_bugs([ map { $_->bug_id } @$matches ]); - my @results = map { $self->_flag_state_activity_to_hash($_, $params) } - grep { $user->can_see_bug($_->bug_id) && _can_see_attachment($user, $_) } - @$matches; - return \@results; + } + + ThrowCodeError('param_required', + {param => 'limit', function => 'Review.flag_activity()'}) + if defined $params->{offset} && !defined $params->{limit}; + + my $limit = delete $params->{limit}; + my $offset = delete $params->{offset}; + my $after = delete $params->{after}; + my $before = delete $params->{before}; + my $max_results = Bugzilla->params->{max_search_results}; + + if (!$limit || $limit > $max_results) { + $limit = $max_results; + } + + if ($after && $after =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { + $after = $1; + } + else { + my $now = DateTime->now; + $now->subtract(days => 30); + $after = $now->ymd('-'); + } + + if ($before && $before =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { + $before = $1; + } + else { + my $now = DateTime->now; + $before = $now->ymd('-'); + } + + $match_criteria{LIMIT} = $limit; + $match_criteria{OFFSET} = $offset if defined $offset; + $match_criteria{WHERE} + = {'date(flag_when) BETWEEN ? AND ?' => [$after, $before]}; + + # Throw error if no other parameters have been passed other than limit and offset + if (!grep(!/^(LIMIT|OFFSET)$/, keys %match_criteria)) { + ThrowUserError('flag_activity_parameters_required'); + } + + my $matches + = Bugzilla::Extension::Review::FlagStateActivity->match(\%match_criteria); + my $user = Bugzilla->user; + $user->visible_bugs([map { $_->bug_id } @$matches]); + my @results + = map { $self->_flag_state_activity_to_hash($_, $params) } + grep { $user->can_see_bug($_->bug_id) && _can_see_attachment($user, $_) } + @$matches; + return \@results; } sub _can_see_attachment { - my ($user, $flag_state_activity) = @_; + my ($user, $flag_state_activity) = @_; - return 1 if !$flag_state_activity->attachment_id; - return 0 if $flag_state_activity->attachment->isprivate && !$user->is_insider; - return 1; + return 1 if !$flag_state_activity->attachment_id; + return 0 if $flag_state_activity->attachment->isprivate && !$user->is_insider; + return 1; } sub rest_resources { - return [ - # bug-id - qr{^/review/suggestions/(\d+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { bug_id => $_[0] }; - }, - }, - }, - # product/component - qr{^/review/suggestions/([^/]+)/(.+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { product => $_[0], component => $_[1] }; - }, - }, + return [ + # bug-id + qr{^/review/suggestions/(\d+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {bug_id => $_[0]}; }, - # just product - qr{^/review/suggestions/([^/]+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { product => $_[0] }; - }, - }, + }, + }, + + # product/component + qr{^/review/suggestions/([^/]+)/(.+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {product => $_[0], component => $_[1]}; }, - # named parameters - qr{^/review/suggestions$}, { - GET => { - method => 'suggestions', - }, + }, + }, + + # just product + qr{^/review/suggestions/([^/]+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {product => $_[0]}; }, - # flag activity by flag id - qr{^/review/flag_activity/(\d+)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { flag_id => $_[0] } - }, - }, + }, + }, + + # named parameters + qr{^/review/suggestions$}, + {GET => {method => 'suggestions',},}, + + # flag activity by flag id + qr{^/review/flag_activity/(\d+)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {flag_id => $_[0]}; }, - qr{^/review/flag_activity/type_name/(\w+)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { type_name => $_[0] } - }, - }, + }, + }, + qr{^/review/flag_activity/type_name/(\w+)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {type_name => $_[0]}; }, - # flag activity by user - qr{^/review/flag_activity/(requestee|setter|type_id)/(.*)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { $_[0] => $_[1] }; - }, - }, + }, + }, + + # flag activity by user + qr{^/review/flag_activity/(requestee|setter|type_id)/(.*)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {$_[0] => $_[1]}; }, - # flag activity with only query strings - qr{^/review/flag_activity$}, { - GET => { method => 'flag_activity' }, - }, - ]; + }, + }, + + # flag activity with only query strings + qr{^/review/flag_activity$}, + {GET => {method => 'flag_activity'},}, + ]; } sub _flag_state_activity_to_hash { - my ($self, $fsa, $params) = @_; - - my %flag = ( - id => $self->type('int', $fsa->id), - creation_time => $self->type('string', $fsa->flag_when), - type => $self->_flagtype_to_hash($fsa->type), - setter => $self->_user_to_hash($fsa->setter), - bug_id => $self->type('int', $fsa->bug_id), - attachment_id => $self->type('int', $fsa->attachment_id), - status => $self->type('string', $fsa->status), - ); - - $flag{requestee} = $self->_user_to_hash($fsa->requestee) if $fsa->requestee; - $flag{flag_id} = $self->type('int', $fsa->flag_id) unless $params->{flag_id}; - - return filter($params, \%flag); + my ($self, $fsa, $params) = @_; + + my %flag = ( + id => $self->type('int', $fsa->id), + creation_time => $self->type('string', $fsa->flag_when), + type => $self->_flagtype_to_hash($fsa->type), + setter => $self->_user_to_hash($fsa->setter), + bug_id => $self->type('int', $fsa->bug_id), + attachment_id => $self->type('int', $fsa->attachment_id), + status => $self->type('string', $fsa->status), + ); + + $flag{requestee} = $self->_user_to_hash($fsa->requestee) if $fsa->requestee; + $flag{flag_id} = $self->type('int', $fsa->flag_id) unless $params->{flag_id}; + + return filter($params, \%flag); } sub _flagtype_to_hash { - my ($self, $flagtype) = @_; - my $user = Bugzilla->user; - - return { - id => $self->type('int', $flagtype->id), - name => $self->type('string', $flagtype->name), - description => $self->type('string', $flagtype->description), - type => $self->type('string', $flagtype->target_type), - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), - }; + my ($self, $flagtype) = @_; + my $user = Bugzilla->user; + + return { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), + }; } sub _user_to_hash { - my ($self, $user) = @_; + my ($self, $user) = @_; - return { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - }; + return { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + }; } 1; diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm index 8d877a253..5c2dc615a 100644 --- a/extensions/SecureMail/Config.pm +++ b/extensions/SecureMail/Config.pm @@ -28,25 +28,19 @@ use warnings; use constant NAME => 'SecureMail'; use constant REQUIRED_MODULES => [ - { - package => 'Crypt-OpenPGP', - module => 'Crypt::OpenPGP', - # 1.02 added the ability for new() to take KeyRing objects for the - # PubRing argument. - version => '1.02', - # 1.04 hangs - https://rt.cpan.org/Public/Bug/Display.html?id=68018 - # blacklist => [ '1.04' ], - }, - { - package => 'Crypt-SMIME', - module => 'Crypt::SMIME', - version => 0, - }, - { - package => 'HTML-Tree', - module => 'HTML::Tree', - version => 0, - } + { + package => 'Crypt-OpenPGP', + module => 'Crypt::OpenPGP', + + # 1.02 added the ability for new() to take KeyRing objects for the + # PubRing argument. + version => '1.02', + + # 1.04 hangs - https://rt.cpan.org/Public/Bug/Display.html?id=68018 + # blacklist => [ '1.04' ], + }, + {package => 'Crypt-SMIME', module => 'Crypt::SMIME', version => 0,}, + {package => 'HTML-Tree', module => 'HTML::Tree', version => 0,} ]; __PACKAGE__->NAME; diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm index 2b5e1bdd6..9790c0828 100644 --- a/extensions/SecureMail/Extension.pm +++ b/extensions/SecureMail/Extension.pm @@ -59,12 +59,12 @@ use constant SECURE_ALL => 2; # public_key text in the 'profiles' table - stores public key ############################################################################## sub install_update_db { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('groups', 'secure_mail', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('profiles', 'public_key', { TYPE => 'LONGTEXT' }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('groups', 'secure_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'public_key', {TYPE => 'LONGTEXT'}); } ############################################################################## @@ -72,194 +72,193 @@ sub install_update_db { ############################################################################## BEGIN { - *Bugzilla::Group::secure_mail = \&_group_secure_mail; - *Bugzilla::User::public_key = \&_user_public_key; - *Bugzilla::securemail_groups = \&_securemail_groups; + *Bugzilla::Group::secure_mail = \&_group_secure_mail; + *Bugzilla::User::public_key = \&_user_public_key; + *Bugzilla::securemail_groups = \&_securemail_groups; } sub _group_secure_mail { return $_[0]->{'secure_mail'}; } sub _securemail_groups { - return Bugzilla->dbh->selectcol_arrayref("SELECT name FROM groups WHERE secure_mail = 1") // []; + return Bugzilla->dbh->selectcol_arrayref( + "SELECT name FROM groups WHERE secure_mail = 1") // []; } # We want to lazy-load the public_key. sub _user_public_key { - my $self = shift; - if (!exists $self->{public_key}) { - ($self->{public_key}) = Bugzilla->dbh->selectrow_array( - "SELECT public_key FROM profiles WHERE userid = ?", - undef, - $self->id - ); - } - return $self->{public_key}; + my $self = shift; + if (!exists $self->{public_key}) { + ($self->{public_key}) + = Bugzilla->dbh->selectrow_array( + "SELECT public_key FROM profiles WHERE userid = ?", + undef, $self->id); + } + return $self->{public_key}; } # Make sure generic functions know about the additional fields in the user # and group objects. sub object_columns { - my ($self, $args) = @_; - my $class = $args->{'class'}; - my $columns = $args->{'columns'}; - - if ($class->isa('Bugzilla::Group')) { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info($class->DB_TABLE, 'secure_mail')) { - push @$columns, 'secure_mail'; - } + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $columns = $args->{'columns'}; + + if ($class->isa('Bugzilla::Group')) { + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info($class->DB_TABLE, 'secure_mail')) { + push @$columns, 'secure_mail'; } + } } # Plug appropriate validators so we can check the validity of the two # fields created by this extension, when new values are submitted. sub object_validators { - my ($self, $args) = @_; - my %args = %{ $args }; - my ($invocant, $validators) = @args{qw(class validators)}; + my ($self, $args) = @_; + my %args = %{$args}; + my ($invocant, $validators) = @args{qw(class validators)}; - if ($invocant->isa('Bugzilla::Group')) { - $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean; - } - elsif ($invocant->isa('Bugzilla::User')) { - $validators->{'public_key'} = sub { - my ($self, $value) = @_; - $value = trim($value) || ''; - - return $value if $value eq ''; - - if ($value =~ /PUBLIC KEY/) { - # PGP keys must be ASCII-armoured. - my $tct = Bugzilla::Extension::SecureMail::TCT->new( - public_key => $value, - command => Bugzilla->localconfig->{tct_bin}, - ); - unless ($tct->is_valid->get) { - ThrowUserError( 'securemail_invalid_key', { errstr => 'key is invalid or expired' } ); - } - } - elsif ($value =~ /BEGIN CERTIFICATE/) { - # S/MIME Keys must be in PEM format (Base64-encoded X.509) - # - # Crypt::SMIME seems not to like tainted values - it claims - # they aren't scalars! - trick_taint($value); - - my $smime = Crypt::SMIME->new(); - eval { - $smime->setPublicKey([$value]); - }; - if ($@) { - ThrowUserError('securemail_invalid_key', - { errstr => $@ }); - } - } - else { - ThrowUserError('securemail_invalid_key'); - } - - return $value; - }; - } + if ($invocant->isa('Bugzilla::Group')) { + $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean; + } + elsif ($invocant->isa('Bugzilla::User')) { + $validators->{'public_key'} = sub { + my ($self, $value) = @_; + $value = trim($value) || ''; + + return $value if $value eq ''; + + if ($value =~ /PUBLIC KEY/) { + + # PGP keys must be ASCII-armoured. + my $tct = Bugzilla::Extension::SecureMail::TCT->new( + public_key => $value, + command => Bugzilla->localconfig->{tct_bin}, + ); + unless ($tct->is_valid->get) { + ThrowUserError('securemail_invalid_key', + {errstr => 'key is invalid or expired'}); + } + } + elsif ($value =~ /BEGIN CERTIFICATE/) { + + # S/MIME Keys must be in PEM format (Base64-encoded X.509) + # + # Crypt::SMIME seems not to like tainted values - it claims + # they aren't scalars! + trick_taint($value); + + my $smime = Crypt::SMIME->new(); + eval { $smime->setPublicKey([$value]); }; + if ($@) { + ThrowUserError('securemail_invalid_key', {errstr => $@}); + } + } + else { + ThrowUserError('securemail_invalid_key'); + } + + return $value; + }; + } } # When creating a 'group' object, set up the secure_mail field appropriately. sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{'class'}; - my $params = $args->{'params'}; + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $params = $args->{'params'}; - if ($class->isa('Bugzilla::Group')) { - $params->{secure_mail} = Bugzilla->cgi->param('secure_mail'); - } + if ($class->isa('Bugzilla::Group')) { + $params->{secure_mail} = Bugzilla->cgi->param('secure_mail'); + } } # On update, make sure the updating process knows about our new columns. sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{'object'}; - my $columns = $args->{'columns'}; + my ($self, $args) = @_; + my $object = $args->{'object'}; + my $columns = $args->{'columns'}; - if ($object->isa('Bugzilla::Group')) { - # This seems like a convenient moment to extract this value... - $object->set('secure_mail', Bugzilla->cgi->param('secure_mail')); + if ($object->isa('Bugzilla::Group')) { - push(@$columns, 'secure_mail'); - } - elsif ($object->isa('Bugzilla::User')) { - push(@$columns, 'public_key'); - } + # This seems like a convenient moment to extract this value... + $object->set('secure_mail', Bugzilla->cgi->param('secure_mail')); + + push(@$columns, 'secure_mail'); + } + elsif ($object->isa('Bugzilla::User')) { + push(@$columns, 'public_key'); + } } # Handle the setting and changing of the public key. sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; - my $params = Bugzilla->input_params; - - return unless $tab eq 'securemail'; - - # Create a new user object so we don't mess with the main one, as we - # don't know where it's been... - my $user = new Bugzilla::User(Bugzilla->user->id); - - if ($save) { - $user->set('public_key', $params->{'public_key'}); - $user->update(); - - # Send user a test email - if ($user->public_key) { - _send_test_email($user); - $vars->{'test_email_sent'} = 1; - } + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + my $params = Bugzilla->input_params; + + return unless $tab eq 'securemail'; + + # Create a new user object so we don't mess with the main one, as we + # don't know where it's been... + my $user = new Bugzilla::User(Bugzilla->user->id); + + if ($save) { + $user->set('public_key', $params->{'public_key'}); + $user->update(); + + # Send user a test email + if ($user->public_key) { + _send_test_email($user); + $vars->{'test_email_sent'} = 1; } + } - $vars->{'public_key'} = $user->public_key; + $vars->{'public_key'} = $user->public_key; - # Set the 'handled' scalar reference to true so that the caller - # knows the panel name is valid and that an extension took care of it. - $$handled = 1; + # Set the 'handled' scalar reference to true so that the caller + # knows the panel name is valid and that an extension took care of it. + $$handled = 1; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - # Bug dependency emails contain the subject of the dependent bug - # right before the diffs when a status has gone from open/closed - # or closed/open. We need to sanitize the subject of change.blocker - # similar to how we do referenced bugs - return unless - $file eq 'email/bugmail.html.tmpl' - || $file eq 'email/bugmail.txt.tmpl'; - - if (defined $vars->{diffs}) { - foreach my $change (@{ $vars->{diffs} }) { - next if !defined $change->{blocker}; - if (grep($_->secure_mail, @{ $change->{blocker}->groups_in })) { - $change->{blocker}->{short_desc} = "(Secure bug)"; - } - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + # Bug dependency emails contain the subject of the dependent bug + # right before the diffs when a status has gone from open/closed + # or closed/open. We need to sanitize the subject of change.blocker + # similar to how we do referenced bugs + return + unless $file eq 'email/bugmail.html.tmpl' + || $file eq 'email/bugmail.txt.tmpl'; + + if (defined $vars->{diffs}) { + foreach my $change (@{$vars->{diffs}}) { + next if !defined $change->{blocker}; + if (grep($_->secure_mail, @{$change->{blocker}->groups_in})) { + $change->{blocker}->{short_desc} = "(Secure bug)"; + } } + } } sub _send_test_email { - my ($user) = @_; - my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); + my ($user) = @_; + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); - my $vars = { - to_user => $user->email, - }; + my $vars = {to_user => $user->email,}; - my $msg = ""; - $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + my $msg = ""; + $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); } ############################################################################## @@ -268,365 +267,382 @@ sub _send_test_email { # determine if the bug should be encrypted at the time it is generated sub bugmail_enqueue { - my ($self, $args) = @_; - my $vars = $args->{vars}; - if (_should_secure_bug($vars->{bug})) { - $vars->{bugzilla_encrypt} = 1; - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + if (_should_secure_bug($vars->{bug})) { + $vars->{bugzilla_encrypt} = 1; + } } sub bugmail_generate { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $email = $args->{email}; - if ($vars->{bugzilla_encrypt}) { - $email->header_set('X-Bugzilla-Encrypt', 1); - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $email = $args->{email}; + if ($vars->{bugzilla_encrypt}) { + $email->header_set('X-Bugzilla-Encrypt', 1); + } } sub mailer_before_send { - my ($self, $args) = @_; - - my $email = $args->{'email'}; - my $body = $email->body; - - # Decide whether to make secure. - # This is a bit of a hack; it would be nice if it were more clear - # what sort a particular email is. - my $is_bugmail = $email->header('X-Bugzilla-Status') || - $email->header('X-Bugzilla-Type') eq 'request'; - my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s); - my $is_test_email = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0; - my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0; - my $encrypt_header = $email->header('X-Bugzilla-Encrypt') ? 1 : 0; - - if ($is_bugmail - || $is_passwordmail - || $is_test_email - || $is_whine_email - || $encrypt_header - ) { - # Convert the email's To address into a User object - my $login = $email->header('To'); - my $emailsuffix = Bugzilla->params->{'emailsuffix'}; - $login =~ s/$emailsuffix$//; - my $user = new Bugzilla::User({ name => $login }); - - # Default to secure. (Of course, this means if this extension has a - # bug, lots of people are going to get bugmail falsely claiming their - # bugs are secure and they need to add a key...) - my $make_secure = SECURE_ALL; - - if ($is_bugmail) { - # This is also a bit of a hack, but there's no header with the - # bug ID in. So we take the first number in the subject. - my ($bug_id) = ($email->header('Subject') =~ /\[\D+(\d+)\]/); - my $bug = new Bugzilla::Bug($bug_id); - if (!_should_secure_bug($bug)) { - $make_secure = SECURE_NONE; - } - # If the insider group has securemail enabled.. - my $insider_group = Bugzilla::Group->new({ name => Bugzilla->params->{'insidergroup'} }); - if ($insider_group - && $insider_group->secure_mail - && $make_secure == SECURE_NONE) - { - my $comment_is_private = Bugzilla->dbh->selectcol_arrayref( - "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when", - undef, $bug_id); - # Encrypt if there are private comments on an otherwise public bug - if (scalar $email->parts > 1) { - $email->walk_parts(sub { - my $part = shift; - my $content_type = $part->content_type; - $body = $part->body if $content_type && $content_type =~ /^text\/plain/; - }); - } - while ($body =~ /[\r\n]--- Comment #(\d+)/g) { - my $comment_number = $1; - if ($comment_number && $comment_is_private->[$comment_number]) { - $make_secure = SECURE_BODY; - last; - } - } - # Encrypt if updating a private attachment without a comment - if ($email->header('X-Bugzilla-Changed-Fields') - && $email->header('X-Bugzilla-Attach-ID')) - { - my $attachment = Bugzilla::Attachment->new($email->header('X-Bugzilla-Attach-ID')); - if ($attachment && $attachment->isprivate) { - $make_secure = SECURE_BODY; - } - } - } - } - elsif ($is_passwordmail) { - # Mail is made unsecure only if the user does not have a public - # key and is not in any security groups. So specifying a public - # key OR being in a security group means the mail is kept secure - # (but, as noted above, the check is the other way around because - # we default to secure). - if ($user && - !$user->public_key && - !grep($_->secure_mail, @{ $user->groups })) - { - $make_secure = SECURE_NONE; - } - } - elsif ($is_whine_email) { - # When a whine email has one or more secure bugs in the body, then - # encrypt the entire email body. Subject can be left alone as it - # comes from the whine settings. - $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE; + my ($self, $args) = @_; + + my $email = $args->{'email'}; + my $body = $email->body; + + # Decide whether to make secure. + # This is a bit of a hack; it would be nice if it were more clear + # what sort a particular email is. + my $is_bugmail = $email->header('X-Bugzilla-Status') + || $email->header('X-Bugzilla-Type') eq 'request'; + my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s); + my $is_test_email + = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0; + my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0; + my $encrypt_header = $email->header('X-Bugzilla-Encrypt') ? 1 : 0; + + if ( $is_bugmail + || $is_passwordmail + || $is_test_email + || $is_whine_email + || $encrypt_header) + { + # Convert the email's To address into a User object + my $login = $email->header('To'); + my $emailsuffix = Bugzilla->params->{'emailsuffix'}; + $login =~ s/$emailsuffix$//; + my $user = new Bugzilla::User({name => $login}); + + # Default to secure. (Of course, this means if this extension has a + # bug, lots of people are going to get bugmail falsely claiming their + # bugs are secure and they need to add a key...) + my $make_secure = SECURE_ALL; + + if ($is_bugmail) { + + # This is also a bit of a hack, but there's no header with the + # bug ID in. So we take the first number in the subject. + my ($bug_id) = ($email->header('Subject') =~ /\[\D+(\d+)\]/); + my $bug = new Bugzilla::Bug($bug_id); + if (!_should_secure_bug($bug)) { + $make_secure = SECURE_NONE; + } + + # If the insider group has securemail enabled.. + my $insider_group + = Bugzilla::Group->new({name => Bugzilla->params->{'insidergroup'}}); + if ( $insider_group + && $insider_group->secure_mail + && $make_secure == SECURE_NONE) + { + my $comment_is_private + = Bugzilla->dbh->selectcol_arrayref( + "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when", + undef, $bug_id); + + # Encrypt if there are private comments on an otherwise public bug + if (scalar $email->parts > 1) { + $email->walk_parts(sub { + my $part = shift; + my $content_type = $part->content_type; + $body = $part->body if $content_type && $content_type =~ /^text\/plain/; + }); } - elsif ($encrypt_header) { - # Templates or code may set the X-Bugzilla-Encrypt header to - # trigger encryption of emails. Remove that header from the email. - $email->header_set('X-Bugzilla-Encrypt'); + while ($body =~ /[\r\n]--- Comment #(\d+)/g) { + my $comment_number = $1; + if ($comment_number && $comment_is_private->[$comment_number]) { + $make_secure = SECURE_BODY; + last; + } } - # If finding the user fails for some reason, but we determine we - # should be encrypting, we want to make the mail safe. An empty key - # does that. - my $public_key = $user ? $user->public_key : ''; - - # Check if the new bugmail prefix should be added to the subject. - my $add_new = ($email->header('X-Bugzilla-Type') eq 'new' && - $user && - $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0; - - if ($make_secure == SECURE_NONE) { - # Filter the bug_links in HTML email in case the bugs the links - # point are "secured" bugs and the user may not be able to see - # the summaries. - _filter_bug_links($email); - } - else { - _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, $add_new); + # Encrypt if updating a private attachment without a comment + if ( $email->header('X-Bugzilla-Changed-Fields') + && $email->header('X-Bugzilla-Attach-ID')) + { + my $attachment + = Bugzilla::Attachment->new($email->header('X-Bugzilla-Attach-ID')); + if ($attachment && $attachment->isprivate) { + $make_secure = SECURE_BODY; + } } + } + } + elsif ($is_passwordmail) { + + # Mail is made unsecure only if the user does not have a public + # key and is not in any security groups. So specifying a public + # key OR being in a security group means the mail is kept secure + # (but, as noted above, the check is the other way around because + # we default to secure). + if ($user && !$user->public_key && !grep($_->secure_mail, @{$user->groups})) { + $make_secure = SECURE_NONE; + } + } + elsif ($is_whine_email) { + + # When a whine email has one or more secure bugs in the body, then + # encrypt the entire email body. Subject can be left alone as it + # comes from the whine settings. + $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE; + } + elsif ($encrypt_header) { + + # Templates or code may set the X-Bugzilla-Encrypt header to + # trigger encryption of emails. Remove that header from the email. + $email->header_set('X-Bugzilla-Encrypt'); + } + + # If finding the user fails for some reason, but we determine we + # should be encrypting, we want to make the mail safe. An empty key + # does that. + my $public_key = $user ? $user->public_key : ''; + + # Check if the new bugmail prefix should be added to the subject. + my $add_new + = ( $email->header('X-Bugzilla-Type') eq 'new' + && $user + && $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0; + + if ($make_secure == SECURE_NONE) { + + # Filter the bug_links in HTML email in case the bugs the links + # point are "secured" bugs and the user may not be able to see + # the summaries. + _filter_bug_links($email); + } + else { + _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, + $add_new); } + } } # Custom hook for bugzilla.mozilla.org (see bug 752400) sub bugmail_referenced_bugs { - my ($self, $args) = @_; - # Sanitise subjects of referenced bugs. - my $referenced_bugs = $args->{'referenced_bugs'}; - # No need to sanitise subjects if the entire email will be secured. - return if _should_secure_bug($args->{'updated_bug'}); - # Replace the subject if required - foreach my $ref (@$referenced_bugs) { - if (grep($_->secure_mail, @{ $ref->{'bug'}->groups_in })) { - $ref->{'short_desc'} = "(Secure bug)"; - } + my ($self, $args) = @_; + + # Sanitise subjects of referenced bugs. + my $referenced_bugs = $args->{'referenced_bugs'}; + + # No need to sanitise subjects if the entire email will be secured. + return if _should_secure_bug($args->{'updated_bug'}); + + # Replace the subject if required + foreach my $ref (@$referenced_bugs) { + if (grep($_->secure_mail, @{$ref->{'bug'}->groups_in})) { + $ref->{'short_desc'} = "(Secure bug)"; } + } } sub _should_secure_bug { - my ($bug) = @_; - # If there's a problem with the bug, err on the side of caution and mark it - # as secure. - return - !$bug - || $bug->{'error'} - || grep($_->secure_mail, @{ $bug->groups_in }); + my ($bug) = @_; + + # If there's a problem with the bug, err on the side of caution and mark it + # as secure. + return !$bug || $bug->{'error'} || grep($_->secure_mail, @{$bug->groups_in}); } sub _should_secure_whine { - my ($email) = @_; - my $should_secure = 0; - $email->walk_parts(sub { - my $part = shift; - my $content_type = $part->content_type; - return if !$content_type || $content_type !~ /^text\/plain/; - my $body = $part->body; - my @bugids = $body =~ /Bug (\d+):/g; - foreach my $id (@bugids) { - $id = trim($id); - next if !$id; - my $bug = new Bugzilla::Bug($id); - if ($bug && _should_secure_bug($bug)) { - $should_secure = 1; - last; - } - } - }); - return $should_secure ? 1 : 0; + my ($email) = @_; + my $should_secure = 0; + $email->walk_parts(sub { + my $part = shift; + my $content_type = $part->content_type; + return if !$content_type || $content_type !~ /^text\/plain/; + my $body = $part->body; + my @bugids = $body =~ /Bug (\d+):/g; + foreach my $id (@bugids) { + $id = trim($id); + next if !$id; + my $bug = new Bugzilla::Bug($id); + if ($bug && _should_secure_bug($bug)) { + $should_secure = 1; + last; + } + } + }); + return $should_secure ? 1 : 0; } sub _make_secure { - my ($email, $key, $sanitise_subject, $add_new) = @_; - - # Add header showing this email has been secured - $email->header_set('X-Bugzilla-Secure-Email', 'Yes'); - - my $subject = $email->header('Subject'); - my ($bug_id) = $subject =~ /\[\D+(\d+)\]/; - - my $key_type = 0; - if ($key && $key =~ /PUBLIC KEY/) { - $key_type = 'PGP'; + my ($email, $key, $sanitise_subject, $add_new) = @_; + + # Add header showing this email has been secured + $email->header_set('X-Bugzilla-Secure-Email', 'Yes'); + + my $subject = $email->header('Subject'); + my ($bug_id) = $subject =~ /\[\D+(\d+)\]/; + + my $key_type = 0; + if ($key && $key =~ /PUBLIC KEY/) { + $key_type = 'PGP'; + } + elsif ($key && $key =~ /BEGIN CERTIFICATE/) { + $key_type = 'S/MIME'; + } + + if ($key_type eq 'PGP') { + ################## + # PGP Encryption # + ################## + + my $tct = Bugzilla::Extension::SecureMail::TCT->new( + public_key => $key, + command => Bugzilla->localconfig->{tct_bin}, + ); + + if (scalar $email->parts > 1) { + my $old_boundary = $email->{ct}{attributes}{boundary}; + my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n"; + + # We need to do some fix up of each part for proper encoding and then + # stringify all parts for encrypting. We have to retain the old + # boundaries as well so that the email client can reconstruct the + # original message properly. + $email->walk_parts(\&_fix_encoding); + + $email->walk_parts(sub { + my ($part) = @_; + if ($sanitise_subject) { + _insert_subject($part, $subject); + } + return if $part->parts > 1; # Top-level + $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n"; + }); + $to_encrypt .= "--$old_boundary--"; + + # Now create the new properly formatted PGP parts containing the + # encrypted original message + my @new_parts = ( + Email::MIME->create( + attributes => + {content_type => 'application/pgp-encrypted', encoding => '7bit',}, + body => "Version: 1\n", + ), + Email::MIME->create( + attributes => { + content_type => 'application/octet-stream', + filename => 'encrypted.asc', + disposition => 'inline', + encoding => '7bit', + }, + body => _tct_encrypt($tct, $to_encrypt, $bug_id) + ), + ); + $email->parts_set(\@new_parts); + my $new_boundary = $email->{ct}{attributes}{boundary}; + + # Redo the old content type header with the new boundaries + # and other information needed for PGP + $email->header_set("Content-Type", + "multipart/encrypted; " + . "protocol=\"application/pgp-encrypted\"; " + . "boundary=\"$new_boundary\""); } - elsif ($key && $key =~ /BEGIN CERTIFICATE/) { - $key_type = 'S/MIME'; + else { + _fix_encoding($email); + if ($sanitise_subject) { + _insert_subject($email, $subject); + } + $email->body_set(_tct_encrypt($tct, $email->body, $bug_id)); } + } - if ($key_type eq 'PGP') { - ################## - # PGP Encryption # - ################## + elsif ($key_type eq 'S/MIME') { + ##################### + # S/MIME Encryption # + ##################### - my $tct = Bugzilla::Extension::SecureMail::TCT->new( - public_key => $key, - command => Bugzilla->localconfig->{tct_bin}, - ); + $email->walk_parts(\&_fix_encoding); - if (scalar $email->parts > 1) { - my $old_boundary = $email->{ct}{attributes}{boundary}; - my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n"; - - # We need to do some fix up of each part for proper encoding and then - # stringify all parts for encrypting. We have to retain the old - # boundaries as well so that the email client can reconstruct the - # original message properly. - $email->walk_parts(\&_fix_encoding); - - $email->walk_parts(sub { - my ($part) = @_; - if ($sanitise_subject) { - _insert_subject($part, $subject); - } - return if $part->parts > 1; # Top-level - $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n"; - }); - $to_encrypt .= "--$old_boundary--"; - - # Now create the new properly formatted PGP parts containing the - # encrypted original message - my @new_parts = ( - Email::MIME->create( - attributes => { - content_type => 'application/pgp-encrypted', - encoding => '7bit', - }, - body => "Version: 1\n", - ), - Email::MIME->create( - attributes => { - content_type => 'application/octet-stream', - filename => 'encrypted.asc', - disposition => 'inline', - encoding => '7bit', - }, - body => _tct_encrypt($tct, $to_encrypt, $bug_id) - ), - ); - $email->parts_set(\@new_parts); - my $new_boundary = $email->{ct}{attributes}{boundary}; - # Redo the old content type header with the new boundaries - # and other information needed for PGP - $email->header_set("Content-Type", - "multipart/encrypted; " . - "protocol=\"application/pgp-encrypted\"; " . - "boundary=\"$new_boundary\""); - } - else { - _fix_encoding($email); - if ($sanitise_subject) { - _insert_subject($email, $subject); - } - $email->body_set(_tct_encrypt($tct, $email->body, $bug_id)); - } + if ($sanitise_subject) { + $email->walk_parts(sub { _insert_subject($_[0], $subject) }); } - elsif ($key_type eq 'S/MIME') { - ##################### - # S/MIME Encryption # - ##################### + my $smime = Crypt::SMIME->new(); + my $encrypted; - $email->walk_parts(\&_fix_encoding); + eval { + $smime->setPublicKey([$key]); + $encrypted = $smime->encrypt($email->as_string()); + }; - if ($sanitise_subject) { - $email->walk_parts(sub { _insert_subject($_[0], $subject) }); - } + if (!$@) { - my $smime = Crypt::SMIME->new(); - my $encrypted; - - eval { - $smime->setPublicKey([$key]); - $encrypted = $smime->encrypt($email->as_string()); - }; - - if (!$@) { - # We can't replace the Email::MIME object, so we have to swap - # out its component parts. - my $enc_obj = new Email::MIME($encrypted); - $email->header_obj_set($enc_obj->header_obj()); - $email->parts_set([]); - $email->body_set($enc_obj->body()); - $email->content_type_set('application/pkcs7-mime'); - $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'}; - } - else { - $email->body_set('Error during Encryption: ' . $@); - } + # We can't replace the Email::MIME object, so we have to swap + # out its component parts. + my $enc_obj = new Email::MIME($encrypted); + $email->header_obj_set($enc_obj->header_obj()); + $email->parts_set([]); + $email->body_set($enc_obj->body()); + $email->content_type_set('application/pkcs7-mime'); + $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'}; } else { - # No encryption key provided; send a generic, safe email. - my $template = Bugzilla->template; - my $message; - my $email_type = $email->header('X-Bugzilla-Type'); - my $vars = { - 'urlbase' => Bugzilla->localconfig->{urlbase}, - 'bug_id' => $bug_id, - 'maintainer' => Bugzilla->params->{'maintainer'}, - 'email_type' => $email_type - }; - - $template->process('account/email/encryption-required.txt.tmpl', - $vars, \$message) - || ThrowTemplateError($template->error()); - - $email->parts_set([]); - $email->content_type_set('text/plain'); - $email->body_set($message); + $email->body_set('Error during Encryption: ' . $@); } + } + else { + # No encryption key provided; send a generic, safe email. + my $template = Bugzilla->template; + my $message; + my $email_type = $email->header('X-Bugzilla-Type'); + my $vars = { + 'urlbase' => Bugzilla->localconfig->{urlbase}, + 'bug_id' => $bug_id, + 'maintainer' => Bugzilla->params->{'maintainer'}, + 'email_type' => $email_type + }; - if ($sanitise_subject) { - # This is designed to still work if the admin changes the word - # 'bug' to something else. However, it could break if they change - # the format of the subject line in another way. - my $new = $add_new ? ' New:' : ''; - my $product = $email->header('X-Bugzilla-Product'); - my $component = $email->header('X-Bugzilla-Component'); - # Note: the $bug_id is required within the parentheses in order to keep - # gmail's threading algorithm happy. - $subject =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id in $product :: $component)/; - { - # avoid excessive line wrapping done by Encode. - local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; - $email->header_set('Subject', encode('MIME-Q', $subject)); - } + $template->process('account/email/encryption-required.txt.tmpl', + $vars, \$message) + || ThrowTemplateError($template->error()); + + $email->parts_set([]); + $email->content_type_set('text/plain'); + $email->body_set($message); + } + + if ($sanitise_subject) { + + # This is designed to still work if the admin changes the word + # 'bug' to something else. However, it could break if they change + # the format of the subject line in another way. + my $new = $add_new ? ' New:' : ''; + my $product = $email->header('X-Bugzilla-Product'); + my $component = $email->header('X-Bugzilla-Component'); + + # Note: the $bug_id is required within the parentheses in order to keep + # gmail's threading algorithm happy. + $subject + =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id in $product :: $component)/; + { + # avoid excessive line wrapping done by Encode. + local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; + $email->header_set('Subject', encode('MIME-Q', $subject)); } + } } sub _tct_encrypt { - my ($tct, $text, $bug_id) = @_; - - my $comment = Bugzilla->localconfig->{urlbase} . ( $bug_id ? 'show_bug.cgi?id=' . $bug_id : '' ); - my $encrypted; - my $ok = eval { $encrypted = $tct->encrypt( $text, $comment )->get; 1 }; - if (!$ok) { - WARN("Error: $@"); - $encrypted = "$comment\nOpenPGP Encryption failed. Check if your key is expired."; - } - elsif (!$encrypted) { - WARN('message empty!'); - $encrypted = "$comment\nOpenPGP Encryption failed for unknown reason."; - } - - return $encrypted; + my ($tct, $text, $bug_id) = @_; + + my $comment = Bugzilla->localconfig->{urlbase} + . ($bug_id ? 'show_bug.cgi?id=' . $bug_id : ''); + my $encrypted; + my $ok = eval { $encrypted = $tct->encrypt($text, $comment)->get; 1 }; + if (!$ok) { + WARN("Error: $@"); + $encrypted + = "$comment\nOpenPGP Encryption failed. Check if your key is expired."; + } + elsif (!$encrypted) { + WARN('message empty!'); + $encrypted = "$comment\nOpenPGP Encryption failed for unknown reason."; + } + + return $encrypted; } # Insert the subject into the part's body, as the subject of the message will @@ -634,69 +650,67 @@ sub _tct_encrypt { # XXX this incorrectly assumes all parts of the message are the body # we should only alter parts whose parent is multipart/alternative sub _insert_subject { - my ($part, $subject) = @_; - my $content_type = $part->content_type or return; - if ($content_type =~ /^text\/plain/) { - $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str); - } - elsif ($content_type =~ /^text\/html/) { - my $tree = HTML::Tree->new->parse_content($part->body_str); - my $body = $tree->look_down(qw(_tag body)); - $body->unshift_content(['strong', "Subject: $subject"], ['br']); - $part->body_str_set($tree->as_HTML); - $tree->delete; - } + my ($part, $subject) = @_; + my $content_type = $part->content_type or return; + if ($content_type =~ /^text\/plain/) { + $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str); + } + elsif ($content_type =~ /^text\/html/) { + my $tree = HTML::Tree->new->parse_content($part->body_str); + my $body = $tree->look_down(qw(_tag body)); + $body->unshift_content(['strong', "Subject: $subject"], ['br']); + $part->body_str_set($tree->as_HTML); + $tree->delete; + } } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _filter_bug_links { - my ($email) = @_; - $email->walk_parts(sub { - my $part = shift; - _fix_encoding($part); - my $content_type = $part->content_type; - return if !$content_type || $content_type !~ /text\/html/; - my $tree = HTML::Tree->new->parse_content($part->body_str); - my @links = $tree->look_down( _tag => q{a}, class => qr/bz_bug_link/ ); - my $updated = 0; - foreach my $link (@links) { - my $href = $link->attr('href'); - my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/; - my $bug = new Bugzilla::Bug($bug_id); - if ($bug && _should_secure_bug($bug)) { - $link->attr('title', '(secure bug)'); - $link->attr('class', 'bz_bug_link'); - $updated = 1; - } - } - if ($updated) { - $part->body_str_set($tree->as_HTML); - } - $tree->delete; - }); + my ($email) = @_; + $email->walk_parts(sub { + my $part = shift; + _fix_encoding($part); + my $content_type = $part->content_type; + return if !$content_type || $content_type !~ /text\/html/; + my $tree = HTML::Tree->new->parse_content($part->body_str); + my @links = $tree->look_down(_tag => q{a}, class => qr/bz_bug_link/); + my $updated = 0; + foreach my $link (@links) { + my $href = $link->attr('href'); + my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/; + my $bug = new Bugzilla::Bug($bug_id); + if ($bug && _should_secure_bug($bug)) { + $link->attr('title', '(secure bug)'); + $link->attr('class', 'bz_bug_link'); + $updated = 1; + } + } + if ($updated) { + $part->body_str_set($tree->as_HTML); + } + $tree->delete; + }); } __PACKAGE__->NAME; diff --git a/extensions/SecureMail/lib/TCT.pm b/extensions/SecureMail/lib/TCT.pm index 3a16309c2..f3de8ca39 100644 --- a/extensions/SecureMail/lib/TCT.pm +++ b/extensions/SecureMail/lib/TCT.pm @@ -15,76 +15,64 @@ use Future::Utils qw(call); use Future; use IO::Async::Process; -has 'public_key' => ( is => 'ro', required => 1 ); -has 'public_key_file' => ( is => 'lazy' ); -has 'is_valid' => ( is => 'lazy' ); -has 'command' => ( is => 'ro', default => 'tct' ); +has 'public_key' => (is => 'ro', required => 1); +has 'public_key_file' => (is => 'lazy'); +has 'is_valid' => (is => 'lazy'); +has 'command' => (is => 'ro', default => 'tct'); sub _build_public_key_file { - my ($self) = @_; - my $fh = File::Temp->new(SUFFIX => '.pubkey'); - $fh->print($self->public_key); - $fh->close; - return $fh; + my ($self) = @_; + my $fh = File::Temp->new(SUFFIX => '.pubkey'); + $fh->print($self->public_key); + $fh->close; + return $fh; } sub _build_is_valid { - my ($self) = @_; - - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my ($stderr, $stdout); - my $process = IO::Async::Process->new( - command => [$self->command, 'check', '-k', $self->public_key_file ], - stderr => { - into => \$stderr, - }, - stdout => { - into => \$stdout, - }, - on_finish => on_finish($exit_f), - on_exception => on_exception($self->command, $exit_f), - ); - $loop->add($process); - - return $exit_f->then( - sub { - my ($rv) = @_; - Future->wrap($rv == 0); - } - ); + my ($self) = @_; + + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my ($stderr, $stdout); + my $process = IO::Async::Process->new( + command => [$self->command, 'check', '-k', $self->public_key_file], + stderr => {into => \$stderr,}, + stdout => {into => \$stdout,}, + on_finish => on_finish($exit_f), + on_exception => on_exception($self->command, $exit_f), + ); + $loop->add($process); + + return $exit_f->then(sub { + my ($rv) = @_; + Future->wrap($rv == 0); + }); } sub encrypt { - my ($self, $input, $comment) = @_; - $self->is_valid->then( - sub { - my ($is_valid) = @_; - call { - die 'invalid public key!' unless $is_valid; - - my $output; - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my @command = ( $self->command, 'encrypt', '-k', $self->public_key_file ); - push @command, '--comment', $comment if $comment; - my $process = IO::Async::Process->new( - command => \@command, - stdin => { - from => $input, - }, - stdout => { - into => \$output, - }, - on_finish => on_finish($exit_f), - on_exception => on_exception($self->command, $exit_f), - ); - $loop->add($process); - - return $exit_f->then(sub { Future->wrap($output) }); - } - } - ); + my ($self, $input, $comment) = @_; + $self->is_valid->then(sub { + my ($is_valid) = @_; + call { + die 'invalid public key!' unless $is_valid; + + my $output; + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my @command = ($self->command, 'encrypt', '-k', $self->public_key_file); + push @command, '--comment', $comment if $comment; + my $process = IO::Async::Process->new( + command => \@command, + stdin => {from => $input,}, + stdout => {into => \$output,}, + on_finish => on_finish($exit_f), + on_exception => on_exception($self->command, $exit_f), + ); + $loop->add($process); + + return $exit_f->then(sub { Future->wrap($output) }); + } + }); } 1; diff --git a/extensions/ShadowBugs/Config.pm b/extensions/ShadowBugs/Config.pm index 6999edaf3..a45948dd4 100644 --- a/extensions/ShadowBugs/Config.pm +++ b/extensions/ShadowBugs/Config.pm @@ -8,7 +8,7 @@ package Bugzilla::Extension::ShadowBugs; use strict; -use constant NAME => 'ShadowBugs'; +use constant NAME => 'ShadowBugs'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ShadowBugs/Extension.pm b/extensions/ShadowBugs/Extension.pm index a9a1e0861..a1eb4a8c1 100644 --- a/extensions/ShadowBugs/Extension.pm +++ b/extensions/ShadowBugs/Extension.pm @@ -19,81 +19,83 @@ use Bugzilla::User; our $VERSION = '1'; BEGIN { - *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden; - *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj; + *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden; + *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj; } # Determine if the shadow-bug / shadowed-by fields are visibile on the # specified bug. sub _is_cf_shadow_bug_hidden { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # completely hide unless you're a member of the right group - return 1 unless Bugzilla->user->in_group('can_shadow_bugs'); + # completely hide unless you're a member of the right group + return 1 unless Bugzilla->user->in_group('can_shadow_bugs'); - my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); - if ($is_public) { - # hide on public bugs, unless it's shadowed - my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field}); - return 1 if !@$related; - } + my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); + if ($is_public) { + + # hide on public bugs, unless it's shadowed + my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field}); + return 1 if !@$related; + } } sub _cf_shadow_bug_obj { - my ($self) = @_; - return unless $self->cf_shadow_bug; - return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug); + my ($self) = @_; + return unless $self->cf_shadow_bug; + return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug); } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - Bugzilla->process_cache->{shadow_bug_field} ||= Bugzilla::Field->new({ name => 'cf_shadow_bug' }); - - return unless Bugzilla->user->in_group('can_shadow_bugs'); - return unless - $file eq 'bug/edit.html.tmpl' - || $file eq 'bug/show.html.tmpl' - || $file eq 'bug/show-header.html.tmpl'; - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - return unless $bug && $bug->cf_shadow_bug; - $vars->{is_shadow_bug} = 1; - - if ($file eq 'bug/edit.html.tmpl') { - # load comments from other bug - $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments; - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + Bugzilla->process_cache->{shadow_bug_field} + ||= Bugzilla::Field->new({name => 'cf_shadow_bug'}); + + return unless Bugzilla->user->in_group('can_shadow_bugs'); + return + unless $file eq 'bug/edit.html.tmpl' + || $file eq 'bug/show.html.tmpl' + || $file eq 'bug/show-header.html.tmpl'; + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + return unless $bug && $bug->cf_shadow_bug; + $vars->{is_shadow_bug} = 1; + + if ($file eq 'bug/edit.html.tmpl') { + + # load comments from other bug + $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments; + } } sub bug_end_of_update { - my ($self, $args) = @_; - - # don't allow shadowing non-public bugs - if (exists $args->{changes}->{cf_shadow_bug}) { - my ($old_id, $new_id) = @{ $args->{changes}->{cf_shadow_bug} }; - if ($new_id) { - if (!Bugzilla::User->new()->can_see_bug($new_id)) { - ThrowUserError('illegal_shadow_bug_public', { id => $new_id }); - } - } + my ($self, $args) = @_; + + # don't allow shadowing non-public bugs + if (exists $args->{changes}->{cf_shadow_bug}) { + my ($old_id, $new_id) = @{$args->{changes}->{cf_shadow_bug}}; + if ($new_id) { + if (!Bugzilla::User->new()->can_see_bug($new_id)) { + ThrowUserError('illegal_shadow_bug_public', {id => $new_id}); + } } + } + + # if a shadow bug is made public, clear the shadow_bug field + if (exists $args->{changes}->{bug_group}) { + my $bug = $args->{bug}; + return unless my $shadow_id = $bug->cf_shadow_bug; + my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); + if ($is_public) { + Bugzilla->dbh->do("UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?", + undef, $bug->id); + LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '', Bugzilla->user->id, + $args->{timestamp}); - # if a shadow bug is made public, clear the shadow_bug field - if (exists $args->{changes}->{bug_group}) { - my $bug = $args->{bug}; - return unless my $shadow_id = $bug->cf_shadow_bug; - my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); - if ($is_public) { - Bugzilla->dbh->do( - "UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?", - undef, $bug->id); - LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '', - Bugzilla->user->id, $args->{timestamp}); - - } } + } } __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/Config.pm b/extensions/SiteMapIndex/Config.pm index 980938503..3ff922167 100644 --- a/extensions/SiteMapIndex/Config.pm +++ b/extensions/SiteMapIndex/Config.pm @@ -28,12 +28,8 @@ use warnings; use constant NAME => 'SiteMapIndex'; -use constant REQUIRED_MODULES => [ - { - package => 'IO-Compress-Gzip', - module => 'IO::Compress::Gzip', - version => 0, - } -]; +use constant REQUIRED_MODULES => + [{package => 'IO-Compress-Gzip', module => 'IO::Compress::Gzip', version => 0, + }]; __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/Extension.pm b/extensions/SiteMapIndex/Extension.pm index 3d4342e0e..c606702ae 100644 --- a/extensions/SiteMapIndex/Extension.pm +++ b/extensions/SiteMapIndex/Extension.pm @@ -46,32 +46,32 @@ use POSIX; ######### sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; - - return if $file ne 'global/header.html.tmpl'; - return unless (exists $vars->{bug} || exists $vars->{bugs}); - my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}]; - return if ref $bugs ne 'ARRAY'; - - foreach my $bug (@$bugs) { - if (!bug_is_ok_to_index($bug)) { - $vars->{sitemap_noindex} = 1; - last; - } + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + + return if $file ne 'global/header.html.tmpl'; + return unless (exists $vars->{bug} || exists $vars->{bugs}); + my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}]; + return if ref $bugs ne 'ARRAY'; + + foreach my $bug (@$bugs) { + if (!bug_is_ok_to_index($bug)) { + $vars->{sitemap_noindex} = 1; + last; } + } } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{page_id}; - - if ($page =~ m{^sitemap/sitemap\.}) { - my $map = generate_sitemap(__PACKAGE__->NAME); - print Bugzilla->cgi->header('text/xml'); - print $map; - exit; - } + my ($self, $args) = @_; + my $page = $args->{page_id}; + + if ($page =~ m{^sitemap/sitemap\.}) { + my $map = generate_sitemap(__PACKAGE__->NAME); + print Bugzilla->cgi->header('text/xml'); + print $map; + exit; + } } ################ @@ -79,54 +79,54 @@ sub page_before_template { ################ sub install_before_final_checks { - my ($self) = @_; - if (!Bugzilla->localconfig->{urlbase}) { - print STDERR get_text('sitemap_no_urlbase'), "\n"; - return; - } - if (Bugzilla->params->{'requirelogin'}) { - print STDERR get_text('sitemap_requirelogin'), "\n"; - return; - } - - return if (Bugzilla->localconfig->{urlbase} ne 'https://bugzilla.mozilla.org/'); + my ($self) = @_; + if (!Bugzilla->localconfig->{urlbase}) { + print STDERR get_text('sitemap_no_urlbase'), "\n"; + return; + } + if (Bugzilla->params->{'requirelogin'}) { + print STDERR get_text('sitemap_requirelogin'), "\n"; + return; + } + + return if (Bugzilla->localconfig->{urlbase} ne 'https://bugzilla.mozilla.org/'); } sub install_filesystem { - my ($self, $args) = @_; - my $create_dirs = $args->{'create_dirs'}; - my $recurse_dirs = $args->{'recurse_dirs'}; - my $htaccess = $args->{'htaccess'}; - - # Create the sitemap directory to store the index and sitemap files - my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; - - $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE; - - $recurse_dirs->{$sitemap_path} = { - files => Bugzilla::Install::Filesystem::CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE, - dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE - }; - - # Create a htaccess file that allows the sitemap files to be served out - $htaccess->{"$sitemap_path/.htaccess"} = { - perms => Bugzilla::Install::Filesystem::WS_SERVE, - contents => <{'create_dirs'}; + my $recurse_dirs = $args->{'recurse_dirs'}; + my $htaccess = $args->{'htaccess'}; + + # Create the sitemap directory to store the index and sitemap files + my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; + + $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE; + + $recurse_dirs->{$sitemap_path} = { + files => Bugzilla::Install::Filesystem::CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE, + dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE + }; + + # Create a htaccess file that allows the sitemap files to be served out + $htaccess->{"$sitemap_path/.htaccess"} = { + perms => Bugzilla::Install::Filesystem::WS_SERVE, + contents => < Allow from all Deny from all EOT - }; + }; } sub before_robots_txt { - my ($self, $args) = @_; - $args->{vars}{SITEMAP_URL} = Bugzilla->localconfig->{urlbase} . SITEMAP_URL; + my ($self, $args) = @_; + $args->{vars}{SITEMAP_URL} = Bugzilla->localconfig->{urlbase} . SITEMAP_URL; } __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/lib/Constants.pm b/extensions/SiteMapIndex/lib/Constants.pm index 4f404c8b1..bd098d16a 100644 --- a/extensions/SiteMapIndex/lib/Constants.pm +++ b/extensions/SiteMapIndex/lib/Constants.pm @@ -27,10 +27,10 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - SITEMAP_AGE - SITEMAP_MAX - SITEMAP_DELAY - SITEMAP_URL + SITEMAP_AGE + SITEMAP_MAX + SITEMAP_DELAY + SITEMAP_URL ); # This is the amount of hours a sitemap index and it's files are considered diff --git a/extensions/SiteMapIndex/lib/Util.pm b/extensions/SiteMapIndex/lib/Util.pm index 4519461b4..fb945e324 100644 --- a/extensions/SiteMapIndex/lib/Util.pm +++ b/extensions/SiteMapIndex/lib/Util.pm @@ -28,8 +28,8 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - generate_sitemap - bug_is_ok_to_index + generate_sitemap + bug_is_ok_to_index ); use Bugzilla::Extension::SiteMapIndex::Constants; @@ -41,169 +41,176 @@ use Scalar::Util qw(blessed); use IO::Compress::Gzip qw(gzip $GzipError); sub too_young_date { - my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone); - $hours_ago->subtract(hours => SITEMAP_DELAY); - return $hours_ago; + my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $hours_ago->subtract(hours => SITEMAP_DELAY); + return $hours_ago; } sub bug_is_ok_to_index { - my ($bug) = @_; - return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug') && !$bug->{error}; - my $creation_ts = datetime_from($bug->creation_ts); - return ($creation_ts && $creation_ts lt too_young_date()) ? 1 : 0; + my ($bug) = @_; + return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug') && !$bug->{error}; + my $creation_ts = datetime_from($bug->creation_ts); + return ($creation_ts && $creation_ts lt too_young_date()) ? 1 : 0; } # We put two things in the Sitemap: a list of Browse links for products, # and links to bugs. sub generate_sitemap { - my ($extension_name) = @_; - - # If file is less than SITEMAP_AGE hours old, then read in and send to caller. - # If greater, then regenerate and send the new version. - my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; - if (-e $index_file) { - my $index_mtime = (stat($index_file))[9]; - my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours - if ($index_hours < SITEMAP_AGE) { - my $index_fh = new IO::File($index_file, 'r'); - $index_fh || die "Could not open current sitemap index: $!"; - my $index_xml; - { local $/; $index_xml = <$index_fh> } - $index_fh->close() || die "Could not close current sitemap index: $!"; - - return $index_xml; - } + my ($extension_name) = @_; + + # If file is less than SITEMAP_AGE hours old, then read in and send to caller. + # If greater, then regenerate and send the new version. + my $index_file + = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + if (-e $index_file) { + my $index_mtime = (stat($index_file))[9]; + my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours + if ($index_hours < SITEMAP_AGE) { + my $index_fh = new IO::File($index_file, 'r'); + $index_fh || die "Could not open current sitemap index: $!"; + my $index_xml; + { local $/; $index_xml = <$index_fh> } + $index_fh->close() || die "Could not close current sitemap index: $!"; + + return $index_xml; } - - # Set the atime and mtime of the index file to the current time - # in case another request is made before we finish. - utime(undef, undef, $index_file); - - # Sitemaps must never contain private data. - Bugzilla->logout_request(); - my $user = Bugzilla->user; - my $products = $user->get_accessible_products; - - my $num_bugs = SITEMAP_MAX - scalar(@$products); - # We do this date math outside of the database because databases - # usually do better with a straight comparison value. - my $hours_ago = too_young_date(); - - # We don't use Bugzilla::Bug objects, because this could be a tremendous - # amount of data, and we only want a little. Also, we only display - # bugs that are not in any group. We show the last $num_bugs - # most-recently-updated bugs. - my $dbh = Bugzilla->dbh; - my $bug_sth = $dbh->prepare( - 'SELECT bugs.bug_id, bugs.delta_ts + } + + # Set the atime and mtime of the index file to the current time + # in case another request is made before we finish. + utime(undef, undef, $index_file); + + # Sitemaps must never contain private data. + Bugzilla->logout_request(); + my $user = Bugzilla->user; + my $products = $user->get_accessible_products; + + my $num_bugs = SITEMAP_MAX - scalar(@$products); + + # We do this date math outside of the database because databases + # usually do better with a straight comparison value. + my $hours_ago = too_young_date(); + + # We don't use Bugzilla::Bug objects, because this could be a tremendous + # amount of data, and we only want a little. Also, we only display + # bugs that are not in any group. We show the last $num_bugs + # most-recently-updated bugs. + my $dbh = Bugzilla->dbh; + my $bug_sth = $dbh->prepare( + 'SELECT bugs.bug_id, bugs.delta_ts FROM bugs LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id WHERE bug_group_map.bug_id IS NULL AND creation_ts < ? - ' . $dbh->sql_limit($num_bugs, '?')); + ' . $dbh->sql_limit($num_bugs, '?') + ); - my $filecount = 1; - my $filelist = []; - my $offset = 0; + my $filecount = 1; + my $filelist = []; + my $offset = 0; - while (1) { - my $bugs = []; + while (1) { + my $bugs = []; - $bug_sth->execute($hours_ago, $offset); + $bug_sth->execute($hours_ago, $offset); - while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) { - push(@$bugs, { bug_id => $bug_id, delta_ts => $delta_ts }); - } + while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) { + push(@$bugs, {bug_id => $bug_id, delta_ts => $delta_ts}); + } - last if !@$bugs; + last if !@$bugs; - # We only need the product links in the first sitemap file - $products = [] if $filecount > 1; + # We only need the product links in the first sitemap file + $products = [] if $filecount > 1; - push(@$filelist, _generate_sitemap_file($extension_name, $filecount, $products, $bugs)); + push(@$filelist, + _generate_sitemap_file($extension_name, $filecount, $products, $bugs)); - $filecount++; - $offset += $num_bugs; - } + $filecount++; + $offset += $num_bugs; + } - # Generate index file - return _generate_sitemap_index($extension_name, $filelist); + # Generate index file + return _generate_sitemap_index($extension_name, $filelist); } sub _generate_sitemap_index { - my ($extension_name, $filelist) = @_; + my ($extension_name, $filelist) = @_; - my $dbh = Bugzilla->dbh; - my $timestamp = $dbh->selectrow_array( - "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + my $dbh = Bugzilla->dbh; + my $timestamp = $dbh->selectrow_array( + "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); - my $index_xml = < END - foreach my $filename (@$filelist) { - $index_xml .= " + foreach my $filename (@$filelist) { + $index_xml .= " - " . Bugzilla->localconfig->{urlbase} . "data/$extension_name/$filename + " + . Bugzilla->localconfig->{urlbase} . "data/$extension_name/$filename $timestamp "; - } + } - $index_xml .= < END - my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; - my $index_fh = new IO::File($index_file, 'w'); - $index_fh || die "Could not open new sitemap index: $!"; - print $index_fh $index_xml; - $index_fh->close() || die "Could not close new sitemap index: $!"; + my $index_file + = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + my $index_fh = new IO::File($index_file, 'w'); + $index_fh || die "Could not open new sitemap index: $!"; + print $index_fh $index_xml; + $index_fh->close() || die "Could not close new sitemap index: $!"; - return $index_xml; + return $index_xml; } sub _generate_sitemap_file { - my ($extension_name, $filecount, $products, $bugs) = @_; + my ($extension_name, $filecount, $products, $bugs) = @_; - my $bug_url = Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id='; - my $product_url = Bugzilla->localconfig->{urlbase} . 'describecomponents.cgi?product='; + my $bug_url = Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id='; + my $product_url + = Bugzilla->localconfig->{urlbase} . 'describecomponents.cgi?product='; - my $sitemap_xml = < END - foreach my $product (@$products) { - $sitemap_xml .= " + foreach my $product (@$products) { + $sitemap_xml .= " " . $product_url . url_quote($product->name) . " daily 0.4 "; - } + } - foreach my $bug (@$bugs) { - $sitemap_xml .= " + foreach my $bug (@$bugs) { + $sitemap_xml .= " " . $bug_url . $bug->{bug_id} . " " . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . " "; - } + } - $sitemap_xml .= < END - # Write the compressed sitemap data to a file in the cgi root so that they can - # be accessed by the search engines. - my $filename = "sitemap$filecount.xml.gz"; - gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename" - || die "gzip failed: $GzipError\n"; + # Write the compressed sitemap data to a file in the cgi root so that they can + # be accessed by the search engines. + my $filename = "sitemap$filecount.xml.gz"; + gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename" + || die "gzip failed: $GzipError\n"; - return $filename; + return $filename; } 1; diff --git a/extensions/Splinter/Extension.pm b/extensions/Splinter/Extension.pm index eb2006f47..f5a2f41cb 100644 --- a/extensions/Splinter/Extension.pm +++ b/extensions/Splinter/Extension.pm @@ -28,135 +28,140 @@ use Bugzilla::Extension::Splinter::Util; our $VERSION = '0.1'; BEGIN { - *Bugzilla::splinter_review_base = \&get_review_base; - *Bugzilla::splinter_review_url = \&_get_review_url; + *Bugzilla::splinter_review_base = \&get_review_base; + *Bugzilla::splinter_review_url = \&_get_review_url; } sub _get_review_url { - my ($class, $bug_id, $attach_id) = @_; - return get_review_url(Bugzilla::Bug->check({ id => $bug_id, cache => 1 }), $attach_id); + my ($class, $bug_id, $attach_id) = @_; + return get_review_url(Bugzilla::Bug->check({id => $bug_id, cache => 1}), + $attach_id); } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - - if ($page eq 'splinter.html') { - my $user = Bugzilla->user; - - # We can either provide just a bug id to see a list - # of prior reviews by the user, or just an attachment - # id to go directly to a review page for the attachment. - # If both are give they will be checked later to make - # sure they are connected. - - my $input = Bugzilla->input_params; - if ($input->{'bug'}) { - $vars->{'bug_id'} = $input->{'bug'}; - $vars->{'attach_id'} = $input->{'attachment'}; - $vars->{'bug'} = Bugzilla::Bug->check({ id => $input->{'bug'}, cache => 1 }); - } - - if ($input->{'attachment'}) { - my $attachment = Bugzilla::Attachment->check({ id => $input->{'attachment'} }); - - # Check to see if the user can see the bug this attachment is connected to. - Bugzilla::Bug->check($attachment->bug_id); - if ($attachment->isprivate - && $user->id != $attachment->attacher->id - && !$user->is_insider) - { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment'}); - } - - # If the user provided both a bug id and an attachment id, they must - # be connected to each other - if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) { - ThrowUserError('bug_attach_id_mismatch'); - } - - # The patch is going to be displayed in a HTML page and if the utf8 - # param is enabled, we have to encode attachment data as utf8. - if (Bugzilla->params->{'utf8'}) { - $attachment->data; # load data - utf8::decode($attachment->{data}); - } - - $vars->{'attach_id'} = $attachment->id; - if ($user->id && $attachment->contenttype eq "text/x-github-pull-request" && $attachment->can_review) { - $vars->{'attach_data'} = $attachment->fetch_github_pr_diff; - } - else { - $vars->{'attach_data'} = $attachment->data; - } - $vars->{'attach_is_crlf'} = $vars->{'attach_data'} =~ /\012\015/ ? 1 : 0; - } - - my $field_object = new Bugzilla::Field({ name => 'attachments.status' }); - my $statuses; - if ($field_object) { - $statuses = [map { $_->name } @{ $field_object->legal_values }]; - } else { - $statuses = []; - } - $vars->{'attachment_statuses'} = $statuses; + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + + if ($page eq 'splinter.html') { + my $user = Bugzilla->user; + + # We can either provide just a bug id to see a list + # of prior reviews by the user, or just an attachment + # id to go directly to a review page for the attachment. + # If both are give they will be checked later to make + # sure they are connected. + + my $input = Bugzilla->input_params; + if ($input->{'bug'}) { + $vars->{'bug_id'} = $input->{'bug'}; + $vars->{'attach_id'} = $input->{'attachment'}; + $vars->{'bug'} = Bugzilla::Bug->check({id => $input->{'bug'}, cache => 1}); } + + if ($input->{'attachment'}) { + my $attachment = Bugzilla::Attachment->check({id => $input->{'attachment'}}); + + # Check to see if the user can see the bug this attachment is connected to. + Bugzilla::Bug->check($attachment->bug_id); + if ( $attachment->isprivate + && $user->id != $attachment->attacher->id + && !$user->is_insider) + { + ThrowUserError('auth_failure', {action => 'access', object => 'attachment'}); + } + + # If the user provided both a bug id and an attachment id, they must + # be connected to each other + if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) { + ThrowUserError('bug_attach_id_mismatch'); + } + + # The patch is going to be displayed in a HTML page and if the utf8 + # param is enabled, we have to encode attachment data as utf8. + if (Bugzilla->params->{'utf8'}) { + $attachment->data; # load data + utf8::decode($attachment->{data}); + } + + $vars->{'attach_id'} = $attachment->id; + if ( $user->id + && $attachment->contenttype eq "text/x-github-pull-request" + && $attachment->can_review) + { + $vars->{'attach_data'} = $attachment->fetch_github_pr_diff; + } + else { + $vars->{'attach_data'} = $attachment->data; + } + $vars->{'attach_is_crlf'} = $vars->{'attach_data'} =~ /\012\015/ ? 1 : 0; + } + + my $field_object = new Bugzilla::Field({name => 'attachments.status'}); + my $statuses; + if ($field_object) { + $statuses = [map { $_->name } @{$field_object->legal_values}]; + } + else { + $statuses = []; + } + $vars->{'attachment_statuses'} = $statuses; + } } sub bug_format_comment { - my ($self, $args) = @_; - - my $bug = $args->{'bug'}; - my $regexes = $args->{'regexes'}; - my $text = $args->{'text'}; - - # Add [review] link to the end of "Created attachment" comments - # - # We need to work around the way that the hook works, which is intended - # to avoid overlapping matches, since we *want* an overlapping match - # here (the normal handling of "Created attachment"), so we add in - # dummy text and then replace in the regular expression we return from - # the hook. - $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?) + my ($self, $args) = @_; + + my $bug = $args->{'bug'}; + my $regexes = $args->{'regexes'}; + my $text = $args->{'text'}; + + # Add [review] link to the end of "Created attachment" comments + # + # We need to work around the way that the hook works, which is intended + # to avoid overlapping matches, since we *want* an overlapping match + # here (the normal handling of "Created attachment"), so we add in + # dummy text and then replace in the regular expression we return from + # the hook. + $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?) ~(push(@$regexes, { match => qr/__REVIEW__$2/, replace => get_review_link("$2", "[review]") })) && (attachment_id_is_patch($2) ? "$1 __REVIEW__$2" : $1) ~egmx; - # And linkify "Review of attachment", this is less of a workaround since - # there is no issue with overlap; note that there is an assumption that - # there is only one match in the text we are linkifying, since they all - # get the same link. - my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/; - - if ($$text =~ $REVIEW_RE) { - my $attach_id = $1; - my $review_link = get_review_link($attach_id, "Review"); - my $attach_link = Bugzilla::Template::get_attachment_link($attach_id, "attachment $attach_id"); - - push(@$regexes, { match => $REVIEW_RE, - replace => "$review_link of $attach_link:"}); - } + # And linkify "Review of attachment", this is less of a workaround since + # there is no issue with overlap; note that there is an assumption that + # there is only one match in the text we are linkifying, since they all + # get the same link. + my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/; + + if ($$text =~ $REVIEW_RE) { + my $attach_id = $1; + my $review_link = get_review_link($attach_id, "Review"); + my $attach_link = Bugzilla::Template::get_attachment_link($attach_id, + "attachment $attach_id"); + + push(@$regexes, + {match => $REVIEW_RE, replace => "$review_link of $attach_link:"}); + } } sub config_add_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config"; + my $modules = $args->{panel_modules}; + $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config"; } sub mailer_before_send { - my ($self, $args) = @_; - - # Post-process bug mail to add review links to bug mail. - # It would be nice to be able to hook in earlier in the - # process when the email body is being formatted in the - # style of the bug-format_comment link for HTML but this - # is the only hook available as of Bugzilla-3.4. - add_review_links_to_email($args->{'email'}); + my ($self, $args) = @_; + + # Post-process bug mail to add review links to bug mail. + # It would be nice to be able to hook in earlier in the + # process when the email body is being formatted in the + # style of the bug-format_comment link for HTML but this + # is the only hook available as of Bugzilla-3.4. + add_review_links_to_email($args->{'email'}); } __PACKAGE__->NAME; diff --git a/extensions/Splinter/lib/Config.pm b/extensions/Splinter/lib/Config.pm index fb3c16074..d3675c111 100644 --- a/extensions/Splinter/lib/Config.pm +++ b/extensions/Splinter/lib/Config.pm @@ -31,17 +31,13 @@ use Bugzilla::Config::Common; our $sortkey = 1350; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'splinter_base', - type => 't', - default => 'page.cgi?id=splinter.html', - }, - ); + my @param_list = ( + {name => 'splinter_base', type => 't', default => 'page.cgi?id=splinter.html',}, + ); - return @param_list; + return @param_list; } 1; diff --git a/extensions/Splinter/lib/Util.pm b/extensions/Splinter/lib/Util.pm index c85bb9b3b..0b7b2ff12 100644 --- a/extensions/Splinter/lib/Util.pm +++ b/extensions/Splinter/lib/Util.pm @@ -32,12 +32,12 @@ use Email::MIME::ContentType qw(parse_content_type); use base qw(Exporter); @Bugzilla::Extension::Splinter::Util::EXPORT = qw( - attachment_is_visible - attachment_id_is_patch - get_review_base - get_review_url - get_review_link - add_review_links_to_email + attachment_is_visible + attachment_id_is_patch + get_review_base + get_review_url + get_review_link + add_review_links_to_email ); # Validates an attachment ID. @@ -51,81 +51,95 @@ use base qw(Exporter); # Returns an attachment object. # Based on code from attachment.cgi sub attachment_id_is_valid { - my ($attach_id, $dont_validate_access) = @_; + my ($attach_id, $dont_validate_access) = @_; - # Validate the specified attachment id. - detaint_natural($attach_id) || return 0; + # Validate the specified attachment id. + detaint_natural($attach_id) || return 0; - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 }) - || return 0; + # Make sure the attachment exists in the database. + my $attachment + = new Bugzilla::Attachment({id => $attach_id, cache => 1}) || return 0; - return $attachment - if ($dont_validate_access || attachment_is_visible($attachment)); + return $attachment + if ($dont_validate_access || attachment_is_visible($attachment)); } # Checks if the current user can see an attachment # Based on code from attachment.cgi sub attachment_is_visible { - my $attachment = shift; + my $attachment = shift; - $attachment->isa('Bugzilla::Attachment') || return 0; + $attachment->isa('Bugzilla::Attachment') || return 0; - return (Bugzilla->user->can_see_bug($attachment->bug->id) - && (!$attachment->isprivate - || Bugzilla->user->id == $attachment->attacher->id - || Bugzilla->user->is_insider)); + return ( + Bugzilla->user->can_see_bug($attachment->bug->id) + && (!$attachment->isprivate + || Bugzilla->user->id == $attachment->attacher->id + || Bugzilla->user->is_insider) + ); } sub attachment_id_is_patch { - my $attach_id = shift; - my $attachment = attachment_id_is_valid($attach_id); - return ($attachment - && ($attachment->ispatch - || ($attachment->contenttype eq "text/x-github-pull-request" && $attachment->external_redirect))); + my $attach_id = shift; + my $attachment = attachment_id_is_valid($attach_id); + return ( + $attachment + && ( + $attachment->ispatch + || ( $attachment->contenttype eq "text/x-github-pull-request" + && $attachment->external_redirect) + ) + ); } sub get_review_base { - my $base = Bugzilla->params->{'splinter_base'}; - $base =~ s!/$!!; - my $urlbase = Bugzilla->localconfig->{urlbase}; - $urlbase =~ s!/$!! if $base =~ "^/"; - $base = $urlbase . $base; - return $base; + my $base = Bugzilla->params->{'splinter_base'}; + $base =~ s!/$!!; + my $urlbase = Bugzilla->localconfig->{urlbase}; + $urlbase =~ s!/$!! if $base =~ "^/"; + $base = $urlbase . $base; + return $base; } sub get_review_url { - my ($bug, $attach_id) = @_; - my $base = get_review_base(); - my $bug_id = $bug->id; - return $base . ($base =~ /\?/ ? '&' : '?') . "bug=$bug_id&attachment=$attach_id"; + my ($bug, $attach_id) = @_; + my $base = get_review_base(); + my $bug_id = $bug->id; + return + $base + . ($base =~ /\?/ ? '&' : '?') + . "bug=$bug_id&attachment=$attach_id"; } sub get_review_link { - my ($attach_id, $link_text) = @_; - - my $attachment = attachment_id_is_valid($attach_id); - - if (attachment_id_is_patch($attach_id)) { - return "$link_text"; - } - else { - return $link_text; - } + my ($attach_id, $link_text) = @_; + + my $attachment = attachment_id_is_valid($attach_id); + + if (attachment_id_is_patch($attach_id)) { + return + "$link_text"; + } + else { + return $link_text; + } } sub munge_create_attachment { - my ($bug, $intro_text, $attach_id, $view_link) = @_; - - if (attachment_id_is_patch($attach_id)) { - return ("$intro_text" . - " View: $view_link\015\012" . - " Review: " . get_review_url($bug, $attach_id, 1) . "\015\012"); - } - else { - return ("$intro_text --> ($view_link)"); - } + my ($bug, $intro_text, $attach_id, $view_link) = @_; + + if (attachment_id_is_patch($attach_id)) { + return ("$intro_text" + . " View: $view_link\015\012" + . " Review: " + . get_review_url($bug, $attach_id, 1) + . "\015\012"); + } + else { + return ("$intro_text --> ($view_link)"); + } } # This adds review links into a bug mail before we send it out. @@ -133,64 +147,62 @@ sub munge_create_attachment { # RFC-2822 style \r\n, we need handle line ends carefully. # (\015 and \012 are used because Perl \n is platform-dependent) sub add_review_links_to_email { - my $email = shift; - return if $email->parts > 1; - return unless $email->content_type =~ m#^text/#; + my $email = shift; + return if $email->parts > 1; + return unless $email->content_type =~ m#^text/#; - _fix_encoding($email); - my $body = $email->body_str; + _fix_encoding($email); + my $body = $email->body_str; - my $new_body = 0; - my $bug; + my $new_body = 0; + my $bug; - if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/ - && Bugzilla->user->can_see_bug($1)) - { - $bug = Bugzilla::Bug->new({ id => $1, cache => 1 }); - } + if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/ + && Bugzilla->user->can_see_bug($1)) + { + $bug = Bugzilla::Bug->new({id => $1, cache => 1}); + } - return unless defined $bug; + return unless defined $bug; - if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) { - $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:) + if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) { + $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:) ~"$1\015\012 --> (" . get_review_url($bug, $2, 1) . ")" ~egx; - $new_body = 1; - } + $new_body = 1; + } - if ($body =~ /Created attachment \d+\015\012 --> /) { - $body =~ s~(Created\ attachment\ (\d+)\015\012) + if ($body =~ /Created attachment \d+\015\012 --> /) { + $body =~ s~(Created\ attachment\ (\d+)\015\012) \ -->\ \(([^\015\012]*)\)[^\015\012]* ~munge_create_attachment($bug, $1, $2, $3) ~egx; - $new_body = 1; - } + $new_body = 1; + } - $email->body_str_set($body) if $new_body; + $email->body_str_set($body) if $new_body; } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } 1; diff --git a/extensions/TagNewUsers/Config.pm b/extensions/TagNewUsers/Config.pm index c791e3a07..cc3676bff 100644 --- a/extensions/TagNewUsers/Config.pm +++ b/extensions/TagNewUsers/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'TagNewUsers'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'TagNewUsers'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/TagNewUsers/Extension.pm b/extensions/TagNewUsers/Extension.pm index 1810f204f..6280f1697 100644 --- a/extensions/TagNewUsers/Extension.pm +++ b/extensions/TagNewUsers/Extension.pm @@ -34,85 +34,88 @@ our $VERSION = '1'; # sub install_update_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - if (!$dbh->bz_column_info('profiles', 'comment_count')) { - $dbh->bz_add_column('profiles', 'comment_count', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); - my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?'); - my $ra = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who'); - my $count = 1; - my $total = scalar @$ra; - foreach my $ra_row (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($user_id, $count) = @$ra_row; - $sth->execute($count, $user_id); - } + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if (!$dbh->bz_column_info('profiles', 'comment_count')) { + $dbh->bz_add_column('profiles', 'comment_count', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?'); + my $ra + = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who'); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($user_id, $count) = @$ra_row; + $sth->execute($count, $user_id); } + } - if (!$dbh->bz_column_info('profiles', 'creation_ts')) { - $dbh->bz_add_column('profiles', 'creation_ts', - {TYPE => 'DATETIME'}); - my $creation_date_fieldid = get_field_id('creation_ts'); - my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?'); - my $ra = $dbh->selectall_arrayref(" + if (!$dbh->bz_column_info('profiles', 'creation_ts')) { + $dbh->bz_add_column('profiles', 'creation_ts', {TYPE => 'DATETIME'}); + my $creation_date_fieldid = get_field_id('creation_ts'); + my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?'); + my $ra = $dbh->selectall_arrayref(" SELECT p.userid, a.profiles_when FROM profiles p LEFT JOIN profiles_activity a ON a.userid=p.userid AND a.fieldid=$creation_date_fieldid "); - my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - my $count = 1; - my $total = scalar @$ra; - foreach my $ra_row (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($user_id, $when) = @$ra_row; - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - $when = $now; - } - - $sth->execute($when, $user_id); - } - } - - if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) { - $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'}); - my $sth_update = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?'); - my $sth_select = $dbh->prepare( - 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' . $dbh->sql_limit(1) + my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($user_id, $when) = @$ra_row; + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " + . $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " + . $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " + . $dbh->sql_limit(1), + undef, $user_id ); - my $ra = $dbh->selectcol_arrayref('SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1'); - my $count = 1; - my $total = scalar @$ra; - foreach my $user_id (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - $sth_select->execute($user_id); - my ($bug_id) = $sth_select->fetchrow_array; - $sth_update->execute($bug_id, $user_id); - } + } + if (!$when) { + $when = $now; + } + + $sth->execute($when, $user_id); + } + } + + if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) { + $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'}); + my $sth_update + = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?'); + my $sth_select + = $dbh->prepare( + 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' + . $dbh->sql_limit(1)); + my $ra = $dbh->selectcol_arrayref( + 'SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1'); + my $count = 1; + my $total = scalar @$ra; + foreach my $user_id (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + $sth_select->execute($user_id); + my ($bug_id) = $sth_select->fetchrow_array; + $sth_update->execute($bug_id, $user_id); } + } } # @@ -120,32 +123,34 @@ sub install_update_db { # sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(comment_count creation_ts first_patch_bug_id); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(comment_count creation_ts first_patch_bug_id); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); - if ($dbh->bz_column_info($class->DB_TABLE, 'comment_count')) { - $params->{comment_count} = 0; - } - if ($dbh->bz_column_info($class->DB_TABLE, 'creation_ts')) { - $params->{creation_ts} = $timestamp; - } - } elsif ($class->isa('Bugzilla::Attachment')) { - if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) { - Bugzilla->user->first_patch_bug_id($params->{bug}->id); - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + if ($dbh->bz_column_info($class->DB_TABLE, 'comment_count')) { + $params->{comment_count} = 0; + } + if ($dbh->bz_column_info($class->DB_TABLE, 'creation_ts')) { + $params->{creation_ts} = $timestamp; + } + } + elsif ($class->isa('Bugzilla::Attachment')) { + if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) { + Bugzilla->user->first_patch_bug_id($params->{bug}->id); } + } } # @@ -153,74 +158,70 @@ sub object_before_create { # BEGIN { - *Bugzilla::User::comment_count = \&_comment_count; - *Bugzilla::User::creation_ts = \&_creation_ts; - *Bugzilla::User::update_comment_count = \&_update_comment_count; - *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id; - *Bugzilla::User::is_new = \&_is_new; - *Bugzilla::User::creation_age = \&_creation_age; + *Bugzilla::User::comment_count = \&_comment_count; + *Bugzilla::User::creation_ts = \&_creation_ts; + *Bugzilla::User::update_comment_count = \&_update_comment_count; + *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id; + *Bugzilla::User::is_new = \&_is_new; + *Bugzilla::User::creation_age = \&_creation_age; } sub _comment_count { return $_[0]->{comment_count} } -sub _creation_ts { return $_[0]->{creation_ts} } +sub _creation_ts { return $_[0]->{creation_ts} } sub _update_comment_count { - my $self = shift; - my $dbh = Bugzilla->dbh; - - # no need to update this counter for users which are no longer new - return unless $self->is_new; - - my $id = $self->id; - my ($count) = $dbh->selectrow_array( - "SELECT COUNT(*) FROM longdescs WHERE who=?", - undef, $id - ); - return if $self->{comment_count} == $count; - $dbh->do( - 'UPDATE profiles SET comment_count=? WHERE userid=?', - undef, $count, $id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $id }); - $self->{comment_count} = $count; + my $self = shift; + my $dbh = Bugzilla->dbh; + + # no need to update this counter for users which are no longer new + return unless $self->is_new; + + my $id = $self->id; + my ($count) + = $dbh->selectrow_array("SELECT COUNT(*) FROM longdescs WHERE who=?", + undef, $id); + return if $self->{comment_count} == $count; + $dbh->do('UPDATE profiles SET comment_count=? WHERE userid=?', + undef, $count, $id); + Bugzilla->memcached->clear({table => 'profiles', id => $id}); + $self->{comment_count} = $count; } sub _first_patch_bug_id { - my ($self, $bug_id) = @_; - return $self->{first_patch_bug_id} unless defined $bug_id; - - Bugzilla->dbh->do( - 'UPDATE profiles SET first_patch_bug_id=? WHERE userid=?', - undef, $bug_id, $self->id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); - $self->{first_patch_bug_id} = $bug_id; + my ($self, $bug_id) = @_; + return $self->{first_patch_bug_id} unless defined $bug_id; + + Bugzilla->dbh->do('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?', + undef, $bug_id, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); + $self->{first_patch_bug_id} = $bug_id; } sub _is_new { - my ($self) = @_; - - if (!exists $self->{is_new}) { - if ($self->in_group('editbugs')) { - $self->{is_new} = 0; - } else { - $self->{is_new} = ($self->comment_count <= COMMENT_COUNT) - || ($self->creation_age <= PROFILE_AGE); - } + my ($self) = @_; + + if (!exists $self->{is_new}) { + if ($self->in_group('editbugs')) { + $self->{is_new} = 0; + } + else { + $self->{is_new} = ($self->comment_count <= COMMENT_COUNT) + || ($self->creation_age <= PROFILE_AGE); } + } - return $self->{is_new}; + return $self->{is_new}; } sub _creation_age { - my ($self) = @_; + my ($self) = @_; - if (!exists $self->{creation_age}) { - my $age = sprintf("%.0f", (time() - str2time($self->creation_ts)) / 86400); - $self->{creation_age} = $age; - } + if (!exists $self->{creation_age}) { + my $age = sprintf("%.0f", (time() - str2time($self->creation_ts)) / 86400); + $self->{creation_age} = $age; + } - return $self->{creation_age}; + return $self->{creation_age}; } # @@ -228,49 +229,48 @@ sub _creation_age { # sub bug_end_of_create { - Bugzilla->user->update_comment_count(); + Bugzilla->user->update_comment_count(); } sub bug_end_of_update { - Bugzilla->user->update_comment_count(); + Bugzilla->user->update_comment_count(); } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; - - my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/); - my $changer_login = $email->header('X-Bugzilla-Who'); - my $changed_fields = $email->header('X-Bugzilla-Changed-Fields'); - - if ($bug_id - && $changer_login - && $changed_fields =~ /attachments.created/) + my ($self, $args) = @_; + my $email = $args->{email}; + + my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/); + my $changer_login = $email->header('X-Bugzilla-Who'); + my $changed_fields = $email->header('X-Bugzilla-Changed-Fields'); + + if ($bug_id && $changer_login && $changed_fields =~ /attachments.created/) { + my $changer = Bugzilla::User->new({name => $changer_login}); + if ( $changer + && $changer->first_patch_bug_id + && $changer->first_patch_bug_id == $bug_id) { - my $changer = Bugzilla::User->new({ name => $changer_login }); - if ($changer - && $changer->first_patch_bug_id - && $changer->first_patch_bug_id == $bug_id) - { - $email->header_set('X-Bugzilla-FirstPatch' => $bug_id); - } + $email->header_set('X-Bugzilla-FirstPatch' => $bug_id); } + } } sub webservice_user_get { - my ($self, $args) = @_; - my ($webservice, $params, $users) = @$args{qw(webservice params users)}; - - return unless filter_wants($params, 'is_new'); - - foreach my $user (@$users) { - # Most of the time the hash values are XMLRPC::Data objects - my $email = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'}; - if ($email) { - my $user_obj = Bugzilla::User->new({ name => $email }); - $user->{'is_new'} = $webservice->type('boolean', $user_obj->is_new ? 1 : 0); - } + my ($self, $args) = @_; + my ($webservice, $params, $users) = @$args{qw(webservice params users)}; + + return unless filter_wants($params, 'is_new'); + + foreach my $user (@$users) { + + # Most of the time the hash values are XMLRPC::Data objects + my $email + = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'}; + if ($email) { + my $user_obj = Bugzilla::User->new({name => $email}); + $user->{'is_new'} = $webservice->type('boolean', $user_obj->is_new ? 1 : 0); } + } } __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/Config.pm b/extensions/TrackingFlags/Config.pm index d0bc5ca20..6f5b9be39 100644 --- a/extensions/TrackingFlags/Config.pm +++ b/extensions/TrackingFlags/Config.pm @@ -13,15 +13,9 @@ use warnings; use constant NAME => 'TrackingFlags'; -use constant REQUIRED_MODULES => [ - { - package => 'JSON-XS', - module => 'JSON::XS', - version => '2.0' - }, -]; +use constant REQUIRED_MODULES => + [{package => 'JSON-XS', module => 'JSON::XS', version => '2.0'},]; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/Extension.pm b/extensions/TrackingFlags/Extension.pm index 5f6715fc8..fea0240c8 100644 --- a/extensions/TrackingFlags/Extension.pm +++ b/extensions/TrackingFlags/Extension.pm @@ -34,823 +34,701 @@ our $VERSION = '1'; our @FLAG_CACHE; BEGIN { - *Bugzilla::tracking_flags = \&_tracking_flags; - *Bugzilla::tracking_flag_names = \&_tracking_flag_names; + *Bugzilla::tracking_flags = \&_tracking_flags; + *Bugzilla::tracking_flag_names = \&_tracking_flag_names; } sub _tracking_flags { - return Bugzilla::Extension::TrackingFlags::Flag->get_all(); + return Bugzilla::Extension::TrackingFlags::Flag->get_all(); } sub _tracking_flag_names { - return Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); + return Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - - if ($page eq 'tracking_flags_admin_list.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_list($vars); - - } elsif ($page eq 'tracking_flags_admin_edit.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_edit($vars); - } + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'tracking_flags_admin_list.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_list($vars); + + } + elsif ($page eq 'tracking_flags_admin_edit.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_edit($vars); + } } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - if ($file eq 'bug/create/create.html.tmpl') { - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $vars->{'product'}->name, - enter_bug => 1, - is_active => 1, - }); - - $vars->{tracking_flags} = $flags; - $vars->{tracking_flags_json} = _flags_to_json($flags); - $vars->{tracking_flag_types} = FLAG_TYPES; - $vars->{tracking_flag_components} = _flags_to_components($flags, $vars->{product}); - $vars->{highest_status_firefox} = _get_highest_status_firefox($flags); - } - elsif ($file eq 'bug/edit.html.tmpl'|| $file eq 'bug/show.xml.tmpl' - || $file eq 'email/bugmail.html.tmpl' || $file eq 'email/bugmail.txt.tmpl') - { - # note: bug/edit.html.tmpl doesn't support multiple bugs - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - - if ($bug && !$bug->{error}) { - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $bug->product, - component => $bug->component, - bug_id => $bug->id, - is_active => 1, - }); - - $vars->{tracking_flags} = $flags; - $vars->{tracking_flags_json} = _flags_to_json($flags); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + if ($file eq 'bug/create/create.html.tmpl') { + my $flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'product'}->name, enter_bug => 1, is_active => 1, + }); + + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); + $vars->{tracking_flag_types} = FLAG_TYPES; + $vars->{tracking_flag_components} + = _flags_to_components($flags, $vars->{product}); + $vars->{highest_status_firefox} = _get_highest_status_firefox($flags); + } + elsif ($file eq 'bug/edit.html.tmpl' + || $file eq 'bug/show.xml.tmpl' + || $file eq 'email/bugmail.html.tmpl' + || $file eq 'email/bugmail.txt.tmpl') + { + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + + if ($bug && !$bug->{error}) { + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, + component => $bug->component, + bug_id => $bug->id, + is_active => 1, + }); - $vars->{'tracking_flag_types'} = FLAG_TYPES; - } - elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) { - $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $vars->{'one_product'}->name, - is_active => 1 - }); + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); } + + $vars->{'tracking_flag_types'} = FLAG_TYPES; + } + elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) { + $vars->{'tracking_flags'} + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'one_product'}->name, is_active => 1 + }); + } } sub _flags_to_json { - my ($flags) = @_; + my ($flags) = @_; - my $json = { - flags => {}, - types => [], - comments => {}, - }; + my $json = {flags => {}, types => [], comments => {},}; - my %type_map = map { $_->{name} => $_ } @{ FLAG_TYPES() }; - foreach my $flag (@$flags) { - my $flag_type = $flag->flag_type; + my %type_map = map { $_->{name} => $_ } @{FLAG_TYPES()}; + foreach my $flag (@$flags) { + my $flag_type = $flag->flag_type; - $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value; + $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value; - if ($type_map{$flag_type}->{collapsed} - && !grep { $_ eq $flag_type } @{ $json->{types} }) - { - push @{ $json->{types} }, $flag_type; - } + if ($type_map{$flag_type}->{collapsed} && !grep { $_ eq $flag_type } + @{$json->{types}}) + { + push @{$json->{types}}, $flag_type; + } - foreach my $value (@{ $flag->values }) { - if (defined($value->comment) && $value->comment ne '') { - $json->{comments}->{$flag->name}->{$value->value} = $value->comment; - } - } + foreach my $value (@{$flag->values}) { + if (defined($value->comment) && $value->comment ne '') { + $json->{comments}->{$flag->name}->{$value->value} = $value->comment; + } } + } - return encode_json($json); + return encode_json($json); } sub _flags_to_components { - my ($flags, $product) = @_; - - # for each component, generate a list of visible tracking flags - my $json = {}; - foreach my $component (@{ $product->components }) { - next unless $component->is_active; - foreach my $flag (@$flags) { - foreach my $visibility (@{ $flag->visibility }) { - if ($visibility->product_id == $product->id - && (!$visibility->component_id || $visibility->component_id == $component->id)) - { - $json->{$component->name} //= []; - push @{ $json->{$component->name} }, $flag->name; - } - } + my ($flags, $product) = @_; + + # for each component, generate a list of visible tracking flags + my $json = {}; + foreach my $component (@{$product->components}) { + next unless $component->is_active; + foreach my $flag (@$flags) { + foreach my $visibility (@{$flag->visibility}) { + if ($visibility->product_id == $product->id + && (!$visibility->component_id || $visibility->component_id == $component->id)) + { + $json->{$component->name} //= []; + push @{$json->{$component->name}}, $flag->name; } + } } - return encode_json($json); + } + return encode_json($json); } sub _get_highest_status_firefox { - my ($flags) = @_; - - my @status_flags = - sort { $b <=> $a } - map { $_->name =~ /(\d+)$/; $1 } - grep { $_->is_active && $_->name =~ /^cf_status_firefox\d/ } - @$flags; - return @status_flags ? $status_flags[0] : undef; + my ($flags) = @_; + + my @status_flags + = sort { $b <=> $a } + map { $_->name =~ /(\d+)$/; $1 } + grep { $_->is_active && $_->name =~ /^cf_status_firefox\d/ } @$flags; + return @status_flags ? $status_flags[0] : undef; } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'tracking_flags'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - field_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - name => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - description => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - type => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => '0', - }, - enter_bug => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - is_active => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - ], - INDEXES => [ - tracking_flags_idx => { - FIELDS => ['name'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_values'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - setter_group_id => { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { - TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - }, - value => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => '0', - }, - enter_bug => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - is_active => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - comment => { - TYPE => 'TEXT', - NOTNULL => 0, - }, - ], - INDEXES => [ - tracking_flags_values_idx => { - FIELDS => ['tracking_flag_id', 'value'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_bugs'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - value => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - ], - INDEXES => [ - tracking_flags_bugs_idx => { - FIELDS => ['tracking_flag_id', 'bug_id'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_visibility'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - ], - INDEXES => [ - tracking_flags_visibility_idx => { - FIELDS => ['tracking_flag_id', 'product_id', 'component_id'], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'tracking_flags'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + field_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1,}, + description => {TYPE => 'varchar(64)', NOTNULL => 1,}, + type => {TYPE => 'varchar(64)', NOTNULL => 1,}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0',}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + ], + INDEXES => [tracking_flags_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'tracking_flags_values'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + setter_group_id => { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL',}, + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1,}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0',}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + comment => {TYPE => 'TEXT', NOTNULL => 0,}, + ], + INDEXES => [ + tracking_flags_values_idx => + {FIELDS => ['tracking_flag_id', 'value'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'tracking_flags_bugs'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1,}, + ], + INDEXES => [ + tracking_flags_bugs_idx => + {FIELDS => ['tracking_flag_id', 'bug_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'tracking_flags_visibility'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + ], + INDEXES => [ + tracking_flags_visibility_idx => { + FIELDS => ['tracking_flag_id', 'product_id', 'component_id'], + TYPE => 'UNIQUE', + }, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - - my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id'); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'CASCADE'; - $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk); - } - - $dbh->bz_add_column( - 'tracking_flags', - 'enter_bug', - { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - } - ); - $dbh->bz_add_column( - 'tracking_flags_values', - 'comment', - { - TYPE => 'TEXT', - NOTNULL => 0, - }, - ); + my $dbh = Bugzilla->dbh; + + my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id'); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'CASCADE'; + $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk); + } + + $dbh->bz_add_column('tracking_flags', 'enter_bug', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}); + $dbh->bz_add_column('tracking_flags_values', 'comment', + {TYPE => 'TEXT', NOTNULL => 0,}, + ); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } sub active_custom_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my $params = $args->{'params'}; - my $product = $params->{'product'}; - my $component = $params->{'component'}; - - return if $params->{skip_extensions}; - # Create a hash of current fields based on field names - my %field_hash = map { $_->name => $_ } @$$fields; - - my @tracking_flags; - if ($product) { - $params->{'product_id'} = $product->id; - $params->{'component_id'} = $component->id if $component; - $params->{'is_active'} = 1; - @tracking_flags = @{ Bugzilla::Extension::TrackingFlags::Flag->match($params) }; - } - else { - @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - } - - # Add tracking flags to fields hash replacing if already exists for our - # flag object instead of the usual Field.pm object - foreach my $flag (@tracking_flags) { - $field_hash{$flag->name} = $flag; - } - - @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash; + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my $params = $args->{'params'}; + my $product = $params->{'product'}; + my $component = $params->{'component'}; + + return if $params->{skip_extensions}; + + # Create a hash of current fields based on field names + my %field_hash = map { $_->name => $_ } @$$fields; + + my @tracking_flags; + if ($product) { + $params->{'product_id'} = $product->id; + $params->{'component_id'} = $component->id if $component; + $params->{'is_active'} = 1; + @tracking_flags = @{Bugzilla::Extension::TrackingFlags::Flag->match($params)}; + } + else { + @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + } + + # Add tracking flags to fields hash replacing if already exists for our + # flag object instead of the usual Field.pm object + foreach my $flag (@tracking_flags) { + $field_hash{$flag->name} = $flag; + } + + @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash; } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - my $dbh = Bugzilla->dbh; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $columns->{$flag->name} = { - name => "COALESCE(map_" . $flag->name . ".value, '---')", - title => $flag->description - }; - } + my ($self, $args) = @_; + my $columns = $args->{columns}; + my $dbh = Bugzilla->dbh; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $columns->{$flag->name} = { + name => "COALESCE(map_" . $flag->name . ".value, '---')", + title => $flag->description + }; + } } sub buglist_column_joins { - my ($self, $args) = @_; - # if there are elements in the tracking_flags array, then they have been - # removed from the query, so we mustn't generate joins - return if scalar @{ $args->{search}->{tracking_flags} }; - - my $column_joins = $args->{'column_joins'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $column_joins->{$flag->name} = { - as => 'map_' . $flag->name, - table => 'tracking_flags_bugs', - extra => [ 'map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id ] - }; - } + my ($self, $args) = @_; + + # if there are elements in the tracking_flags array, then they have been + # removed from the query, so we mustn't generate joins + return if scalar @{$args->{search}->{tracking_flags}}; + + my $column_joins = $args->{'column_joins'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $column_joins->{$flag->name} = { + as => 'map_' . $flag->name, + table => 'tracking_flags_bugs', + extra => ['map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id] + }; + } } sub bug_create_cf_accessors { - my ($self, $args) = @_; - # Create the custom accessors for the flag values - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - my $flag_name = $flag->name; - if (!Bugzilla::Bug->can($flag_name)) { - my $accessor = sub { - my $self = shift; - return $self->{$flag_name} if defined $self->{$flag_name}; - if (!exists $self->{'_tf_bug_values_preloaded'}) { - # preload all values currently set for this bug - my $bug_values - = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $self->id }); - foreach my $value (@$bug_values) { - $self->{$value->tracking_flag->name} = $value->value; - } - $self->{'_tf_bug_values_preloaded'} = 1; - } - return $self->{$flag_name} ||= '---'; - }; - no strict 'refs'; - *{"Bugzilla::Bug::$flag_name"} = $accessor; - } - if (!Bugzilla::Bug->can("set_$flag_name")) { - my $setter = sub { - my ($self, $value) = @_; - $value = ref($value) eq 'ARRAY' - ? $value->[0] - : $value; - $self->set($flag_name, $value); - }; - no strict 'refs'; - *{"Bugzilla::Bug::set_$flag_name"} = $setter; + my ($self, $args) = @_; + + # Create the custom accessors for the flag values + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + my $flag_name = $flag->name; + if (!Bugzilla::Bug->can($flag_name)) { + my $accessor = sub { + my $self = shift; + return $self->{$flag_name} if defined $self->{$flag_name}; + if (!exists $self->{'_tf_bug_values_preloaded'}) { + + # preload all values currently set for this bug + my $bug_values + = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({bug_id => $self->id}); + foreach my $value (@$bug_values) { + $self->{$value->tracking_flag->name} = $value->value; + } + $self->{'_tf_bug_values_preloaded'} = 1; } + return $self->{$flag_name} ||= '---'; + }; + no strict 'refs'; + *{"Bugzilla::Bug::$flag_name"} = $accessor; + } + if (!Bugzilla::Bug->can("set_$flag_name")) { + my $setter = sub { + my ($self, $value) = @_; + $value = ref($value) eq 'ARRAY' ? $value->[0] : $value; + $self->set($flag_name, $value); + }; + no strict 'refs'; + *{"Bugzilla::Bug::set_$flag_name"} = $setter; } + } } sub bug_editable_bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - push(@$fields, $flag->name); - } + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + push(@$fields, $flag->name); + } } sub search_operator_field_override { - my ($self, $args) = @_; - my $operators = $args->{'operators'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $operators->{$flag->name} = { - _non_changed => sub { - _tracking_flags_search_nonchanged($flag->flag_id, @_) - } - }; - } + my ($self, $args) = @_; + my $operators = $args->{'operators'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $operators->{$flag->name} = { + _non_changed => sub { + _tracking_flags_search_nonchanged($flag->flag_id, @_); + } + }; + } } sub _tracking_flags_search_nonchanged { - my ($flag_id, $search, $args) = @_; - my ($bugs_table, $chart_id, $joins, $value, $operator) = - @$args{qw(bugs_table chart_id joins value operator)}; - my $dbh = Bugzilla->dbh; - - return if ($operator =~ m/^changed/); - - my $bugs_alias = "tracking_flags_bugs_$chart_id"; - my $flags_alias = "tracking_flags_$chart_id"; - - my $bugs_join = { - table => 'tracking_flags_bugs', - as => $bugs_alias, - from => $bugs_table . ".bug_id", - to => "bug_id", - extra => [$bugs_alias . ".tracking_flag_id = $flag_id"] - }; - - push(@$joins, $bugs_join); - - if ($operator eq 'isempty' or $operator eq 'isnotempty') { - $args->{'full_field'} = "$bugs_alias.value"; - } - else { - $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')"; - } + my ($flag_id, $search, $args) = @_; + my ($bugs_table, $chart_id, $joins, $value, $operator) + = @$args{qw(bugs_table chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + return if ($operator =~ m/^changed/); + + my $bugs_alias = "tracking_flags_bugs_$chart_id"; + my $flags_alias = "tracking_flags_$chart_id"; + + my $bugs_join = { + table => 'tracking_flags_bugs', + as => $bugs_alias, + from => $bugs_table . ".bug_id", + to => "bug_id", + extra => [$bugs_alias . ".tracking_flag_id = $flag_id"] + }; + + push(@$joins, $bugs_join); + + if ($operator eq 'isempty' or $operator eq 'isnotempty') { + $args->{'full_field'} = "$bugs_alias.value"; + } + else { + $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')"; + } } sub request_cleanup { - foreach my $flag (@FLAG_CACHE) { - my $bug_flag = delete $flag->{bug_flag}; - if ($bug_flag) { - delete $bug_flag->{bug}; - delete $bug_flag->{tracking_flag}; - } - foreach my $value (@{ $flag->{values} }) { - delete $value->{tracking_flag}; - } + foreach my $flag (@FLAG_CACHE) { + my $bug_flag = delete $flag->{bug_flag}; + if ($bug_flag) { + delete $bug_flag->{bug}; + delete $bug_flag->{tracking_flag}; + } + foreach my $value (@{$flag->{values}}) { + delete $value->{tracking_flag}; } - @FLAG_CACHE = (); + } + @FLAG_CACHE = (); } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $timestamp = $args->{'timestamp'}; - my $user = Bugzilla->user; + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $user = Bugzilla->user; - my $params = Bugzilla->request_cache->{tracking_flags_create_params}; - return if !$params; + my $params = Bugzilla->request_cache->{tracking_flags_create_params}; + return if !$params; - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $bug->product, - component => $bug->component, - is_active => 1, + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, component => $bug->component, is_active => 1, }); - foreach my $flag (@$tracking_flags) { - next if !$params->{$flag->name}; - foreach my $value (@{$flag->values}) { - next if $value->value ne $params->{$flag->name}; - next if $value->value eq '---'; # do not insert if value is '---', same as empty - if (!$flag->can_set_value($value->value)) { - ThrowUserError('tracking_flags_change_denied', - { flag => $flag, value => $value }); - } - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $flag->flag_id, - bug_id => $bug->id, - value => $value->value, - }); - # Add the name/value pair to the bug object - $bug->{$flag->name} = $value->value; - } + foreach my $flag (@$tracking_flags) { + next if !$params->{$flag->name}; + foreach my $value (@{$flag->values}) { + next if $value->value ne $params->{$flag->name}; + next if $value->value eq '---'; # do not insert if value is '---', same as empty + if (!$flag->can_set_value($value->value)) { + ThrowUserError('tracking_flags_change_denied', + {flag => $flag, value => $value}); + } + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $flag->flag_id, + bug_id => $bug->id, + value => $value->value, + }); + + # Add the name/value pair to the bug object + $bug->{$flag->name} = $value->value; } + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $params = $args->{params}; + my ($self, $args) = @_; + my $object = $args->{object}; + my $params = $args->{params}; - return unless $object->isa('Bugzilla::Bug'); + return unless $object->isa('Bugzilla::Bug'); - # Do not filter by product/component as we may be changing those - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - bug_id => $object->id, - is_active => 1, + # Do not filter by product/component as we may be changing those + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + bug_id => $object->id, is_active => 1, }); - foreach my $flag (@$tracking_flags) { - my $flag_name = $flag->name; - if (exists $params->{$flag_name}) { - my $value = ref($params->{$flag_name}) eq 'ARRAY' - ? $params->{$flag_name}->[0] - : $params->{$flag_name}; - $object->set($flag_name, $value); - } + foreach my $flag (@$tracking_flags) { + my $flag_name = $flag->name; + if (exists $params->{$flag_name}) { + my $value + = ref($params->{$flag_name}) eq 'ARRAY' + ? $params->{$flag_name}->[0] + : $params->{$flag_name}; + $object->set($flag_name, $value); } + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $field, $old_value, $new_value, $priv_results) - = @$args{qw(bug field old_value new_value priv_results)}; - - return if $field !~ /^cf_/ or $old_value eq $new_value; - return unless my $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field }); - - if ($flag->can_set_value($new_value)) { - push @$priv_results, PRIVILEGES_REQUIRED_NONE; - } - else { - push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED; - } + my ($self, $args) = @_; + my ($bug, $field, $old_value, $new_value, $priv_results) + = @$args{qw(bug field old_value new_value priv_results)}; + + return if $field !~ /^cf_/ or $old_value eq $new_value; + return + unless my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({name => $field}); + + if ($flag->can_set_value($new_value)) { + push @$priv_results, PRIVILEGES_REQUIRED_NONE; + } + else { + push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED; + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $timestamp, $changes) - = @$args{qw(bug old_bug timestamp changes)}; - my $user = Bugzilla->user; - - # Do not filter by product/component as we may be changing those - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - bug_id => $bug->id, - is_active => 1, + my ($self, $args) = @_; + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + my $user = Bugzilla->user; + + # Do not filter by product/component as we may be changing those + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + bug_id => $bug->id, is_active => 1, }); - my $product_id = $bug->product_id; - my $component_id = $bug->component_id; - my $is_visible = sub { - $_->product_id == $product_id && (!$_->component_id || $_->component_id == $component_id); - }; + my $product_id = $bug->product_id; + my $component_id = $bug->component_id; + my $is_visible = sub { + $_->product_id == $product_id + && (!$_->component_id || $_->component_id == $component_id); + }; + + my (@flag_changes); + foreach my $flag (@$tracking_flags) { + my $flag_name = $flag->name; + my $new_value = $bug->$flag_name; + my $old_value = $old_bug->$flag_name; + + if ($flag->bug_flag->id) { + my $visibility = $flag->visibility; + if (none { $is_visible->() } @$visibility) { + push(@flag_changes, {flag => $flag, added => '---', removed => $new_value}); + next; + } + } - my (@flag_changes); - foreach my $flag (@$tracking_flags) { - my $flag_name = $flag->name; - my $new_value = $bug->$flag_name; - my $old_value = $old_bug->$flag_name; - - if ($flag->bug_flag->id) { - my $visibility = $flag->visibility; - if (none { $is_visible->() } @$visibility) { - push(@flag_changes, { flag => $flag, - added => '---', - removed => $new_value }); - next; - } - } + if ($new_value ne $old_value) { - if ($new_value ne $old_value) { - # Do not allow if the user cannot set the old value or the new value - if (!$flag->can_set_value($new_value)) { - ThrowUserError('tracking_flags_change_denied', - { flag => $flag, value => $new_value }); - } - push(@flag_changes, { flag => $flag, - added => $new_value, - removed => $old_value }); - } + # Do not allow if the user cannot set the old value or the new value + if (!$flag->can_set_value($new_value)) { + ThrowUserError('tracking_flags_change_denied', + {flag => $flag, value => $new_value}); + } + push(@flag_changes, + {flag => $flag, added => $new_value, removed => $old_value}); } + } - foreach my $change (@flag_changes) { - my $flag = $change->{'flag'}; - my $added = $change->{'added'}; - my $removed = $change->{'removed'}; + foreach my $change (@flag_changes) { + my $flag = $change->{'flag'}; + my $added = $change->{'added'}; + my $removed = $change->{'removed'}; - if ($added eq '---') { - $flag->bug_flag->remove_from_db(); - } - elsif ($removed eq '---') { - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $flag->flag_id, - bug_id => $bug->id, - value => $added, - }); - } - else { - $flag->bug_flag->set_value($added); - $flag->bug_flag->update($timestamp); - } + if ($added eq '---') { + $flag->bug_flag->remove_from_db(); + } + elsif ($removed eq '---') { + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $flag->flag_id, bug_id => $bug->id, value => $added, + }); + } + else { + $flag->bug_flag->set_value($added); + $flag->bug_flag->update($timestamp); + } - $changes->{$flag->name} = [ $removed, $added ]; - LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, $timestamp); + $changes->{$flag->name} = [$removed, $added]; + LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, + $timestamp); - # Update the name/value pair in the bug object - $bug->{$flag->name} = $added; - } + # Update the name/value pair in the bug object + $bug->{$flag->name} = $added; + } } sub bug_end_of_create_validators { - my ($self, $args) = @_; - my $params = $args->{params}; - - # We need to stash away any params that are setting/updating tracking - # flags early on. Otherwise set_all or insert_create_data will complain. - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {}; - foreach my $flag (@tracking_flags) { - my $flag_name = $flag->name; - if (defined $params->{$flag_name}) { - $cache->{$flag_name} = delete $params->{$flag_name}; - } + my ($self, $args) = @_; + my $params = $args->{params}; + + # We need to stash away any params that are setting/updating tracking + # flags early on. Otherwise set_all or insert_create_data will complain. + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {}; + foreach my $flag (@tracking_flags) { + my $flag_name = $flag->name; + if (defined $params->{$flag_name}) { + $cache->{$flag_name} = delete $params->{$flag_name}; } + } } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; + my ($self, $args) = @_; + my $email = $args->{email}; - # Add X-Bugzilla-Tracking header or add to it - # if already exists - if ($email->header('X-Bugzilla-ID')) { - my $bug_id = $email->header('X-Bugzilla-ID'); + # Add X-Bugzilla-Tracking header or add to it + # if already exists + if ($email->header('X-Bugzilla-ID')) { + my $bug_id = $email->header('X-Bugzilla-ID'); - my $tracking_flags - = Bugzilla::Extension::TrackingFlags::Flag->match({ bug_id => $bug_id }); + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({bug_id => $bug_id}); - my @set_values = (); - foreach my $flag (@$tracking_flags) { - next if $flag->bug_flag->value eq '---'; - push(@set_values, $flag->description . ":" . $flag->bug_flag->value); - } + my @set_values = (); + foreach my $flag (@$tracking_flags) { + next if $flag->bug_flag->value eq '---'; + push(@set_values, $flag->description . ":" . $flag->bug_flag->value); + } - if (@set_values) { - my $set_values_string = join(' ', @set_values); - if ($email->header('X-Bugzilla-Tracking')) { - $set_values_string = $email->header('X-Bugzilla-Tracking') . - " " . $set_values_string; - } - $email->header_set('X-Bugzilla-Tracking' => $set_values_string); - } + if (@set_values) { + my $set_values_string = join(' ', @set_values); + if ($email->header('X-Bugzilla-Tracking')) { + $set_values_string + = $email->header('X-Bugzilla-Tracking') . " " . $set_values_string; + } + $email->header_set('X-Bugzilla-Tracking' => $set_values_string); } + } } # Purpose: generically handle generating pretty blocking/status "flags" from # custom field names. sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; - - foreach my $name (keys %$map) { - if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) { - my $type = $1; - my $product = $2; - my $version = $3; - - if ($version) { - $version = join('.', split(//, $version)); - } - - my $pretty_name = $type; - if ($product) { - $pretty_name .= "-" . $product; - } - if ($version) { - $pretty_name .= $version; - } - - $map->{$pretty_name} = $name; - } + my ($self, $args) = @_; + my $map = $args->{'map'}; + + foreach my $name (keys %$map) { + if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) { + my $type = $1; + my $product = $2; + my $version = $3; + + if ($version) { + $version = join('.', split(//, $version)); + } + + my $pretty_name = $type; + if ($product) { + $pretty_name .= "-" . $product; + } + if ($version) { + $pretty_name .= $version; + } + + $map->{$pretty_name} = $name; } + } } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; + my ($self, $args) = @_; + my $status = $args->{status}; - $status->('tracking_flags_check'); + $status->('tracking_flags_check'); - my ($count) = Bugzilla->dbh->selectrow_array(" + my ($count) = Bugzilla->dbh->selectrow_array(" SELECT COUNT(*) FROM tracking_flags_visibility INNER JOIN components ON components.id = tracking_flags_visibility.component_id WHERE tracking_flags_visibility.product_id <> components.product_id "); - if ($count) { - $status->('tracking_flags_alert', undef, 'alert'); - $status->('tracking_flags_repair'); - } + if ($count) { + $status->('tracking_flags_alert', undef, 'alert'); + $status->('tracking_flags_repair'); + } } sub sanitycheck_repair { - my ($self, $args) = @_; - return unless Bugzilla->cgi->param('tracking_flags_repair'); + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('tracking_flags_repair'); - my $status = $args->{'status'}; - my $dbh = Bugzilla->dbh; - $status->('tracking_flags_repairing'); + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('tracking_flags_repairing'); - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT DISTINCT tracking_flags_visibility.product_id AS bad_product_id, components.product_id AS good_product_id, tracking_flags_visibility.component_id FROM tracking_flags_visibility INNER JOIN components ON components.id = tracking_flags_visibility.component_id WHERE tracking_flags_visibility.product_id <> components.product_id - ", - { Slice => {} } - ); - foreach my $row (@$rows) { - $dbh->do(" + ", {Slice => {}}); + foreach my $row (@$rows) { + $dbh->do(" UPDATE tracking_flags_visibility SET product_id=? WHERE product_id=? AND component_id=? - ", undef, - $row->{good_product_id}, - $row->{bad_product_id}, - $row->{component_id}, - ); - } + ", undef, $row->{good_product_id}, $row->{bad_product_id}, + $row->{component_id},); + } } __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/bin/bug_825946.pl b/extensions/TrackingFlags/bin/bug_825946.pl index 896dc5448..8a340175b 100755 --- a/extensions/TrackingFlags/bin/bug_825946.pl +++ b/extensions/TrackingFlags/bin/bug_825946.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants qw( USAGE_MODE_CMDLINE ); @@ -51,8 +51,8 @@ SQL my %visible; foreach my $row (@$tf_vis) { - my ($tracking_flag_id, $product_id, $component_id) = @$row; - $visible{$tracking_flag_id}{$product_id}{$component_id // 'ALL'} = 1; + my ($tracking_flag_id, $product_id, $component_id) = @$row; + $visible{$tracking_flag_id}{$product_id}{$component_id // 'ALL'} = 1; } my %bugs = map { $_->[0] => 1 } @$tf_bugs; @@ -66,13 +66,16 @@ my $removed = 0; $dbh->bz_start_transaction(); my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); foreach my $tf_bug (@$tf_bugs) { - my ($flag_name, $value, $bug_id, $tf_id, $product_id, $component_id) = @$tf_bug; - unless ($visible{$tf_id}{$product_id}{$component_id} || $visible{$tf_id}{$product_id}{ALL}) { - $dbh->do("DELETE FROM tracking_flags_bugs WHERE tracking_flag_id = ? AND bug_id = ?", - undef, $tf_id, $bug_id); - LogActivityEntry($bug_id, $flag_name, $value, '---', $user->id, $timestamp); - $removed++; - } + my ($flag_name, $value, $bug_id, $tf_id, $product_id, $component_id) = @$tf_bug; + unless ($visible{$tf_id}{$product_id}{$component_id} + || $visible{$tf_id}{$product_id}{ALL}) + { + $dbh->do( + "DELETE FROM tracking_flags_bugs WHERE tracking_flag_id = ? AND bug_id = ?", + undef, $tf_id, $bug_id); + LogActivityEntry($bug_id, $flag_name, $value, '---', $user->id, $timestamp); + $removed++; + } } $dbh->bz_commit_transaction(); diff --git a/extensions/TrackingFlags/bin/bulk_flag_clear.pl b/extensions/TrackingFlags/bin/bulk_flag_clear.pl index 305fbf883..470269f13 100755 --- a/extensions/TrackingFlags/bin/bulk_flag_clear.pl +++ b/extensions/TrackingFlags/bin/bulk_flag_clear.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants; @@ -27,21 +27,14 @@ use Getopt::Long; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $config = {}; -GetOptions( - $config, - "trace=i", - "update_db", - "flag=s", - "modified_before=s", - "modified_after=s", - "value=s" -) or exit; +GetOptions($config, "trace=i", "update_db", "flag=s", "modified_before=s", + "modified_after=s", "value=s") + or exit; unless ($config->{flag} - && ($config->{modified_before} - || $config->{modified_after} - || $config->{value})) + && ($config->{modified_before} || $config->{modified_after} || $config->{value}) + ) { - die <check({ name => $config->{flag} }); -push @where, 'tracking_flags_bugs.tracking_flag_id = ?'; +my $flag + = Bugzilla::Extension::TrackingFlags::Flag->check({name => $config->{flag}}); +push @where, 'tracking_flags_bugs.tracking_flag_id = ?'; push @values, $flag->flag_id; if ($config->{modified_before}) { - push @where, 'bugs.delta_ts < ?'; - push @values, $config->{modified_before}; + push @where, 'bugs.delta_ts < ?'; + push @values, $config->{modified_before}; } if ($config->{modified_after}) { - push @where, 'bugs.delta_ts > ?'; - push @values, $config->{modified_after}; + push @where, 'bugs.delta_ts > ?'; + push @values, $config->{modified_after}; } if ($config->{value}) { - push @where, 'tracking_flags_bugs.value = ?'; - push @values, $config->{value}; + push @where, 'tracking_flags_bugs.value = ?'; + push @values, $config->{value}; } my $sql = " @@ -99,37 +93,35 @@ $dbh->{TraceLevel} = $config->{trace} if $config->{trace}; my $bug_ids = $dbh->selectcol_arrayref($sql, undef, @values); if (!@$bug_ids) { - die "no matching bugs found\n"; + die "no matching bugs found\n"; } if (!$config->{update_db}) { - print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n"; - print "--update_db not provided, no changes made to the database\n"; - exit; + print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n"; + print "--update_db not provided, no changes made to the database\n"; + exit; } # update bugs -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); + # put our nobody user into all groups to avoid permissions issues $nobody->{groups} = [Bugzilla::Group->get_all]; Bugzilla->set_user($nobody); foreach my $bug_id (@$bug_ids) { - print "updating bug $bug_id\n"; - $dbh->bz_start_transaction; - - # update the bug - # this will deal with history for us but not send bugmail - my $bug = Bugzilla::Bug->check({ id => $bug_id }); - $bug->set_all({ $flag->name => '---' }); - $bug->update; - - # update lastdiffed to skip bugmail for this change - $dbh->do( - "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", - undef, - $bug->id - ); - $dbh->bz_commit_transaction; + print "updating bug $bug_id\n"; + $dbh->bz_start_transaction; + + # update the bug + # this will deal with history for us but not send bugmail + my $bug = Bugzilla::Bug->check({id => $bug_id}); + $bug->set_all({$flag->name => '---'}); + $bug->update; + + # update lastdiffed to skip bugmail for this change + $dbh->do("UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", + undef, $bug->id); + $dbh->bz_commit_transaction; } diff --git a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl index cd55f5f83..97b8eccd5 100755 --- a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl +++ b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl @@ -16,8 +16,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants; @@ -39,10 +39,7 @@ use Data::Dumper; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my ($dry_run, $trace) = (0, 0); -GetOptions( - "dry-run" => \$dry_run, - "trace" => \$trace, -) or exit; +GetOptions("dry-run" => \$dry_run, "trace" => \$trace,) or exit; my $dbh = Bugzilla->dbh; @@ -52,263 +49,271 @@ my %product_cache; my %component_cache; sub migrate_flag_visibility { - my ($new_flag, $products) = @_; + my ($new_flag, $products) = @_; + + # Create product/component visibility + foreach my $prod_name (keys %$products) { + $product_cache{$prod_name} ||= Bugzilla::Product->new({name => $prod_name}); + if (!$product_cache{$prod_name}) { + warn "No such product $prod_name\n"; + next; + } - # Create product/component visibility - foreach my $prod_name (keys %$products) { - $product_cache{$prod_name} ||= Bugzilla::Product->new({ name => $prod_name }); - if (!$product_cache{$prod_name}) { - warn "No such product $prod_name\n"; - next; + # If no components specified then we do Product/__any__ + # otherwise, we enter an entry for each Product/Component + my $components = $products->{$prod_name}; + if (!@$components) { + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $new_flag->flag_id, + product_id => $product_cache{$prod_name}->id, + component_id => undef + }); + } + else { + foreach my $comp_name (@$components) { + my $comp_matches = []; + + # If the component is a regexp, we need to find all components + # matching the regex and insert each individually + if (ref $comp_name eq 'Regexp') { + my $comp_re = $comp_name; + $comp_re =~ s/\?\-xism://; + $comp_re =~ s/\(//; + $comp_re =~ s/\)//; + $comp_matches = $dbh->selectcol_arrayref( + 'SELECT components.name FROM components + WHERE components.product_id = ? + AND ' + . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . ' + ORDER BY components.name', undef, $product_cache{$prod_name}->id + ); + } + else { + $comp_matches = [$comp_name]; } - # If no components specified then we do Product/__any__ - # otherwise, we enter an entry for each Product/Component - my $components = $products->{$prod_name}; - if (!@$components) { - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $new_flag->flag_id, - product_id => $product_cache{$prod_name}->id, - component_id => undef + foreach my $comp_match (@$comp_matches) { + $component_cache{"${prod_name}:${comp_match}"} + ||= Bugzilla::Component->new({ + name => $comp_match, product => $product_cache{$prod_name} }); + if (!$component_cache{"${prod_name}:${comp_match}"}) { + warn "No such product $prod_name and component $comp_match\n"; + next; + } + + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $new_flag->flag_id, + product_id => $product_cache{$prod_name}->id, + component_id => $component_cache{"${prod_name}:${comp_match}"}->id, + }); } - else { - foreach my $comp_name (@$components) { - my $comp_matches = []; - # If the component is a regexp, we need to find all components - # matching the regex and insert each individually - if (ref $comp_name eq 'Regexp') { - my $comp_re = $comp_name; - $comp_re =~ s/\?\-xism://; - $comp_re =~ s/\(//; - $comp_re =~ s/\)//; - $comp_matches = $dbh->selectcol_arrayref( - 'SELECT components.name FROM components - WHERE components.product_id = ? - AND ' . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . ' - ORDER BY components.name', - undef, - $product_cache{$prod_name}->id); - } - else { - $comp_matches = [ $comp_name ]; - } - - foreach my $comp_match (@$comp_matches) { - $component_cache{"${prod_name}:${comp_match}"} - ||= Bugzilla::Component->new({ name => $comp_match, - product => $product_cache{$prod_name} }); - if (!$component_cache{"${prod_name}:${comp_match}"}) { - warn "No such product $prod_name and component $comp_match\n"; - next; - } - - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $new_flag->flag_id, - product_id => $product_cache{$prod_name}->id, - component_id => $component_cache{"${prod_name}:${comp_match}"}->id, - }); - } - } - } + } } + } } sub migrate_flag_values { - my ($new_flag, $field) = @_; - - print "Migrating flag values..."; - - my %blocking_trusted_requesters - = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters}; - my %blocking_trusted_setters - = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters}; - my %status_trusted_wanters - = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters}; - my %status_trusted_setters - = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters}; - - my %group_cache; - foreach my $value (@{ $field->legal_values }) { - my $group_name = 'everyone'; - - if ($field->name =~ /^cf_(blocking|tracking)_/) { - if ($value->name ne '---' && $value->name !~ '\?$') { - $group_name = get_setter_group($field->name, \%blocking_trusted_setters); - } - if ($value->name eq '?') { - $group_name = get_setter_group($field->name, \%blocking_trusted_requesters); - } - } elsif ($field->name =~ /^cf_status_/) { - if ($value->name eq 'wanted') { - $group_name = get_setter_group($field->name, \%status_trusted_wanters); - } elsif ($value->name ne '---' && $value->name ne '?') { - $group_name = get_setter_group($field->name, \%status_trusted_setters); - } - } + my ($new_flag, $field) = @_; + + print "Migrating flag values..."; + + my %blocking_trusted_requesters + = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters}; + my %blocking_trusted_setters + = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters}; + my %status_trusted_wanters + = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters}; + my %status_trusted_setters + = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters}; + + my %group_cache; + foreach my $value (@{$field->legal_values}) { + my $group_name = 'everyone'; + + if ($field->name =~ /^cf_(blocking|tracking)_/) { + if ($value->name ne '---' && $value->name !~ '\?$') { + $group_name = get_setter_group($field->name, \%blocking_trusted_setters); + } + if ($value->name eq '?') { + $group_name = get_setter_group($field->name, \%blocking_trusted_requesters); + } + } + elsif ($field->name =~ /^cf_status_/) { + if ($value->name eq 'wanted') { + $group_name = get_setter_group($field->name, \%status_trusted_wanters); + } + elsif ($value->name ne '---' && $value->name ne '?') { + $group_name = get_setter_group($field->name, \%status_trusted_setters); + } + } - $group_cache{$group_name} ||= Bugzilla::Group->new({ name => $group_name }); - $group_cache{$group_name} || die "Setter group '$group_name' does not exist"; + $group_cache{$group_name} ||= Bugzilla::Group->new({name => $group_name}); + $group_cache{$group_name} || die "Setter group '$group_name' does not exist"; - Bugzilla::Extension::TrackingFlags::Flag::Value->create({ - tracking_flag_id => $new_flag->flag_id, - value => $value->name, - setter_group_id => $group_cache{$group_name}->id, - sortkey => $value->sortkey, - is_active => $value->is_active - }); - } + Bugzilla::Extension::TrackingFlags::Flag::Value->create({ + tracking_flag_id => $new_flag->flag_id, + value => $value->name, + setter_group_id => $group_cache{$group_name}->id, + sortkey => $value->sortkey, + is_active => $value->is_active + }); + } - print "done.\n"; + print "done.\n"; } sub get_setter_group { - my ($field, $trusted) = @_; - my $setter_group = $trusted->{'_default'} || ""; - foreach my $dfield (keys %$trusted) { - if ($field =~ $dfield) { - $setter_group = $trusted->{$dfield}; - } + my ($field, $trusted) = @_; + my $setter_group = $trusted->{'_default'} || ""; + foreach my $dfield (keys %$trusted) { + if ($field =~ $dfield) { + $setter_group = $trusted->{$dfield}; } - return $setter_group; + } + return $setter_group; } sub migrate_flag_bugs { - my ($new_flag, $field) = @_; + my ($new_flag, $field) = @_; - print "Migrating bug values..."; + print "Migrating bug values..."; - my $bugs = $dbh->selectall_arrayref("SELECT bug_id, " . $field->name . " + my $bugs = $dbh->selectall_arrayref( + "SELECT bug_id, " . $field->name . " FROM bugs WHERE " . $field->name . " != '---' - ORDER BY bug_id"); - local $| = 1; - my $count = 1; - my $total = scalar @$bugs; - foreach my $row (@$bugs) { - my ($id, $value) = @$row; - indicate_progress({ current => $count++, total => $total, every => 25 }); - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $new_flag->flag_id, - bug_id => $id, - value => $value, - - }); - } - - print "done.\n"; + ORDER BY bug_id" + ); + local $| = 1; + my $count = 1; + my $total = scalar @$bugs; + foreach my $row (@$bugs) { + my ($id, $value) = @$row; + indicate_progress({current => $count++, total => $total, every => 25}); + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $new_flag->flag_id, + bug_id => $id, + value => $value, + + }); + } + + print "done.\n"; } sub migrate_flag_activity { - my ($new_flag, $field) = @_; + my ($new_flag, $field) = @_; - print "Migating flag activity..."; + print "Migating flag activity..."; - my $new_field = Bugzilla::Field->new({ name => $new_flag->name }); - $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?", - undef, $new_field->id, $field->id); + my $new_field = Bugzilla::Field->new({name => $new_flag->name}); + $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?", + undef, $new_field->id, $field->id); - print "done.\n"; + print "done.\n"; } sub do_migration { - my $bmo_tracking_flags = $Bugzilla::Extension::BMO::Data::cf_visible_in_products; - my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags; - my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags; + my $bmo_tracking_flags + = $Bugzilla::Extension::BMO::Data::cf_visible_in_products; + my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags; + my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags; - my $fields = Bugzilla::Field->match({ custom => 1, - type => FIELD_TYPE_SINGLE_SELECT }); + my $fields + = Bugzilla::Field->match({custom => 1, type => FIELD_TYPE_SINGLE_SELECT}); - my @drop_columns; - foreach my $field (@$fields) { - next if $field->name !~ /^cf_(blocking|tracking|status)_/; + my @drop_columns; + foreach my $field (@$fields) { + next if $field->name !~ /^cf_(blocking|tracking|status)_/; - foreach my $field_re (keys %$bmo_tracking_flags) { - next if $field->name !~ $field_re; + foreach my $field_re (keys %$bmo_tracking_flags) { + next if $field->name !~ $field_re; - # Create the new tracking flag if not exists - my $new_flag - = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field->name }); + # Create the new tracking flag if not exists + my $new_flag + = Bugzilla::Extension::TrackingFlags::Flag->new({name => $field->name}); - next if $new_flag; + next if $new_flag; - print "----------------------------------\n" . - "Migrating custom tracking field " . $field->name . "...\n"; + print "----------------------------------\n" + . "Migrating custom tracking field " + . $field->name . "...\n"; - my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old + my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old - my $type = grep($field->name =~ $_, @$bmo_project_flags) - ? 'project' - : 'tracking'; + my $type + = grep($field->name =~ $_, @$bmo_project_flags) ? 'project' : 'tracking'; - my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1; + my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1; - $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({ - name => $new_flag_name, - description => $field->description, - type => $type, - sortkey => $field->sortkey, - is_active => $is_active, - enter_bug => $field->enter_bug, - }); + $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({ + name => $new_flag_name, + description => $field->description, + type => $type, + sortkey => $field->sortkey, + is_active => $is_active, + enter_bug => $field->enter_bug, + }); - migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re}); + migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re}); - migrate_flag_values($new_flag, $field); + migrate_flag_values($new_flag, $field); - migrate_flag_bugs($new_flag, $field); + migrate_flag_bugs($new_flag, $field); - migrate_flag_activity($new_flag, $field); + migrate_flag_activity($new_flag, $field); - push(@drop_columns, $field->name); + push(@drop_columns, $field->name); - # Remove the old flag entry from fielddefs - $dbh->do("DELETE FROM fielddefs WHERE name = ?", - undef, $field->name); + # Remove the old flag entry from fielddefs + $dbh->do("DELETE FROM fielddefs WHERE name = ?", undef, $field->name); - # Rename the new flag - $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?", - undef, $field->name, $new_flag_name); + # Rename the new flag + $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?", + undef, $field->name, $new_flag_name); - $new_flag->set_name($field->name); - $new_flag->update; + $new_flag->set_name($field->name); + $new_flag->update; - # more than one regex could possibly match but we only want the first one - last; - } + # more than one regex could possibly match but we only want the first one + last; } + } - # Drop each custom flag's value table and the column from the bz schema object - if (!$dry_run && @drop_columns) { - print "Dropping value tables and updating bz schema object...\n"; + # Drop each custom flag's value table and the column from the bz schema object + if (!$dry_run && @drop_columns) { + print "Dropping value tables and updating bz schema object...\n"; - foreach my $column (@drop_columns) { - # Drop the values table - $dbh->bz_drop_table($column); + foreach my $column (@drop_columns) { - # Drop the bugs table column from the bz schema object - $dbh->_bz_real_schema->delete_column('bugs', $column); - $dbh->_bz_store_real_schema; - } + # Drop the values table + $dbh->bz_drop_table($column); - # Do the one alter table to drop all columns at once - $dbh->do("ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns)); + # Drop the bugs table column from the bz schema object + $dbh->_bz_real_schema->delete_column('bugs', $column); + $dbh->_bz_store_real_schema; } + + # Do the one alter table to drop all columns at once + $dbh->do( + "ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns)); + } } # Start Main eval { - if ($dry_run) { - print "** dry run : no changes to the database will be made **\n"; - $dbh->bz_start_transaction(); - } - print "Starting migration...\n"; - do_migration(); - $dbh->bz_rollback_transaction() if $dry_run; - print "All done!\n"; + if ($dry_run) { + print "** dry run : no changes to the database will be made **\n"; + $dbh->bz_start_transaction(); + } + print "Starting migration...\n"; + do_migration(); + $dbh->bz_rollback_transaction() if $dry_run; + print "All done!\n"; }; if ($@) { - $dbh->bz_rollback_transaction() if $dry_run; - die "$@" if $@; + $dbh->bz_rollback_transaction() if $dry_run; + die "$@" if $@; } diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm index 50a0e0a61..e6ff9a31a 100644 --- a/extensions/TrackingFlags/lib/Admin.pm +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -30,8 +30,8 @@ use Scalar::Util qw(blessed); use base qw(Exporter); our @EXPORT = qw( - admin_list - admin_edit + admin_list + admin_edit ); # @@ -39,177 +39,189 @@ our @EXPORT = qw( # sub admin_list { - my ($vars) = @_; - $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; - $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; + my ($vars) = @_; + $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; + $vars->{flags} = [Bugzilla::Extension::TrackingFlags::Flag->get_all()]; } sub admin_edit { - my ($vars, $page) = @_; - my $input = Bugzilla->input_params; - - $vars->{groups} = _groups_to_json(); - $vars->{mode} = $input->{mode} || 'new'; - $vars->{flag_id} = $input->{flag_id} || 0; - $vars->{tracking_flag_types} = FLAG_TYPES; - - if ($input->{delete}) { - my $token = $input->{token}; - check_hash_token($token, ['tracking_flags_edit']); - delete_token($token); - - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); - $flag->remove_from_db(); - - $vars->{message} = 'tracking_flag_deleted'; - $vars->{flag} = $flag; - $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; - - print Bugzilla->cgi->header; - my $template = Bugzilla->template; - $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - - } elsif ($input->{save}) { - my $token = $input->{token}; - check_hash_token($token, ['tracking_flags_edit']); - delete_token($token); - - my ($flag, $values, $visibilities) = _load_from_input($input, $vars); - _validate($flag, $values, $visibilities); - my $flag_obj = _update_db($flag, $values, $visibilities); - - $vars->{flag} = $flag_obj; - $vars->{values} = _flag_values_to_json($values); - $vars->{visibility} = _flag_visibility_to_json($visibilities); - - if ($vars->{mode} eq 'new') { - $vars->{message} = 'tracking_flag_created'; - } else { - $vars->{message} = 'tracking_flag_updated'; - } - $vars->{mode} = 'edit'; - - } else { - # initial load - - if ($vars->{mode} eq 'edit') { - # edit - straight load - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); - $vars->{flag} = $flag; - $vars->{values} = _flag_values_to_json($flag->values); - $vars->{visibility} = _flag_visibility_to_json($flag->visibility); - $vars->{can_delete} = !$flag->bug_count; - - } elsif ($vars->{mode} eq 'copy') { - # copy - load the source flag - $vars->{mode} = 'new'; - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); - - # increment the number at the end of the name and description - if ($flag->name =~ /^(\D+)(\d+)$/) { - $flag->set_name("$1" . ($2 + 1)); - } - if ($flag->description =~ /^(\D+)([\d\.]+)$/) { - my $description = $1; - my $version = $2; - if ($version =~ /\./) { - my ($major, $minor) = split(/\./, $version); - $minor++; - $version = "$major.$minor"; - } - else { - $version++; - } - $flag->set_description($description . $version); - } - $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); - $flag->set_type($flag->flag_type); - $flag->set_enter_bug($flag->enter_bug); - # always default new flags as active, even when copying an inactive one - $flag->set_is_active(1); - - $vars->{flag} = $flag; - $vars->{values} = _flag_values_to_json($flag->values, 1); - $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); - $vars->{can_delete} = 0; - - } else { - $vars->{mode} = 'new'; - $vars->{flag} = { - sortkey => 0, - enter_bug => 1, - is_active => 1, - }; - $vars->{values} = _flag_values_to_json([ - { - id => 0, - value => '---', - setter_group_id => '', - is_active => 1, - comment => '', - }, - ]); - $vars->{visibility} = ''; - $vars->{can_delete} = 0; - } + my ($vars, $page) = @_; + my $input = Bugzilla->input_params; + + $vars->{groups} = _groups_to_json(); + $vars->{mode} = $input->{mode} || 'new'; + $vars->{flag_id} = $input->{flag_id} || 0; + $vars->{tracking_flag_types} = FLAG_TYPES; + + if ($input->{delete}) { + my $token = $input->{token}; + check_hash_token($token, ['tracking_flags_edit']); + delete_token($token); + + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{flag_id}}); + $flag->remove_from_db(); + + $vars->{message} = 'tracking_flag_deleted'; + $vars->{flag} = $flag; + $vars->{flags} = [Bugzilla::Extension::TrackingFlags::Flag->get_all()]; + + print Bugzilla->cgi->header; + my $template = Bugzilla->template; + $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + + } + elsif ($input->{save}) { + my $token = $input->{token}; + check_hash_token($token, ['tracking_flags_edit']); + delete_token($token); + + my ($flag, $values, $visibilities) = _load_from_input($input, $vars); + _validate($flag, $values, $visibilities); + my $flag_obj = _update_db($flag, $values, $visibilities); + + $vars->{flag} = $flag_obj; + $vars->{values} = _flag_values_to_json($values); + $vars->{visibility} = _flag_visibility_to_json($visibilities); + + if ($vars->{mode} eq 'new') { + $vars->{message} = 'tracking_flag_created'; } -} + else { + $vars->{message} = 'tracking_flag_updated'; + } + $vars->{mode} = 'edit'; + + } + else { + # initial load + + if ($vars->{mode} eq 'edit') { + + # edit - straight load + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{flag_id}}); + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility); + $vars->{can_delete} = !$flag->bug_count; -sub _load_from_input { - my ($input, $vars) = @_; - - # flag - - my $flag = { - id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), - name => trim($input->{flag_name} || ''), - description => trim($input->{flag_desc} || ''), - sortkey => $input->{flag_sort} || 0, - type => trim($input->{flag_type} || ''), - enter_bug => $input->{flag_enter_bug} ? 1 : 0, - is_active => $input->{flag_active} ? 1 : 0, - }; - detaint_natural($flag->{id}); - detaint_natural($flag->{sortkey}); - detaint_natural($flag->{enter_bug}); - detaint_natural($flag->{is_active}); - - # values - - my $values = decode_json($input->{values} || '[]'); - foreach my $value (@$values) { - $value->{value} = '' unless exists $value->{value} && defined $value->{value}; - $value->{setter_group_id} = '' unless $value->{setter_group_id}; - $value->{is_active} = $value->{is_active} ? 1 : 0; } + elsif ($vars->{mode} eq 'copy') { + + # copy - load the source flag + $vars->{mode} = 'new'; + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{copy_from}}); + + # increment the number at the end of the name and description + if ($flag->name =~ /^(\D+)(\d+)$/) { + $flag->set_name("$1" . ($2 + 1)); + } + if ($flag->description =~ /^(\D+)([\d\.]+)$/) { + my $description = $1; + my $version = $2; + if ($version =~ /\./) { + my ($major, $minor) = split(/\./, $version); + $minor++; + $version = "$major.$minor"; + } + else { + $version++; + } + $flag->set_description($description . $version); + } + $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); + $flag->set_type($flag->flag_type); + $flag->set_enter_bug($flag->enter_bug); + + # always default new flags as active, even when copying an inactive one + $flag->set_is_active(1); - # vibility + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values, 1); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); + $vars->{can_delete} = 0; - my $visibilities = decode_json($input->{visibility} || '[]'); - foreach my $visibility (@$visibilities) { - $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; - $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; } + else { + $vars->{mode} = 'new'; + $vars->{flag} = {sortkey => 0, enter_bug => 1, is_active => 1,}; + $vars->{values} = _flag_values_to_json([ + { + id => 0, + value => '---', + setter_group_id => '', + is_active => 1, + comment => '', + }, + ]); + $vars->{visibility} = ''; + $vars->{can_delete} = 0; + } + } +} - return ($flag, $values, $visibilities); +sub _load_from_input { + my ($input, $vars) = @_; + + # flag + + my $flag = { + id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), + name => trim($input->{flag_name} || ''), + description => trim($input->{flag_desc} || ''), + sortkey => $input->{flag_sort} || 0, + type => trim($input->{flag_type} || ''), + enter_bug => $input->{flag_enter_bug} ? 1 : 0, + is_active => $input->{flag_active} ? 1 : 0, + }; + detaint_natural($flag->{id}); + detaint_natural($flag->{sortkey}); + detaint_natural($flag->{enter_bug}); + detaint_natural($flag->{is_active}); + + # values + + my $values = decode_json($input->{values} || '[]'); + foreach my $value (@$values) { + $value->{value} = '' unless exists $value->{value} && defined $value->{value}; + $value->{setter_group_id} = '' unless $value->{setter_group_id}; + $value->{is_active} = $value->{is_active} ? 1 : 0; + } + + # vibility + + my $visibilities = decode_json($input->{visibility} || '[]'); + foreach my $visibility (@$visibilities) { + $visibility->{product} = '' + unless exists $visibility->{product} && defined $visibility->{product}; + $visibility->{component} = '' + unless exists $visibility->{component} && defined $visibility->{component}; + } + + return ($flag, $values, $visibilities); } sub _next_unique_sortkey { - my ($sortkey) = @_; + my ($sortkey) = @_; - my %current; - foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { - $current{$flag->sortkey} = 1; - } + my %current; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { + $current{$flag->sortkey} = 1; + } - $sortkey += 5; - $sortkey += 5 while exists $current{$sortkey}; - return $sortkey; + $sortkey += 5; + $sortkey += 5 while exists $current{$sortkey}; + return $sortkey; } # @@ -217,77 +229,83 @@ sub _next_unique_sortkey { # sub _validate { - my ($flag, $values, $visibilities) = @_; - - # flag - - my @missing; - push @missing, 'Field Name' if $flag->{name} eq ''; - push @missing, 'Field Description' if $flag->{description} eq ''; - push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; - scalar(@missing) - && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); - - $flag->{name} =~ /^cf_/ - || ThrowUserError('tracking_flags_cf_prefix'); - - if ($flag->{id}) { - my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); - if ($flag->{name} ne $old_flag->name) { - Bugzilla::Field->new({ name => $flag->{name} }) - && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); - } - } else { - Bugzilla::Field->new({ name => $flag->{name} }) - && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + my ($flag, $values, $visibilities) = @_; + + # flag + + my @missing; + push @missing, 'Field Name' if $flag->{name} eq ''; + push @missing, 'Field Description' if $flag->{description} eq ''; + push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', {fields => \@missing}); + + $flag->{name} =~ /^cf_/ || ThrowUserError('tracking_flags_cf_prefix'); + + if ($flag->{id}) { + my $old_flag + = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $flag->{id}}); + if ($flag->{name} ne $old_flag->name) { + Bugzilla::Field->new({name => $flag->{name}}) + && ThrowUserError('field_already_exists', {field => {name => $flag->{name}}}); } + } + else { + Bugzilla::Field->new({name => $flag->{name}}) + && ThrowUserError('field_already_exists', {field => {name => $flag->{name}}}); + } - # values + # values - scalar(@$values) - || ThrowUserError('tracking_flags_missing_values'); + scalar(@$values) || ThrowUserError('tracking_flags_missing_values'); - my %seen; - foreach my $value (@$values) { - my $v = $value->{value}; + my %seen; + foreach my $value (@$values) { + my $v = $value->{value}; - $v eq '' - && ThrowUserError('tracking_flags_missing_value'); + $v eq '' && ThrowUserError('tracking_flags_missing_value'); - exists $seen{$v} - && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); - $seen{$v} = 1; + exists $seen{$v} + && ThrowUserError('tracking_flags_duplicate_value', {value => $v}); + $seen{$v} = 1; - push @missing, "Setter for $v" if !$value->{setter_group_id}; - } - scalar(@missing) - && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + push @missing, "Setter for $v" if !$value->{setter_group_id}; + } + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', {fields => \@missing}); - # visibility + # visibility - scalar(@$visibilities) - || ThrowUserError('tracking_flags_missing_visibility'); + scalar(@$visibilities) || ThrowUserError('tracking_flags_missing_visibility'); - %seen = (); - foreach my $visibility (@$visibilities) { - my $name = $visibility->{product} . ':' . $visibility->{component}; + %seen = (); + foreach my $visibility (@$visibilities) { + my $name = $visibility->{product} . ':' . $visibility->{component}; - exists $seen{$name} - && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); + exists $seen{$name} + && ThrowUserError('tracking_flags_duplicate_visibility', {name => $name}); - $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) - || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); + $visibility->{product_obj} + = Bugzilla::Product->new({name => $visibility->{product}}) + || ThrowCodeError('tracking_flags_invalid_product', + {product => $visibility->{product}}); - if ($visibility->{component} ne '') { - $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, - name => $visibility->{component} }) - || ThrowCodeError('tracking_flags_invalid_component', { - product => $visibility->{product}, - component_name => $visibility->{component}, - }); + if ($visibility->{component} ne '') { + $visibility->{component_obj} + = Bugzilla::Component->new({ + product => $visibility->{product_obj}, name => $visibility->{component} + }) + || ThrowCodeError( + 'tracking_flags_invalid_component', + { + product => $visibility->{product}, + component_name => $visibility->{component}, } + ); } + } } @@ -296,106 +314,115 @@ sub _validate { # sub _update_db { - my ($flag, $values, $visibilities) = @_; - my $dbh = Bugzilla->dbh; + my ($flag, $values, $visibilities) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $flag_obj = _update_db_flag($flag); - _update_db_values($flag_obj, $flag, $values); - _update_db_visibility($flag_obj, $flag, $visibilities); - $dbh->bz_commit_transaction(); + $dbh->bz_start_transaction(); + my $flag_obj = _update_db_flag($flag); + _update_db_values($flag_obj, $flag, $values); + _update_db_visibility($flag_obj, $flag, $visibilities); + $dbh->bz_commit_transaction(); - return $flag_obj; + return $flag_obj; } sub _update_db_flag { - my ($flag) = @_; + my ($flag) = @_; + + my $object_set = { + name => $flag->{name}, + description => $flag->{description}, + sortkey => $flag->{sortkey}, + type => $flag->{type}, + enter_bug => $flag->{enter_bug}, + is_active => $flag->{is_active}, + }; + + my $flag_obj; + if ($flag->{id}) { + + # update existing flag + $flag_obj + = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $flag->{id}}); + $flag_obj->set_all($object_set); + $flag_obj->update(); + + } + else { + # create new flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + } + + return $flag_obj; +} - my $object_set = { - name => $flag->{name}, - description => $flag->{description}, - sortkey => $flag->{sortkey}, - type => $flag->{type}, - enter_bug => $flag->{enter_bug}, - is_active => $flag->{is_active}, - }; +sub _update_db_values { + my ($flag_obj, $flag, $values) = @_; - my $flag_obj; - if ($flag->{id}) { - # update existing flag - $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); - $flag_obj->set_all($object_set); - $flag_obj->update(); - - } else { - # create new flag - $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + # delete + foreach my $current_value (@{$flag_obj->values}) { + if (!grep { $_->{id} == $current_value->id } @$values) { + $current_value->remove_from_db(); } + } - return $flag_obj; -} + # add/update + my $sortkey = 0; + foreach my $value (@{$values}) { + $sortkey += 10; -sub _update_db_values { - my ($flag_obj, $flag, $values) = @_; + my $object_set = { + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active}, + sortkey => $sortkey, + comment => $value->{comment}, + }; - # delete - foreach my $current_value (@{ $flag_obj->values }) { - if (!grep { $_->{id} == $current_value->id } @$values) { - $current_value->remove_from_db(); - } + if ($value->{id}) { + my $value_obj + = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag value', id => $flag->{id}}); + my $old_value = $value_obj->value; + $value_obj->set_all($object_set); + $value_obj->update(); + Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ + value_obj => $value_obj, + old_value => $old_value, + new_value => $value_obj->value, + }); } - - # add/update - my $sortkey = 0; - foreach my $value (@{ $values }) { - $sortkey += 10; - - my $object_set = { - value => $value->{value}, - setter_group_id => $value->{setter_group_id}, - is_active => $value->{is_active}, - sortkey => $sortkey, - comment => $value->{comment}, - }; - - if ($value->{id}) { - my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); - my $old_value = $value_obj->value; - $value_obj->set_all($object_set); - $value_obj->update(); - Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ - value_obj => $value_obj, - old_value => $old_value, - new_value => $value_obj->value, - }); - } else { - $object_set->{tracking_flag_id} = $flag_obj->flag_id; - Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); - } + else { + $object_set->{tracking_flag_id} = $flag_obj->flag_id; + Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); } + } } sub _update_db_visibility { - my ($flag_obj, $flag, $visibilities) = @_; + my ($flag_obj, $flag, $visibilities) = @_; - # delete - foreach my $current_visibility (@{ $flag_obj->visibility }) { - if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { - $current_visibility->remove_from_db(); - } - } - - # add - foreach my $visibility (@{ $visibilities }) { - next if $visibility->{id}; - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $flag_obj->flag_id, - product_id => $visibility->{product_obj}->id, - component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, - }); + # delete + foreach my $current_visibility (@{$flag_obj->visibility}) { + if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { + $current_visibility->remove_from_db(); } + } + + # add + foreach my $visibility (@{$visibilities}) { + next if $visibility->{id}; + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $flag_obj->flag_id, + product_id => $visibility->{product_obj}->id, + component_id => $visibility->{component} + ? $visibility->{component_obj}->id + : undef, + }); + } } # @@ -403,62 +430,66 @@ sub _update_db_visibility { # sub _groups_to_json { - my @data; - foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { - push @data, { - id => $group->id, - name => $group->name, - }; - } - return encode_json(\@data); + my @data; + foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { + push @data, {id => $group->id, name => $group->name,}; + } + return encode_json(\@data); } sub _flag_values_to_json { - my ($values, $is_copy) = @_; - # setting is_copy will set the id's to zero, to force new values rather - # than editing existing ones - my @data; - foreach my $value (@$values) { - push @data, { - id => $is_copy ? 0 : $value->{id}, - value => $value->{value}, - setter_group_id => $value->{setter_group_id}, - is_active => $value->{is_active} ? JSON::true : JSON::false, - comment => $value->{comment} // '', - }; - } - return encode_json(\@data); + my ($values, $is_copy) = @_; + + # setting is_copy will set the id's to zero, to force new values rather + # than editing existing ones + my @data; + foreach my $value (@$values) { + push @data, + { + id => $is_copy ? 0 : $value->{id}, + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active} ? JSON::true : JSON::false, + comment => $value->{comment} // '', + }; + } + return encode_json(\@data); } sub _flag_visibility_to_json { - my ($visibilities, $is_copy) = @_; - # setting is_copy will set the id's to zero, to force new visibilites - # rather than editing existing ones - my @data; - - foreach my $visibility (@$visibilities) { - my $product = exists $visibility->{product_id} - ? $visibility->product->name - : $visibility->{product}; - my $component; - if (exists $visibility->{component_id} && $visibility->{component_id}) { - $component = $visibility->component->name; - } elsif (exists $visibility->{component}) { - $component = $visibility->{component}; - } else { - $component = undef; - } - push @data, { - id => $is_copy ? 0 : $visibility->{id}, - product => $product, - component => $component, - }; + my ($visibilities, $is_copy) = @_; + + # setting is_copy will set the id's to zero, to force new visibilites + # rather than editing existing ones + my @data; + + foreach my $visibility (@$visibilities) { + my $product + = exists $visibility->{product_id} + ? $visibility->product->name + : $visibility->{product}; + my $component; + if (exists $visibility->{component_id} && $visibility->{component_id}) { + $component = $visibility->component->name; + } + elsif (exists $visibility->{component}) { + $component = $visibility->{component}; + } + else { + $component = undef; } - @data = sort { - lc($a->{product}) cmp lc($b->{product}) - || lc($a->{component}) cmp lc($b->{component}) - } @data; - return encode_json(\@data); + push @data, + { + id => $is_copy ? 0 : $visibility->{id}, + product => $product, + component => $component, + }; + } + @data = sort { + lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}) + } @data; + return encode_json(\@data); } 1; diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm index 00827aa7a..842e7501d 100644 --- a/extensions/TrackingFlags/lib/Constants.pm +++ b/extensions/TrackingFlags/lib/Constants.pm @@ -14,31 +14,31 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAG_TYPES + FLAG_TYPES ); sub FLAG_TYPES { - my @flag_types = ( - { - name => 'project', - description => 'Project Flags', - collapsed => 0, - sortkey => 0 - }, - { - name => 'tracking', - description => 'Tracking Flags', - collapsed => 1, - sortkey => 1 - }, - { - name => 'blocking', - description => 'Blocking Flags', - collapsed => 1, - sortkey => 2 - }, - ); - return [ sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types ]; + my @flag_types = ( + { + name => 'project', + description => 'Project Flags', + collapsed => 0, + sortkey => 0 + }, + { + name => 'tracking', + description => 'Tracking Flags', + collapsed => 1, + sortkey => 1 + }, + { + name => 'blocking', + description => 'Blocking Flags', + collapsed => 1, + sortkey => 2 + }, + ); + return [sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types]; } 1; diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm index 82c0314e3..c1ecbc4ad 100644 --- a/extensions/TrackingFlags/lib/Flag.pm +++ b/extensions/TrackingFlags/lib/Flag.pm @@ -30,43 +30,43 @@ use Bugzilla::Extension::TrackingFlags::Flag::Visibility; use constant DB_TABLE => 'tracking_flags'; use constant DB_COLUMNS => qw( - id - field_id - name - description - type - sortkey - enter_bug - is_active + id + field_id + name + description + type + sortkey + enter_bug + is_active ); use constant LIST_ORDER => 'sortkey'; use constant UPDATE_COLUMNS => qw( - name - description - type - sortkey - enter_bug - is_active + name + description + type + sortkey + enter_bug + is_active ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - type => \&_check_type, - sortkey => \&_check_sortkey, - enter_bug => \&Bugzilla::Object::check_boolean, - is_active => \&Bugzilla::Object::check_boolean, + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, }; use constant UPDATE_VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - type => \&_check_type, - sortkey => \&_check_sortkey, - enter_bug => \&Bugzilla::Object::check_boolean, - is_active => \&Bugzilla::Object::check_boolean, + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, }; ############################### @@ -74,257 +74,260 @@ use constant UPDATE_VALIDATORS => { ############################### sub new { - my $class = shift; - my $param = shift; - my $cache = Bugzilla->request_cache; - - if (!ref $param - && exists $cache->{'tracking_flags'} - && exists $cache->{'tracking_flags'}->{$param}) - { - return $cache->{'tracking_flags'}->{$param}; - } - - return $class->SUPER::new($param); + my $class = shift; + my $param = shift; + my $cache = Bugzilla->request_cache; + + if (!ref $param + && exists $cache->{'tracking_flags'} + && exists $cache->{'tracking_flags'}->{$param}) + { + return $cache->{'tracking_flags'}->{$param}; + } + + return $class->SUPER::new($param); } sub new_from_hash { - my $class = shift; - my $cache = Bugzilla->request_cache->{'tracking_flags'} //= {}; - my $flag = $class->SUPER::new_from_hash(@_); - if ($flag) { - push @Bugzilla::Extension::TrackingFlags::FLAG_CACHE, $flag; - } - return $flag; + my $class = shift; + my $cache = Bugzilla->request_cache->{'tracking_flags'} //= {}; + my $flag = $class->SUPER::new_from_hash(@_); + if ($flag) { + push @Bugzilla::Extension::TrackingFlags::FLAG_CACHE, $flag; + } + return $flag; } sub create { - my $class = shift; - my $params = shift; - my $dbh = Bugzilla->dbh; - my $flag; - - # Disable bug updates temporarily to avoid conflicts. - SetParam('disable_bug_updates', 1); - write_params(); - - eval { - $dbh->bz_start_transaction(); - - $params = $class->run_create_validators($params); - - # We have to create an entry for this new flag - # in the fielddefs table for use elsewhere. We cannot - # use Bugzilla::Field->create as it will create the - # additional tables needed by custom fields which we - # do not need. Also we do this so as not to add a - # another column to the bugs table. - # We will create the entry as a custom field with a - # type of FIELD_TYPE_EXTENSION so Bugzilla will skip - # these field types in certain parts of the core code. - $dbh->do("INSERT INTO fielddefs + my $class = shift; + my $params = shift; + my $dbh = Bugzilla->dbh; + my $flag; + + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); + + eval { + $dbh->bz_start_transaction(); + + $params = $class->run_create_validators($params); + + # We have to create an entry for this new flag + # in the fielddefs table for use elsewhere. We cannot + # use Bugzilla::Field->create as it will create the + # additional tables needed by custom fields which we + # do not need. Also we do this so as not to add a + # another column to the bugs table. + # We will create the entry as a custom field with a + # type of FIELD_TYPE_EXTENSION so Bugzilla will skip + # these field types in certain parts of the core code. + $dbh->do( + "INSERT INTO fielddefs (name, description, sortkey, type, custom, obsolete, buglist) VALUES - (?, ?, ?, ?, ?, ?, ?)", - undef, - $params->{'name'}, - $params->{'description'}, - $params->{'sortkey'}, - FIELD_TYPE_EXTENSION, - 1, 0, 1); - $params->{'field_id'} = $dbh->bz_last_key; - - $flag = $class->SUPER::create($params); - - $dbh->bz_commit_transaction(); - }; - my $error = "$@"; - SetParam('disable_bug_updates', 0); - write_params(); - die $error if $error; + (?, ?, ?, ?, ?, ?, ?)", undef, $params->{'name'}, + $params->{'description'}, $params->{'sortkey'}, FIELD_TYPE_EXTENSION, 1, 0, 1 + ); + $params->{'field_id'} = $dbh->bz_last_key; + + $flag = $class->SUPER::create($params); - # fielddefs has been changed so we need to clear global config - Bugzilla->memcached->clear_config(); + $dbh->bz_commit_transaction(); + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; - return $flag; + # fielddefs has been changed so we need to clear global config + Bugzilla->memcached->clear_config(); + + return $flag; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $old_self = $self->new($self->flag_id); + my $old_self = $self->new($self->flag_id); - # HACK! Bugzilla::Object::update uses hardcoded $self->id - # instead of $self->{ID_FIELD} so we need to reverse field_id - # and the real id temporarily - my $field_id = $self->id; - $self->{'field_id'} = $self->{'id'}; + # HACK! Bugzilla::Object::update uses hardcoded $self->id + # instead of $self->{ID_FIELD} so we need to reverse field_id + # and the real id temporarily + my $field_id = $self->id; + $self->{'field_id'} = $self->{'id'}; - my $changes = $self->SUPER::update(@_); + my $changes = $self->SUPER::update(@_); - $self->{'field_id'} = $field_id; + $self->{'field_id'} = $field_id; - # Update the fielddefs entry - $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", - undef, - $self->name, $self->description, $old_self->name); + # Update the fielddefs entry + $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", + undef, $self->name, $self->description, $old_self->name); - # Update request_cache - my $cache = Bugzilla->request_cache; - if (exists $cache->{'tracking_flags'}) { - $cache->{'tracking_flags'}->{$self->flag_id} = $self; - } + # Update request_cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + $cache->{'tracking_flags'}->{$self->flag_id} = $self; + } - # fielddefs has been changed so we need to clear global config - Bugzilla->memcached->clear_config(); + # fielddefs has been changed so we need to clear global config + Bugzilla->memcached->clear_config(); - return $changes; + return $changes; } sub match { - my $class = shift; - my ($params) = @_; - - # Use later for preload - my $bug_id = delete $params->{'bug_id'}; - - # Retrieve all flags relevant for the given product and component - if (!exists $params->{'id'} - && ($params->{'component'} || $params->{'component_id'} - || $params->{'product'} || $params->{'product_id'})) - { - my $visible_flags - = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); - my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; - - delete $params->{'component'} if exists $params->{'component'}; - delete $params->{'component_id'} if exists $params->{'component_id'}; - delete $params->{'product'} if exists $params->{'product'}; - delete $params->{'product_id'} if exists $params->{'product_id'}; - - $params->{'id'} = \@flag_ids; - } - - # We need to return inactive flags if a value has been set - my $is_active_filter = delete $params->{is_active}; - - my $flags = $class->SUPER::match($params); - preload_all_the_things($flags, { bug_id => $bug_id }); - - if ($is_active_filter) { - $flags = [ grep { $_->is_active || exists $_->{bug_flag} } @$flags ]; - } - return [ sort { $a->sortkey <=> $b->sortkey } @$flags ]; + my $class = shift; + my ($params) = @_; + + # Use later for preload + my $bug_id = delete $params->{'bug_id'}; + + # Retrieve all flags relevant for the given product and component + if ( + !exists $params->{'id'} + && ( $params->{'component'} + || $params->{'component_id'} + || $params->{'product'} + || $params->{'product_id'}) + ) + { + my $visible_flags + = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); + my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; + + delete $params->{'component'} if exists $params->{'component'}; + delete $params->{'component_id'} if exists $params->{'component_id'}; + delete $params->{'product'} if exists $params->{'product'}; + delete $params->{'product_id'} if exists $params->{'product_id'}; + + $params->{'id'} = \@flag_ids; + } + + # We need to return inactive flags if a value has been set + my $is_active_filter = delete $params->{is_active}; + + my $flags = $class->SUPER::match($params); + preload_all_the_things($flags, {bug_id => $bug_id}); + + if ($is_active_filter) { + $flags = [grep { $_->is_active || exists $_->{bug_flag} } @$flags]; + } + return [sort { $a->sortkey <=> $b->sortkey } @$flags]; } sub get_all { - my $self = shift; - my $cache = Bugzilla->request_cache; - if (!exists $cache->{'tracking_flags'}) { - my @tracking_flags = $self->SUPER::get_all(@_); - preload_all_the_things(\@tracking_flags); - my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; - $cache->{'tracking_flags'} = \%tracking_flags_hash; - } - return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } - values %{ $cache->{'tracking_flags'} }; + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags'}) { + my @tracking_flags = $self->SUPER::get_all(@_); + preload_all_the_things(\@tracking_flags); + my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; + $cache->{'tracking_flags'} = \%tracking_flags_hash; + } + return + sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } + values %{$cache->{'tracking_flags'}}; } # avoids the overhead of pre-loading if just the field names are required sub get_all_names { - my $self = shift; - my $cache = Bugzilla->request_cache; - if (!exists $cache->{'tracking_flags_names'}) { - $cache->{'tracking_flags_names'} = - Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags ORDER BY name"); - } - return @{ $cache->{'tracking_flags_names'} }; + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags_names'}) { + $cache->{'tracking_flags_names'} = Bugzilla->dbh->selectcol_arrayref( + "SELECT name FROM tracking_flags ORDER BY name"); + } + return @{$cache->{'tracking_flags_names'}}; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - # Check to see if tracking_flags_bugs table has records - if ($self->bug_count) { - ThrowUserError('tracking_flag_has_contents', { flag => $self }); - } + # Check to see if tracking_flags_bugs table has records + if ($self->bug_count) { + ThrowUserError('tracking_flag_has_contents', {flag => $self}); + } - # Disable bug updates temporarily to avoid conflicts. - SetParam('disable_bug_updates', 1); - write_params(); + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); - eval { - $dbh->bz_start_transaction(); + eval { + $dbh->bz_start_transaction(); - $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); - $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); + $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); + $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Remove from request cache - my $cache = Bugzilla->request_cache; - if (exists $cache->{'tracking_flags'}) { - delete $cache->{'tracking_flags'}->{$self->flag_id}; - } - }; - my $error = "$@"; - SetParam('disable_bug_updates', 0); - write_params(); - die $error if $error; + # Remove from request cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + delete $cache->{'tracking_flags'}->{$self->flag_id}; + } + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; } sub preload_all_the_things { - my ($flags, $params) = @_; + my ($flags, $params) = @_; - my %flag_hash = map { $_->flag_id => $_ } @$flags; - my @flag_ids = keys %flag_hash; - return unless @flag_ids; + my %flag_hash = map { $_->flag_id => $_ } @$flags; + my @flag_ids = keys %flag_hash; + return unless @flag_ids; - # Preload values - my $value_objects - = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids }); + # Preload values + my $value_objects = Bugzilla::Extension::TrackingFlags::Flag::Value->match( + {tracking_flag_id => \@flag_ids}); - # Now populate the tracking flags with this set of value objects. - foreach my $obj (@$value_objects) { - my $flag_id = $obj->tracking_flag_id; + # Now populate the tracking flags with this set of value objects. + foreach my $obj (@$value_objects) { + my $flag_id = $obj->tracking_flag_id; - # Prepopulate the tracking flag object in the value object - $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + # Prepopulate the tracking flag object in the value object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; - # Prepopulate the current value objects for this tracking flag - $flag_hash{$flag_id}->{'values'} ||= []; - push(@{$flag_hash{$flag_id}->{'values'}}, $obj); - } + # Prepopulate the current value objects for this tracking flag + $flag_hash{$flag_id}->{'values'} ||= []; + push(@{$flag_hash{$flag_id}->{'values'}}, $obj); + } - # Preload bug values if a bug_id is passed - if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { - # We don't want to use @flag_ids here as we want all flags attached to this bug - # even if they are inactive. - my $bug_objects - = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} }); - # Now populate the tracking flags with this set of objects. - # Also we add them to the flag hash since we want them to be visible even if - # they are not longer applicable to this product/component. - foreach my $obj (@$bug_objects) { - my $flag_id = $obj->tracking_flag_id; - - # Load the flag object if it does not yet exist. - # This can happen if the bug value tracking flag - # is no longer visible for the product/component - $flag_hash{$flag_id} - ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); - - # Prepopulate the tracking flag object in the bug flag object - $obj->{'tracking_flag'} = $flag_hash{$flag_id}; - - # Prepopulate the the current bug flag object for the tracking flag - $flag_hash{$flag_id}->{'bug_flag'} = $obj; - } + # Preload bug values if a bug_id is passed + if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { + + # We don't want to use @flag_ids here as we want all flags attached to this bug + # even if they are inactive. + my $bug_objects = Bugzilla::Extension::TrackingFlags::Flag::Bug->match( + {bug_id => $params->{'bug_id'}}); + + # Now populate the tracking flags with this set of objects. + # Also we add them to the flag hash since we want them to be visible even if + # they are not longer applicable to this product/component. + foreach my $obj (@$bug_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Load the flag object if it does not yet exist. + # This can happen if the bug value tracking flag + # is no longer visible for the product/component + $flag_hash{$flag_id} + ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); + + # Prepopulate the tracking flag object in the bug flag object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the the current bug flag object for the tracking flag + $flag_hash{$flag_id}->{'bug_flag'} = $obj; } + } - @$flags = values %flag_hash; + @$flags = values %flag_hash; } ############################### @@ -332,125 +335,126 @@ sub preload_all_the_things { ############################### sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowCodeError('param_required', { param => 'name' }); - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowCodeError('param_required', {param => 'name'}); + return $name; } sub _check_description { - my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowCodeError( 'param_required', { param => 'description' } ); - return $description; + my ($invocant, $description) = @_; + $description = trim($description); + $description || ThrowCodeError('param_required', {param => 'description'}); + return $description; } sub _check_type { - my ($invocant, $type) = @_; - $type = trim($type); - $type || ThrowCodeError( 'param_required', { param => 'type' } ); - grep($_->{name} eq $type, @{FLAG_TYPES()}) - || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type }); - return $type; + my ($invocant, $type) = @_; + $type = trim($type); + $type || ThrowCodeError('param_required', {param => 'type'}); + grep($_->{name} eq $type, @{FLAG_TYPES()}) + || ThrowUserError('tracking_flags_invalid_flag_type', {type => $type}); + return $type; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $sortkey}); + return $sortkey; } ############################### #### Setters #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_type { $_[0]->set('type', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } ############################### #### Accessors #### ############################### -sub flag_id { return $_[0]->{'id'}; } -sub name { return $_[0]->{'name'}; } +sub flag_id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } sub description { return $_[0]->{'description'}; } -sub flag_type { return $_[0]->{'type'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub enter_bug { return $_[0]->{'enter_bug'}; } -sub is_active { return $_[0]->{'is_active'}; } +sub flag_type { return $_[0]->{'type'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub enter_bug { return $_[0]->{'enter_bug'}; } +sub is_active { return $_[0]->{'is_active'}; } sub values { - return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({ - tracking_flag_id => $_[0]->flag_id - }); + return $_[0]->{'values'} + ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match( + {tracking_flag_id => $_[0]->flag_id}); } sub visibility { - return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({ - tracking_flag_id => $_[0]->flag_id - }); + return $_[0]->{'visibility'} + ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match( + {tracking_flag_id => $_[0]->flag_id}); } sub can_set_value { - my ($self, $new_value, $user) = @_; - $user ||= Bugzilla->user; - my $new_value_obj; - foreach my $value (@{$self->values}) { - if ($value->value eq $new_value) { - $new_value_obj = $value; - last; - } + my ($self, $new_value, $user) = @_; + $user ||= Bugzilla->user; + my $new_value_obj; + foreach my $value (@{$self->values}) { + if ($value->value eq $new_value) { + $new_value_obj = $value; + last; } - return $new_value_obj - && $new_value_obj->setter_group - && $user->in_group($new_value_obj->setter_group->name) - ? 1 - : 0; + } + return + $new_value_obj + && $new_value_obj->setter_group + && $user->in_group($new_value_obj->setter_group->name) ? 1 : 0; } sub bug_flag { - my ($self, $bug_id) = @_; - # Return the current bug value object if defined unless the passed bug_id does - # not equal the current bug value objects id. - if (defined $self->{'bug_flag'} - && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) - { - return $self->{'bug_flag'}; - } - - # Flag::Bug->new will return a default bug value object if $params undefined - my $params = !$bug_id - ? undef - : { condition => "tracking_flag_id = ? AND bug_id = ?", - values => [ $self->flag_id, $bug_id ] }; - return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); + my ($self, $bug_id) = @_; + + # Return the current bug value object if defined unless the passed bug_id does + # not equal the current bug value objects id. + if (defined $self->{'bug_flag'} + && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) + { + return $self->{'bug_flag'}; + } + + # Flag::Bug->new will return a default bug value object if $params undefined + my $params = !$bug_id + ? undef + : { + condition => "tracking_flag_id = ? AND bug_id = ?", + values => [$self->flag_id, $bug_id] + }; + return $self->{'bug_flag'} + = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); } sub bug_count { - my ($self) = @_; - return $self->{'bug_count'} if defined $self->{'bug_count'}; - my $dbh = Bugzilla->dbh; - return $self->{'bug_count'} = scalar $dbh->selectrow_array(" + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'bug_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM tracking_flags_bugs - WHERE tracking_flag_id = ?", - undef, $self->flag_id); + WHERE tracking_flag_id = ?", undef, $self->flag_id); } sub activity_count { - my ($self) = @_; - return $self->{'activity_count'} if defined $self->{'activity_count'}; - my $dbh = Bugzilla->dbh; - return $self->{'activity_count'} = scalar $dbh->selectrow_array(" + my ($self) = @_; + return $self->{'activity_count'} if defined $self->{'activity_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'activity_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM bugs_activity - WHERE fieldid = ?", - undef, $self->id); + WHERE fieldid = ?", undef, $self->id); } ###################################### @@ -460,25 +464,25 @@ sub activity_count { # Here we return 'field_id' instead of the real # id as we want other Bugzilla code to treat this # as a Bugzilla::Field object in certain places. -sub id { return $_[0]->{'field_id'}; } +sub id { return $_[0]->{'field_id'}; } sub type { return FIELD_TYPE_EXTENSION; } -sub legal_values { return $_[0]->values; } -sub custom { return 1; } -sub in_new_bugmail { return 1; } +sub legal_values { return $_[0]->values; } +sub custom { return 1; } +sub in_new_bugmail { return 1; } sub obsolete { return $_[0]->is_active ? 0 : 1; } -sub buglist { return 1; } -sub is_select { return 1; } -sub is_abnormal { return 1; } -sub is_timetracking { return 0; } +sub buglist { return 1; } +sub is_select { return 1; } +sub is_abnormal { return 1; } +sub is_timetracking { return 0; } sub visibility_field { return undef; } sub visibility_values { return undef; } sub controls_visibility_of { return undef; } sub value_field { return undef; } sub controls_values_of { return undef; } -sub is_visible_on_bug { return 1; } -sub is_relationship { return 0; } -sub reverse_desc { return ''; } -sub is_mandatory { return 0; } -sub is_numeric { return 0; } +sub is_visible_on_bug { return 1; } +sub is_relationship { return 0; } +sub reverse_desc { return ''; } +sub is_mandatory { return 0; } +sub is_numeric { return 0; } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm index 7be661720..9d9c5ce8c 100644 --- a/extensions/TrackingFlags/lib/Flag/Bug.pm +++ b/extensions/TrackingFlags/lib/Flag/Bug.pm @@ -24,32 +24,26 @@ use Scalar::Util qw(blessed); #### Initialization #### ############################### -use constant DEFAULT_FLAG_BUG => { - 'id' => 0, - 'tracking_flag_id' => 0, - 'bug_id' => 0, - 'value' => '---', -}; +use constant DEFAULT_FLAG_BUG => + {'id' => 0, 'tracking_flag_id' => 0, 'bug_id' => 0, 'value' => '---',}; use constant DB_TABLE => 'tracking_flags_bugs'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - bug_id - value + id + tracking_flag_id + bug_id + value ); use constant LIST_ORDER => 'id'; use constant UPDATE_COLUMNS => qw( - value + value ); -use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - value => \&_check_value, -}; +use constant VALIDATORS => + {tracking_flag_id => \&_check_tracking_flag, value => \&_check_value,}; use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -60,67 +54,67 @@ use constant AUDIT_REMOVES => 0; ############################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; - - my $self; - if ($param) { - $self = $class->SUPER::new(@_); - if (!$self) { - $self = DEFAULT_FLAG_BUG; - bless($self, $class); - } - } - else { - $self = DEFAULT_FLAG_BUG; - bless($self, $class); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $self; + if ($param) { + $self = $class->SUPER::new(@_); + if (!$self) { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); } + } + else { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } - return $self + return $self; } sub match { - my $class = shift; - my $bug_flags = $class->SUPER::match(@_); - preload_all_the_things($bug_flags); - return $bug_flags; + my $class = shift; + my $bug_flags = $class->SUPER::match(@_); + preload_all_the_things($bug_flags); + return $bug_flags; } sub remove_from_db { - my ($self) = @_; - $self->SUPER::remove_from_db(); - $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; - $self->{'value'} = '---'; + my ($self) = @_; + $self->SUPER::remove_from_db(); + $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; + $self->{'value'} = '---'; } sub preload_all_the_things { - my ($bug_flags) = @_; - my $cache = Bugzilla->request_cache; - - # Preload tracking flag objects - my @tracking_flag_ids; - foreach my $bug_flag (@$bug_flags) { - if (exists $cache->{'tracking_flags'} - && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) - { - $bug_flag->{'tracking_flag'} - = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; - next; - } - push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + my ($bug_flags) = @_; + my $cache = Bugzilla->request_cache; + + # Preload tracking flag objects + my @tracking_flag_ids; + foreach my $bug_flag (@$bug_flags) { + if (exists $cache->{'tracking_flags'} + && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) + { + $bug_flag->{'tracking_flag'} + = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; + next; } + push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + } - return unless @tracking_flag_ids; + return unless @tracking_flag_ids; - my $tracking_flags - = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids }); - my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; + my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match( + {id => \@tracking_flag_ids}); + my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; - foreach my $bug_flag (@$bug_flags) { - next if exists $bug_flag->{'tracking_flag'}; - $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; - } + foreach my $bug_flag (@$bug_flags) { + next if exists $bug_flag->{'tracking_flag'}; + $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; + } } ############################## @@ -128,15 +122,15 @@ sub preload_all_the_things { ############################## sub update_all_values { - my ($invocant, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do( - "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?", - undef, - $params->{new_value}, - $params->{value_obj}->tracking_flag_id, - $params->{old_value}, - ); + my ($invocant, $params) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?", + undef, + $params->{new_value}, + $params->{value_obj}->tracking_flag_id, + $params->{old_value}, + ); } ############################### @@ -144,19 +138,21 @@ sub update_all_values { ############################### sub _check_value { - my ($invocant, $value) = @_; - $value || ThrowCodeError('param_required', { param => 'value' }); - return $value; + my ($invocant, $value) = @_; + $value || ThrowCodeError('param_required', {param => 'value'}); + return $value; } sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({id => $flag, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } ############################### @@ -170,19 +166,17 @@ sub set_value { $_[0]->set('value', $_[1]); } ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -sub value { return $_[0]->{'value'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub value { return $_[0]->{'value'}; } sub bug { - return $_[0]->{'bug'} ||= Bugzilla::Bug->new({ - id => $_[0]->bug_id, cache => 1 - }); + return $_[0]->{'bug'} ||= Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); } sub tracking_flag { - return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ - id => $_[0]->tracking_flag_id, cache => 1 - }); + return $_[0]->{'tracking_flag'} + ||= Bugzilla::Extension::TrackingFlags::Flag->new( + {id => $_[0]->tracking_flag_id, cache => 1}); } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm index 4f2aacc3a..52d63970d 100644 --- a/extensions/TrackingFlags/lib/Flag/Value.pm +++ b/extensions/TrackingFlags/lib/Flag/Value.pm @@ -25,32 +25,32 @@ use Scalar::Util qw(blessed weaken); use constant DB_TABLE => 'tracking_flags_values'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - setter_group_id - value - sortkey - is_active - comment + id + tracking_flag_id + setter_group_id + value + sortkey + is_active + comment ); use constant LIST_ORDER => 'sortkey'; use constant UPDATE_COLUMNS => qw( - setter_group_id - value - sortkey - is_active - comment + setter_group_id + value + sortkey + is_active + comment ); use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - setter_group_id => \&_check_setter_group, - value => \&_check_value, - sortkey => \&_check_sortkey, - is_active => \&Bugzilla::Object::check_boolean, - comment => \&_check_comment, + tracking_flag_id => \&_check_tracking_flag, + setter_group_id => \&_check_setter_group, + value => \&_check_value, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + comment => \&_check_comment, }; ############################### @@ -58,43 +58,47 @@ use constant VALIDATORS => { ############################### sub _check_value { - my ($invocant, $value) = @_; - defined $value || ThrowCodeError('param_required', { param => 'value' }); - return $value; + my ($invocant, $value) = @_; + defined $value || ThrowCodeError('param_required', {param => 'value'}); + return $value; } sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({id => $flag, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } sub _check_setter_group { - my ($invocant, $group) = @_; - if (blessed $group) { - return $group->id; - } - $group = Bugzilla::Group->new({ id => $group, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group }); + my ($invocant, $group) = @_; + if (blessed $group) { return $group->id; + } + $group + = Bugzilla::Group->new({id => $group, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'setter_group_id', value => $group}); + return $group->id; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $sortkey}); + return $sortkey; } sub _check_comment { - my ($invocant, $value) = @_; - return undef unless defined $value; - $value = trim($value); - return $value eq '' ? undef : $value; + my ($invocant, $value) = @_; + return undef unless defined $value; + $value = trim($value); + return $value eq '' ? undef : $value; } ############################### @@ -102,38 +106,37 @@ sub _check_comment { ############################### sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); } -sub set_value { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } -sub set_comment { $_[0]->set('comment', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_comment { $_[0]->set('comment', $_[1]); } ############################### #### Accessors #### ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub setter_group_id { return $_[0]->{'setter_group_id'}; } -sub value { return $_[0]->{'value'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub is_active { return $_[0]->{'is_active'}; } -sub comment { return $_[0]->{'comment'}; } +sub setter_group_id { return $_[0]->{'setter_group_id'}; } +sub value { return $_[0]->{'value'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'is_active'}; } +sub comment { return $_[0]->{'comment'}; } sub tracking_flag { - return $_[0]->{'tracking_flag'} if $_[0]->{'tracking_flag'}; - my $tf = $_[0]->{'tracking_flag'} = Bugzilla::Extension::TrackingFlags::Flag->new({ - id => $_[0]->tracking_flag_id, cache => 1 - }); - weaken($_[0]->{'tracking_flag'}); - return $tf; + return $_[0]->{'tracking_flag'} if $_[0]->{'tracking_flag'}; + my $tf = $_[0]->{'tracking_flag'} + = Bugzilla::Extension::TrackingFlags::Flag->new( + {id => $_[0]->tracking_flag_id, cache => 1}); + weaken($_[0]->{'tracking_flag'}); + return $tf; } sub setter_group { - if ($_[0]->setter_group_id) { - $_[0]->{'setter_group'} ||= Bugzilla::Group->new({ - id => $_[0]->setter_group_id, cache => 1 - }); - } - return $_[0]->{'setter_group'}; + if ($_[0]->setter_group_id) { + $_[0]->{'setter_group'} + ||= Bugzilla::Group->new({id => $_[0]->setter_group_id, cache => 1}); + } + return $_[0]->{'setter_group'}; } ######################################## @@ -141,6 +144,6 @@ sub setter_group { ######################################## sub name { return $_[0]->{'value'}; } -sub is_visible_on_bug { return 1; } +sub is_visible_on_bug { return 1; } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm index 878c16f99..e90c0bf22 100644 --- a/extensions/TrackingFlags/lib/Flag/Visibility.pm +++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm @@ -25,20 +25,20 @@ use Scalar::Util qw(blessed); use constant DB_TABLE => 'tracking_flags_visibility'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - product_id - component_id + id + tracking_flag_id + product_id + component_id ); use constant LIST_ORDER => 'id'; -use constant UPDATE_COLUMNS => (); # imutable +use constant UPDATE_COLUMNS => (); # imutable use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - product_id => \&_check_product, - component_id => \&_check_component, + tracking_flag_id => \&_check_tracking_flag, + product_id => \&_check_product, + component_id => \&_check_component, }; ############################### @@ -46,66 +46,72 @@ use constant VALIDATORS => { ############################### sub match { - my $class= shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - # Allow matching component and product by name - # (in addition to matching by ID). - # Borrowed from Bugzilla::Bug::match - my %translate_fields = ( - product => 'Bugzilla::Product', - component => 'Bugzilla::Component', - ); - - foreach my $field (keys %translate_fields) { - my @ids; - # Convert names to ids. We use "exists" everywhere since people can - # legally specify "undef" to mean IS NULL - if (exists $params->{$field}) { - my $names = $params->{$field}; - my $type = $translate_fields{$field}; - my $objects = Bugzilla::Object::match($type, { name => $names }); - push(@ids, map { $_->id } @$objects); - } - # You can also specify ids directly as arguments to this function, - # so include them in the list if they have been specified. - if (exists $params->{"${field}_id"}) { - my $current_ids = $params->{"${field}_id"}; - my @id_array = ref $current_ids ? @$current_ids : ($current_ids); - push(@ids, @id_array); - } - # We do this "or" instead of a "scalar(@ids)" to handle the case - # when people passed only invalid object names. Otherwise we'd - # end up with a SUPER::match call with zero criteria (which dies). - if (exists $params->{$field} or exists $params->{"${field}_id"}) { - delete $params->{$field}; - $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids; - } + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # Allow matching component and product by name + # (in addition to matching by ID). + # Borrowed from Bugzilla::Bug::match + my %translate_fields + = (product => 'Bugzilla::Product', component => 'Bugzilla::Component',); + + foreach my $field (keys %translate_fields) { + my @ids; + + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $objects = Bugzilla::Object::match($type, {name => $names}); + push(@ids, map { $_->id } @$objects); } - # If we aren't matching on the product, use the default matching code - if (!exists $params->{product_id}) { - return $class->SUPER::match(@_); + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); } - my @criteria = ("1=1"); - - if ($params->{product_id}) { - push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); - if ($params->{component_id}) { - my $component_id = $params->{component_id}; - push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) . - " OR component_id IS NULL)"); - } + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + delete $params->{$field}; + $params->{"${field}_id"} = scalar(@ids) == 1 ? [$ids[0]] : \@ids; + } + } + + # If we aren't matching on the product, use the default matching code + if (!exists $params->{product_id}) { + return $class->SUPER::match(@_); + } + + my @criteria = ("1=1"); + + if ($params->{product_id}) { + push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); + if ($params->{component_id}) { + my $component_id = $params->{component_id}; + push(@criteria, + "(" + . $dbh->sql_in('component_id', $params->{'component_id'}) + . " OR component_id IS NULL)"); } + } - my $where = join(' AND ', @criteria); - my $flag_ids = $dbh->selectcol_arrayref("SELECT id + my $where = join(' AND ', @criteria); + my $flag_ids = $dbh->selectcol_arrayref( + "SELECT id FROM tracking_flags_visibility - WHERE $where"); + WHERE $where" + ); - return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids); + return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list( + $flag_ids); } ############################### @@ -113,34 +119,40 @@ sub match { ############################### sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($flag) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } sub _check_product { - my ($invocant, $product) = @_; - if (blessed $product) { - return $product->id; - } - $product = Bugzilla::Product->new($product) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product }); + my ($invocant, $product) = @_; + if (blessed $product) { return $product->id; + } + $product + = Bugzilla::Product->new($product) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'product_id', value => $product}); + return $product->id; } sub _check_component { - my ($invocant, $component) = @_; - return undef unless defined $component; - if (blessed $component) { - return $component->id; - } - $component = Bugzilla::Component->new($component) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component }); + my ($invocant, $component) = @_; + return undef unless defined $component; + if (blessed $component) { return $component->id; + } + $component + = Bugzilla::Component->new($component) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'component_id', value => $component}); + return $component->id; } ############################### @@ -148,26 +160,27 @@ sub _check_component { ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub product_id { return $_[0]->{'product_id'}; } -sub component_id { return $_[0]->{'component_id'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub component_id { return $_[0]->{'component_id'}; } sub tracking_flag { - my ($self) = @_; - $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); - return $self->{'tracking_flag'}; + my ($self) = @_; + $self->{'tracking_flag'} + ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); + return $self->{'tracking_flag'}; } sub product { - my ($self) = @_; - $self->{'product'} ||= Bugzilla::Product->new($self->product_id); - return $self->{'product'}; + my ($self) = @_; + $self->{'product'} ||= Bugzilla::Product->new($self->product_id); + return $self->{'product'}; } sub component { - my ($self) = @_; - return undef unless $self->component_id; - $self->{'component'} ||= Bugzilla::Component->new($self->component_id); - return $self->{'component'}; + my ($self) = @_; + return undef unless $self->component_id; + $self->{'component'} ||= Bugzilla::Component->new($self->component_id); + return $self->{'component'}; } 1; diff --git a/extensions/TypeSniffer/Config.pm b/extensions/TypeSniffer/Config.pm index 4545f69f6..336f92c8f 100644 --- a/extensions/TypeSniffer/Config.pm +++ b/extensions/TypeSniffer/Config.pm @@ -28,16 +28,8 @@ use warnings; use constant NAME => 'TypeSniffer'; use constant REQUIRED_MODULES => [ - { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0' - }, - { - package => 'IO-stringy', - module => 'IO::Scalar', - version => '0' - }, + {package => 'File-MimeInfo', module => 'File::MimeInfo::Magic', version => '0'}, + {package => 'IO-stringy', module => 'IO::Scalar', version => '0'}, ]; -__PACKAGE__->NAME; \ No newline at end of file +__PACKAGE__->NAME; diff --git a/extensions/TypeSniffer/Extension.pm b/extensions/TypeSniffer/Extension.pm index 6c34cb169..52170f683 100644 --- a/extensions/TypeSniffer/Extension.pm +++ b/extensions/TypeSniffer/Extension.pm @@ -33,9 +33,7 @@ use IO::Scalar; our $VERSION = '1'; # These extensions override/supplement File::MimeInfo::Magic's detection. -our %EXTENSION_OVERRIDES = ( - '.lang' => 'text/plain', -); +our %EXTENSION_OVERRIDES = ('.lang' => 'text/plain',); ################################################################################ # This extension uses magic to guess MIME types for data where the browser has @@ -43,62 +41,63 @@ our %EXTENSION_OVERRIDES = ( # extension, or it's a text type with a non-.txt file extension). ################################################################################ sub attachment_process_data { - my ($self, $args) = @_; - my $attributes = $args->{'attributes'}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $attributes = $args->{'attributes'}; + my $params = Bugzilla->input_params; - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer. - if ($params->{'contenttypemethod'} && - $params->{'contenttypemethod'} eq 'autodetect' && - $attributes->{'mimetype'} eq 'application/octet-stream') - { - my $filename = $attributes->{'filename'} . ''; + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer. + if ( $params->{'contenttypemethod'} + && $params->{'contenttypemethod'} eq 'autodetect' + && $attributes->{'mimetype'} eq 'application/octet-stream') + { + my $filename = $attributes->{'filename'} . ''; - # Check for an override first - if ($filename =~ /^.+(\..+$)/) { - my $ext = lc($1); - if (exists $EXTENSION_OVERRIDES{$ext}) { - $attributes->{'mimetype'} = $EXTENSION_OVERRIDES{$ext}; - return; - } - } + # Check for an override first + if ($filename =~ /^.+(\..+$)/) { + my $ext = lc($1); + if (exists $EXTENSION_OVERRIDES{$ext}) { + $attributes->{'mimetype'} = $EXTENSION_OVERRIDES{$ext}; + return; + } + } - # Then try file extension detection - my $mimetype = mimetype($filename); - if ($mimetype) { - $attributes->{'mimetype'} = $mimetype; - return; - } + # Then try file extension detection + my $mimetype = mimetype($filename); + if ($mimetype) { + $attributes->{'mimetype'} = $mimetype; + return; + } - # data attribute can be either scalar data or filehandle - # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create - my $fh = $attributes->{'data'}; - if (!ref($fh)) { - my $data = $attributes->{'data'}; - $fh = new IO::Scalar \$data; - } - else { - # CGI.pm sends us an Fh that isn't actually an IO::Handle, but - # has a method for getting an actual handle out of it. - if (!$fh->isa('IO::Handle')) { - $fh = $fh->handle; - # ->handle returns an literal IO::Handle, even though the - # underlying object is a file. So we rebless it to be a proper - # IO::File object so that we can call ->seek on it and so on. - # Just in case CGI.pm fixes this some day, we check ->isa first. - if (!$fh->isa('IO::File')) { - bless $fh, 'IO::File'; - } - } - } + # data attribute can be either scalar data or filehandle + # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create + my $fh = $attributes->{'data'}; + if (!ref($fh)) { + my $data = $attributes->{'data'}; + $fh = new IO::Scalar \$data; + } + else { + # CGI.pm sends us an Fh that isn't actually an IO::Handle, but + # has a method for getting an actual handle out of it. + if (!$fh->isa('IO::Handle')) { + $fh = $fh->handle; - $mimetype = mimetype($fh); - $fh->seek(0, 0); - if ($mimetype) { - $attributes->{'mimetype'} = $mimetype; + # ->handle returns an literal IO::Handle, even though the + # underlying object is a file. So we rebless it to be a proper + # IO::File object so that we can call ->seek on it and so on. + # Just in case CGI.pm fixes this some day, we check ->isa first. + if (!$fh->isa('IO::File')) { + bless $fh, 'IO::File'; } + } + } + + $mimetype = mimetype($fh); + $fh->seek(0, 0); + if ($mimetype) { + $attributes->{'mimetype'} = $mimetype; } + } } __PACKAGE__->NAME; diff --git a/extensions/UserProfile/Config.pm b/extensions/UserProfile/Config.pm index 99fae1610..83b562163 100644 --- a/extensions/UserProfile/Config.pm +++ b/extensions/UserProfile/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'UserProfile'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'UserProfile'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/UserProfile/Extension.pm b/extensions/UserProfile/Extension.pm index 9171b942d..cc8be3f1f 100644 --- a/extensions/UserProfile/Extension.pm +++ b/extensions/UserProfile/Extension.pm @@ -29,44 +29,43 @@ our $VERSION = '1'; # BEGIN { - *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts; - *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts; - *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts; - *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts; - *Bugzilla::User::address = \&_user_address; + *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts; + *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts; + *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts; + *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts; + *Bugzilla::User::address = \&_user_address; } -sub _user_last_activity_ts { $_[0]->{last_activity_ts} } -sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} } +sub _user_last_activity_ts { $_[0]->{last_activity_ts} } +sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} } + sub _user_address { - my $mode = Bugzilla->usage_mode; + my $mode = Bugzilla->usage_mode; - Email::Address->disable_cache if any { $mode == $_ } USAGE_MODE_CMDLINE, USAGE_MODE_TEST, USAGE_MODE_EMAIL; - return Email::Address->new(undef, $_[0]->email); + Email::Address->disable_cache + if any { $mode == $_ } USAGE_MODE_CMDLINE, USAGE_MODE_TEST, USAGE_MODE_EMAIL; + return Email::Address->new(undef, $_[0]->email); } -sub _user_set_last_activity_ts { - my ($self, $value) = @_; - $self->set('last_activity_ts', $_[1]); +sub _user_set_last_activity_ts { + my ($self, $value) = @_; + $self->set('last_activity_ts', $_[1]); - # we update the database directly to avoid audit_log entries - Bugzilla->dbh->do( - "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", - undef, - $value, $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); + # we update the database directly to avoid audit_log entries + Bugzilla->dbh->do("UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", + undef, $value, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); } sub _user_clear_last_statistics_ts { - my ($self) = @_; - $self->set('last_statistics_ts', undef); - - # we update the database directly to avoid audit_log entries - Bugzilla->dbh->do( - "UPDATE profiles SET last_statistics_ts = NULL WHERE userid = ?", - undef, - $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); + my ($self) = @_; + $self->set('last_statistics_ts', undef); + + # we update the database directly to avoid audit_log entries + Bugzilla->dbh->do( + "UPDATE profiles SET last_statistics_ts = NULL WHERE userid = ?", + undef, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); } # @@ -76,327 +75,331 @@ sub _user_clear_last_statistics_ts { sub request_cleanup { Email::Address->purge_cache } sub bug_after_create { - my ($self, $args) = @_; - $self->_bug_touched($args); + my ($self, $args) = @_; + $self->_bug_touched($args); } sub bug_after_update { - my ($self, $args) = @_; - $self->_bug_touched($args); + my ($self, $args) = @_; + $self->_bug_touched($args); } sub _bug_touched { - my ($self, $args) = @_; - my $bug = $args->{bug}; - - my $user = Bugzilla->user; - my ($assigned_to, $qa_contact); - - # bug update - if (exists $args->{changes}) { - return unless - scalar(keys %{ $args->{changes} }) - || exists $args->{bug}->{added_comments}; - - # if the assignee or qa-contact is changed to someone other than the - # current user, update them - if (exists $args->{changes}->{assigned_to} - && $args->{changes}->{assigned_to}->[1] ne $user->login) - { - $assigned_to = $bug->assigned_to; - } - if (exists $args->{changes}->{qa_contact} - && ($args->{changes}->{qa_contact}->[1] || '') ne $user->login) - { - $qa_contact = $bug->qa_contact; - } - - # if the product is changed, we need to recount everyone involved with - # this bug - if (exists $args->{changes}->{product}) { - tag_for_recount_from_bug($bug->id); - } - + my ($self, $args) = @_; + my $bug = $args->{bug}; + + my $user = Bugzilla->user; + my ($assigned_to, $qa_contact); + + # bug update + if (exists $args->{changes}) { + return + unless scalar(keys %{$args->{changes}}) + || exists $args->{bug}->{added_comments}; + + # if the assignee or qa-contact is changed to someone other than the + # current user, update them + if (exists $args->{changes}->{assigned_to} + && $args->{changes}->{assigned_to}->[1] ne $user->login) + { + $assigned_to = $bug->assigned_to; } - # new bug - else { - # if the assignee or qa-contact is created set to someone other than - # the current user, update them - if ($bug->assigned_to->id != $user->id) { - $assigned_to = $bug->assigned_to; - } - if ($bug->qa_contact && $bug->qa_contact->id != $user->id) { - $qa_contact = $bug->qa_contact; - } + if (exists $args->{changes}->{qa_contact} + && ($args->{changes}->{qa_contact}->[1] || '') ne $user->login) + { + $qa_contact = $bug->qa_contact; } - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + # if the product is changed, we need to recount everyone involved with + # this bug + if (exists $args->{changes}->{product}) { + tag_for_recount_from_bug($bug->id); + } - # update user's last_activity_ts + } + + # new bug + else { + # if the assignee or qa-contact is created set to someone other than + # the current user, update them + if ($bug->assigned_to->id != $user->id) { + $assigned_to = $bug->assigned_to; + } + if ($bug->qa_contact && $bug->qa_contact->id != $user->id) { + $qa_contact = $bug->qa_contact; + } + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + # update user's last_activity_ts + eval { + $user->set_last_activity_ts($args->{timestamp}); + $self->_recalc_remove($user); + }; + if ($@) { + warn $@; + $self->_recalc_insert($user); + } + + # clear the last_statistics_ts for assignee/qa-contact to force a recount + # at the next poll + if ($assigned_to) { eval { - $user->set_last_activity_ts($args->{timestamp}); - $self->_recalc_remove($user); + $assigned_to->clear_last_statistics_ts(); + $self->_recalc_remove($assigned_to); }; if ($@) { - warn $@; - $self->_recalc_insert($user); + warn $@; + $self->_recalc_insert($assigned_to); } - - # clear the last_statistics_ts for assignee/qa-contact to force a recount - # at the next poll - if ($assigned_to) { - eval { - $assigned_to->clear_last_statistics_ts(); - $self->_recalc_remove($assigned_to); - }; - if ($@) { - warn $@; - $self->_recalc_insert($assigned_to); - } - } - if ($qa_contact) { - eval { - $qa_contact->clear_last_statistics_ts(); - $self->_recalc_remove($qa_contact); - }; - if ($@) { - warn $@; - $self->_recalc_insert($qa_contact); - } + } + if ($qa_contact) { + eval { + $qa_contact->clear_last_statistics_ts(); + $self->_recalc_remove($qa_contact); + }; + if ($@) { + warn $@; + $self->_recalc_insert($qa_contact); } + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub _recalc_insert { - my ($self, $user) = @_; - Bugzilla->dbh->do( - "INSERT IGNORE INTO profiles_statistics_recalc SET user_id=?", - undef, $user->id - ); + my ($self, $user) = @_; + Bugzilla->dbh->do("INSERT IGNORE INTO profiles_statistics_recalc SET user_id=?", + undef, $user->id); } sub _recalc_remove { - my ($self, $user) = @_; - Bugzilla->dbh->do( - "DELETE FROM profiles_statistics_recalc WHERE user_id=?", - undef, $user->id - ); + my ($self, $user) = @_; + Bugzilla->dbh->do("DELETE FROM profiles_statistics_recalc WHERE user_id=?", + undef, $user->id); } sub object_end_of_create { - my ($self, $args) = @_; - $self->_object_touched($args); + my ($self, $args) = @_; + $self->_object_touched($args); } sub object_end_of_update { - my ($self, $args) = @_; - $self->_object_touched($args); + my ($self, $args) = @_; + $self->_object_touched($args); } sub _object_touched { - my ($self, $args) = @_; - my $object = $args->{object} - or return; - return if exists $args->{changes} && !scalar(keys %{ $args->{changes} }); - - if ($object->isa('Bugzilla::Attachment')) { - # if an attachment is created or updated, that counts as user activity - my $user = Bugzilla->user; - my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - eval { - $user->set_last_activity_ts($timestamp); - $self->_recalc_remove($user); - }; - if ($@) { - warn $@; - $self->_recalc_insert($user); - } - } - elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) { - # if a product is renamed by an admin, rename in the - # profiles_statistics_products table - Bugzilla->dbh->do( - "UPDATE profiles_statistics_products SET product=? where product=?", - undef, - $args->{changes}->{name}->[1], $args->{changes}->{name}->[0], - ); + my ($self, $args) = @_; + my $object = $args->{object} or return; + return if exists $args->{changes} && !scalar(keys %{$args->{changes}}); + + if ($object->isa('Bugzilla::Attachment')) { + + # if an attachment is created or updated, that counts as user activity + my $user = Bugzilla->user; + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + eval { + $user->set_last_activity_ts($timestamp); + $self->_recalc_remove($user); + }; + if ($@) { + warn $@; + $self->_recalc_insert($user); } + } + elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) { + + # if a product is renamed by an admin, rename in the + # profiles_statistics_products table + Bugzilla->dbh->do( + "UPDATE profiles_statistics_products SET product=? where product=?", + undef, + $args->{changes}->{name}->[1], + $args->{changes}->{name}->[0], + ); + } } sub reorg_move_bugs { - my ($self, $args) = @_; - my $bug_ids = $args->{bug_ids}; - printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids); - my $count = 0; - foreach my $bug_id (@$bug_ids) { - $count += tag_for_recount_from_bug($bug_id); - } - print "Updated $count users.\n"; + my ($self, $args) = @_; + my $bug_ids = $args->{bug_ids}; + printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids); + my $count = 0; + foreach my $bug_id (@$bug_ids) { + $count += tag_for_recount_from_bug($bug_id); + } + print "Updated $count users.\n"; } sub merge_users_before { - my ($self, $args) = @_; - my ($old_id, $new_id) = @$args{qw(old_id new_id)}; - # when users are merged, we have to delete all the statistics for both users - # we'll recalcuate the stats after the merge - print "deleting user profile statistics for $old_id and $new_id\n"; - my $dbh = Bugzilla->dbh; - foreach my $table (qw( profiles_statistics profiles_statistics_status profiles_statistics_products )) { - $dbh->do("DELETE FROM $table WHERE " . $dbh->sql_in('user_id', [ $old_id, $new_id ])); - } + my ($self, $args) = @_; + my ($old_id, $new_id) = @$args{qw(old_id new_id)}; + + # when users are merged, we have to delete all the statistics for both users + # we'll recalcuate the stats after the merge + print "deleting user profile statistics for $old_id and $new_id\n"; + my $dbh = Bugzilla->dbh; + foreach my $table ( + qw( profiles_statistics profiles_statistics_status profiles_statistics_products ) + ) + { + $dbh->do( + "DELETE FROM $table WHERE " . $dbh->sql_in('user_id', [$old_id, $new_id])); + } } sub merge_users_after { - my ($self, $args) = @_; - my $new_id = $args->{new_id}; - print "generating user profile statistics $new_id\n"; - update_statistics_by_user($new_id); + my ($self, $args) = @_; + my $new_id = $args->{new_id}; + print "generating user profile statistics $new_id\n"; + update_statistics_by_user($new_id); } sub webservice_user_get { - my ($self, $args) = @_; - my ($service, $users) = @$args{qw(webservice users)}; - - my $dbh = Bugzilla->dbh; - my $ids = [ - map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } - grep { exists $_->{id} } - @$users - ]; - return unless @$ids; - my $timestamps = $dbh->selectall_hashref( - "SELECT userid,last_activity_ts FROM profiles WHERE " . $dbh->sql_in('userid', $ids), - 'userid', - ); - foreach my $user (@$users) { - my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; - $user->{last_activity} = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts}); - } + my ($self, $args) = @_; + my ($service, $users) = @$args{qw(webservice users)}; + + my $dbh = Bugzilla->dbh; + my $ids = [map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } + grep { exists $_->{id} } @$users]; + return unless @$ids; + my $timestamps = $dbh->selectall_hashref( + "SELECT userid,last_activity_ts FROM profiles WHERE " + . $dbh->sql_in('userid', $ids), + 'userid', + ); + foreach my $user (@$users) { + my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; + $user->{last_activity} + = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts}); + } } sub template_before_create { - my ($self, $args) = @_; - $args->{config}->{FILTERS}->{timeago} = sub { - my ($time_str) = @_; - return time_ago(datetime_from($time_str, 'UTC')); - }; + my ($self, $args) = @_; + $args->{config}->{FILTERS}->{timeago} = sub { + my ($time_str) = @_; + return time_ago(datetime_from($time_str, 'UTC')); + }; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'user_profile.html'; - my $user = Bugzilla->user; - - # determine user to display - my ($target, $login); - my $input = Bugzilla->input_params; - if (my $user_id = $input->{user_id}) { - # load from user_id - $user_id = 0 if $user_id =~ /\D/; - $target = Bugzilla::User->check({ id => $user_id }); - } else { - # loading from login name requires authentication - Bugzilla->login(LOGIN_REQUIRED); - $login = $input->{login}; - if (!$login) { - # show current user's profile by default - $target = $user; - } else { - my $limit = Bugzilla->params->{'maxusermatches'} + 1; - my $users = Bugzilla::User::match($login, $limit, 1); - if (scalar(@$users) == 1) { - # always allow singular matches without confirmation - $target = $users->[0]; - } else { - Bugzilla::User::match_field({ 'login' => {'type' => 'single'} }); - $target = Bugzilla::User->check($login); - } - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'user_profile.html'; + my $user = Bugzilla->user; + + # determine user to display + my ($target, $login); + my $input = Bugzilla->input_params; + if (my $user_id = $input->{user_id}) { + + # load from user_id + $user_id = 0 if $user_id =~ /\D/; + $target = Bugzilla::User->check({id => $user_id}); + } + else { + # loading from login name requires authentication + Bugzilla->login(LOGIN_REQUIRED); + $login = $input->{login}; + if (!$login) { + + # show current user's profile by default + $target = $user; + } + else { + my $limit = Bugzilla->params->{'maxusermatches'} + 1; + my $users = Bugzilla::User::match($login, $limit, 1); + if (scalar(@$users) == 1) { + + # always allow singular matches without confirmation + $target = $users->[0]; + } + else { + Bugzilla::User::match_field({'login' => {'type' => 'single'}}); + $target = Bugzilla::User->check($login); + } } - $login ||= $target->login; + } + $login ||= $target->login; - # load statistics into $vars - my $dbh = Bugzilla->switch_to_shadow_db; + # load statistics into $vars + my $dbh = Bugzilla->switch_to_shadow_db; - my $stats = $dbh->selectall_hashref( - "SELECT name, count + my $stats = $dbh->selectall_hashref( + "SELECT name, count FROM profiles_statistics - WHERE user_id = ?", - "name", - undef, - $target->id, - ); - map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats; + WHERE user_id = ?", "name", undef, $target->id, + ); + map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats; - my $statuses = $dbh->selectall_hashref( - "SELECT status, count + my $statuses = $dbh->selectall_hashref( + "SELECT status, count FROM profiles_statistics_status - WHERE user_id = ?", - "status", - undef, - $target->id, - ); - map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses; + WHERE user_id = ?", "status", undef, $target->id, + ); + map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses; - my $products = $dbh->selectall_arrayref( - "SELECT product, count + my $products = $dbh->selectall_arrayref( + "SELECT product, count FROM profiles_statistics_products WHERE user_id = ? - ORDER BY product = '', count DESC", - { Slice => {} }, - $target->id, - ); - - # ensure there's always an "other" product entry - my ($other_product) = grep { $_->{product} eq '' } @$products; - if (!$other_product) { - $other_product = { product => '', count => 0 }; - push @$products, $other_product; + ORDER BY product = '', count DESC", {Slice => {}}, $target->id, + ); + + # ensure there's always an "other" product entry + my ($other_product) = grep { $_->{product} eq '' } @$products; + if (!$other_product) { + $other_product = {product => '', count => 0}; + push @$products, $other_product; + } + + # load product objects and validate product visibility + foreach my $product (@$products) { + next if $product->{product} eq ''; + my $product_obj = Bugzilla::Product->new({name => $product->{product}}); + if (!$product_obj || !$user->can_see_product($product_obj->name)) { + + # products not accessible to current user are moved into "other" + $other_product->{count} += $product->{count}; + $product->{count} = 0; } - - # load product objects and validate product visibility - foreach my $product (@$products) { - next if $product->{product} eq ''; - my $product_obj = Bugzilla::Product->new({ name => $product->{product} }); - if (!$product_obj || !$user->can_see_product($product_obj->name)) { - # products not accessible to current user are moved into "other" - $other_product->{count} += $product->{count}; - $product->{count} = 0; - } else { - $product->{product} = $product_obj; - } + else { + $product->{product} = $product_obj; } + } - # set other's name, and remove empty products - $other_product->{product} = { name => 'Other' }; - $products = [ grep { $_->{count} } @$products ]; + # set other's name, and remove empty products + $other_product->{product} = {name => 'Other'}; + $products = [grep { $_->{count} } @$products]; - $vars->{stats} = $stats; - $vars->{statuses} = $statuses; - $vars->{products} = $products; - $vars->{login} = $login; - $vars->{target} = $target; + $vars->{stats} = $stats; + $vars->{statuses} = $statuses; + $vars->{products} = $products; + $vars->{login} = $login; + $vars->{target} = $target; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(last_activity_ts last_statistics_ts); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(last_activity_ts last_statistics_ts); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::User')) { - push(@$columns, qw(last_activity_ts last_statistics_ts)); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::User')) { + push(@$columns, qw(last_activity_ts last_statistics_ts)); + } } # @@ -404,161 +407,96 @@ sub object_update_columns { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'profiles_statistics'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - name => { - TYPE => 'VARCHAR(30)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_name_idx => { - FIELDS => [ 'user_id', 'name' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_status'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - status => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_status_idx => { - FIELDS => [ 'user_id', 'status' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_products'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - product => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_products_idx => { - FIELDS => [ 'user_id', 'product' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_recalc'} = { - FIELDS => [ - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - profiles_statistics_recalc_idx => { - FIELDS => [ 'user_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_recalc'} = { - FIELDS => [ - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - profiles_statistics_recalc_idx => { - FIELDS => [ 'user_id' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'profiles_statistics'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + name => {TYPE => 'VARCHAR(30)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_name_idx => + {FIELDS => ['user_id', 'name'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_status'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + status => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_status_idx => + {FIELDS => ['user_id', 'status'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_products'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + product => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_products_idx => + {FIELDS => ['user_id', 'product'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_recalc'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + profiles_statistics_recalc_idx => {FIELDS => ['user_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_recalc'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + profiles_statistics_recalc_idx => {FIELDS => ['user_id'], TYPE => 'UNIQUE',}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('profiles', 'last_activity_ts', { TYPE => 'DATETIME' }); - $dbh->bz_add_column('profiles', 'last_statistics_ts', { TYPE => 'DATETIME' }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('profiles', 'last_activity_ts', {TYPE => 'DATETIME'}); + $dbh->bz_add_column('profiles', 'last_statistics_ts', {TYPE => 'DATETIME'}); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; - my $extensions_dir = bz_locations()->{'extensionsdir'}; - my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; - $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl"; + $files->{$script_name} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; + $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl"; + $files->{$script_name} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } __PACKAGE__->NAME; diff --git a/extensions/UserProfile/bin/migrate.pl b/extensions/UserProfile/bin/migrate.pl index dd257f6bd..08c9f54f4 100755 --- a/extensions/UserProfile/bin/migrate.pl +++ b/extensions/UserProfile/bin/migrate.pl @@ -26,7 +26,7 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; my $user_ids = $dbh->selectcol_arrayref( - "SELECT userid + "SELECT userid FROM profiles WHERE last_activity_ts IS NULL ORDER BY userid" @@ -34,11 +34,9 @@ my $user_ids = $dbh->selectcol_arrayref( my ($current, $total) = (1, scalar(@$user_ids)); foreach my $user_id (@$user_ids) { - indicate_progress({ current => $current++, total => $total, every => 25 }); - my $ts = last_user_activity($user_id); - next unless $ts; - $dbh->do( - "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", - undef, - $ts, $user_id); + indicate_progress({current => $current++, total => $total, every => 25}); + my $ts = last_user_activity($user_id); + next unless $ts; + $dbh->do("UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", + undef, $ts, $user_id); } diff --git a/extensions/UserProfile/bin/update.pl b/extensions/UserProfile/bin/update.pl index 4d704a1f0..bdbcef329 100755 --- a/extensions/UserProfile/bin/update.pl +++ b/extensions/UserProfile/bin/update.pl @@ -26,56 +26,50 @@ my $user_ids; my $verbose = grep { $_ eq '-v' } @ARGV; $user_ids = $dbh->selectcol_arrayref( - "SELECT user_id + "SELECT user_id FROM profiles_statistics_recalc - ORDER BY user_id", - { Slice => {} } + ORDER BY user_id", {Slice => {}} ); if (@$user_ids) { - print "recalculating last_user_activity\n"; - my ($count, $total) = (0, scalar(@$user_ids)); - foreach my $user_id (@$user_ids) { - if ($verbose) { - $count++; - my $login = user_id_to_login($user_id); - print "$count/$total $login ($user_id)\n"; - } - $dbh->do( - "UPDATE profiles - SET last_activity_ts = ?, - last_statistics_ts = NULL - WHERE userid = ?", - undef, - last_user_activity($user_id), - $user_id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); + print "recalculating last_user_activity\n"; + my ($count, $total) = (0, scalar(@$user_ids)); + foreach my $user_id (@$user_ids) { + if ($verbose) { + $count++; + my $login = user_id_to_login($user_id); + print "$count/$total $login ($user_id)\n"; } $dbh->do( - "DELETE FROM profiles_statistics_recalc WHERE " . $dbh->sql_in('user_id', $user_ids) + "UPDATE profiles + SET last_activity_ts = ?, + last_statistics_ts = NULL + WHERE userid = ?", undef, last_user_activity($user_id), $user_id ); + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); + } + $dbh->do("DELETE FROM profiles_statistics_recalc WHERE " + . $dbh->sql_in('user_id', $user_ids)); } $user_ids = $dbh->selectcol_arrayref( - "SELECT userid + "SELECT userid FROM profiles WHERE last_activity_ts IS NOT NULL AND (last_statistics_ts IS NULL OR last_activity_ts > last_statistics_ts) - ORDER BY userid", - { Slice => {} } + ORDER BY userid", {Slice => {}} ); if (@$user_ids) { - $verbose && print "updating statistics\n"; - my ($count, $total) = (0, scalar(@$user_ids)); - foreach my $user_id (@$user_ids) { - if ($verbose) { - $count++; - my $login = user_id_to_login($user_id); - print "$count/$total $login ($user_id)\n"; - } - update_statistics_by_user($user_id); + $verbose && print "updating statistics\n"; + my ($count, $total) = (0, scalar(@$user_ids)); + foreach my $user_id (@$user_ids) { + if ($verbose) { + $count++; + my $login = user_id_to_login($user_id); + print "$count/$total $login ($user_id)\n"; } + update_statistics_by_user($user_id); + } } diff --git a/extensions/UserProfile/lib/Util.pm b/extensions/UserProfile/lib/Util.pm index 6b2eff098..54a9c2eea 100644 --- a/extensions/UserProfile/lib/Util.pm +++ b/extensions/UserProfile/lib/Util.pm @@ -13,44 +13,45 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( update_statistics_by_user - tag_for_recount_from_bug - last_user_activity ); + tag_for_recount_from_bug + last_user_activity ); use Bugzilla; sub update_statistics_by_user { - my ($user_id) = @_; + my ($user_id) = @_; - # run all our queries on the slaves + # run all our queries on the slaves - my $dbh = Bugzilla->switch_to_shadow_db(); + my $dbh = Bugzilla->switch_to_shadow_db(); - my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # grab the current values + # grab the current values - my $last_statistics_ts = _get_last_statistics_ts($user_id); + my $last_statistics_ts = _get_last_statistics_ts($user_id); - my $statistics = _get_stats($user_id, 'profiles_statistics', 'name'); - my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status'); - my $by_product = _get_stats($user_id, 'profiles_statistics_products', 'product'); + my $statistics = _get_stats($user_id, 'profiles_statistics', 'name'); + my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status'); + my $by_product + = _get_stats($user_id, 'profiles_statistics_products', 'product'); - # bugs filed - _update_statistics($statistics, 'bugs_filed', [ $user_id ], <switch_to_main_db(); - $dbh->bz_start_transaction(); + $dbh = Bugzilla->switch_to_main_db(); + $dbh->bz_start_transaction(); - # commit updated statistics + # commit updated statistics - _set_stats($statistics, $user_id, 'profiles_statistics', 'name') - if _has_dirty($statistics); - _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status') - if _has_dirty($by_status); - _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product') - if _has_dirty($by_product); + _set_stats($statistics, $user_id, 'profiles_statistics', 'name') + if _has_dirty($statistics); + _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status') + if _has_dirty($by_status); + _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product') + if _has_dirty($by_product); - # update the user's last_statistics_ts - _set_last_statistics_ts($user_id, $now); + # update the user's last_statistics_ts + _set_last_statistics_ts($user_id, $now); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub tag_for_recount_from_bug { - my ($bug_id) = @_; - my $dbh = Bugzilla->dbh; - # get a list of all users associated with this bug - my $user_ids = $dbh->selectcol_arrayref(<dbh; + + # get a list of all users associated with this bug + my $user_ids + = $dbh->selectcol_arrayref(<do( - "UPDATE profiles SET last_statistics_ts=NULL WHERE " . $dbh->sql_in('userid', $user_ids) - ); - foreach my $id (@$user_ids) { - Bugzilla->memcached->clear({ table => 'profiles', id => $id }); - } - return scalar(@$user_ids); + + # clear last_statistics_ts + $dbh->do("UPDATE profiles SET last_statistics_ts=NULL WHERE " + . $dbh->sql_in('userid', $user_ids)); + foreach my $id (@$user_ids) { + Bugzilla->memcached->clear({table => 'profiles', id => $id}); + } + return scalar(@$user_ids); } sub last_user_activity { - # last comment, or change to a bug (excluding CC changes) - my ($user_id) = @_; - return Bugzilla->dbh->selectrow_array(<dbh->selectrow_array( + <dbh->selectrow_array( - "SELECT last_statistics_ts FROM profiles WHERE userid = ?", - undef, $user_id - ); + my ($user_id) = @_; + return Bugzilla->dbh->selectrow_array( + "SELECT last_statistics_ts FROM profiles WHERE userid = ?", + undef, $user_id); } sub _set_last_statistics_ts { - my ($user_id, $timestamp) = @_; - Bugzilla->dbh->do( - "UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?", - undef, - $timestamp, $user_id, - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); + my ($user_id, $timestamp) = @_; + Bugzilla->dbh->do("UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?", + undef, $timestamp, $user_id,); + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); } sub _update_statistics { - my ($statistics, $name, $values, $sql) = @_; - my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values); - if (!exists $statistics->{$name}) { - $statistics->{$name} = { - id => 0, - count => $count, - dirty => 1, - }; - } elsif ($statistics->{$name}->{count} != $count) { - $statistics->{$name}->{count} = $count; - $statistics->{$name}->{dirty} = 1; - }; + my ($statistics, $name, $values, $sql) = @_; + my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values); + if (!exists $statistics->{$name}) { + $statistics->{$name} = {id => 0, count => $count, dirty => 1,}; + } + elsif ($statistics->{$name}->{count} != $count) { + $statistics->{$name}->{count} = $count; + $statistics->{$name}->{dirty} = 1; + } } sub _activity_by_status { - my ($by_status, $user_id) = @_; - my $dbh = Bugzilla->dbh; + my ($by_status, $user_id) = @_; + my $dbh = Bugzilla->dbh; - # we actually track both status and resolution changes as statuses - my @values = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution')); - my $rows = $dbh->selectall_arrayref(< {} }, @values); + # we actually track both status and resolution changes as statuses + my @values + = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution')); + my $rows = $dbh->selectall_arrayref(< {}}, @values); SELECT added AS status, COUNT(*) AS count FROM bugs_activity WHERE who = ? @@ -254,29 +255,26 @@ sub _activity_by_status { GROUP BY added EOF - foreach my $row (@$rows) { - my $status = $row->{status}; - if (!exists $by_status->{$status}) { - $by_status->{$status} = { - id => 0, - count => $row->{count}, - dirty => 1, - }; - } elsif ($by_status->{$status}->{count} != $row->{count}) { - $by_status->{$status}->{count} = $row->{count}; - $by_status->{$status}->{dirty} = 1; - } + foreach my $row (@$rows) { + my $status = $row->{status}; + if (!exists $by_status->{$status}) { + $by_status->{$status} = {id => 0, count => $row->{count}, dirty => 1,}; + } + elsif ($by_status->{$status}->{count} != $row->{count}) { + $by_status->{$status}->{count} = $row->{count}; + $by_status->{$status}->{dirty} = 1; } + } } sub _activity_by_product { - my ($by_product, $user_id) = @_; - my $dbh = Bugzilla->dbh; + my ($by_product, $user_id) = @_; + my $dbh = Bugzilla->dbh; - my %products; + my %products; - # changes - my $rows = $dbh->selectall_arrayref(< {} }, $user_id); + # changes + my $rows = $dbh->selectall_arrayref(< {}}, $user_id); SELECT products.name AS product, count(*) AS count FROM bugs_activity INNER JOIN bugs ON bugs.bug_id = bugs_activity.bug_id @@ -284,10 +282,10 @@ sub _activity_by_product { WHERE who = ? GROUP BY bugs.product_id EOF - map { $products{$_->{product}} += $_->{count} } @$rows; + map { $products{$_->{product}} += $_->{count} } @$rows; - # comments - $rows = $dbh->selectall_arrayref(< {} }, $user_id); + # comments + $rows = $dbh->selectall_arrayref(< {}}, $user_id); SELECT products.name AS product, count(*) AS count FROM longdescs INNER JOIN bugs ON bugs.bug_id = longdescs.bug_id @@ -295,96 +293,89 @@ EOF WHERE who = ? GROUP BY bugs.product_id EOF - map { $products{$_->{product}} += $_->{count} } @$rows; - - # store only the top 10 and 'other' (which is an empty string) - my @sorted = sort { $products{$b} <=> $products{$a} } keys %products; - my @other; - @other = splice(@sorted, 10) if scalar(@sorted) > 10; - map { $products{''} += $products{$_} } @other; - push @sorted, '' if $products{''}; - - # update by_product - foreach my $product (@sorted) { - if (!exists $by_product->{$product}) { - $by_product->{$product} = { - id => 0, - count => $products{$product}, - dirty => 1, - }; - } elsif ($by_product->{$product}->{count} != $products{$product}) { - $by_product->{$product}->{count} = $products{$product}; - $by_product->{$product}->{dirty} = 1; - } + map { $products{$_->{product}} += $_->{count} } @$rows; + + # store only the top 10 and 'other' (which is an empty string) + my @sorted = sort { $products{$b} <=> $products{$a} } keys %products; + my @other; + @other = splice(@sorted, 10) if scalar(@sorted) > 10; + map { $products{''} += $products{$_} } @other; + push @sorted, '' if $products{''}; + + # update by_product + foreach my $product (@sorted) { + if (!exists $by_product->{$product}) { + $by_product->{$product} = {id => 0, count => $products{$product}, dirty => 1,}; } - foreach my $product (keys %$by_product) { - if (!grep { $_ eq $product } @sorted) { - delete $by_product->{$product}; - } + elsif ($by_product->{$product}->{count} != $products{$product}) { + $by_product->{$product}->{count} = $products{$product}; + $by_product->{$product}->{dirty} = 1; } + } + foreach my $product (keys %$by_product) { + if (!grep { $_ eq $product } @sorted) { + delete $by_product->{$product}; + } + } } our $_field_id_cache; + sub _field_id { - my ($name) = @_; - if (!$_field_id_cache) { - my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs"); - foreach my $row (@$rows) { - $_field_id_cache->{$row->[1]} = $row->[0]; - } + my ($name) = @_; + if (!$_field_id_cache) { + my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs"); + foreach my $row (@$rows) { + $_field_id_cache->{$row->[1]} = $row->[0]; } - return $_field_id_cache->{$name}; + } + return $_field_id_cache->{$name}; } sub _get_stats { - my ($user_id, $table, $name_field) = @_; - my $result = {}; - my $rows = Bugzilla->dbh->selectall_arrayref( - "SELECT * FROM $table WHERE user_id = ?", - { Slice => {} }, - $user_id, - ); - foreach my $row (@$rows) { - unless (defined $row->{$name_field}) { - print "$user_id $table $name_field\n"; - die; - } - $result->{$row->{$name_field}} = { - id => $row->{id}, - count => $row->{count}, - dirty => 0, - } + my ($user_id, $table, $name_field) = @_; + my $result = {}; + my $rows + = Bugzilla->dbh->selectall_arrayref("SELECT * FROM $table WHERE user_id = ?", + {Slice => {}}, $user_id,); + foreach my $row (@$rows) { + unless (defined $row->{$name_field}) { + print "$user_id $table $name_field\n"; + die; } - return $result; + $result->{$row->{$name_field}} + = {id => $row->{id}, count => $row->{count}, dirty => 0,}; + } + return $result; } sub _set_stats { - my ($statistics, $user_id, $table, $name_field) = @_; - my $dbh = Bugzilla->dbh; - foreach my $name (keys %$statistics) { - next unless $statistics->{$name}->{dirty}; - if ($statistics->{$name}->{id}) { - $dbh->do( - "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?", - undef, - $statistics->{$name}->{count}, $user_id, $name, - ); - } else { - $dbh->do( - "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)", - undef, - $user_id, $name, $statistics->{$name}->{count}, - ); - } + my ($statistics, $user_id, $table, $name_field) = @_; + my $dbh = Bugzilla->dbh; + foreach my $name (keys %$statistics) { + next unless $statistics->{$name}->{dirty}; + if ($statistics->{$name}->{id}) { + $dbh->do( + "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?", + undef, $statistics->{$name}->{count}, + $user_id, $name, + ); + } + else { + $dbh->do( + "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)", + undef, $user_id, $name, $statistics->{$name}->{count}, + ); } + } } sub _has_dirty { - my ($statistics) = @_; - foreach my $name (keys %$statistics) { - return 1 if $statistics->{$name}->{dirty}; - } - return 0; + my ($statistics) = @_; + foreach my $name (keys %$statistics) { + return 1 if $statistics->{$name}->{dirty}; + } + return 0; } 1; diff --git a/extensions/UserStory/Config.pm b/extensions/UserStory/Config.pm index 8668deaa7..baf1776d3 100644 --- a/extensions/UserStory/Config.pm +++ b/extensions/UserStory/Config.pm @@ -12,13 +12,8 @@ use strict; use warnings; use constant NAME => 'UserStory'; -use constant REQUIRED_MODULES => [ - { - package => 'Text-Diff', - module => 'Text::Diff', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Text-Diff', module => 'Text::Diff', version => 0,},]; use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/UserStory/Extension.pm b/extensions/UserStory/Extension.pm index f9fdb3f29..0be43173b 100644 --- a/extensions/UserStory/Extension.pm +++ b/extensions/UserStory/Extension.pm @@ -23,82 +23,82 @@ use Bugzilla::Extension::BMO::FakeBug; use Text::Diff; BEGIN { - *Bugzilla::Bug::user_story_visible = \&_bug_user_story_visible; - *Bugzilla::Extension::BMO::FakeBug::user_story_visible = \&_bug_user_story_visible; + *Bugzilla::Bug::user_story_visible = \&_bug_user_story_visible; + *Bugzilla::Extension::BMO::FakeBug::user_story_visible + = \&_bug_user_story_visible; } sub _bug_user_story_visible { - my ($self) = @_; - if (!exists $self->{user_story_visible}) { - # Visible by default - $self->{user_story_visible} = 1; - my ($product, $component) = ($self->product, $self->component); - my $exclude_components = []; - if (exists USER_STORY_EXCLUDE->{$product}) { - $exclude_components = USER_STORY_EXCLUDE->{$product}; - if (scalar(@$exclude_components) == 0 - || ($component && grep { $_ eq $component } @$exclude_components)) - { - $self->{user_story_visible} = 0; - } - } - $self->{user_story_exclude_components} = $exclude_components; + my ($self) = @_; + if (!exists $self->{user_story_visible}) { + + # Visible by default + $self->{user_story_visible} = 1; + my ($product, $component) = ($self->product, $self->component); + my $exclude_components = []; + if (exists USER_STORY_EXCLUDE->{$product}) { + $exclude_components = USER_STORY_EXCLUDE->{$product}; + if (scalar(@$exclude_components) == 0 + || ($component && grep { $_ eq $component } @$exclude_components)) + { + $self->{user_story_visible} = 0; + } } - return ($self->{user_story_visible}, $self->{user_story_exclude_components}); + $self->{user_story_exclude_components} = $exclude_components; + } + return ($self->{user_story_visible}, $self->{user_story_exclude_components}); } # ensure user is allowed to edit the story sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $field, $priv_results) = @$args{qw(bug field priv_results)}; - return unless $field eq 'cf_user_story'; - if (!Bugzilla->user->in_group(USER_STORY_GROUP)) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my ($bug, $field, $priv_results) = @$args{qw(bug field priv_results)}; + return unless $field eq 'cf_user_story'; + if (!Bugzilla->user->in_group(USER_STORY_GROUP)) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } # store just a diff of the changes in the bugs_activity table sub bug_update_before_logging { - my ($self, $args) = @_; - my $changes = $args->{changes}; - return unless exists $changes->{cf_user_story}; - my $diff = diff( - \$changes->{cf_user_story}->[0], - \$changes->{cf_user_story}->[1], - { - CONTEXT => 0, - }, - ); - $changes->{cf_user_story} = [ '', $diff ]; + my ($self, $args) = @_; + my $changes = $args->{changes}; + return unless exists $changes->{cf_user_story}; + my $diff = diff( + \$changes->{cf_user_story}->[0], + \$changes->{cf_user_story}->[1], + {CONTEXT => 0,}, + ); + $changes->{cf_user_story} = ['', $diff]; } # stop inline-history from displaying changes to the user story sub inline_history_activtiy { - my ($self, $args) = @_; - foreach my $activity (@{ $args->{activity} }) { - foreach my $change (@{ $activity->{changes} }) { - if ($change->{fieldname} eq 'cf_user_story') { - $change->{removed} = ''; - $change->{added} = '(updated)'; - } - } + my ($self, $args) = @_; + foreach my $activity (@{$args->{activity}}) { + foreach my $change (@{$activity->{changes}}) { + if ($change->{fieldname} eq 'cf_user_story') { + $change->{removed} = ''; + $change->{added} = '(updated)'; + } } + } } # create cf_user_story field sub install_update_db { - my ($self, $args) = @_; - return if Bugzilla::Field->new({ name => 'cf_user_story'}); - Bugzilla::Field->create({ - name => 'cf_user_story', - description => 'User Story', - type => FIELD_TYPE_TEXTAREA, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 0, - }); + my ($self, $args) = @_; + return if Bugzilla::Field->new({name => 'cf_user_story'}); + Bugzilla::Field->create({ + name => 'cf_user_story', + description => 'User Story', + type => FIELD_TYPE_TEXTAREA, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 0, + }); } __PACKAGE__->NAME; diff --git a/extensions/UserStory/lib/Constants.pm b/extensions/UserStory/lib/Constants.pm index 6a1c0c449..49f6b0d22 100644 --- a/extensions/UserStory/lib/Constants.pm +++ b/extensions/UserStory/lib/Constants.pm @@ -25,6 +25,6 @@ use constant USER_STORY_GROUP => 'editbugs'; # Don't show User Story on Developer Tools component, visible on all other # Firefox components # 'Firefox' => ['Developer Tools'], -use constant USER_STORY_EXCLUDE => { }; +use constant USER_STORY_EXCLUDE => {}; 1; diff --git a/extensions/Voting/Config.pm b/extensions/Voting/Config.pm index 97c44933e..d72caa3cc 100644 --- a/extensions/Voting/Config.pm +++ b/extensions/Voting/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'Voting'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm index 198b065fa..523ee653d 100644 --- a/extensions/Voting/Extension.pm +++ b/extensions/Voting/Extension.pm @@ -23,15 +23,16 @@ use Bugzilla::Token; use List::Util qw(min sum); -use constant VERSION => BUGZILLA_VERSION; +use constant VERSION => BUGZILLA_VERSION; use constant DEFAULT_VOTES_PER_BUG => 1; + # These came from Bugzilla itself, so they maintain the old numbers # they had before. use constant CMT_POPULAR_VOTES => 3; -use constant REL_VOTER => 4; +use constant REL_VOTER => 4; BEGIN { - *Bugzilla::Bug::user_votes = \&_bug_user_votes; + *Bugzilla::Bug::user_votes = \&_bug_user_votes; } ################ @@ -39,70 +40,71 @@ BEGIN { ################ BEGIN { - *Bugzilla::Bug::votes = \&votes; + *Bugzilla::Bug::votes = \&votes; } sub votes { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{votes} if exists $self->{votes}; + return $self->{votes} if exists $self->{votes}; - $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', - undef, $self->id); - return $self->{votes}; + $self->{votes} + = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', + undef, $self->id); + return $self->{votes}; } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'votes'} = { - FIELDS => [ - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - vote_count => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - votes_who_idx => ['who'], - votes_bug_id_idx => ['bug_id'], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'votes'} = { + FIELDS => [ + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + vote_count => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [votes_who_idx => ['who'], votes_bug_id_idx => ['bug_id'],], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla, - # so updates to the columns for old versions of Bugzilla happen in - # Bugzilla::Install::DB, and can't safely be moved to this extension. - - my $field = new Bugzilla::Field({ name => 'votes' }); - if (!$field) { - Bugzilla::Field->create( - { name => 'votes', description => 'Votes', buglist => 1 }); - } - - $dbh->bz_add_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('products', 'maxvotesperbug', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); - $dbh->bz_add_column('products', 'votestoconfirm', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - - $dbh->bz_add_column('bugs', 'votes', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']); - - # maxvotesperbug used to default to 10,000, which isn't very sensible. - my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug'); - if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) { - $dbh->bz_alter_column('products', 'maxvotesperbug', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); - } + my $dbh = Bugzilla->dbh; + + # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla, + # so updates to the columns for old versions of Bugzilla happen in + # Bugzilla::Install::DB, and can't safely be moved to this extension. + + my $field = new Bugzilla::Field({name => 'votes'}); + if (!$field) { + Bugzilla::Field->create( + {name => 'votes', description => 'Votes', buglist => 1}); + } + + $dbh->bz_add_column('products', 'votesperuser', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('products', 'maxvotesperbug', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); + $dbh->bz_add_column('products', 'votestoconfirm', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + $dbh->bz_add_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']); + + # maxvotesperbug used to default to 10,000, which isn't very sensible. + my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug'); + if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) { + $dbh->bz_alter_column('products', 'maxvotesperbug', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); + } } ########### @@ -110,102 +112,108 @@ sub install_update_db { ########### sub _bug_user_votes { - my ($self) = @_; - return $self->{'user_votes'} if exists $self->{'user_votes'}; - $self->{'user_votes'} = Bugzilla->dbh->selectrow_array( - "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?", - undef, $self->id, Bugzilla->user->id); - return $self->{'user_votes'}; + my ($self) = @_; + return $self->{'user_votes'} if exists $self->{'user_votes'}; + $self->{'user_votes'} + = Bugzilla->dbh->selectrow_array( + "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?", + undef, $self->id, Bugzilla->user->id); + return $self->{'user_votes'}; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - push(@$columns, 'votes'); - } - elsif ($class->isa('Bugzilla::Product')) { - push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + push(@$columns, 'votes'); + } + elsif ($class->isa('Bugzilla::Product')) { + push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{fields}; - push(@$fields, 'votes'); + my ($self, $args) = @_; + my $fields = $args->{fields}; + push(@$fields, 'votes'); } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - if ($class->isa('Bugzilla::Product')) { - $validators->{'votesperuser'} = \&_check_votesperuser; - $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug; - $validators->{'votestoconfirm'} = \&_check_votestoconfirm; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + if ($class->isa('Bugzilla::Product')) { + $validators->{'votesperuser'} = \&_check_votesperuser; + $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug; + $validators->{'votestoconfirm'} = \&_check_votestoconfirm; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - if ($class->isa('Bugzilla::Bug')) { - # Don't ever allow people to directly specify "votes" into the bugs - # table. - delete $params->{votes}; - } - elsif ($class->isa('Bugzilla::Product')) { - my $input = Bugzilla->input_params; - $params->{votesperuser} = $input->{'votesperuser'}; - $params->{maxvotesperbug} = $input->{'maxvotesperbug'}; - $params->{votestoconfirm} = $input->{'votestoconfirm'}; - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + if ($class->isa('Bugzilla::Bug')) { + + # Don't ever allow people to directly specify "votes" into the bugs + # table. + delete $params->{votes}; + } + elsif ($class->isa('Bugzilla::Product')) { + my $input = Bugzilla->input_params; + $params->{votesperuser} = $input->{'votesperuser'}; + $params->{maxvotesperbug} = $input->{'maxvotesperbug'}; + $params->{votestoconfirm} = $input->{'votestoconfirm'}; + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object) = $args->{object}; - if ($object->isa('Bugzilla::Product')) { - my $input = Bugzilla->input_params; - $object->set('votesperuser', $input->{'votesperuser'}); - $object->set('maxvotesperbug', $input->{'maxvotesperbug'}); - $object->set('votestoconfirm', $input->{'votestoconfirm'}); - } + my ($self, $args) = @_; + my ($object) = $args->{object}; + if ($object->isa('Bugzilla::Product')) { + my $input = Bugzilla->input_params; + $object->set('votesperuser', $input->{'votesperuser'}); + $object->set('maxvotesperbug', $input->{'maxvotesperbug'}); + $object->set('votestoconfirm', $input->{'votestoconfirm'}); + } } sub object_end_of_update { - my ($self, $args) = @_; - my ($object, $changes) = @$args{qw(object changes)}; - if ( $object->isa('Bugzilla::Product') - and ($changes->{maxvotesperbug} or $changes->{votesperuser} - or $changes->{votestoconfirm}) ) - { - _modify_bug_votes($object, $changes); - } + my ($self, $args) = @_; + my ($object, $changes) = @$args{qw(object changes)}; + if ( + $object->isa('Bugzilla::Product') + and ($changes->{maxvotesperbug} + or $changes->{votesperuser} + or $changes->{votestoconfirm}) + ) + { + _modify_bug_votes($object, $changes); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $changes) = @$args{qw(bug changes)}; - - if ($changes->{'product'}) { - my @msgs; - # If some votes have been removed, RemoveVotes() returns - # a list of messages to send to voters. - @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); - _confirm_if_vote_confirmed($bug); - - foreach my $msg (@msgs) { - MessageToMTA($msg); - } + my ($self, $args) = @_; + my ($bug, $changes) = @$args{qw(bug changes)}; + + if ($changes->{'product'}) { + my @msgs; + + # If some votes have been removed, RemoveVotes() returns + # a list of messages to send to voters. + @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); + _confirm_if_vote_confirmed($bug); + + foreach my $msg (@msgs) { + MessageToMTA($msg); } + } } ############# @@ -213,27 +221,28 @@ sub bug_end_of_update { ############# sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{REL_VOTER} = REL_VOTER; - $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES; - $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG; + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{VARIABLES}{constants}; + $constants->{REL_VOTER} = REL_VOTER; + $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES; + $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG; } sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; - if ($file eq 'admin/users/confirm-delete.html.tmpl') { - my $who = $vars->{otheruser}; - my $votes = Bugzilla->dbh->selectrow_array( - 'SELECT COUNT(*) FROM votes WHERE who = ?', undef, $who->id); - if ($votes) { - $vars->{other_safe} = 1; - $vars->{votes} = $votes; - } - } + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + if ($file eq 'admin/users/confirm-delete.html.tmpl') { + my $who = $vars->{otheruser}; + my $votes + = Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM votes WHERE who = ?', + undef, $who->id); + if ($votes) { + $vars->{other_safe} = 1; + $vars->{votes} = $votes; + } + } } ########### @@ -241,19 +250,19 @@ sub template_before_process { ########### sub bugmail_recipients { - my ($self, $args) = @_; - my ($bug, $recipients) = @$args{qw(bug recipients)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($bug, $recipients) = @$args{qw(bug recipients)}; + my $dbh = Bugzilla->dbh; - my $voters = $dbh->selectcol_arrayref( - "SELECT who FROM votes WHERE bug_id = ?", undef, $bug->id); - $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters); + my $voters = $dbh->selectcol_arrayref("SELECT who FROM votes WHERE bug_id = ?", + undef, $bug->id); + $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters); } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_VOTER} = 'Voter'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_VOTER} = 'Voter'; } ############### @@ -261,59 +270,59 @@ sub bugmail_relationships { ############### sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; - - # Vote Cache - $status->('voting_count_start'); - my $dbh = Bugzilla->dbh; - my %cached_counts = @{ $dbh->selectcol_arrayref( - 'SELECT bug_id, votes FROM bugs', {Columns=>[1,2]}) }; - - my %real_counts = @{ $dbh->selectcol_arrayref( - 'SELECT bug_id, SUM(vote_count) FROM votes ' - . $dbh->sql_group_by('bug_id'), {Columns=>[1,2]}) }; - - my $needs_rebuild; - foreach my $id (keys %cached_counts) { - my $cached_count = $cached_counts{$id}; - my $real_count = $real_counts{$id} || 0; - if ($cached_count < 0) { - $status->('voting_count_alert', { id => $id }, 'alert'); - } - elsif ($cached_count != $real_count) { - $status->('voting_cache_alert', { id => $id }, 'alert'); - $needs_rebuild = 1; - } - } - - $status->('voting_cache_rebuild_fix') if $needs_rebuild; + my ($self, $args) = @_; + my $status = $args->{status}; + + # Vote Cache + $status->('voting_count_start'); + my $dbh = Bugzilla->dbh; + my %cached_counts = @{$dbh->selectcol_arrayref('SELECT bug_id, votes FROM bugs', + {Columns => [1, 2]})}; + + my %real_counts = @{ + $dbh->selectcol_arrayref( + 'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id'), + {Columns => [1, 2]}) + }; + + my $needs_rebuild; + foreach my $id (keys %cached_counts) { + my $cached_count = $cached_counts{$id}; + my $real_count = $real_counts{$id} || 0; + if ($cached_count < 0) { + $status->('voting_count_alert', {id => $id}, 'alert'); + } + elsif ($cached_count != $real_count) { + $status->('voting_cache_alert', {id => $id}, 'alert'); + $needs_rebuild = 1; + } + } + + $status->('voting_cache_rebuild_fix') if $needs_rebuild; } sub sanitycheck_repair { - my ($self, $args) = @_; - my $status = $args->{status}; - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - return if !$input->{rebuild_vote_cache}; - - $status->('voting_cache_rebuild_start'); - $dbh->bz_start_transaction(); - $dbh->do('UPDATE bugs SET votes = 0'); - - my $sth = $dbh->prepare( - 'SELECT bug_id, SUM(vote_count) FROM votes ' - . $dbh->sql_group_by('bug_id')); - $sth->execute(); - - my $sth_update = $dbh->prepare( - 'UPDATE bugs SET votes = ? WHERE bug_id = ?'); - while (my ($id, $count) = $sth->fetchrow_array) { - $sth_update->execute($count, $id); - } - $dbh->bz_commit_transaction(); - $status->('voting_cache_rebuild_end'); + my ($self, $args) = @_; + my $status = $args->{status}; + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + return if !$input->{rebuild_vote_cache}; + + $status->('voting_cache_rebuild_start'); + $dbh->bz_start_transaction(); + $dbh->do('UPDATE bugs SET votes = 0'); + + my $sth = $dbh->prepare( + 'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id')); + $sth->execute(); + + my $sth_update = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); + while (my ($id, $count) = $sth->fetchrow_array) { + $sth_update->execute($count, $id); + } + $dbh->bz_commit_transaction(); + $status->('voting_cache_rebuild_end'); } @@ -322,35 +331,36 @@ sub sanitycheck_repair { ############## sub _check_votesperuser { - return _check_votes(0, @_); + return _check_votes(0, @_); } sub _check_maxvotesperbug { - return _check_votes(DEFAULT_VOTES_PER_BUG, @_); + return _check_votes(DEFAULT_VOTES_PER_BUG, @_); } sub _check_votestoconfirm { - return _check_votes(0, @_); + return _check_votes(0, @_); } # This subroutine is only used internally by other _check_votes_* validators. sub _check_votes { - my ($default, $invocant, $votes, $field) = @_; - - detaint_natural($votes) if defined $votes; - # On product creation, if the number of votes is not a valid integer, - # we silently fall back to the given default value. - # If the product already exists and the change is illegal, we complain. - if (!defined $votes) { - if (ref $invocant) { - ThrowUserError('voting_product_illegal_votes', - { field => $field, votes => $_[2] }); - } - else { - $votes = $default; - } + my ($default, $invocant, $votes, $field) = @_; + + detaint_natural($votes) if defined $votes; + + # On product creation, if the number of votes is not a valid integer, + # we silently fall back to the given default value. + # If the product already exists and the change is illegal, we complain. + if (!defined $votes) { + if (ref $invocant) { + ThrowUserError('voting_product_illegal_votes', + {field => $field, votes => $_[2]}); } - return $votes; + else { + $votes = $default; + } + } + return $votes; } ######### @@ -358,291 +368,322 @@ sub _check_votes { ######### sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{page_id}; - my $vars = $args->{vars}; - - if ($page =~ m{^voting/bug\.}) { - _page_bug($vars); - } - elsif ($page =~ m{^voting/user\.}) { - _page_user($vars); - } + my ($self, $args) = @_; + my $page = $args->{page_id}; + my $vars = $args->{vars}; + + if ($page =~ m{^voting/bug\.}) { + _page_bug($vars); + } + elsif ($page =~ m{^voting/user\.}) { + _page_user($vars); + } } sub _page_bug { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; - my $bug_id = $input->{bug_id}; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $input->{bug_id}; + my $bug = Bugzilla::Bug->check($bug_id); - $vars->{'bug'} = $bug; - $vars->{'users'} = - $dbh->selectall_arrayref('SELECT profiles.login_name, + $vars->{'bug'} = $bug; + $vars->{'users'} = $dbh->selectall_arrayref( + 'SELECT profiles.login_name, profiles.userid AS id, votes.vote_count FROM votes INNER JOIN profiles ON profiles.userid = votes.who - WHERE votes.bug_id = ?', - {Slice=>{}}, $bug->id); + WHERE votes.bug_id = ?', {Slice => {}}, $bug->id + ); } sub _page_user { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $input = Bugzilla->input_params; - my $action = $input->{action}; - if ($action and $action eq 'vote') { - _update_votes($vars); - } + my $action = $input->{action}; + if ($action and $action eq 'vote') { + _update_votes($vars); + } - # If a bug_id is given, and we're editing, we'll add it to the votes list. + # If a bug_id is given, and we're editing, we'll add it to the votes list. - my $bug_id = $input->{bug_id}; - $bug_id = $bug_id->[0] if ref($bug_id) eq 'ARRAY'; - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id; - my $who_id = $input->{user_id} || $user->id; + my $bug_id = $input->{bug_id}; + $bug_id = $bug_id->[0] if ref($bug_id) eq 'ARRAY'; + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}) if $bug_id; + my $who_id = $input->{user_id} || $user->id; - # Logged-out users must specify a user_id. - Bugzilla->login(LOGIN_REQUIRED) if !$who_id; + # Logged-out users must specify a user_id. + Bugzilla->login(LOGIN_REQUIRED) if !$who_id; - my $who = Bugzilla::User->check({ id => $who_id }); + my $who = Bugzilla::User->check({id => $who_id}); - my $canedit = $user->id == $who->id; + my $canedit = $user->id == $who->id; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - if ($canedit && $bug) { - # Make sure there is an entry for this bug - # in the vote table, just so that things display right. - my $has_votes = $dbh->selectrow_array('SELECT vote_count FROM votes - WHERE bug_id = ? AND who = ?', - undef, ($bug->id, $who->id)); - if (!$has_votes) { - $dbh->do('INSERT INTO votes (who, bug_id, vote_count) - VALUES (?, ?, 0)', undef, ($who->id, $bug->id)); - } + if ($canedit && $bug) { + + # Make sure there is an entry for this bug + # in the vote table, just so that things display right. + my $has_votes = $dbh->selectrow_array( + 'SELECT vote_count FROM votes + WHERE bug_id = ? AND who = ?', undef, + ($bug->id, $who->id) + ); + if (!$has_votes) { + $dbh->do( + 'INSERT INTO votes (who, bug_id, vote_count) + VALUES (?, ?, 0)', undef, ($who->id, $bug->id) + ); } + } - my (@products, @all_bug_ids); - # Read the votes data for this user for each product. - foreach my $product (@{ $user->get_selectable_products }) { - next unless ($product->{votesperuser} > 0); + my (@products, @all_bug_ids); - my $vote_list = - $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count + # Read the votes data for this user for each product. + foreach my $product (@{$user->get_selectable_products}) { + next unless ($product->{votesperuser} > 0); + + my $vote_list = $dbh->selectall_arrayref( + 'SELECT votes.bug_id, votes.vote_count FROM votes INNER JOIN bugs ON votes.bug_id = bugs.bug_id WHERE votes.who = ? - AND bugs.product_id = ?', - undef, ($who->id, $product->id)); - - my %votes = map { $_->[0] => $_->[1] } @$vote_list; - my @bug_ids = sort keys %votes; - # Exclude bugs that the user can no longer see. - @bug_ids = @{ $user->visible_bugs(\@bug_ids) }; - next unless scalar @bug_ids; - - push(@all_bug_ids, @bug_ids); - my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - $_->{count} = $votes{$_->id} foreach @bugs; - # We include votes from bugs that the user can no longer see. - my $total = sum(values %votes) || 0; - - my $onevoteonly = 0; - $onevoteonly = 1 if (min($product->{votesperuser}, - $product->{maxvotesperbug}) == 1); - - push(@products, { name => $product->name, - bugs => \@bugs, - bug_ids => \@bug_ids, - onevoteonly => $onevoteonly, - total => $total, - maxvotes => $product->{votesperuser}, - maxperbug => $product->{maxvotesperbug} }); - } - - if ($canedit && $bug) { - $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', - undef, $who->id); - } - $dbh->bz_commit_transaction(); - - $vars->{'canedit'} = $canedit; - $vars->{'voting_user'} = { "login" => $who->name }; - $vars->{'products'} = \@products; - $vars->{'this_bug'} = $bug; - $vars->{'all_bug_ids'} = \@all_bug_ids; + AND bugs.product_id = ?', undef, + ($who->id, $product->id) + ); + + my %votes = map { $_->[0] => $_->[1] } @$vote_list; + my @bug_ids = sort keys %votes; + + # Exclude bugs that the user can no longer see. + @bug_ids = @{$user->visible_bugs(\@bug_ids)}; + next unless scalar @bug_ids; + + push(@all_bug_ids, @bug_ids); + my @bugs = @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + $_->{count} = $votes{$_->id} foreach @bugs; + + # We include votes from bugs that the user can no longer see. + my $total = sum(values %votes) || 0; + + my $onevoteonly = 0; + $onevoteonly = 1 + if (min($product->{votesperuser}, $product->{maxvotesperbug}) == 1); + + push( + @products, + { + name => $product->name, + bugs => \@bugs, + bug_ids => \@bug_ids, + onevoteonly => $onevoteonly, + total => $total, + maxvotes => $product->{votesperuser}, + maxperbug => $product->{maxvotesperbug} + } + ); + } + + if ($canedit && $bug) { + $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', undef, $who->id); + } + $dbh->bz_commit_transaction(); + + $vars->{'canedit'} = $canedit; + $vars->{'voting_user'} = {"login" => $who->name}; + $vars->{'products'} = \@products; + $vars->{'this_bug'} = $bug; + $vars->{'all_bug_ids'} = \@all_bug_ids; } sub _update_votes { - my ($vars) = @_; - - ############################################################################ - # Begin Data/Security Validation - ############################################################################ - - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $input = Bugzilla->input_params; - - # Build a list of bug IDs for which votes have been submitted. Votes - # are submitted in form fields in which the field names are the bug - # IDs and the field values are the number of votes. - - my @buglist = grep {/^\d+$/} keys %$input; - my (%bugs, %votes); - - # If no bugs are in the buglist, let's make sure the user gets notified - # that their votes will get nuked if they continue. - if (scalar(@buglist) == 0) { - if (!defined $cgi->param('delete_all_votes')) { - print $cgi->header(); - $template->process("voting/delete-all.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } - elsif ($cgi->param('delete_all_votes') == 0) { - print $cgi->redirect("page.cgi?id=voting/user.html"); - exit; + my ($vars) = @_; + + ############################################################################ + # Begin Data/Security Validation + ############################################################################ + + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $input = Bugzilla->input_params; + + # Build a list of bug IDs for which votes have been submitted. Votes + # are submitted in form fields in which the field names are the bug + # IDs and the field values are the number of votes. + + my @buglist = grep {/^\d+$/} keys %$input; + my (%bugs, %votes); + + # If no bugs are in the buglist, let's make sure the user gets notified + # that their votes will get nuked if they continue. + if (scalar(@buglist) == 0) { + if (!defined $cgi->param('delete_all_votes')) { + print $cgi->header(); + $template->process("voting/delete-all.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + elsif ($cgi->param('delete_all_votes') == 0) { + print $cgi->redirect("page.cgi?id=voting/user.html"); + exit; + } + } + else { + $user->visible_bugs(\@buglist); + my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist); + $bugs{$_->id} = $_ foreach @$bugs_obj; + } + + # Call check_is_visible() on each bug to make sure it is an existing bug + # that the user is authorized to access, and make sure the number of votes + # submitted is also an integer. + foreach my $id (@buglist) { + my $bug = $bugs{$id} + or ThrowUserError('bug_id_does_not_exist', {bug_id => $id}); + $bug->check_is_visible; + $id = $bug->id; + $votes{$id} = $input->{$id}; + detaint_natural($votes{$id}) || ThrowUserError("voting_must_be_nonnegative"); + } + + my $token = $cgi->param('token'); + check_hash_token($token, ['vote']); + + ############################################################################ + # End Data/Security Validation + ############################################################################ + my $who = $user->id; + + # If the user is voting for bugs, make sure they aren't overstuffing + # the ballot box. + if (scalar @buglist) { + my (%prodcount, %products); + foreach my $bug_id (keys %bugs) { + my $bug = $bugs{$bug_id}; + my $prod = $bug->product; + $products{$prod} ||= $bug->product_obj; + $prodcount{$prod} ||= 0; + $prodcount{$prod} += $votes{$bug_id}; + + # Make sure we haven't broken the votes-per-bug limit + ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) || ThrowUserError( + "voting_too_many_votes_for_bug", + { + max => $products{$prod}->{maxvotesperbug}, + product => $prod, + votes => $votes{$bug_id} } - } - else { - $user->visible_bugs(\@buglist); - my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist); - $bugs{$_->id} = $_ foreach @$bugs_obj; + ); } - # Call check_is_visible() on each bug to make sure it is an existing bug - # that the user is authorized to access, and make sure the number of votes - # submitted is also an integer. - foreach my $id (@buglist) { - my $bug = $bugs{$id} - or ThrowUserError('bug_id_does_not_exist', { bug_id => $id }); - $bug->check_is_visible; - $id = $bug->id; - $votes{$id} = $input->{$id}; - detaint_natural($votes{$id}) - || ThrowUserError("voting_must_be_nonnegative"); - } - - my $token = $cgi->param('token'); - check_hash_token($token, ['vote']); - - ############################################################################ - # End Data/Security Validation - ############################################################################ - my $who = $user->id; - - # If the user is voting for bugs, make sure they aren't overstuffing - # the ballot box. - if (scalar @buglist) { - my (%prodcount, %products); - foreach my $bug_id (keys %bugs) { - my $bug = $bugs{$bug_id}; - my $prod = $bug->product; - $products{$prod} ||= $bug->product_obj; - $prodcount{$prod} ||= 0; - $prodcount{$prod} += $votes{$bug_id}; - - # Make sure we haven't broken the votes-per-bug limit - ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) - || ThrowUserError("voting_too_many_votes_for_bug", - {max => $products{$prod}->{maxvotesperbug}, - product => $prod, - votes => $votes{$bug_id}}); - } - - # Make sure we haven't broken the votes-per-product limit - foreach my $prod (keys(%prodcount)) { - ($prodcount{$prod} <= $products{$prod}->{votesperuser}) - || ThrowUserError("voting_too_many_votes_for_product", - {max => $products{$prod}->{votesperuser}, - product => $prod, - votes => $prodcount{$prod}}); + # Make sure we haven't broken the votes-per-product limit + foreach my $prod (keys(%prodcount)) { + ($prodcount{$prod} <= $products{$prod}->{votesperuser}) || ThrowUserError( + "voting_too_many_votes_for_product", + { + max => $products{$prod}->{votesperuser}, + product => $prod, + votes => $prodcount{$prod} } + ); } + } - # Update the user's votes in the database. - $dbh->bz_start_transaction(); + # Update the user's votes in the database. + $dbh->bz_start_transaction(); - my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes - WHERE who = ?', undef, $who); + my $old_list = $dbh->selectall_arrayref( + 'SELECT bug_id, vote_count FROM votes + WHERE who = ?', undef, $who + ); - my %old_votes = map { $_->[0] => $_->[1] } @$old_list; + my %old_votes = map { $_->[0] => $_->[1] } @$old_list; - my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count) - VALUES (?, ?, ?)'); - my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ? - WHERE bug_id = ? AND who = ?'); + my $sth_insertVotes = $dbh->prepare( + 'INSERT INTO votes (who, bug_id, vote_count) + VALUES (?, ?, ?)' + ); + my $sth_updateVotes = $dbh->prepare( + 'UPDATE votes SET vote_count = ? + WHERE bug_id = ? AND who = ?' + ); - my %affected = map { $_ => 1 } (@buglist, keys %old_votes); - my @deleted_votes; - - foreach my $id (keys %affected) { - if (!$votes{$id}) { - push(@deleted_votes, $id); - next; - } - if ($votes{$id} == ($old_votes{$id} || 0)) { - delete $affected{$id}; - next; - } - # We use 'defined' in case 0 was accidentally stored in the DB. - if (defined $old_votes{$id}) { - $sth_updateVotes->execute($votes{$id}, $id, $who); - } - else { - $sth_insertVotes->execute($who, $id, $votes{$id}); - } - } + my %affected = map { $_ => 1 } (@buglist, keys %old_votes); + my @deleted_votes; - if (@deleted_votes) { - $dbh->do('DELETE FROM votes WHERE who = ? AND ' . - $dbh->sql_in('bug_id', \@deleted_votes), undef, $who); + foreach my $id (keys %affected) { + if (!$votes{$id}) { + push(@deleted_votes, $id); + next; } - - # Update the cached values in the bugs table - my @updated_bugs = (); - - my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes - WHERE bug_id = ?"); - - $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); - - foreach my $id (keys %affected) { - $sth_getVotes->execute($id); - my $v = $sth_getVotes->fetchrow_array || 0; - $sth_updateVotes->execute($v, $id); - $bugs{$id}->{votes} = $v if $bugs{$id}; - my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id); - push (@updated_bugs, $id) if $confirmed; + if ($votes{$id} == ($old_votes{$id} || 0)) { + delete $affected{$id}; + next; } - $dbh->bz_commit_transaction(); - - print $cgi->header() if scalar @updated_bugs; - $vars->{'type'} = "votes"; - $vars->{'title_tag'} = 'change_votes'; - foreach my $bug_id (@updated_bugs) { - $vars->{'id'} = $bug_id; - $vars->{'sent_bugmail'} = - Bugzilla::BugMail::Send($bug_id, { 'changer' => $user }); - - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - # Set header_done to 1 only after the first bug. - $vars->{'header_done'} = 1; + # We use 'defined' in case 0 was accidentally stored in the DB. + if (defined $old_votes{$id}) { + $sth_updateVotes->execute($votes{$id}, $id, $who); } - $vars->{'votes_recorded'} = 1; + else { + $sth_insertVotes->execute($who, $id, $votes{$id}); + } + } + + if (@deleted_votes) { + $dbh->do( + 'DELETE FROM votes WHERE who = ? AND ' + . $dbh->sql_in('bug_id', \@deleted_votes), + undef, $who + ); + } + + # Update the cached values in the bugs table + my @updated_bugs = (); + + my $sth_getVotes = $dbh->prepare( + "SELECT SUM(vote_count) FROM votes + WHERE bug_id = ?" + ); + + $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); + + foreach my $id (keys %affected) { + $sth_getVotes->execute($id); + my $v = $sth_getVotes->fetchrow_array || 0; + $sth_updateVotes->execute($v, $id); + $bugs{$id}->{votes} = $v if $bugs{$id}; + my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id); + push(@updated_bugs, $id) if $confirmed; + } + + $dbh->bz_commit_transaction(); + + print $cgi->header() if scalar @updated_bugs; + $vars->{'type'} = "votes"; + $vars->{'title_tag'} = 'change_votes'; + foreach my $bug_id (@updated_bugs) { + $vars->{'id'} = $bug_id; + $vars->{'sent_bugmail'} + = Bugzilla::BugMail::Send($bug_id, {'changer' => $user}); + + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + # Set header_done to 1 only after the first bug. + $vars->{'header_done'} = 1; + } + $vars->{'votes_recorded'} = 1; } ###################### @@ -650,234 +691,246 @@ sub _update_votes { ###################### sub _modify_bug_votes { - my ($product, $changes) = @_; - my $dbh = Bugzilla->dbh; - my @msgs; + my ($product, $changes) = @_; + my $dbh = Bugzilla->dbh; + my @msgs; - # 1. too many votes for a single user on a single bug. - my @toomanyvotes_list; - if ($product->{maxvotesperbug} < $product->{votesperuser}) { - my $votes = $dbh->selectall_arrayref( - 'SELECT votes.who, votes.bug_id + # 1. too many votes for a single user on a single bug. + my @toomanyvotes_list; + if ($product->{maxvotesperbug} < $product->{votesperuser}) { + my $votes = $dbh->selectall_arrayref( + 'SELECT votes.who, votes.bug_id FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id WHERE bugs.product_id = ? - AND votes.vote_count > ?', - undef, ($product->id, $product->{maxvotesperbug})); + AND votes.vote_count > ?', undef, + ($product->id, $product->{maxvotesperbug}) + ); + + foreach my $vote (@$votes) { + my ($who, $id) = (@$vote); - foreach my $vote (@$votes) { - my ($who, $id) = (@$vote); - # If some votes are removed, _remove_votes() returns a list - # of messages to send to voters. - push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug')); - my $name = user_id_to_login($who); + # If some votes are removed, _remove_votes() returns a list + # of messages to send to voters. + push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug')); + my $name = user_id_to_login($who); - push(@toomanyvotes_list, {id => $id, name => $name}); - } + push(@toomanyvotes_list, {id => $id, name => $name}); } + } - $changes->{'_too_many_votes'} = \@toomanyvotes_list; + $changes->{'_too_many_votes'} = \@toomanyvotes_list; - # 2. too many total votes for a single user. - # This part doesn't work in the general case because _remove_votes - # doesn't enforce votesperuser (except per-bug when it's less - # than maxvotesperbug). See _remove_votes(). + # 2. too many total votes for a single user. + # This part doesn't work in the general case because _remove_votes + # doesn't enforce votesperuser (except per-bug when it's less + # than maxvotesperbug). See _remove_votes(). - my $votes = $dbh->selectall_arrayref( - 'SELECT votes.who, votes.vote_count + my $votes = $dbh->selectall_arrayref( + 'SELECT votes.who, votes.vote_count FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id - WHERE bugs.product_id = ?', - undef, $product->id); + WHERE bugs.product_id = ?', undef, $product->id + ); - my %counts; - foreach my $vote (@$votes) { - my ($who, $count) = @$vote; - if (!defined $counts{$who}) { - $counts{$who} = $count; - } else { - $counts{$who} += $count; - } + my %counts; + foreach my $vote (@$votes) { + my ($who, $count) = @$vote; + if (!defined $counts{$who}) { + $counts{$who} = $count; + } + else { + $counts{$who} += $count; } + } - my @toomanytotalvotes_list; - foreach my $who (keys(%counts)) { - if ($counts{$who} > $product->{votesperuser}) { - my $bug_ids = $dbh->selectcol_arrayref( - 'SELECT votes.bug_id + my @toomanytotalvotes_list; + foreach my $who (keys(%counts)) { + if ($counts{$who} > $product->{votesperuser}) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT votes.bug_id FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id WHERE bugs.product_id = ? - AND votes.who = ?', - undef, $product->id, $who); - - foreach my $bug_id (@$bug_ids) { - # _remove_votes returns a list of messages to send - # in case some voters had too many votes. - push(@msgs, _remove_votes($bug_id, $who, - 'votes_too_many_per_user')); - my $name = user_id_to_login($who); - - push(@toomanytotalvotes_list, {id => $bug_id, name => $name}); - } - } - } - - $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; - - # 3. enough votes to confirm - my $bug_list = $dbh->selectcol_arrayref( - 'SELECT bug_id FROM bugs - WHERE product_id = ? AND bug_status = ? AND votes >= ?', - undef, ($product->id, 'UNCONFIRMED', $product->{votestoconfirm})); - - my @updated_bugs; - foreach my $bug_id (@$bug_list) { - my $confirmed = _confirm_if_vote_confirmed($bug_id, $product); - push (@updated_bugs, $bug_id) if $confirmed; - } - $changes->{'_confirmed_bugs'} = \@updated_bugs; - - # Now that changes are done, we can send emails to voters. - foreach my $msg (@msgs) { - MessageToMTA($msg); - } - # And send out emails about changed bugs - foreach my $bug_id (@updated_bugs) { - my $sent_bugmail = Bugzilla::BugMail::Send( - $bug_id, { changer => Bugzilla->user }); - $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; - } + AND votes.who = ?', undef, $product->id, $who + ); + + foreach my $bug_id (@$bug_ids) { + + # _remove_votes returns a list of messages to send + # in case some voters had too many votes. + push(@msgs, _remove_votes($bug_id, $who, 'votes_too_many_per_user')); + my $name = user_id_to_login($who); + + push(@toomanytotalvotes_list, {id => $bug_id, name => $name}); + } + } + } + + $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; + + # 3. enough votes to confirm + my $bug_list = $dbh->selectcol_arrayref( + 'SELECT bug_id FROM bugs + WHERE product_id = ? AND bug_status = ? AND votes >= ?', undef, + ($product->id, 'UNCONFIRMED', $product->{votestoconfirm}) + ); + + my @updated_bugs; + foreach my $bug_id (@$bug_list) { + my $confirmed = _confirm_if_vote_confirmed($bug_id, $product); + push(@updated_bugs, $bug_id) if $confirmed; + } + $changes->{'_confirmed_bugs'} = \@updated_bugs; + + # Now that changes are done, we can send emails to voters. + foreach my $msg (@msgs) { + MessageToMTA($msg); + } + + # And send out emails about changed bugs + foreach my $bug_id (@updated_bugs) { + my $sent_bugmail + = Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; + } } # If a bug is moved to a product which allows less votes per bug # compared to the previous product, extra votes need to be removed. sub _remove_votes { - my ($id, $who, $reason) = (@_); - my $dbh = Bugzilla->dbh; - - my $whopart = ($who) ? " AND votes.who = $who" : ""; - - my $sth = $dbh->prepare("SELECT profiles.login_name, " . - "profiles.userid, votes.vote_count, " . - "products.votesperuser, products.maxvotesperbug " . - "FROM profiles " . - "LEFT JOIN votes ON profiles.userid = votes.who " . - "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " . - "LEFT JOIN products ON products.id = bugs.product_id " . - "WHERE votes.bug_id = ? " . $whopart); - $sth->execute($id); - my @list; - while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) { - push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]); - } - - # @messages stores all emails which have to be sent, if any. - # This array is passed to the caller which will send these emails itself. - my @messages = (); - - if (scalar(@list)) { - foreach my $ref (@list) { - my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); - - $maxvotesperbug = min($votesperuser, $maxvotesperbug); - - # If this product allows voting and the user's votes are in - # the acceptable range, then don't do anything. - next if $votesperuser && $oldvotes <= $maxvotesperbug; - - # If the user has more votes on this bug than this product - # allows, then reduce the number of votes so it fits - my $newvotes = $maxvotesperbug; - - my $removedvotes = $oldvotes - $newvotes; - - if ($newvotes) { - $dbh->do("UPDATE votes SET vote_count = ? " . - "WHERE bug_id = ? AND who = ?", - undef, ($newvotes, $id, $userid)); - } else { - $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", - undef, ($id, $userid)); - } - - # Notice that we did not make sure that the user fit within the $votesperuser - # range. This is considered to be an acceptable alternative to losing votes - # during product moves. Then next time the user attempts to change their votes, - # they will be forced to fit within the $votesperuser limit. - - # Now lets send the e-mail to alert the user to the fact that their votes have - # been reduced or removed. - my $vars = { - 'to' => $name . Bugzilla->params->{'emailsuffix'}, - 'bugid' => $id, - 'reason' => $reason, - - 'votesremoved' => $removedvotes, - 'votesold' => $oldvotes, - 'votesnew' => $newvotes, - }; - - my $voter = new Bugzilla::User($userid); - my $template = Bugzilla->template_inner($voter->setting('lang')); - - my $msg; - $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); - push(@messages, $msg); - } - - my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " . - "FROM votes WHERE bug_id = ?", - undef, $id) || 0; - $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", - undef, ($votes, $id)); - } - # Now return the array containing emails to be sent. - return @messages; + my ($id, $who, $reason) = (@_); + my $dbh = Bugzilla->dbh; + + my $whopart = ($who) ? " AND votes.who = $who" : ""; + + my $sth + = $dbh->prepare("SELECT profiles.login_name, " + . "profiles.userid, votes.vote_count, " + . "products.votesperuser, products.maxvotesperbug " + . "FROM profiles " + . "LEFT JOIN votes ON profiles.userid = votes.who " + . "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " + . "LEFT JOIN products ON products.id = bugs.product_id " + . "WHERE votes.bug_id = ? " + . $whopart); + $sth->execute($id); + my @list; + while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) + = $sth->fetchrow_array()) + { + push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]); + } + + # @messages stores all emails which have to be sent, if any. + # This array is passed to the caller which will send these emails itself. + my @messages = (); + + if (scalar(@list)) { + foreach my $ref (@list) { + my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); + + $maxvotesperbug = min($votesperuser, $maxvotesperbug); + + # If this product allows voting and the user's votes are in + # the acceptable range, then don't do anything. + next if $votesperuser && $oldvotes <= $maxvotesperbug; + + # If the user has more votes on this bug than this product + # allows, then reduce the number of votes so it fits + my $newvotes = $maxvotesperbug; + + my $removedvotes = $oldvotes - $newvotes; + + if ($newvotes) { + $dbh->do("UPDATE votes SET vote_count = ? " . "WHERE bug_id = ? AND who = ?", + undef, ($newvotes, $id, $userid)); + } + else { + $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", + undef, ($id, $userid)); + } + + # Notice that we did not make sure that the user fit within the $votesperuser + # range. This is considered to be an acceptable alternative to losing votes + # during product moves. Then next time the user attempts to change their votes, + # they will be forced to fit within the $votesperuser limit. + + # Now lets send the e-mail to alert the user to the fact that their votes have + # been reduced or removed. + my $vars = { + 'to' => $name . Bugzilla->params->{'emailsuffix'}, + 'bugid' => $id, + 'reason' => $reason, + + 'votesremoved' => $removedvotes, + 'votesold' => $oldvotes, + 'votesnew' => $newvotes, + }; + + my $voter = new Bugzilla::User($userid); + my $template = Bugzilla->template_inner($voter->setting('lang')); + + my $msg; + $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); + push(@messages, $msg); + } + + my $votes + = $dbh->selectrow_array( + "SELECT SUM(vote_count) " . "FROM votes WHERE bug_id = ?", + undef, $id) + || 0; + $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", undef, ($votes, $id)); + } + + # Now return the array containing emails to be sent. + return @messages; } # If a user votes for a bug, or the number of votes required to # confirm a bug has been reduced, check if the bug is now confirmed. sub _confirm_if_vote_confirmed { - my ($id, $product) = @_; - my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 }); - $product ||= $bug->product_obj; - - my $ret = 0; - if (!$bug->everconfirmed - and $product->{votestoconfirm} - and $bug->votes >= $product->{votestoconfirm}) - { - $bug->add_comment('', { type => CMT_POPULAR_VOTES }); - - if ($bug->bug_status eq 'UNCONFIRMED') { - # Get a valid open state. - my $new_status; - foreach my $state (@{$bug->status->can_change_to}) { - if ($state->is_open && $state->name ne 'UNCONFIRMED') { - $new_status = $state->name; - last; - } - } - ThrowCodeError('voting_no_open_bug_status') unless $new_status; - - # We cannot call $bug->set_bug_status() here, because a user without - # canconfirm privs should still be able to confirm a bug by - # popular vote. We already know the new status is valid, so it's safe. - $bug->{bug_status} = $new_status; - $bug->{everconfirmed} = 1; - delete $bug->{'status'}; # Contains the status object. - } - else { - # If the bug is in a closed state, only set everconfirmed to 1. - # Do not call $bug->_set_everconfirmed(), for the same reason as above. - $bug->{everconfirmed} = 1; + my ($id, $product) = @_; + my $bug = ref $id ? $id : new Bugzilla::Bug({id => $id, cache => 1}); + $product ||= $bug->product_obj; + + my $ret = 0; + if ( !$bug->everconfirmed + and $product->{votestoconfirm} + and $bug->votes >= $product->{votestoconfirm}) + { + $bug->add_comment('', {type => CMT_POPULAR_VOTES}); + + if ($bug->bug_status eq 'UNCONFIRMED') { + + # Get a valid open state. + my $new_status; + foreach my $state (@{$bug->status->can_change_to}) { + if ($state->is_open && $state->name ne 'UNCONFIRMED') { + $new_status = $state->name; + last; } - $bug->update(); + } + ThrowCodeError('voting_no_open_bug_status') unless $new_status; - $ret = 1; + # We cannot call $bug->set_bug_status() here, because a user without + # canconfirm privs should still be able to confirm a bug by + # popular vote. We already know the new status is valid, so it's safe. + $bug->{bug_status} = $new_status; + $bug->{everconfirmed} = 1; + delete $bug->{'status'}; # Contains the status object. } - return $ret; + else { + # If the bug is in a closed state, only set everconfirmed to 1. + # Do not call $bug->_set_everconfirmed(), for the same reason as above. + $bug->{everconfirmed} = 1; + } + $bug->update(); + + $ret = 1; + } + return $ret; } diff --git a/extensions/ZPushNotify/Config.pm b/extensions/ZPushNotify/Config.pm index 1a31285e2..04c607974 100644 --- a/extensions/ZPushNotify/Config.pm +++ b/extensions/ZPushNotify/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'ZPushNotify'; +use constant NAME => 'ZPushNotify'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ZPushNotify/Extension.pm b/extensions/ZPushNotify/Extension.pm index df3ab8f46..7c3b6cbf0 100644 --- a/extensions/ZPushNotify/Extension.pm +++ b/extensions/ZPushNotify/Extension.pm @@ -21,46 +21,38 @@ use Bugzilla; # sub _notify { - my ($bug_id, $delta_ts) = @_; - # beacuse the push_notify table is hot, we defer updating it until the - # request has completed. this ensures we are outside the scope of any - # transaction blocks. + my ($bug_id, $delta_ts) = @_; - my $stash = Bugzilla->request_cache->{ZPushNotify_stash} ||= []; - push @$stash, { bug_id => $bug_id, delta_ts => $delta_ts }; + # beacuse the push_notify table is hot, we defer updating it until the + # request has completed. this ensures we are outside the scope of any + # transaction blocks. + + my $stash = Bugzilla->request_cache->{ZPushNotify_stash} ||= []; + push @$stash, {bug_id => $bug_id, delta_ts => $delta_ts}; } sub request_cleanup { - my $stash = Bugzilla->request_cache->{ZPushNotify_stash} - || return; - - my $dbh = Bugzilla->dbh; - foreach my $rh (@$stash) { - # using REPLACE INTO or INSERT .. ON DUPLICATE KEY UPDATE results in a - # lock on the bugs table due to the FK. this way is more verbose but - # only locks the push_notify table. - $dbh->bz_start_transaction(); - my ($id) = $dbh->selectrow_array( - "SELECT id FROM push_notify WHERE bug_id=?", - undef, - $rh->{bug_id} - ); - if ($id) { - $dbh->do( - "UPDATE push_notify SET delta_ts=? WHERE id=?", - undef, - $rh->{delta_ts}, $id - ); - } - else { - $dbh->do( - "INSERT INTO push_notify (bug_id, delta_ts) VALUES (?, ?)", - undef, - $rh->{bug_id}, $rh->{delta_ts} - ); - } - $dbh->bz_commit_transaction(); + my $stash = Bugzilla->request_cache->{ZPushNotify_stash} || return; + + my $dbh = Bugzilla->dbh; + foreach my $rh (@$stash) { + + # using REPLACE INTO or INSERT .. ON DUPLICATE KEY UPDATE results in a + # lock on the bugs table due to the FK. this way is more verbose but + # only locks the push_notify table. + $dbh->bz_start_transaction(); + my ($id) = $dbh->selectrow_array("SELECT id FROM push_notify WHERE bug_id=?", + undef, $rh->{bug_id}); + if ($id) { + $dbh->do("UPDATE push_notify SET delta_ts=? WHERE id=?", + undef, $rh->{delta_ts}, $id); + } + else { + $dbh->do("INSERT INTO push_notify (bug_id, delta_ts) VALUES (?, ?)", + undef, $rh->{bug_id}, $rh->{delta_ts}); } + $dbh->bz_commit_transaction(); + } } # @@ -68,59 +60,59 @@ sub request_cleanup { # sub object_end_of_create { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless Bugzilla->params->{enable_simple_push}; - return unless $object->isa('Bugzilla::Flag'); - _notify($object->bug->id, $object->creation_date); + my ($self, $args) = @_; + my $object = $args->{object}; + return unless Bugzilla->params->{enable_simple_push}; + return unless $object->isa('Bugzilla::Flag'); + _notify($object->bug->id, $object->creation_date); } sub flag_updated { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - my $changes = $args->{changes}; - return unless Bugzilla->params->{enable_simple_push}; - return unless scalar(keys %$changes); - _notify($flag->bug->id, $timestamp); + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + my $changes = $args->{changes}; + return unless Bugzilla->params->{enable_simple_push}; + return unless scalar(keys %$changes); + _notify($flag->bug->id, $timestamp); } sub flag_deleted { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - return unless Bugzilla->params->{enable_simple_push}; - _notify($flag->bug->id, $timestamp); + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + return unless Bugzilla->params->{enable_simple_push}; + _notify($flag->bug->id, $timestamp); } sub attachment_end_of_update { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - return unless scalar keys %{ $args->{changes} }; - return unless my $object = $args->{object}; - _notify($object->bug->id, $object->modification_time); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + return unless scalar keys %{$args->{changes}}; + return unless my $object = $args->{object}; + _notify($object->bug->id, $object->modification_time); } sub object_before_delete { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - return unless my $object = $args->{object}; - if ($object->isa('Bugzilla::Attachment')) { - my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - _notify($object->bug->id, $timestamp); - } + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + return unless my $object = $args->{object}; + if ($object->isa('Bugzilla::Attachment')) { + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + _notify($object->bug->id, $timestamp); + } } sub bug_end_of_update_delta_ts { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - _notify($args->{bug_id}, $args->{timestamp}); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + _notify($args->{bug_id}, $args->{timestamp}); } sub bug_end_of_create { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - _notify($args->{bug}->id, $args->{timestamp}); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + _notify($args->{bug}->id, $args->{timestamp}); } # @@ -128,44 +120,25 @@ sub bug_end_of_create { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'push_notify'} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' - }, - }, - delta_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_notify_idx => { - FIELDS => [ 'bug_id' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'push_notify'} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + }, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [push_notify_idx => {FIELDS => ['bug_id'], TYPE => 'UNIQUE',},], + }; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'enable_simple_push', - type => 'b', - default => 0, - }; + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + {name => 'enable_simple_push', type => 'b', default => 0,}; } __PACKAGE__->NAME; diff --git a/extensions/create.pl b/extensions/create.pl index 8927c172c..3d3bfaa93 100755 --- a/extensions/create.pl +++ b/extensions/create.pl @@ -27,7 +27,7 @@ my $base_dir = bz_locations()->{'extensionsdir'}; my $name = $ARGV[0] or ThrowUserError('extension_create_no_name'); if ($name !~ /^[A-Z]/) { - ThrowUserError('extension_first_letter_caps', { name => $name }); + ThrowUserError('extension_first_letter_caps', {name => $name}); } my $extension_dir = "$base_dir/$name"; @@ -35,33 +35,33 @@ mkpath($extension_dir) || die "$extension_dir already exists or cannot be created.\n"; my $lcname = lc($name); -foreach my $path (qw(lib web template/en/default/hook), - "template/en/default/$lcname") +foreach + my $path (qw(lib web template/en/default/hook), "template/en/default/$lcname") { - mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!"; + mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!"; } my $year = DateTime->now()->year; -my $template = Bugzilla->template; -my $vars = { year => $year, name => $name, path => $extension_dir }; +my $template = Bugzilla->template; +my $vars = {year => $year, name => $name, path => $extension_dir}; my %create_files = ( - 'config.pm.tmpl' => 'Config.pm', - 'extension.pm.tmpl' => 'Extension.pm', - 'util.pm.tmpl' => 'lib/Util.pm', - 'web-readme.txt.tmpl' => 'web/README', - 'hook-readme.txt.tmpl' => 'template/en/default/hook/README', - 'name-readme.txt.tmpl' => "template/en/default/$lcname/README", + 'config.pm.tmpl' => 'Config.pm', + 'extension.pm.tmpl' => 'Extension.pm', + 'util.pm.tmpl' => 'lib/Util.pm', + 'web-readme.txt.tmpl' => 'web/README', + 'hook-readme.txt.tmpl' => 'template/en/default/hook/README', + 'name-readme.txt.tmpl' => "template/en/default/$lcname/README", ); foreach my $template_file (keys %create_files) { - my $target = $create_files{$template_file}; - my $output; - $template->process("extensions/$template_file", $vars, \$output) - or ThrowTemplateError($template->error()); - open(my $fh, '>', "$extension_dir/$target"); - print $fh $output; - close($fh); + my $target = $create_files{$template_file}; + my $output; + $template->process("extensions/$template_file", $vars, \$output) + or ThrowTemplateError($template->error()); + open(my $fh, '>', "$extension_dir/$target"); + print $fh $output; + close($fh); } print get_text('extension_created', $vars), "\n"; -- cgit v1.2.3-24-g4f1b