diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index a8cd371129..ee075f295c 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -16,7 +16,9 @@ use Bugzilla::User; use Bugzilla::Constants; use Bugzilla::Util; use Bugzilla::Bug; +use Bugzilla::Attachment; use Bugzilla::Comment; +use Bugzilla::FlagType; use Bugzilla::Logging; use Bugzilla::Mailer; use Bugzilla::Hook; @@ -199,6 +201,50 @@ sub Send { } } + # Bug 1883428: someone a flag was requested of, or who requested a flag + # that's now been granted/denied, becomes a recipient even without any + # other role on the bug. Flag-type cc_list addresses (admin-configured, + # bypasses per-user opt-in) are added the same way notify() used to. + my @flag_events + = $params->{dep_only} + ? () + : _get_flag_mail_events($bug, $start, $end, \%user_cache); + + foreach my $event (@flag_events) { + # notify() used to skip addressees who couldn't see a private + # attachment; _flag_event_visible_to() matches that so a non-insider + # doesn't get added as a recipient purely to be told about a flag on an + # attachment they can't see (and can't be shown to them in the mail + # body either). + if ($event->{action} eq 'requested') { + my $requestee_id = $event->{requestee_id}; + my $requestee + = $user_cache{$requestee_id} ||= Bugzilla::User->new({id => $requestee_id, cache => 1}); + $event->{requestee} = $requestee; + $recipients{$requestee_id}->{+REL_FLAG_REQUESTEE} = BIT_DIRECT + if _flag_event_visible_to($event, $requestee); + } + elsif ($event->{action} eq 'answered') { + my $requester_id = $event->{requester_id}; + my $requester + = $user_cache{$requester_id} ||= Bugzilla::User->new({id => $requester_id, cache => 1}); + $event->{requester} = $requester; + + # Don't add the requester as a recipient for clearing their own + # request (e.g. cancelling a needinfo they asked for themselves) -- + # old notify() gated on setter != requester for the same reason. + $recipients{$requester_id}->{+REL_FLAG_REQUESTER} = BIT_DIRECT + if $event->{setter}->id != $requester_id + && _flag_event_visible_to($event, $requester); + } + } + + my ($flag_type_cc, $flag_type_cc_raw) + = _get_flag_type_cc($bug, \@flag_events, \%user_cache); + foreach my $user_id (keys %$flag_type_cc) { + $recipients{$user_id}->{+REL_FLAG_TYPE_CC} = BIT_DIRECT; + } + # Make sure %user_cache has every user in it so far referenced foreach my $user_id (keys %recipients) { $user_cache{$user_id} ||= new Bugzilla::User({id => $user_id, cache => 1}); @@ -227,10 +273,29 @@ sub Send { # Mark these people as having the role of the person they are watching foreach my $watch (@$userwatchers) { + my $role_inherited = 0; while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { - $recipients{$watch->[0]}->{$role} |= BIT_WATCHING if $bits & BIT_DIRECT; + + # Flag roles are addressed to a specific person (someone asked + # them, or they asked and got answered) -- watching that person's + # other bug activity isn't the same thing, and without this the + # watcher got a content-free mail (empty reason line, no flag + # section, since the per-recipient flag_events filter only + # matches the actual requestee/requester's own user id). + next + if $role == REL_FLAG_REQUESTEE + || $role == REL_FLAG_REQUESTER + || $role == REL_FLAG_TYPE_CC; + next unless $bits & BIT_DIRECT; + $recipients{$watch->[0]}->{$role} |= BIT_WATCHING; + $role_inherited = 1; } - push(@{$watching{$watch->[0]}}, $watch->[1]); + + # Skip %watching too when nothing was inherited (e.g. the watched + # user's only role was a flag role) -- otherwise this watcher gets a + # spurious "X-Bugzilla-Watch-Reason: None " header for a + # relationship that contributed nothing to the mail. + push(@{$watching{$watch->[0]}}, $watch->[1]) if $role_inherited; } } @@ -272,11 +337,29 @@ sub Send { # Go through each role the user has and see if they want mail in # that role. foreach my $relationship (keys %{$recipients{$user_id}}) { - if ($user->wants_bug_mail( - $bug, $relationship, $start ? \@diffs : [], - $comments, $params->{dep_only}, $changer - )) - { + my $wants_mail; + if ($relationship == REL_FLAG_REQUESTEE) { + + # Same opt-in flag "requested of me" has always used, not the + # normal per-field wants_bug_mail() logic (flags aren't a diffed + # bug field from this recipient's point of view). + $wants_mail = $user->wants_mail([EVT_FLAG_REQUESTED], REL_ANY); + } + elsif ($relationship == REL_FLAG_REQUESTER) { + $wants_mail = $user->wants_mail([EVT_REQUESTED_FLAG], REL_ANY); + } + elsif ($relationship == REL_FLAG_TYPE_CC) { + + # Admin-configured list; always notified, like notify() did. + $wants_mail = 1; + } + else { + $wants_mail = $user->wants_bug_mail( + $bug, $relationship, $start ? \@diffs : [], + $comments, $params->{dep_only}, $changer + ); + } + if ($wants_mail) { $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship}; } } @@ -330,6 +413,25 @@ sub Send { $blocker_short_desc = $blocker_entry->{short_desc} if $blocker_entry; } + # Flag events relevant to this recipient: they were asked, or they + # asked and someone else answered (no need to tell people about + # their own actions), or they're on the flag type's cc_list for + # this event. A recipient who isn't an insider doesn't get told + # about a flag on a private attachment even if they qualify for + # the mail some other way (e.g. they're on the CC list). + my $user_cc_events = $flag_type_cc->{$user_id} || []; + my @user_flag_events = grep { + my $event = $_; + _flag_event_visible_to($event, $user) + && ( + ($event->{action} eq 'requested' && $event->{requestee_id} == $user_id) + || ($event->{action} eq 'answered' + && $event->{requester_id} == $user_id + && $event->{setter}->id != $user_id) + || (grep { $_ == $event } @$user_cc_events) + ) + } @flag_events; + my $sent_mail = sendMail({ to => $user, bug => $bug, @@ -342,12 +444,15 @@ sub Send { referenced_bugs => $referenced_bugs, dep_only => $params->{dep_only}, blocker_short_desc => $blocker_short_desc, + flag_events => \@user_flag_events, }); push(@sent, $user->login) if $sent_mail; } } } + _send_flag_type_cc_raw_mail($bug, $flag_type_cc_raw, $date); + # When sending bugmail about a blocker being reopened or resolved, # we say nothing about changes in the bug being blocked, so we must # not update lastdiffed in this case. @@ -373,6 +478,7 @@ sub sendMail { my $referenced_bugs = $params->{referenced_bugs}; my $dep_only = $params->{dep_only}; my $blocker_short_desc = $params->{blocker_short_desc}; + my $flag_events = $params->{flag_events} || []; my $attach_id; # Only display changes the user is allowed see. @@ -424,7 +530,7 @@ sub sendMail { @send_comments = grep { !$_->is_private } @send_comments; } - if (!scalar(@display_diffs) && !scalar(@send_comments)) { + if (!scalar(@display_diffs) && !scalar(@send_comments) && !scalar(@$flag_events)) { # Whoops, no differences! return 0; @@ -482,6 +588,7 @@ sub sendMail { referenced_bugs => $referenced_bugs, bugmailtype => $bugmailtype, blocker_short_desc => $blocker_short_desc, + flag_events => $flag_events, }; if (Bugzilla->get_param_with_override('use_mailer_queue')) { @@ -521,6 +628,27 @@ sub enqueue { foreach my $reference (@{$vars->{referenced_bugs}}) { $reference->{bug} = _flatten_object($reference->{bug}); } + + # Bug 1883428: flag_events carries live FlagType/Attachment/User objects + # (the setter is a shared %user_cache entry the recipient loop has + # already populated with lazily-built fields) -- flatten them the same + # way, dropping the attachment object (dequeue() re-fetches it by id so + # it can also re-check visibility at send time). + $vars->{flag_events} = [ + map { + { + action => $_->{action}, + attachment_id => $_->{attachment_id}, + requestee_id => $_->{requestee_id}, + requester_id => $_->{requester_id}, + status => $_->{status}, + type => _flatten_object($_->{type}), + setter => _flatten_object($_->{setter}), + requestee => _flatten_object($_->{requestee}), + requester => _flatten_object($_->{requester}), + } + } @{$vars->{flag_events}} + ]; Bugzilla->job_queue->insert('bug_mail', {vars => $vars}); } @@ -560,6 +688,27 @@ sub dequeue { return; } + # Inflate flag_events, then re-check attachment visibility at send time + # (TOCTOU, bug 1883428) -- the enqueue-time private-attachment gate may + # be stale if the attachment was made private, or the recipient lost + # insider access, between enqueue and dequeue. + $vars->{flag_events} = [ + map { + +{ + %$_, + type => Bugzilla::FlagType->new_from_hash($_->{type}), + setter => Bugzilla::User->new_from_hash($_->{setter}), + requestee => $_->{requestee} ? Bugzilla::User->new_from_hash($_->{requestee}) : undef, + requester => $_->{requester} ? Bugzilla::User->new_from_hash($_->{requester}) : undef, + attachment => $_->{attachment_id} + ? Bugzilla::Attachment->new({id => $_->{attachment_id}, cache => 1}) + : undef, + } + } @{$vars->{flag_events}} + ]; + $vars->{flag_events} + = [grep { _flag_event_visible_to($_, $vars->{to_user}) } @{$vars->{flag_events}}]; + $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); $vars->{new_comments} = [map { Bugzilla::Comment->new_from_hash($_) } @{$vars->{new_comments}}]; @@ -701,6 +850,156 @@ sub _generate_bugmail { return $email; } +# True if $user is allowed to see $event's attachment, i.e. it isn't +# private or $user is an insider. Flag events with no attachment are always +# visible. Used everywhere a flag event's exposure to a specific user is +# decided: adding recipients, filtering an already-built recipient's +# flag_events, and the dequeue-time TOCTOU re-check (bug 1883428). +sub _flag_event_visible_to { + my ($event, $user) = @_; + return !$event->{attachment} || !$event->{attachment}->isprivate || ($user && $user->is_insider); +} + +# Find flags that were requested of, or answered by, someone during this +# window, so sendMail() can add a note at the top of the mail for that +# person even if they hold no other role on the bug (bug 1883428). +sub _get_flag_mail_events { + my ($bug, $start, $end, $user_cache) = @_; + my $dbh = Bugzilla->dbh; + + my @args = ($bug->id); + my $when_restriction = ''; + if ($start) { + $when_restriction = ' AND flag_when > ? AND flag_when <= ?'; + push @args, ($start, $end); + } + + my $rows = $dbh->selectall_arrayref( + "SELECT id, flag_id, flag_when, type_id, status, setter_id, requestee_id, + attachment_id + FROM flag_activity + WHERE bug_id = ?$when_restriction + ORDER BY flag_when, id", {Slice => {}}, @args + ); + + my @events; + foreach my $row (@$rows) { + $user_cache->{$row->{setter_id}} + ||= Bugzilla::User->new({id => $row->{setter_id}, cache => 1}); + + if ($row->{status} eq '?' && $row->{requestee_id}) { + push @events, + { + action => 'requested', + type => Bugzilla::FlagType->new({id => $row->{type_id}, cache => 1}), + attachment_id => $row->{attachment_id}, + attachment => $row->{attachment_id} + ? Bugzilla::Attachment->new({id => $row->{attachment_id}, cache => 1}) + : undef, + requestee_id => $row->{requestee_id}, + setter => $user_cache->{$row->{setter_id}}, + }; + } + elsif ($row->{status} eq '+' || $row->{status} eq '-' || $row->{status} eq 'X') { + + # 'X' is a flag cleared without +/- (e.g. needinfo auto-cleared when + # the requestee replies) -- notify.() treated that the same as an + # explicit answer, so we do too. + # + # flags.setter_id gets overwritten to whoever granted/denied/cleared the + # flag, so the requester has to be found by looking back at this + # flag's activity. Only treat it as an answer if the immediately + # preceding row was still '?' -- old notify() gated on that too, so + # e.g. an already-answered flag later cleared by an attachment being + # obsoleted (also logged as status 'X') doesn't re-notify the + # original requester. Break ties on id, not just flag_when: a '?' + # and its answer can land in the same second. + my ($prev_status, $requester_id) = $dbh->selectrow_array( + "SELECT status, setter_id FROM flag_activity + WHERE flag_id = ? AND (flag_when < ? OR (flag_when = ? AND id < ?)) + ORDER BY flag_when DESC, id DESC LIMIT 1", undef, $row->{flag_id}, + $row->{flag_when}, $row->{flag_when}, $row->{id} + ); + next unless $requester_id && $prev_status && $prev_status eq '?'; + + push @events, + { + action => 'answered', + type => Bugzilla::FlagType->new({id => $row->{type_id}, cache => 1}), + attachment_id => $row->{attachment_id}, + attachment => $row->{attachment_id} + ? Bugzilla::Attachment->new({id => $row->{attachment_id}, cache => 1}) + : undef, + requester_id => $requester_id, + status => $row->{status}, + setter => $user_cache->{$row->{setter_id}}, + }; + } + } + return @events; +} + +# A flag type's cc_list is an admin-configured list of addresses notified +# on any status change of that type, independent of role on the bug -- +# this is the same computation notify() used to do per flag change. +# Returns (\%account_recipients keyed by user id, \%raw_addresses keyed by +# email for addresses with no Bugzilla account, each valued with the +# flag_events relevant to that address). +sub _get_flag_type_cc { + my ($bug, $flag_events, $user_cache) = @_; + my @bug_in_groups = grep { $_->{ison} || $_->{mandatory} } @{$bug->groups}; + + my (%account_recipients, %raw_addresses); + foreach my $event (@$flag_events) { + my $cc_list = $event->{type}->cc_list; + next unless $cc_list; + + # Reuse the attachment already loaded by _get_flag_mail_events rather + # than re-fetching: flag_activity is a historical log, so the + # attachment it points at can have since been deleted, and re-fetching + # by id would return undef and die on ->isprivate. + my $attachment_is_private + = $event->{attachment} ? $event->{attachment}->isprivate : 0; + + foreach my $cc (split(/[, ]+/, $cc_list)) { + my $ccuser = Bugzilla::User->new({name => $cc, cache => 1}); + next if @bug_in_groups && (!$ccuser || !$ccuser->can_see_bug($bug->id)); + next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); + + if ($ccuser) { + push @{$account_recipients{$ccuser->id}}, $event; + $user_cache->{$ccuser->id} ||= $ccuser; + } + else { + push @{$raw_addresses{$cc}}, $event; + } + } + } + return (\%account_recipients, \%raw_addresses); +} + +# cc_list can list addresses with no Bugzilla account. The normal recipient +# pipeline above is keyed on user id throughout (wants_bug_mail, language +# prefs, mailer-queue enqueue/dequeue...), so these get a minimal, separate +# notice instead of going through it. +sub _send_flag_type_cc_raw_mail { + my ($bug, $raw_addresses, $date) = @_; + return unless %$raw_addresses; + + my $lang = Bugzilla::User->new()->setting('lang'); + my $template = Bugzilla->template_inner($lang); + + foreach my $to (keys %$raw_addresses) { + my $message; + $template->process( + "email/bugmail-flagtype-cc.txt.tmpl", + {to => $to, bug => $bug, date => $date, flag_events => $raw_addresses->{$to}}, + \$message + ) || ThrowTemplateError($template->error()); + MessageToMTA($message); + } +} + sub _get_diffs { my ($bug, $end, $user_cache) = @_; my $dbh = Bugzilla->dbh; diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 2890df0ca6..b87350386d 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -89,6 +89,7 @@ use Memoize; RELATIONSHIPS REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER + REL_FLAG_REQUESTEE REL_FLAG_REQUESTER REL_FLAG_TYPE_CC REL_ANY POS_EVENTS @@ -355,14 +356,21 @@ use constant REL_CC => 3; # REL 4 was REL_VOTER, before it was moved ino an extension. use constant REL_GLOBAL_WATCHER => 5; +# Bug 1883428: recipients added because of a flag change, independently of +# any other role they may hold on the bug. +use constant REL_FLAG_REQUESTEE => 6; # A flag was requested of them +use constant REL_FLAG_REQUESTER => 7; # Their flag request was granted/denied +use constant REL_FLAG_TYPE_CC => 8; # On the flag type's admin-configured cc_list + # We need these strings for the X-Bugzilla-Reasons header # Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS. # This should be accessed through Bugzilla::BugMail::relationships() instead # of being accessed directly. use constant RELATIONSHIPS => { - REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter", - REL_QA, "QAcontact", REL_CC, "CC", - REL_GLOBAL_WATCHER, "GlobalWatcher" + REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter", + REL_QA, "QAcontact", REL_CC, "CC", + REL_GLOBAL_WATCHER, "GlobalWatcher", REL_FLAG_REQUESTEE, "FlagRequestee", + REL_FLAG_REQUESTER, "FlagRequester", REL_FLAG_TYPE_CC, "FlagTypeCC" }; # Used for global events like EVT_FLAG_REQUESTED diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index 9d90970636..f582bfcaa0 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -48,7 +48,6 @@ use Bugzilla::Hook; use Bugzilla::User; use Bugzilla::Util; use Bugzilla::Error; -use Bugzilla::Mailer; use Bugzilla::Constants; use Bugzilla::Field; @@ -69,8 +68,6 @@ use constant AUDIT_REMOVES => 0; use constant SKIP_REQUESTEE_ON_ERROR => 1; -our $disable_flagmail = 0; - sub DB_COLUMNS { my $dbh = Bugzilla->dbh; return qw( @@ -580,20 +577,15 @@ sub update_flags { $new_flag->{id} = $flag->id; $new_flag->{creation_date} = format_time($timestamp, '%Y-%m-%d %H:%i:%s'); $new_flag->{modification_date} = format_time($timestamp, '%Y-%m-%d %H:%i:%s'); - $class->notify($new_flag, undef, $self, $timestamp); } else { - my $changes = $new_flag->update($timestamp); - if (scalar(keys %$changes)) { - $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); - } + $new_flag->update($timestamp); delete $old_flags{$new_flag->id}; } } # These flags have been deleted. foreach my $old_flag (values %old_flags) { - $class->notify(undef, $old_flag, $self, $timestamp); # BMO - provide a hook which passes the timestamp, # because that isn't passed to remove_from_db(). @@ -714,7 +706,6 @@ sub force_retarget { else { # Track deleted attachment flags. push(@removed, $class->snapshot([$flag])) if $flag->attach_id; - $class->notify(undef, $flag, $bug || $flag->bug); # BMO - provide a hook which passes the timestamp, # because that isn't passed to remove_from_db(). @@ -1066,151 +1057,6 @@ sub extract_flags_from_cgi { return (\@flags, \@new_flags); } -=pod - -=over - -=item C - -Sends an email notification about a flag being created, fulfilled -or deleted. - -=back - -=cut - -sub notify { - my ($class, $flag, $old_flag, $obj, $timestamp) = @_; - - if ($disable_flagmail) { - return; - } - - my ($bug, $attachment); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; - } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; - } - else { - # Not a good time to throw an error. - return; - } - - my $addressee; - - # If the flag is set to '?', maybe the requestee wants a notification. - if ( $flag - && $flag->requestee_id - && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) - { - if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { - $addressee = $flag->requestee; - } - } - elsif ($old_flag - && $old_flag->status eq '?' - && (!$flag || $flag->status ne '?')) - { - if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { - $addressee = $old_flag->setter; - } - } - - my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; - $cc_list //= ''; - - # Is there someone to notify? - return unless ($addressee || $cc_list); - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); - - # If the target bug is restricted to one or more groups, then we need - # to make sure we don't send email about it to unauthorized users - # on the request type's CC: list, so we have to trawl the list for users - # not in those groups or email addresses that don't have an account. - my @bug_in_groups = grep { $_->{'ison'} || $_->{'mandatory'} } @{$bug->groups}; - my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - - my %recipients; - foreach my $cc (split(/[, ]+/, $cc_list)) { - my $ccuser = new Bugzilla::User({name => $cc, cache => 1}); - next - if (scalar(@bug_in_groups) - && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); - next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); - - # Prevent duplicated entries due to case sensitivity. - $cc = $ccuser ? $ccuser->email : $cc; - $recipients{$cc} = $ccuser; - } - - # Only notify if the addressee is allowed to receive the email - # and can see the bug (prevents short_desc leaking via Subject/body). - if ( - $addressee - && $addressee->email_enabled - && ( (!scalar(@bug_in_groups) || $addressee->can_see_bug($bug->bug_id)) - && (!$attachment_is_private || $addressee->is_insider)) - ) - { - $recipients{$addressee->email} = $addressee; - } - - return unless keys %recipients; - - # Process and send notification for each recipient. - # If there are users in the CC list who don't have an account, - # use the default language for email notifications. - my $default_lang; - if (grep { !$_ } values %recipients) { - $default_lang = Bugzilla::User->new()->setting('lang'); - } - - # Get comments on the bug - my $all_comments = $bug->comments({after => $bug->lastdiffed}); - @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; - - # Get public only comments - my $public_comments = [grep { !$_->is_private } @$all_comments]; - - foreach my $to (keys %recipients) { - - # Add threadingmarker to allow flag notification emails to be the - # threaded similar to normal bug change emails. - my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; - - # We only want to show private comments to users in the is_insider group - my $comments = $recipients{$to} - && $recipients{$to}->is_insider ? $all_comments : $public_comments; - - my $vars = { - flag => $flag, - old_flag => $old_flag, - to => $to, - date => $timestamp, - bug => $bug, - attachment => $attachment, - threadingmarker => build_thread_marker($bug->id, $thread_user_id), - new_comments => $comments, - }; - - my $lang = $recipients{$to} ? $recipients{$to}->setting('lang') : $default_lang; - - my $template = Bugzilla->template_inner($lang); - my $message; - $template->process("request/email.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); - } -} - # This is an internal function used by $bug->flag_types # and $attachment->flag_types to collect data about available # flag types and existing flags set on them. You should never diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl index 82053792be..5282418db2 100644 --- a/extensions/BMO/template/en/default/email/bugmail.html.tmpl +++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl @@ -16,6 +16,39 @@ + [% IF flag_events.size %] +
+
    + [% FOREACH event = flag_events %] +
  • + [% IF event.action == 'requested' %] + [% IF event.type.name == 'needinfo' %] + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %] + needs more information from you to work on this [% terms.bug %]. + [% ELSE %] + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %] + has requested [% event.type.name FILTER html %][% " for attachment ${event.attachment_id}" FILTER none IF event.attachment_id %] + from you on this [% terms.bug %]. + [% END %] + [% IF event.attachment && event.attachment.external_redirect %] + [% external = event.attachment.external_redirect %] +
    + [% external.title FILTER html %] + [% END %] + [% ELSIF event.action == 'answered' %] + [% status_word = event.status == '+' ? 'granted' : event.status == '-' ? 'denied' : 'cleared' %] + Your request for [% event.type.name FILTER html %][% " on attachment ${event.attachment_id}" FILTER none IF event.attachment_id %] + has been [% status_word FILTER html %] by + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %]. + [% END %] + [% Hook.process('flag_event', 'email/bugmail.html.tmpl') %] +
  • + [% END %] +
