1 # -*- Mode: perl; indent-tabs-mode: nil -*-
3 # The contents of this file are subject to the Mozilla Public
4 # License Version 1.1 (the "License"); you may not use this file
5 # except in compliance with the License. You may obtain a copy of
6 # the License at http://www.mozilla.org/MPL/
8 # Software distributed under the License is distributed on an "AS
9 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10 # implied. See the License for the specific language governing
11 # rights and limitations under the License.
13 # The Original Code is the Bugzilla Bug Tracking System.
15 # The Initial Developer of the Original Code is Netscape Communications
16 # Corporation. Portions created by Netscape are
17 # Copyright (C) 1998 Netscape Communications Corporation. All
20 # Contributor(s): Myk Melez <myk@mozilla.org>
21 # Jouni Heikniemi <jouni@heikniemi.net>
22 # Frédéric Buclin <LpSolit@gmail.com>
26 package Bugzilla::Flag;
30 Bugzilla::Flag - A module to deal with Bugzilla flag values.
34 Flag.pm provides an interface to flags as stored in Bugzilla.
35 See below for more information.
43 Import relevant functions from that script.
47 Use of private functions / variables outside this module may lead to
48 unexpected results after an upgrade. Please avoid using private
49 functions in other files/modules. Private functions are functions
50 whose names start with _ or a re specifically noted as being private.
56 use Scalar::Util qw(blessed);
57 use Storable qw(dclone);
59 use Bugzilla::FlagType;
65 use Bugzilla::Constants;
68 use base qw(Bugzilla::Object Exporter);
69 @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
71 ###############################
72 #### Initialization ####
73 ###############################
75 use constant DB_TABLE => 'flags';
76 use constant LIST_ORDER => 'id';
77 # Flags are tracked in bugs_activity.
78 use constant AUDIT_CREATES => 0;
79 use constant AUDIT_UPDATES => 0;
80 use constant AUDIT_REMOVES => 0;
82 use constant SKIP_REQUESTEE_ON_ERROR => 1;
84 use constant DB_COLUMNS => qw(
94 use constant UPDATE_COLUMNS => qw(
101 use constant VALIDATORS => {
104 use constant UPDATE_VALIDATORS => {
105 setter => \&_check_setter,
106 status => \&_check_status,
109 ###############################
110 #### Accessors ######
111 ###############################
119 Returns the ID of the flag.
123 Returns the name of the flagtype the flag belongs to.
127 Returns the ID of the bug this flag belongs to.
131 Returns the ID of the attachment this flag belongs to, if any.
135 Returns the status '+', '-', '?' of the flag.
141 sub id { return $_[0]->{'id'}; }
142 sub name { return $_[0]->type->name; }
143 sub type_id { return $_[0]->{'type_id'}; }
144 sub bug_id { return $_[0]->{'bug_id'}; }
145 sub attach_id { return $_[0]->{'attach_id'}; }
146 sub status { return $_[0]->{'status'}; }
147 sub setter_id { return $_[0]->{'setter_id'}; }
148 sub requestee_id { return $_[0]->{'requestee_id'}; }
150 ###############################
152 ###############################
160 Returns the type of the flag, as a Bugzilla::FlagType object.
164 Returns the user who set the flag, as a Bugzilla::User object.
168 Returns the user who has been requested to set the flag, as a
169 Bugzilla::User object.
173 Returns the attachment object the flag belongs to if the flag
174 is an attachment flag, else undefined.
183 $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
184 return $self->{'type'};
190 $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
191 return $self->{'setter'};
197 if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
198 $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
200 return $self->{'requestee'};
205 return undef unless $self->attach_id;
207 require Bugzilla::Attachment;
208 $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
209 return $self->{'attachment'};
215 require Bugzilla::Bug;
216 $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
217 return $self->{'bug'};
220 ################################
221 ## Searching/Retrieving Flags ##
222 ################################
228 =item C<match($criteria)>
230 Queries the database for flags matching the given criteria
231 (specified as a hash of field names and their matching values)
232 and returns an array of matching records.
242 # If the caller specified only bug or attachment flags,
243 # limit the query to those kinds of flags.
244 if (my $type = delete $criteria->{'target_type'}) {
245 if ($type eq 'bug') {
246 $criteria->{'attach_id'} = IS_NULL;
248 elsif (!defined $criteria->{'attach_id'}) {
249 $criteria->{'attach_id'} = NOT_NULL;
252 # Flag->snapshot() calls Flag->match() with bug_id and attach_id
253 # as hash keys, even if attach_id is undefined.
254 if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
255 $criteria->{'attach_id'} = IS_NULL;
258 return $class->SUPER::match(@_);
265 =item C<count($criteria)>
267 Queries the database for flags matching the given criteria
268 (specified as a hash of field names and their matching values)
269 and returns an array of matching records.
277 return scalar @{$class->match(@_)};
280 ######################################################################
281 # Creating and Modifying
282 ######################################################################
285 my ($class, $obj, $params) = @_;
287 my ($bug, $attachment);
288 if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
290 $bug = $attachment->bug;
292 elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
296 ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
299 # Update (or delete) an existing flag.
301 my $flag = $class->check({ id => $params->{id} });
303 # Security check: make sure the flag belongs to the bug/attachment.
304 # We don't check that the user editing the flag can see
305 # the bug/attachment. That's the job of the caller.
306 ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id)
307 || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id)
308 || ThrowCodeError('invalid_flag_association',
309 { bug_id => $bug->id,
310 attach_id => $attachment ? $attachment->id : undef });
312 # Extract the current flag object from the object.
313 my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
314 # If no flagtype can be found for this flag, this means the bug is being
315 # moved into a product/component where the flag is no longer valid.
316 # So either we can attach the flag to another flagtype having the same
317 # name, or we remove the flag.
318 if (!$obj_flagtype) {
319 my $success = $flag->retarget($obj);
320 return unless $success;
322 ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
323 push(@{$obj_flagtype->{flags}}, $flag);
325 my ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
326 # If the flag has the correct type but cannot be found above, this means
327 # the flag is going to be removed (e.g. because this is a pending request
328 # and the attachment is being marked as obsolete).
329 return unless $obj_flag;
331 $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
334 elsif ($params->{type_id}) {
335 # Don't bother validating types the user didn't touch.
336 return if $params->{status} eq 'X';
338 my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} });
339 # Security check: make sure the flag type belongs to the bug/attachment.
340 ($attachment && $flagtype->target_type eq 'attachment'
341 && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types}))
342 || (!$attachment && $flagtype->target_type eq 'bug'
343 && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types}))
344 || ThrowCodeError('invalid_flag_association',
345 { bug_id => $bug->id,
346 attach_id => $attachment ? $attachment->id : undef });
348 # Make sure the flag type is active.
350 || ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
352 # Extract the current flagtype object from the object.
353 my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};
355 # We cannot create a new flag if there is already one and this
356 # flag type is not multiplicable.
357 if (!$flagtype->is_multiplicable) {
358 if (scalar @{$obj_flagtype->{flags}}) {
359 ThrowUserError('flag_type_not_multiplicable', { type => $flagtype });
363 $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
366 ThrowCodeError('param_required', { function => $class . '->set_flag',
367 param => 'id/type_id' });
372 my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;
374 # If it's a new flag, let's create it now.
375 my $obj_flag = $flag || bless({ type_id => $flag_type->id,
378 attach_id => $attachment ?
379 $attachment->id : undef},
382 my $old_status = $obj_flag->status;
383 my $old_requestee_id = $obj_flag->requestee_id;
385 $obj_flag->_set_status($params->{status});
386 $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
388 # The setter field MUST NOT be updated if neither the status
389 # nor the requestee fields changed.
390 if (($obj_flag->status ne $old_status)
391 # The requestee ID can be undefined.
392 || (($obj_flag->requestee_id || 0) != ($old_requestee_id || 0)))
394 $obj_flag->_set_setter($params->{setter});
397 # If the flag is deleted, remove it from the list.
398 if ($obj_flag->status eq 'X') {
399 @{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
401 # Add the newly created flag to the list.
402 elsif (!$obj_flag->id) {
403 push(@{$flag_type->{flags}}, $obj_flag);
411 =item C<create($flag, $timestamp)>
413 Creates a flag record in the database.
420 my ($class, $flag, $timestamp) = @_;
421 $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
424 my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
425 $params->{$_} = $flag->{$_} foreach @columns;
427 $params->{creation_date} = $params->{modification_date} = $timestamp;
429 $flag = $class->SUPER::create($params);
435 my $dbh = Bugzilla->dbh;
436 my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
438 my $changes = $self->SUPER::update(@_);
440 if (scalar(keys %$changes)) {
441 $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
442 undef, ($timestamp, $self->id));
448 my ($class, $flags) = @_;
451 foreach my $flag (@$flags) {
452 my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
453 $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
454 push(@summaries, $summary);
459 sub update_activity {
460 my ($class, $old_summaries, $new_summaries) = @_;
462 my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
463 if (scalar @$removed || scalar @$added) {
464 # Remove flag requester/setter information
465 foreach (@$removed, @$added) { s/^[^:]+:// }
467 $removed = join(", ", @$removed);
468 $added = join(", ", @$added);
469 return ($removed, $added);
475 my ($class, $self, $old_self, $timestamp) = @_;
477 my @old_summaries = $class->snapshot($old_self->flags);
478 my %old_flags = map { $_->id => $_ } @{$old_self->flags};
480 foreach my $new_flag (@{$self->flags}) {
481 if (!$new_flag->id) {
482 # This is a new flag.
483 my $flag = $class->create($new_flag, $timestamp);
484 $new_flag->{id} = $flag->id;
485 $class->notify($new_flag, undef, $self, $timestamp);
488 my $changes = $new_flag->update($timestamp);
489 if (scalar(keys %$changes)) {
490 $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
492 delete $old_flags{$new_flag->id};
495 # These flags have been deleted.
496 foreach my $old_flag (values %old_flags) {
497 $class->notify(undef, $old_flag, $self, $timestamp);
498 $old_flag->remove_from_db();
501 # If the bug has been moved into another product or component,
502 # we must also take care of attachment flags which are no longer valid,
503 # as well as all bug flags which haven't been forgotten above.
504 if ($self->isa('Bugzilla::Bug')
505 && ($self->{_old_product_name} || $self->{_old_component_name}))
507 my @removed = $class->force_cleanup($self);
508 push(@old_summaries, @removed);
511 my @new_summaries = $class->snapshot($self->flags);
512 my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
514 Bugzilla::Hook::process('flag_end_of_update', { object => $self,
515 timestamp => $timestamp,
516 old_flags => \@old_summaries,
517 new_flags => \@new_summaries,
523 my ($self, $obj) = @_;
525 my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};
528 foreach my $flagtype (@flagtypes) {
529 next if !$flagtype->is_active;
530 next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}});
531 next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype))
532 || $self->setter->can_set_flag($flagtype));
534 $self->{type_id} = $flagtype->id;
535 delete $self->{type};
542 # In case the bug's product/component has changed, clear flags that are
545 my ($class, $bug) = @_;
546 my $dbh = Bugzilla->dbh;
548 my $flag_ids = $dbh->selectcol_arrayref(
549 'SELECT DISTINCT flags.id
552 ON flags.bug_id = bugs.bug_id
553 LEFT JOIN flaginclusions AS i
554 ON flags.type_id = i.type_id
555 AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
556 AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
557 WHERE bugs.bug_id = ? AND i.type_id IS NULL',
560 my @removed = $class->force_retarget($flag_ids, $bug);
562 $flag_ids = $dbh->selectcol_arrayref(
563 'SELECT DISTINCT flags.id
564 FROM flags, bugs, flagexclusions e
565 WHERE bugs.bug_id = ?
566 AND flags.bug_id = bugs.bug_id
567 AND flags.type_id = e.type_id
568 AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
569 AND (bugs.component_id = e.component_id OR e.component_id IS NULL)',
572 push(@removed , $class->force_retarget($flag_ids, $bug));
577 my ($class, $flag_ids, $bug) = @_;
578 my $dbh = Bugzilla->dbh;
580 my $flags = $class->new_from_list($flag_ids);
582 foreach my $flag (@$flags) {
583 # $bug is undefined when e.g. editing inclusion and exclusion lists.
584 my $obj = $flag->attachment || $bug || $flag->bug;
585 my $is_retargetted = $flag->retarget($obj);
586 if ($is_retargetted) {
587 $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
588 undef, ($flag->type_id, $flag->id));
591 # Track deleted attachment flags.
592 push(@removed, $class->snapshot([$flag])) if $flag->attach_id;
593 $class->notify(undef, $flag, $bug || $flag->bug);
594 $flag->remove_from_db();
600 ###############################
601 #### Validators ######
602 ###############################
605 my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
608 $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
610 $self->{requestee_id} =
611 $self->{requestee} ? $self->{requestee}->id : undef;
615 my ($self, $setter) = @_;
617 $self->set('setter', $setter);
618 $self->{setter_id} = $self->setter->id;
622 my ($self, $status) = @_;
624 # Store the old flag status. It's needed by _check_setter().
625 $self->{_old_status} = $self->status;
626 $self->set('status', $status);
629 sub _check_requestee {
630 my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
632 # If the flag status is not "?", then no requestee can be defined.
633 return undef if ($self->status ne '?');
635 # Store this value before updating the flag object.
636 my $old_requestee = $self->requestee ? $self->requestee->login : '';
638 if ($self->status eq '?' && $requestee) {
639 $requestee = Bugzilla::User->check($requestee);
645 if ($requestee && $requestee->login ne $old_requestee) {
646 # Make sure the user didn't specify a requestee unless the flag
647 # is specifically requestable. For existing flags, if the requestee
648 # was set before the flag became specifically unrequestable, the
649 # user can either remove him or leave him alone.
650 ThrowCodeError('flag_requestee_disabled', { type => $self->type })
651 if !$self->type->is_requesteeble;
653 # Make sure the requestee can see the bug.
654 # Note that can_see_bug() will query the DB, so if the bug
655 # is being added/removed from some groups and these changes
656 # haven't been committed to the DB yet, they won't be taken
657 # into account here. In this case, old restrictions matters.
658 if (!$requestee->can_see_bug($self->bug_id)) {
659 if ($skip_requestee_on_error) {
663 ThrowUserError('flag_requestee_unauthorized',
664 { flag_type => $self->type,
665 requestee => $requestee,
666 bug_id => $self->bug_id,
667 attach_id => $self->attach_id });
670 # Make sure the requestee can see the private attachment.
671 elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) {
672 if ($skip_requestee_on_error) {
676 ThrowUserError('flag_requestee_unauthorized_attachment',
677 { flag_type => $self->type,
678 requestee => $requestee,
679 bug_id => $self->bug_id,
680 attach_id => $self->attach_id });
683 # Make sure the user is allowed to set the flag.
684 elsif (!$requestee->can_set_flag($self->type)) {
685 if ($skip_requestee_on_error) {
689 ThrowUserError('flag_requestee_needs_privs',
690 {'requestee' => $requestee,
691 'flagtype' => $self->type});
699 my ($self, $setter) = @_;
701 # By default, the currently logged in user is the setter.
702 $setter ||= Bugzilla->user;
703 (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
704 || ThrowCodeError('invalid_user');
706 # set_status() has already been called. So this refers
707 # to the new flag status.
708 my $status = $self->status;
710 # Make sure the user is authorized to modify flags, see bug 180879:
711 # - The flag exists and is unchanged.
712 # - The flag setter can unset flag.
713 # - Users in the request_group can clear pending requests and set flags
714 # and can rerequest set flags.
715 # - Users in the grant_group can set/clear flags, including "+" and "-".
716 unless (($status eq $self->{_old_status})
717 || ($status eq 'X' && $setter->id == Bugzilla->user->id)
718 || (($status eq 'X' || $status eq '?')
719 && $setter->can_request_flag($self->type))
720 || $setter->can_set_flag($self->type))
722 ThrowUserError('flag_update_denied',
723 { name => $self->type->name,
725 old_status => $self->{_old_status} });
728 # If the request is being retargetted, we don't update
729 # the setter, so that the setter gets the notification.
730 if ($status eq '?' && $self->{_old_status} eq '?') {
731 return $self->setter;
737 my ($self, $status) = @_;
739 # - Make sure the status is valid.
740 # - Make sure the user didn't request the flag unless it's requestable.
741 # If the flag existed and was requested before it became unrequestable,
743 if (!grep($status eq $_ , qw(X + - ?))
744 || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable))
746 ThrowUserError('flag_status_invalid', { id => $self->id,
747 status => $status });
752 ######################################################################
754 ######################################################################
760 =item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
762 Checks whether or not there are new flags to create and returns an
763 array of hashes. This array is then passed to Flag::create().
769 sub extract_flags_from_cgi {
770 my ($class, $bug, $attachment, $vars, $skip) = @_;
771 my $cgi = Bugzilla->cgi;
773 my $match_status = Bugzilla::User::match_field({
774 '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
777 $vars->{'match_field'} = 'requestee';
778 if ($match_status == USER_MATCH_FAILED) {
779 $vars->{'message'} = 'user_match_failed';
781 elsif ($match_status == USER_MATCH_MULTIPLE) {
782 $vars->{'message'} = 'user_match_multiple';
785 # Extract a list of flag type IDs from field names.
786 my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
787 @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids);
789 # Extract a list of existing flag IDs.
790 my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
792 return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
794 my (@new_flags, @flags);
795 foreach my $flag_id (@flag_ids) {
796 my $flag = $class->new($flag_id);
797 # If the flag no longer exists, ignore it.
800 my $status = $cgi->param("flag-$flag_id");
802 # If the user entered more than one name into the requestee field
803 # (i.e. they want more than one person to set the flag) we can reuse
804 # the existing flag for the first person (who may well be the existing
805 # requestee), but we have to create new flags for each additional requestee.
806 my @requestees = $cgi->param("requestee-$flag_id");
809 && scalar(@requestees) > 1
810 && $flag->type->is_multiplicable)
812 # The first person, for which we'll reuse the existing flag.
813 $requestee_email = shift(@requestees);
815 # Create new flags like the existing one for each additional person.
816 foreach my $login (@requestees) {
817 push(@new_flags, { type_id => $flag->type_id,
820 skip_roe => $skip });
823 elsif ($status eq "?" && scalar(@requestees)) {
824 # If there are several requestees and the flag type is not multiplicable,
825 # this will fail. But that's the job of the validator to complain. All
826 # we do here is to extract and convert data from the CGI.
827 $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
830 push(@flags, { id => $flag_id,
832 requestee => $requestee_email,
833 skip_roe => $skip });
836 # Get a list of active flag types available for this product/component.
837 my $flag_types = Bugzilla::FlagType::match(
838 { 'product_id' => $bug->{'product_id'},
839 'component_id' => $bug->{'component_id'},
842 foreach my $flagtype_id (@flagtype_ids) {
843 # Checks if there are unexpected flags for the product/component.
844 if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
845 $vars->{'message'} = 'unexpected_flag_types';
850 foreach my $flag_type (@$flag_types) {
851 my $type_id = $flag_type->id;
853 # Bug flags are only valid for bugs, and attachment flags are
854 # only valid for attachments. So don't mix both.
855 next unless ($flag_type->target_type eq 'bug' xor $attachment);
857 # We are only interested in flags the user tries to create.
858 next unless scalar(grep { $_ == $type_id } @flagtype_ids);
860 # Get the number of flags of this type already set for this target.
861 my $has_flags = $class->count(
862 { 'type_id' => $type_id,
863 'target_type' => $attachment ? 'attachment' : 'bug',
864 'bug_id' => $bug->bug_id,
865 'attach_id' => $attachment ? $attachment->id : undef });
867 # Do not create a new flag of this type if this flag type is
868 # not multiplicable and already has a flag set.
869 next if (!$flag_type->is_multiplicable && $has_flags);
871 my $status = $cgi->param("flag_type-$type_id");
872 trick_taint($status);
874 my @logins = $cgi->param("requestee_type-$type_id");
875 if ($status eq "?" && scalar(@logins)) {
876 foreach my $login (@logins) {
877 push (@new_flags, { type_id => $type_id,
880 skip_roe => $skip });
881 last unless $flag_type->is_multiplicable;
885 push (@new_flags, { type_id => $type_id,
886 status => $status });
890 # Return the list of flags to update and/or to create.
891 return (\@flags, \@new_flags);
898 =item C<notify($flag, $old_flag, $object, $timestamp)>
900 Sends an email notification about a flag being created, fulfilled
908 my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
910 my ($bug, $attachment);
911 if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
913 $bug = $attachment->bug;
915 elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
919 # Not a good time to throw an error.
924 # If the flag is set to '?', maybe the requestee wants a notification.
925 if ($flag && $flag->requestee_id
926 && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id))
928 if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
929 $addressee = $flag->requestee;
932 elsif ($old_flag && $old_flag->status eq '?'
933 && (!$flag || $flag->status ne '?'))
935 if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
936 $addressee = $old_flag->setter;
940 my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list;
941 # Is there someone to notify?
942 return unless ($addressee || $cc_list);
944 # The email client will display the Date: header in the desired timezone,
945 # so we can always use UTC here.
946 $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
947 $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC');
949 # If the target bug is restricted to one or more groups, then we need
950 # to make sure we don't send email about it to unauthorized users
951 # on the request type's CC: list, so we have to trawl the list for users
952 # not in those groups or email addresses that don't have an account.
953 my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
954 my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
957 foreach my $cc (split(/[, ]+/, $cc_list)) {
958 my $ccuser = new Bugzilla::User({ name => $cc });
959 next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
960 next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
961 # Prevent duplicated entries due to case sensitivity.
962 $cc = $ccuser ? $ccuser->email : $cc;
963 $recipients{$cc} = $ccuser;
966 # Only notify if the addressee is allowed to receive the email.
967 if ($addressee && $addressee->email_enabled) {
968 $recipients{$addressee->email} = $addressee;
970 # Process and send notification for each recipient.
971 # If there are users in the CC list who don't have an account,
972 # use the default language for email notifications.
974 if (grep { !$_ } values %recipients) {
975 $default_lang = Bugzilla::User->new()->setting('lang');
978 foreach my $to (keys %recipients) {
979 # Add threadingmarker to allow flag notification emails to be the
980 # threaded similar to normal bug change emails.
981 my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
983 my $vars = { 'flag' => $flag,
984 'old_flag' => $old_flag,
986 'date' => $timestamp,
988 'attachment' => $attachment,
989 'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
991 my $lang = $recipients{$to} ?
992 $recipients{$to}->setting('lang') : $default_lang;
994 my $template = Bugzilla->template_inner($lang);
996 $template->process("request/email.txt.tmpl", $vars, \$message)
997 || ThrowTemplateError($template->error());
999 MessageToMTA($message);
1003 # This is an internal function used by $bug->flag_types
1004 # and $attachment->flag_types to collect data about available
1005 # flag types and existing flags set on them. You should never
1006 # call this function directly.
1008 my ($class, $vars) = @_;
1010 my $target_type = $vars->{target_type};
1013 # Retrieve all existing flags for this bug/attachment.
1014 if ($target_type eq 'bug') {
1015 my $bug_id = delete $vars->{bug_id};
1016 $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
1018 elsif ($target_type eq 'attachment') {
1019 my $attach_id = delete $vars->{attach_id};
1020 $flags = $class->match({attach_id => $attach_id});
1023 ThrowCodeError('bad_arg', {argument => 'target_type',
1024 function => $class . '->_flag_types'});
1027 # Get all available flag types for the given product and component.
1028 my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {};
1029 my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars);
1030 my $flag_types = dclone($flag_data);
1032 $_->{flags} = [] foreach @$flag_types;
1033 my %flagtypes = map { $_->id => $_ } @$flag_types;
1035 # Group existing flags per type, and skip those becoming invalid
1036 # (which can happen when a bug is being moved into a new product
1038 @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
1039 push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
1047 =item B<Bugzilla::FlagType>
1056 =item Myk Melez <myk@mozilla.org>
1058 =item Jouni Heikniemi <jouni@heikniemi.net>
1060 =item Kevin Benton <kevin.benton@amd.com>
1062 =item Frédéric Buclin <LpSolit@gmail.com>