Remove references to Rietveld from bugs.webkit.org
[WebKit-https.git] / Websites / bugs.webkit.org / Bugzilla / Flag.pm
1 # -*- Mode: perl; indent-tabs-mode: nil -*-
2 #
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/
7 #
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.
12 #
13 # The Original Code is the Bugzilla Bug Tracking System.
14 #
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
18 # Rights Reserved.
19 #
20 # Contributor(s): Myk Melez <myk@mozilla.org>
21 #                 Jouni Heikniemi <jouni@heikniemi.net>
22 #                 Frédéric Buclin <LpSolit@gmail.com>
23
24 use strict;
25
26 package Bugzilla::Flag;
27
28 =head1 NAME
29
30 Bugzilla::Flag - A module to deal with Bugzilla flag values.
31
32 =head1 SYNOPSIS
33
34 Flag.pm provides an interface to flags as stored in Bugzilla.
35 See below for more information.
36
37 =head1 NOTES
38
39 =over
40
41 =item *
42
43 Import relevant functions from that script.
44
45 =item *
46
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.
51
52 =back
53
54 =cut
55
56 use Scalar::Util qw(blessed);
57 use Storable qw(dclone);
58
59 use Bugzilla::FlagType;
60 use Bugzilla::Hook;
61 use Bugzilla::User;
62 use Bugzilla::Util;
63 use Bugzilla::Error;
64 use Bugzilla::Mailer;
65 use Bugzilla::Constants;
66 use Bugzilla::Field;
67
68 use base qw(Bugzilla::Object Exporter);
69 @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
70
71 ###############################
72 ####    Initialization     ####
73 ###############################
74
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;
81
82 use constant SKIP_REQUESTEE_ON_ERROR => 1;
83
84 use constant DB_COLUMNS => qw(
85     id
86     type_id
87     bug_id
88     attach_id
89     requestee_id
90     setter_id
91     status
92 );
93
94 use constant UPDATE_COLUMNS => qw(
95     requestee_id
96     setter_id
97     status
98     type_id
99 );
100
101 use constant VALIDATORS => {
102 };
103
104 use constant UPDATE_VALIDATORS => {
105     setter => \&_check_setter,
106     status => \&_check_status,
107 };
108
109 ###############################
110 ####      Accessors      ######
111 ###############################
112
113 =head2 METHODS
114
115 =over
116
117 =item C<id>
118
119 Returns the ID of the flag.
120
121 =item C<name>
122
123 Returns the name of the flagtype the flag belongs to.
124
125 =item C<bug_id>
126
127 Returns the ID of the bug this flag belongs to.
128
129 =item C<attach_id>
130
131 Returns the ID of the attachment this flag belongs to, if any.
132
133 =item C<status>
134
135 Returns the status '+', '-', '?' of the flag.
136
137 =back
138
139 =cut
140
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'}; }
149
150 ###############################
151 ####       Methods         ####
152 ###############################
153
154 =pod
155
156 =over
157
158 =item C<type>
159
160 Returns the type of the flag, as a Bugzilla::FlagType object.
161
162 =item C<setter>
163
164 Returns the user who set the flag, as a Bugzilla::User object.
165
166 =item C<requestee>
167
168 Returns the user who has been requested to set the flag, as a
169 Bugzilla::User object.
170
171 =item C<attachment>
172
173 Returns the attachment object the flag belongs to if the flag
174 is an attachment flag, else undefined.
175
176 =back
177
178 =cut
179
180 sub type {
181     my $self = shift;
182
183     $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
184     return $self->{'type'};
185 }
186
187 sub setter {
188     my $self = shift;
189
190     $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
191     return $self->{'setter'};
192 }
193
194 sub requestee {
195     my $self = shift;
196
197     if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
198         $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
199     }
200     return $self->{'requestee'};
201 }
202
203 sub attachment {
204     my $self = shift;
205     return undef unless $self->attach_id;
206
207     require Bugzilla::Attachment;
208     $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
209     return $self->{'attachment'};
210 }
211
212 sub bug {
213     my $self = shift;
214
215     require Bugzilla::Bug;
216     $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
217     return $self->{'bug'};
218 }
219
220 ################################
221 ## Searching/Retrieving Flags ##
222 ################################
223
224 =pod
225
226 =over
227
228 =item C<match($criteria)>
229
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.
233
234 =back
235
236 =cut
237
238 sub match {
239     my $class = shift;
240     my ($criteria) = @_;
241
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;
247         }
248         elsif (!defined $criteria->{'attach_id'}) {
249             $criteria->{'attach_id'} = NOT_NULL;
250         }
251     }
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;
256     }
257
258     return $class->SUPER::match(@_);
259 }
260
261 =pod
262
263 =over
264
265 =item C<count($criteria)>
266
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.
270
271 =back
272
273 =cut
274
275 sub count {
276     my $class = shift;
277     return scalar @{$class->match(@_)};
278 }
279
280 ######################################################################
281 # Creating and Modifying
282 ######################################################################
283
284 sub set_flag {
285     my ($class, $obj, $params) = @_;
286
287     my ($bug, $attachment);
288     if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
289         $attachment = $obj;
290         $bug = $attachment->bug;
291     }
292     elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
293         $bug = $obj;
294     }
295     else {
296         ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
297     }
298
299     # Update (or delete) an existing flag.
300     if ($params->{id}) {
301         my $flag = $class->check({ id => $params->{id} });
302
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 });
311
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;
321
322             ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
323             push(@{$obj_flagtype->{flags}}, $flag);
324         }
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;
330
331         $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
332     }
333     # Create a new flag.
334     elsif ($params->{type_id}) {
335         # Don't bother validating types the user didn't touch.
336         return if $params->{status} eq 'X';
337
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 });
347
348         # Make sure the flag type is active.
349         $flagtype->is_active
350           || ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
351
352         # Extract the current flagtype object from the object.
353         my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};
354
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 });
360             }
361         }
362
363         $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
364     }
365     else {
366         ThrowCodeError('param_required', { function => $class . '->set_flag',
367                                            param    => 'id/type_id' });
368     }
369 }
370
371 sub _validate {
372     my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;
373
374     # If it's a new flag, let's create it now.
375     my $obj_flag = $flag || bless({ type_id   => $flag_type->id,
376                                     status    => '',
377                                     bug_id    => $bug->id,
378                                     attach_id => $attachment ?
379                                                    $attachment->id : undef},
380                                     $class);
381
382     my $old_status = $obj_flag->status;
383     my $old_requestee_id = $obj_flag->requestee_id;
384
385     $obj_flag->_set_status($params->{status});
386     $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
387
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)))
393     {
394         $obj_flag->_set_setter($params->{setter});
395     }
396
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}};
400     }
401     # Add the newly created flag to the list.
402     elsif (!$obj_flag->id) {
403         push(@{$flag_type->{flags}}, $obj_flag);
404     }
405 }
406
407 =pod
408
409 =over
410
411 =item C<create($flag, $timestamp)>
412
413 Creates a flag record in the database.
414
415 =back
416
417 =cut
418
419 sub create {
420     my ($class, $flag, $timestamp) = @_;
421     $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
422
423     my $params = {};
424     my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
425     $params->{$_} = $flag->{$_} foreach @columns;
426
427     $params->{creation_date} = $params->{modification_date} = $timestamp;
428
429     $flag = $class->SUPER::create($params);
430     return $flag;
431 }
432
433 sub update {
434     my $self = shift;
435     my $dbh = Bugzilla->dbh;
436     my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
437
438     my $changes = $self->SUPER::update(@_);
439
440     if (scalar(keys %$changes)) {
441         $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
442                  undef, ($timestamp, $self->id));
443     }
444     return $changes;
445 }
446
447 sub snapshot {
448     my ($class, $flags) = @_;
449
450     my @summaries;
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);
455     }
456     return @summaries;
457 }
458
459 sub update_activity {
460     my ($class, $old_summaries, $new_summaries) = @_;
461
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/^[^:]+:// }
466
467         $removed = join(", ", @$removed);
468         $added = join(", ", @$added);
469         return ($removed, $added);
470     }
471     return ();
472 }
473
474 sub update_flags {
475     my ($class, $self, $old_self, $timestamp) = @_;
476
477     my @old_summaries = $class->snapshot($old_self->flags);
478     my %old_flags = map { $_->id => $_ } @{$old_self->flags};
479
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);
486         }
487         else {
488             my $changes = $new_flag->update($timestamp);
489             if (scalar(keys %$changes)) {
490                 $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
491             }
492             delete $old_flags{$new_flag->id};
493         }
494     }
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();
499     }
500
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}))
506     {
507         my @removed = $class->force_cleanup($self);
508         push(@old_summaries, @removed);
509     }
510
511     my @new_summaries = $class->snapshot($self->flags);
512     my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
513
514     Bugzilla::Hook::process('flag_end_of_update', { object    => $self,
515                                                     timestamp => $timestamp,
516                                                     old_flags => \@old_summaries,
517                                                     new_flags => \@new_summaries,
518                                                   });
519     return @changes;
520 }
521
522 sub retarget {
523     my ($self, $obj) = @_;
524
525     my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};
526
527     my $success = 0;
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));
533
534         $self->{type_id} = $flagtype->id;
535         delete $self->{type};
536         $success = 1;
537         last;
538     }
539     return $success;
540 }
541
542 # In case the bug's product/component has changed, clear flags that are
543 # no longer valid.
544 sub force_cleanup {
545     my ($class, $bug) = @_;
546     my $dbh = Bugzilla->dbh;
547
548     my $flag_ids = $dbh->selectcol_arrayref(
549         'SELECT DISTINCT flags.id
550            FROM flags
551           INNER JOIN bugs
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',
558          undef, $bug->id);
559
560     my @removed = $class->force_retarget($flag_ids, $bug);
561
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)',
570          undef, $bug->id);
571
572     push(@removed , $class->force_retarget($flag_ids, $bug));
573     return @removed;
574 }
575
576 sub force_retarget {
577     my ($class, $flag_ids, $bug) = @_;
578     my $dbh = Bugzilla->dbh;
579
580     my $flags = $class->new_from_list($flag_ids);
581     my @removed;
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));
589         }
590         else {
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();
595         }
596     }
597     return @removed;
598 }
599
600 ###############################
601 ####      Validators     ######
602 ###############################
603
604 sub _set_requestee {
605     my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
606
607     $self->{requestee} =
608       $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
609
610     $self->{requestee_id} =
611       $self->{requestee} ? $self->{requestee}->id : undef;
612 }
613
614 sub _set_setter {
615     my ($self, $setter) = @_;
616
617     $self->set('setter', $setter);
618     $self->{setter_id} = $self->setter->id;
619 }
620
621 sub _set_status {
622     my ($self, $status) = @_;
623
624     # Store the old flag status. It's needed by _check_setter().
625     $self->{_old_status} = $self->status;
626     $self->set('status', $status);
627 }
628
629 sub _check_requestee {
630     my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
631
632     # If the flag status is not "?", then no requestee can be defined.
633     return undef if ($self->status ne '?');
634
635     # Store this value before updating the flag object.
636     my $old_requestee = $self->requestee ? $self->requestee->login : '';
637
638     if ($self->status eq '?' && $requestee) {
639         $requestee = Bugzilla::User->check($requestee);
640     }
641     else {
642         undef $requestee;
643     }
644
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;
652
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) {
660                 undef $requestee;
661             }
662             else {
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 });
668             }
669         }
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) {
673                 undef $requestee;
674             }
675             else {
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 });
681             }
682         }
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) {
686                 undef $requestee;
687             }
688             else {
689                 ThrowUserError('flag_requestee_needs_privs',
690                                {'requestee' => $requestee,
691                                 'flagtype'  => $self->type});
692             }
693         }
694     }
695     return $requestee;
696 }
697
698 sub _check_setter {
699     my ($self, $setter) = @_;
700
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');
705
706     # set_status() has already been called. So this refers
707     # to the new flag status.
708     my $status = $self->status;
709
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))
721     {
722         ThrowUserError('flag_update_denied',
723                         { name       => $self->type->name,
724                           status     => $status,
725                           old_status => $self->{_old_status} });
726     }
727
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;
732     }
733     return $setter;
734 }
735
736 sub _check_status {
737     my ($self, $status) = @_;
738
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,
742     #   leave it as is.
743     if (!grep($status eq $_ , qw(X + - ?))
744         || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable))
745     {
746         ThrowUserError('flag_status_invalid', { id     => $self->id,
747                                                 status => $status });
748     }
749     return $status;
750 }
751
752 ######################################################################
753 # Utility Functions
754 ######################################################################
755
756 =pod
757
758 =over
759
760 =item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
761
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().
764
765 =back
766
767 =cut
768
769 sub extract_flags_from_cgi {
770     my ($class, $bug, $attachment, $vars, $skip) = @_;
771     my $cgi = Bugzilla->cgi;
772
773     my $match_status = Bugzilla::User::match_field({
774         '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
775     }, undef, $skip);
776
777     $vars->{'match_field'} = 'requestee';
778     if ($match_status == USER_MATCH_FAILED) {
779         $vars->{'message'} = 'user_match_failed';
780     }
781     elsif ($match_status == USER_MATCH_MULTIPLE) {
782         $vars->{'message'} = 'user_match_multiple';
783     }
784
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);
788
789     # Extract a list of existing flag IDs.
790     my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
791
792     return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
793
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.
798         next unless $flag;
799
800         my $status = $cgi->param("flag-$flag_id");
801
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");
807         my $requestee_email;
808         if ($status eq "?"
809             && scalar(@requestees) > 1
810             && $flag->type->is_multiplicable)
811         {
812             # The first person, for which we'll reuse the existing flag.
813             $requestee_email = shift(@requestees);
814
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,
818                                    status    => "?",
819                                    requestee => $login,
820                                    skip_roe  => $skip });
821             }
822         }
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") || '');
828         }
829
830         push(@flags, { id        => $flag_id,
831                        status    => $status,
832                        requestee => $requestee_email,
833                        skip_roe  => $skip });
834     }
835
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'},
840           'is_active'    => 1 });
841
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';
846             last;
847         }
848     }
849
850     foreach my $flag_type (@$flag_types) {
851         my $type_id = $flag_type->id;
852
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);
856
857         # We are only interested in flags the user tries to create.
858         next unless scalar(grep { $_ == $type_id } @flagtype_ids);
859
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 });
866
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);
870
871         my $status = $cgi->param("flag_type-$type_id");
872         trick_taint($status);
873
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,
878                                     status    => $status,
879                                     requestee => $login,
880                                     skip_roe  => $skip });
881                 last unless $flag_type->is_multiplicable;
882             }
883         }
884         else {
885             push (@new_flags, { type_id => $type_id,
886                                 status  => $status });
887         }
888     }
889
890     # Return the list of flags to update and/or to create.
891     return (\@flags, \@new_flags);
892 }
893
894 =pod
895
896 =over
897
898 =item C<notify($flag, $old_flag, $object, $timestamp)>
899
900 Sends an email notification about a flag being created, fulfilled
901 or deleted.
902
903 =back
904
905 =cut
906
907 sub notify {
908     my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
909
910     my ($bug, $attachment);
911     if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
912         $attachment = $obj;
913         $bug = $attachment->bug;
914     }
915     elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
916         $bug = $obj;
917     }
918     else {
919         # Not a good time to throw an error.
920         return;
921     }
922
923     my $addressee;
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))
927     {
928         if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
929             $addressee = $flag->requestee;
930         }
931     }
932     elsif ($old_flag && $old_flag->status eq '?'
933            && (!$flag || $flag->status ne '?'))
934     {
935         if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
936             $addressee = $old_flag->setter;
937         }
938     }
939
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);
943
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');
948
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;
955
956     my %recipients;
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;
964     }
965
966     # Only notify if the addressee is allowed to receive the email.
967     if ($addressee && $addressee->email_enabled) {
968         $recipients{$addressee->email} = $addressee;
969     }
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.
973     my $default_lang;
974     if (grep { !$_ } values %recipients) {
975         $default_lang = Bugzilla::User->new()->setting('lang');
976     }
977
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;
982
983         my $vars = { 'flag'            => $flag,
984                      'old_flag'        => $old_flag,
985                      'to'              => $to,
986                      'date'            => $timestamp,
987                      'bug'             => $bug,
988                      'attachment'      => $attachment,
989                      'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
990
991         my $lang = $recipients{$to} ?
992           $recipients{$to}->setting('lang') : $default_lang;
993
994         my $template = Bugzilla->template_inner($lang);
995         my $message;
996         $template->process("request/email.txt.tmpl", $vars, \$message)
997           || ThrowTemplateError($template->error());
998
999         MessageToMTA($message);
1000     }
1001 }
1002
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.
1007 sub _flag_types {
1008     my ($class, $vars) = @_;
1009
1010     my $target_type = $vars->{target_type};
1011     my $flags;
1012
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});
1017     }
1018     elsif ($target_type eq 'attachment') {
1019         my $attach_id = delete $vars->{attach_id};
1020         $flags = $class->match({attach_id => $attach_id});
1021     }
1022     else {
1023         ThrowCodeError('bad_arg', {argument => 'target_type',
1024                                    function => $class . '->_flag_types'});
1025     }
1026
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);
1031
1032     $_->{flags} = [] foreach @$flag_types;
1033     my %flagtypes = map { $_->id => $_ } @$flag_types;
1034
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
1037     # or component).
1038     @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
1039     push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
1040     return $flag_types;
1041 }
1042
1043 =head1 SEE ALSO
1044
1045 =over
1046
1047 =item B<Bugzilla::FlagType>
1048
1049 =back
1050
1051
1052 =head1 CONTRIBUTORS
1053
1054 =over
1055
1056 =item Myk Melez <myk@mozilla.org>
1057
1058 =item Jouni Heikniemi <jouni@heikniemi.net>
1059
1060 =item Kevin Benton <kevin.benton@amd.com>
1061
1062 =item Frédéric Buclin <LpSolit@gmail.com>
1063
1064 =back
1065
1066 =cut
1067
1068 1;