+
+
+ [% END %] + [% IF !to_user.in_group('editbugs') %]
Do not reply to this email. You can add comments to this [% terms.bug %] at diff --git a/extensions/BMO/template/en/default/email/bugmail.txt.tmpl b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl index 6fefa2295d..00fa39eff7 100644 --- a/extensions/BMO/template/en/default/email/bugmail.txt.tmpl +++ b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl @@ -11,6 +11,24 @@ [% isnew = bug.lastdiffed ? 0 : 1 %] +[% FOREACH event = flag_events %] +[% IF event.action == 'requested' %] +[% IF event.type.name == 'needinfo' %] +[%+ event.setter.identity %] needs more information from you to work on this [% terms.bug %]. +[% ELSE %] +[%+ event.setter.identity %] has requested [% event.type.name %][% " for attachment ${event.attachment_id}" IF event.attachment_id %] from you on this [% terms.bug %]. +[% END %] +[% IF event.attachment && event.attachment.external_redirect %] +[% external = event.attachment.external_redirect %] +[%+ external.title _ ": " _ event.attachment.data %] +[% END %] +[% ELSIF event.action == 'answered' %] +[% status_word = event.status == '+' ? 'granted' : event.status == '-' ? 'denied' : 'cleared' %] +Your request for [% event.type.name %][% " on attachment ${event.attachment_id}" IF event.attachment_id %] has been [% status_word %] by [% event.setter.identity %]. +[% END %] +[%+ Hook.process('flag_event', 'email/bugmail.txt.tmpl') %] + +[% END %] [% IF !to_user.in_group('editbugs') %] Do not reply to this email. You can add comments to this [% terms.bug %] at [% END %] diff --git a/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.html.tmpl b/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.html.tmpl new file mode 100644 index 0000000000..0e479a9c74 --- /dev/null +++ b/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.html.tmpl @@ -0,0 +1,18 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% RETURN UNLESS event.action == 'requested' && event.type.name == 'needinfo' %] +
+[% IF event.requestee_id == bug.reporter.id %] +Since you reported this [% terms.bug %], the person asking needs your input to understand it. +Responding to the question will enable developers to take further action on this [% terms.bug %]. +[% ELSE %] +Please respond as soon as possible so developers may take action on this [% terms.bug %]. +[% END %] +If you have questions about responding to needinfo requests, please see the +needinfo user guide. diff --git a/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.txt.tmpl b/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.txt.tmpl new file mode 100644 index 0000000000..6bf288b3e3 --- /dev/null +++ b/extensions/Needinfo/template/en/default/hook/email/bugmail-flag_event.txt.tmpl @@ -0,0 +1,16 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% RETURN UNLESS event.action == 'requested' && event.type.name == 'needinfo' %] +[% IF event.requestee_id == bug.reporter.id %] +Since you reported this [% terms.bug %], the person asking needs your input to understand it. Responding to the question will enable developers to take further action on this [% terms.bug %]. +[% ELSE %] +Please respond as soon as possible so developers may take action on this [% terms.bug %]. +[% END %] +If you have questions about responding to needinfo requests, please see +https://wiki.mozilla.org/BMO/UserGuide#Needinfo_Flag. diff --git a/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl deleted file mode 100644 index 66a6d1ad16..0000000000 --- a/extensions/Needinfo/template/en/default/hook/request/email-after_summary.txt.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -[%# This Source Code Form is subject to the terms of the Mozilla Public - # License, v. 2.0. If a copy of the MPL was not distributed with this - # file, You can obtain one at http://mozilla.org/MPL/2.0/. - # - # This Source Code Form is "Incompatible With Secondary Licenses", as - # defined by the Mozilla Public License, v. 2.0. - #%] - -[% RETURN UNLESS flag && flag.type.name == 'needinfo' && flag.status == '?' %] ---- ---- Hello from [% terms.Bugzilla %], the [% terms.bug %] tracker for Mozilla and Firefox: ---- ---- Someone has asked you for information about this [% terms.bug %]. ---- ---- Please go to [% urlbase %]show_bug.cgi?id=[% bug.bug_id %] to respond ---- to the question below. -[% IF flag.requestee.login_name == bug.reporter.login_name %] ---- ---- Since you reported this [% terms.bug %], then the person asking the ---- question needs more information from you to understand it. ---- ---- Responding to the question will enable developers to take further ---- action on this [% terms.bug %]. -[% ELSE %] ---- ---- Please log into [% terms.Bugzilla %] and respond as soon as possible ---- so developers may take action on this [% terms.bug %]. -[% END %] ---- ---- If you have questions about responding to needinfo requests, please ---- see https://wiki.mozilla.org/BMO/UserGuide#Needinfo_Flag. ---- ---- Thank you ---- ---- diff --git a/extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.html.tmpl b/extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.html.tmpl new file mode 100644 index 0000000000..280924c822 --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.html.tmpl @@ -0,0 +1,14 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% USE Bugzilla %] +[% RETURN UNLESS event.action == 'requested' + && (event.type.name == 'review' || event.type.name == 'feedback') + && event.attachment && event.attachment.can_review %] +
+Review diff --git a/extensions/BMO/template/en/default/hook/request/email-start.txt.tmpl b/extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.txt.tmpl similarity index 54% rename from extensions/BMO/template/en/default/hook/request/email-start.txt.tmpl rename to extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.txt.tmpl index bd2af3d6fa..ff027ac55e 100644 --- a/extensions/BMO/template/en/default/hook/request/email-start.txt.tmpl +++ b/extensions/Splinter/template/en/default/hook/email/bugmail-flag_event.txt.tmpl @@ -6,10 +6,9 @@ # defined by the Mozilla Public License, v. 2.0. #%] -[% - RETURN UNLESS attachment; - external = attachment.external_redirect; - RETURN UNLESS external; - attach_url.title = external.title; - attach_url.href = attachment.data; -%] +[% USE Bugzilla %] +[% RETURN UNLESS event.action == 'requested' + && (event.type.name == 'review' || event.type.name == 'feedback') + && event.attachment && event.attachment.can_review %] + +Review [%+ Bugzilla.splinter_review_url(bug.id, event.attachment_id, 1) FILTER none %] diff --git a/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl deleted file mode 100644 index 20a56dbaad..0000000000 --- a/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -[% USE Bugzilla %] -[% IF flag && flag.status == '?' - && (flag.type.name == 'review' || flag.type.name == 'feedback') - && attachment && attachment.ispatch %] - -Review -[%+ Bugzilla.splinter_review_url(bug.bug_id, attachment.id, 1) FILTER none %] -[%- END %] - diff --git a/t/bugmail-flag-events.t b/t/bugmail-flag-events.t new file mode 100644 index 0000000000..217ef49b6e --- /dev/null +++ b/t/bugmail-flag-events.t @@ -0,0 +1,70 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +############################# +#Bug 1883428: BugMail flag events +############################# + +# Covers Bugzilla::BugMail::_flag_event_visible_to(), the private-attachment +# gate shared by recipient selection, per-recipient flag_events filtering, +# and the dequeue-time TOCTOU re-check. This is the one piece of the +# recipient-selection logic (bug 1883428 review) that's pure Perl and +# doesn't need a live DB to exercise; the flag_activity prev-status query +# and the flag-type cc_list DB lookups still need a real Bugzilla instance +# and are covered by the manual test plan in that bug instead. + +package main; + +use 5.10.1; +use strict; +use warnings; + +use lib qw(. lib local/lib/perl5 t); +use Support::Files; +use Test::More tests => 6; + +BEGIN { use_ok('Bugzilla::BugMail'); } + +package Mock::Attachment; + +sub new { + my ($class, $private) = @_; + return bless {isprivate => $private}, $class; +} +sub isprivate { return $_[0]->{isprivate}; } + +package Mock::User; + +sub new { + my ($class, $insider) = @_; + return bless {is_insider => $insider}, $class; +} +sub is_insider { return $_[0]->{is_insider}; } + +package main; + +## no critic (Variables::ProtectPrivateVars) +my $visible = \&Bugzilla::BugMail::_flag_event_visible_to; + +ok( + $visible->({attachment => undef}, Mock::User->new(0)), + 'no attachment on the event is always visible' +); + +ok($visible->({attachment => Mock::Attachment->new(0)}, Mock::User->new(0)), + 'non-private attachment is visible to a non-insider'); + +ok($visible->({attachment => Mock::Attachment->new(1)}, Mock::User->new(1)), + 'private attachment is visible to an insider'); + +ok(!$visible->({attachment => Mock::Attachment->new(1)}, Mock::User->new(0)), + 'private attachment is hidden from a non-insider'); + +ok( + !$visible->({attachment => Mock::Attachment->new(1)}, undef), + 'private attachment is hidden when there is no user to check' +); diff --git a/template/en/default/email/bugmail-flagtype-cc.txt.tmpl b/template/en/default/email/bugmail-flagtype-cc.txt.tmpl new file mode 100644 index 0000000000..bb72cc1d1b --- /dev/null +++ b/template/en/default/email/bugmail-flagtype-cc.txt.tmpl @@ -0,0 +1,37 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + #%] + +[%# ponytail: minimal fallback for flag type cc_list addresses with no + # Bugzilla account -- the normal bugmail pipeline is keyed on user id + # throughout and has no path to a raw address. Revisit if this needs to + # carry more than a one-line-per-event summary (diffs, comments, HTML). %] + +[% PROCESS global/variables.none.tmpl %] + +From: [% Param('mailfrom') %] +To: [% to %] +Subject: [[% terms.Bug %] [%+ bug.id %]] [% bug.short_desc %] +Date: [% date %] +X-Bugzilla-Type: request +[%+ INCLUDE "email/header-common.txt.tmpl" %] + +[%+ urlbase %]show_bug.cgi?id=[% bug.id %] + +[% FOREACH event = flag_events %] +[% IF event.action == 'requested' %] +[%+ event.setter.identity %] requested [% event.type.name %][% " for attachment ${event.attachment_id}" IF event.attachment_id %]. +[% ELSIF event.action == 'answered' %] +[% status_word = event.status == '+' ? 'granted' : event.status == '-' ? 'denied' : 'cleared' %] +[%+ event.setter.identity %] [% status_word %] [% event.type.name %][% " for attachment ${event.attachment_id}" IF event.attachment_id %]. +[% END %] +[% END %] + +@@body-headers@@ diff --git a/template/en/default/email/bugmail-header.txt.tmpl b/template/en/default/email/bugmail-header.txt.tmpl index b1a440e036..db292acb7a 100644 --- a/template/en/default/email/bugmail-header.txt.tmpl +++ b/template/en/default/email/bugmail-header.txt.tmpl @@ -34,6 +34,10 @@ X-Bugzilla-Reason: [% reasonsheader %] X-Bugzilla-Type: [% bugmailtype %] X-Bugzilla-Watch-Reason: [% reasonswatchheader %] [%+ INCLUDE "email/header-common.txt.tmpl" %] +[% FOREACH event = flag_events %] +[% NEXT UNLESS event.action == 'requested' %] +X-Bugzilla-Flag-Requestee: [% event.requestee.email %] +[% END %] X-Bugzilla-Changed-Fields: [% changedfields.join(" ") %] X-Bugzilla-Changed-Field-Names: [% changedfieldnames.join(" ") %] [%+ threadingmarker %] diff --git a/template/en/default/email/bugmail.html.tmpl b/template/en/default/email/bugmail.html.tmpl index 3f7d3d246b..b828768a9f 100644 --- a/template/en/default/email/bugmail.html.tmpl +++ b/template/en/default/email/bugmail.html.tmpl @@ -26,6 +26,31 @@ + [% IF flag_events.size %] +
    + [% FOREACH event = flag_events %] +
  • + [% IF event.action == 'requested' %] + [% IF event.type.name == 'needinfo' %] + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %] + needs more information from you to work on this [% terms.bug %]. + [% ELSE %] + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %] + has requested [% event.type.name FILTER html %][% " for attachment ${event.attachment_id}" FILTER none IF event.attachment_id %] + from you on this [% terms.bug %]. + [% END %] + [% ELSIF event.action == 'answered' %] + [% status_word = event.status == '+' ? 'granted' : event.status == '-' ? 'denied' : 'cleared' %] + Your request for [% event.type.name FILTER html %][% " on attachment ${event.attachment_id}" FILTER none IF event.attachment_id %] + has been [% status_word FILTER html %] by + [% INCLUDE global/user.html.tmpl user = to_user, who = event.setter %]. + [% END %] + [% Hook.process('flag_event', 'email/bugmail.html.tmpl') %] +
  • + [% END %] +
+
+ [% END %] [% PROCESS generate_diffs %]

[% SET comment_anchor = '' %] diff --git a/template/en/default/email/bugmail.txt.tmpl b/template/en/default/email/bugmail.txt.tmpl index 9bfbc15bf7..56f76c4dc5 100644 --- a/template/en/default/email/bugmail.txt.tmpl +++ b/template/en/default/email/bugmail.txt.tmpl @@ -25,6 +25,20 @@ [% isnew = bug.lastdiffed ? 0 : 1 %] +[% FOREACH event = flag_events %] +[% IF event.action == 'requested' %] +[% IF event.type.name == 'needinfo' %] +[%+ event.setter.identity %] needs more information from you to work on this [% terms.bug %]. +[% ELSE %] +[%+ event.setter.identity %] has requested [% event.type.name %][% " for attachment ${event.attachment_id}" IF event.attachment_id %] from you on this [% terms.bug %]. +[% END %] +[% ELSIF event.action == 'answered' %] +[% status_word = event.status == '+' ? 'granted' : event.status == '-' ? 'denied' : 'cleared' %] +Your request for [% event.type.name %][% " on attachment ${event.attachment_id}" IF event.attachment_id %] has been [% status_word %] by [% event.setter.identity %]. +[% END %] +[%+ Hook.process('flag_event', 'email/bugmail.txt.tmpl') %] + +[% END %] [%+ PROCESS generate_diffs -%] [% FOREACH comment = new_comments %] diff --git a/template/en/default/global/reason-descs.none.tmpl b/template/en/default/global/reason-descs.none.tmpl index b79a5bbb4f..df0c14a144 100644 --- a/template/en/default/global/reason-descs.none.tmpl +++ b/template/en/default/global/reason-descs.none.tmpl @@ -24,6 +24,9 @@ ${constants.REL_QA} => "You are the QA Contact for the ${terms.bug}.", ${constants.REL_CC} => "You are on the CC list for the ${terms.bug}.", ${constants.REL_GLOBAL_WATCHER} => "You are watching all $terms.bug changes.", + ${constants.REL_FLAG_REQUESTEE} => "A flag was requested of you on this ${terms.bug}.", + ${constants.REL_FLAG_REQUESTER} => "You requested a flag that was set on this ${terms.bug}.", + ${constants.REL_FLAG_TYPE_CC} => "You are on the CC list for this flag type.", } %] [% SET watch_reason_descs => { diff --git a/template/en/default/request/email.txt.tmpl b/template/en/default/request/email.txt.tmpl deleted file mode 100644 index 2b9ea37827..0000000000 --- a/template/en/default/request/email.txt.tmpl +++ /dev/null @@ -1,107 +0,0 @@ -[%# The contents of this file are subject to the Mozilla Public - # License Version 1.1 (the "License"); you may not use this file - # except in compliance with the License. You may obtain a copy of - # the License at http://www.mozilla.org/MPL/ - # - # Software distributed under the License is distributed on an "AS - # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or - # implied. See the License for the specific language governing - # rights and limitations under the License. - # - # The Original Code is the Bugzilla Bug Tracking System. - # - # The Initial Developer of the Original Code is Netscape Communications - # Corporation. Portions created by Netscape are - # Copyright (C) 1998 Netscape Communications Corporation. All - # Rights Reserved. - # - # Contributor(s): Myk Melez - # Jeff Hedlund - # Frédéric Buclin - #%] - -[% PROCESS global/variables.none.tmpl %] - -[% bugidsummary = bug.bug_id _ ': ' _ bug.short_desc %] -[% attidsummary = attachment.id _ ': ' _ attachment.description %] -[% flagtype_name = flag ? flag.type.name : old_flag.type.name %] -[% statuses = { '+' => "granted" , '-' => 'not granted' , 'X' => "canceled" , '?' => "asked" } %] - -[% to_identity = "" %] -[% on_behalf_of = 0 %] -[% action = flag.status || 'X' %] - -[% - attach_url = { title => "", href => "" }; - Hook.process("start"); -%] - -[% IF flag && flag.status == '?' %] - [% subject_status = "requested" %] - [% IF flag.setter_id == user.id %] - [% to_identity = flag.requestee.identity _ " for" %] - [% ELSE %] - [% on_behalf_of = 1 %] - [% IF flag.requestee %][% to_identity = " to " _ flag.requestee.identity %][% END %] - [% END %] -[% ELSE %] - [% IF old_flag && old_flag.status == '?' %] - [% to_identity = old_flag.setter.identity _ "'s request for" %] - [% END %] - [% subject_status = statuses.$action %] -[% END %] -From: [% Param('mailfrom') %] -To: [% to %] -Subject: [% flagtype_name %] [%+ subject_status %]: [[% terms.Bug %] [%+ bug.bug_id %]] [% bug.short_desc %] -[%- IF attachment %] : - [Attachment [% attachment.id %]] [% attachment.description FILTER clean_text %][% END %] -Date: [% date %] -X-Bugzilla-Type: request -[%- IF flag.requestee %] -X-Bugzilla-Flag-Requestee: [% flag.requestee.email %] -[% END %] -[%+ INCLUDE "email/header-common.txt.tmpl" %] -[%+ threadingmarker %] - -[%+ USE wrap -%] -[%- FILTER bullet = wrap(80) -%] - -[% IF on_behalf_of %] -[% user.identity %] has reassigned [% flag.setter.identity %]'s request for [% flagtype_name %] -[% to_identity %]: -[% ELSE %] -[% user.identity %] has [% statuses.$action %] [%+ to_identity %] [%+ flagtype_name %]: -[% END %] - -[% terms.Bug %] [%+ bugidsummary %] -[% END %] -[%+ urlbase %]show_bug.cgi?id=[% bug.bug_id %] -[% IF attachment %] - -[% FILTER bullet = wrap(80) %] -Attachment [% attidsummary %] -[%- END %] -[% IF attach_url.href %] -[%+ attach_url.title _ ": " _ attach_url.href %] -[%- END %] -[%+ urlbase %]attachment.cgi?id=[% attachment.id %]&action=edit -[%- END %] - -[%- Hook.process('after_summary') -%] - -[%- FILTER bullet = wrap(80) %] - -[% FOREACH comment = new_comments %] - -[%- IF comment.count %] ---- Comment #[% comment.count %] from [% comment.author.identity %] --- -[% ELSE %] ---- Description --- -[% END %] -[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) FILTER strip_control_chars %] -[% END %] - -[%- END %] - --- [%# Protect the trailing space of the signature marker %] -@@body-headers@@