(CVE-2013-0786) [SECURITY] build_subselect() leaks the existence of products and...
[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 Bugzilla::FlagType;
57 use Bugzilla::Hook;
58 use Bugzilla::User;
59 use Bugzilla::Util;
60 use Bugzilla::Error;
61 use Bugzilla::Mailer;
62 use Bugzilla::Constants;
63 use Bugzilla::Field;
64
65 use base qw(Bugzilla::Object Exporter);
66 @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
67
68 ###############################
69 ####    Initialization     ####
70 ###############################
71
72 use constant DB_COLUMNS => qw(
73     flags.id
74     flags.type_id
75     flags.bug_id
76     flags.attach_id
77     flags.requestee_id
78     flags.setter_id
79     flags.status
80 );
81
82 use constant DB_TABLE => 'flags';
83 use constant LIST_ORDER => 'id';
84
85 use constant SKIP_REQUESTEE_ON_ERROR => 1;
86
87 ###############################
88 ####      Accessors      ######
89 ###############################
90
91 =head2 METHODS
92
93 =over
94
95 =item C<id>
96
97 Returns the ID of the flag.
98
99 =item C<name>
100
101 Returns the name of the flagtype the flag belongs to.
102
103 =item C<bug_id>
104
105 Returns the ID of the bug this flag belongs to.
106
107 =item C<attach_id>
108
109 Returns the ID of the attachment this flag belongs to, if any.
110
111 =item C<status>
112
113 Returns the status '+', '-', '?' of the flag.
114
115 =back
116
117 =cut
118
119 sub id     { return $_[0]->{'id'};     }
120 sub name   { return $_[0]->type->name; }
121 sub bug_id { return $_[0]->{'bug_id'}; }
122 sub attach_id { return $_[0]->{'attach_id'}; }
123 sub status { return $_[0]->{'status'}; }
124
125 ###############################
126 ####       Methods         ####
127 ###############################
128
129 =pod
130
131 =over
132
133 =item C<type>
134
135 Returns the type of the flag, as a Bugzilla::FlagType object.
136
137 =item C<setter>
138
139 Returns the user who set the flag, as a Bugzilla::User object.
140
141 =item C<requestee>
142
143 Returns the user who has been requested to set the flag, as a
144 Bugzilla::User object.
145
146 =item C<attachment>
147
148 Returns the attachment object the flag belongs to if the flag
149 is an attachment flag, else undefined.
150
151 =back
152
153 =cut
154
155 sub type {
156     my $self = shift;
157
158     $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
159     return $self->{'type'};
160 }
161
162 sub setter {
163     my $self = shift;
164
165     $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
166     return $self->{'setter'};
167 }
168
169 sub requestee {
170     my $self = shift;
171
172     if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
173         $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
174     }
175     return $self->{'requestee'};
176 }
177
178 sub attachment {
179     my $self = shift;
180     return undef unless $self->attach_id;
181
182     require Bugzilla::Attachment;
183     $self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
184     return $self->{'attachment'};
185 }
186
187 ################################
188 ## Searching/Retrieving Flags ##
189 ################################
190
191 =pod
192
193 =over
194
195 =item C<has_flags>
196
197 Returns 1 if at least one flag exists in the DB, else 0. This subroutine
198 is mainly used to decide to display the "(My )Requests" link in the footer.
199
200 =back
201
202 =cut
203
204 sub has_flags {
205     my $dbh = Bugzilla->dbh;
206
207     my $has_flags = $dbh->selectrow_array('SELECT 1 FROM flags ' . $dbh->sql_limit(1));
208     return $has_flags || 0;
209 }
210
211 =pod
212
213 =over
214
215 =item C<match($criteria)>
216
217 Queries the database for flags matching the given criteria
218 (specified as a hash of field names and their matching values)
219 and returns an array of matching records.
220
221 =back
222
223 =cut
224
225 sub match {
226     my $class = shift;
227     my ($criteria) = @_;
228
229     # If the caller specified only bug or attachment flags,
230     # limit the query to those kinds of flags.
231     if (my $type = delete $criteria->{'target_type'}) {
232         if ($type eq 'bug') {
233             $criteria->{'attach_id'} = IS_NULL;
234         }
235         elsif (!defined $criteria->{'attach_id'}) {
236             $criteria->{'attach_id'} = NOT_NULL;
237         }
238     }
239     # Flag->snapshot() calls Flag->match() with bug_id and attach_id
240     # as hash keys, even if attach_id is undefined.
241     if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
242         $criteria->{'attach_id'} = IS_NULL;
243     }
244
245     return $class->SUPER::match(@_);
246 }
247
248 =pod
249
250 =over
251
252 =item C<count($criteria)>
253
254 Queries the database for flags matching the given criteria
255 (specified as a hash of field names and their matching values)
256 and returns an array of matching records.
257
258 =back
259
260 =cut
261
262 sub count {
263     my $class = shift;
264     return scalar @{$class->match(@_)};
265 }
266
267 ######################################################################
268 # Creating and Modifying
269 ######################################################################
270
271 =pod
272
273 =over
274
275 =item C<validate($bug_id, $attach_id, $skip_requestee_on_error)>
276
277 Validates fields containing flag modifications.
278
279 If the attachment is new, it has no ID yet and $attach_id is set
280 to -1 to force its check anyway.
281
282 =back
283
284 =cut
285
286 sub validate {
287     my ($bug_id, $attach_id, $skip_requestee_on_error) = @_;
288     my $cgi = Bugzilla->cgi;
289     my $dbh = Bugzilla->dbh;
290
291     # Get a list of flags to validate.  Uses the "map" function
292     # to extract flag IDs from form field names by matching fields
293     # whose name looks like "flag_type-nnn" (new flags) or "flag-nnn"
294     # (existing flags), where "nnn" is the ID, and returning just
295     # the ID portion of matching field names.
296     my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
297     my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
298
299     return unless (scalar(@flagtype_ids) || scalar(@flag_ids));
300
301     # No flag reference should exist when changing several bugs at once.
302     ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;
303
304     # We don't check that these new flags are valid for this bug/attachment,
305     # because the bug may be moved into another product meanwhile.
306     # This check will be done later when creating new flags, see FormToNewFlags().
307
308     if (scalar(@flag_ids)) {
309         # No reference to existing flags should exist when creating a new
310         # attachment.
311         if ($attach_id && ($attach_id < 0)) {
312             ThrowCodeError('flags_not_available', { type => 'a' });
313         }
314
315         # Make sure all existing flags belong to the bug/attachment
316         # they pretend to be.
317         my $field = ($attach_id) ? "attach_id" : "bug_id";
318         my $field_id = $attach_id || $bug_id;
319         my $not = ($attach_id) ? "" : "NOT";
320
321         my $invalid_data =
322             $dbh->selectrow_array(
323                       "SELECT 1 FROM flags
324                         WHERE " 
325                        . $dbh->sql_in('id', \@flag_ids) 
326                        . " AND ($field != ? OR attach_id IS $not NULL) "
327                        . $dbh->sql_limit(1), undef, $field_id);
328
329         if ($invalid_data) {
330             ThrowCodeError('invalid_flag_association',
331                            { bug_id    => $bug_id,
332                              attach_id => $attach_id });
333         }
334     }
335
336     # Validate new flags.
337     foreach my $id (@flagtype_ids) {
338         my $status = $cgi->param("flag_type-$id");
339         my @requestees = $cgi->param("requestee_type-$id");
340         my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
341
342         # Don't bother validating types the user didn't touch.
343         next if $status eq 'X';
344
345         # Make sure the flag type exists. If it doesn't, FormToNewFlags()
346         # will ignore it, so it's safe to ignore it here.
347         my $flag_type = new Bugzilla::FlagType($id);
348         next unless $flag_type;
349
350         # Make sure the flag type is active.
351         unless ($flag_type->is_active) {
352             ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
353         }
354
355         _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
356                   $bug_id, $attach_id, $skip_requestee_on_error);
357     }
358
359     # Validate existing flags.
360     foreach my $id (@flag_ids) {
361         my $status = $cgi->param("flag-$id");
362         my @requestees = $cgi->param("requestee-$id");
363         my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
364
365         # Make sure the flag exists. If it doesn't, process() will ignore it,
366         # so it's safe to ignore it here.
367         my $flag = new Bugzilla::Flag($id);
368         next unless $flag;
369
370         _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
371                   undef, undef, $skip_requestee_on_error);
372     }
373 }
374
375 sub _validate {
376     my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
377         $bug_id, $attach_id, $skip_requestee_on_error) = @_;
378
379     # By default, the flag setter (or requester) is the current user.
380     $setter ||= Bugzilla->user;
381
382     my $id = $flag ? $flag->id : $flag_type->id; # Used in the error messages below.
383     $bug_id ||= $flag->bug_id;
384     $attach_id ||= $flag->attach_id if $flag; # Maybe it's a bug flag.
385
386     # Make sure the user chose a valid status.
387     grep($status eq $_, qw(X + - ?))
388       || ThrowCodeError('flag_status_invalid',
389                         { id => $id, status => $status });
390
391     # Make sure the user didn't request the flag unless it's requestable.
392     # If the flag existed and was requested before it became unrequestable,
393     # leave it as is.
394     if ($status eq '?'
395         && (!$flag || $flag->status ne '?')
396         && !$flag_type->is_requestable)
397     {
398         ThrowCodeError('flag_status_invalid',
399                        { id => $id, status => $status });
400     }
401
402     # Make sure the user didn't specify a requestee unless the flag
403     # is specifically requestable. For existing flags, if the requestee
404     # was set before the flag became specifically unrequestable, don't
405     # let the user change the requestee, but let the user remove it by
406     # entering an empty string for the requestee.
407     if ($status eq '?' && !$flag_type->is_requesteeble) {
408         my $old_requestee = ($flag && $flag->requestee) ?
409                                 $flag->requestee->login : '';
410         my $new_requestee = join('', @$requestees);
411         if ($new_requestee && $new_requestee ne $old_requestee) {
412             ThrowCodeError('flag_requestee_disabled',
413                            { type => $flag_type });
414         }
415     }
416
417     # Make sure the user didn't enter multiple requestees for a flag
418     # that can't be requested from more than one person at a time.
419     if ($status eq '?'
420         && !$flag_type->is_multiplicable
421         && scalar(@$requestees) > 1)
422     {
423         ThrowUserError('flag_not_multiplicable', { type => $flag_type });
424     }
425
426     # Make sure the requestees are authorized to access the bug
427     # (and attachment, if this installation is using the "insider group"
428     # feature and the attachment is marked private).
429     if ($status eq '?' && $flag_type->is_requesteeble) {
430         my $old_requestee = ($flag && $flag->requestee) ?
431                                 $flag->requestee->login : '';
432
433         my @legal_requestees;
434         foreach my $login (@$requestees) {
435             if ($login eq $old_requestee) {
436                 # This requestee was already set. Leave him alone.
437                 push(@legal_requestees, $login);
438                 next;
439             }
440
441             # We know the requestee exists because we ran
442             # Bugzilla::User::match_field before getting here.
443             my $requestee = new Bugzilla::User({ name => $login });
444
445             # Throw an error if the user can't see the bug.
446             # Note that if permissions on this bug are changed,
447             # can_see_bug() will refer to old settings.
448             if (!$requestee->can_see_bug($bug_id)) {
449                 next if $skip_requestee_on_error;
450                 ThrowUserError('flag_requestee_unauthorized',
451                                { flag_type  => $flag_type,
452                                  requestee  => $requestee,
453                                  bug_id     => $bug_id,
454                                  attach_id  => $attach_id });
455             }
456
457             # Throw an error if the target is a private attachment and
458             # the requestee isn't in the group of insiders who can see it.
459             if ($attach_id
460                 && $private_attachment
461                 && Bugzilla->params->{'insidergroup'}
462                 && !$requestee->in_group(Bugzilla->params->{'insidergroup'}))
463             {
464                 next if $skip_requestee_on_error;
465                 ThrowUserError('flag_requestee_unauthorized_attachment',
466                                { flag_type  => $flag_type,
467                                  requestee  => $requestee,
468                                  bug_id     => $bug_id,
469                                  attach_id  => $attach_id });
470             }
471
472             # Throw an error if the user won't be allowed to set the flag.
473             if (!$requestee->can_set_flag($flag_type)) {
474                 next if $skip_requestee_on_error;
475                 ThrowUserError('flag_requestee_needs_privs',
476                                {'requestee' => $requestee,
477                                 'flagtype'  => $flag_type});
478             }
479
480             # This requestee can be set.
481             push(@legal_requestees, $login);
482         }
483
484         # Update the requestee list for this flag.
485         if (scalar(@legal_requestees) < scalar(@$requestees)) {
486             my $field_name = 'requestee_type-' . $flag_type->id;
487             Bugzilla->cgi->delete($field_name);
488             Bugzilla->cgi->param(-name => $field_name, -value => \@legal_requestees);
489         }
490     }
491
492     # Make sure the user is authorized to modify flags, see bug 180879
493     # - The flag exists and is unchanged.
494     return if ($flag && ($status eq $flag->status));
495
496     # - User in the request_group can clear pending requests and set flags
497     #   and can rerequest set flags.
498     return if (($status eq 'X' || $status eq '?')
499                && $setter->can_request_flag($flag_type));
500
501     # - User in the grant_group can set/clear flags, including "+" and "-".
502     return if $setter->can_set_flag($flag_type);
503
504     # - Any other flag modification is denied
505     ThrowUserError('flag_update_denied',
506                     { name       => $flag_type->name,
507                       status     => $status,
508                       old_status => $flag ? $flag->status : 'X' });
509 }
510
511 sub snapshot {
512     my ($class, $bug_id, $attach_id) = @_;
513
514     my $flags = $class->match({ 'bug_id'    => $bug_id,
515                                 'attach_id' => $attach_id });
516     my @summaries;
517     foreach my $flag (@$flags) {
518         my $summary = $flag->type->name . $flag->status;
519         $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
520         push(@summaries, $summary);
521     }
522     return @summaries;
523 }
524
525
526 =pod
527
528 =over
529
530 =item C<process($bug, $attachment, $timestamp, $hr_vars)>
531
532 Processes changes to flags.
533
534 The bug and/or the attachment objects are the ones this flag is about,
535 the timestamp is the date/time the bug was last touched (so that changes
536 to the flag can be stamped with the same date/time).
537
538 =back
539
540 =cut
541
542 sub process {
543     my ($class, $bug, $attachment, $timestamp, $hr_vars) = @_;
544     my $dbh = Bugzilla->dbh;
545     my $cgi = Bugzilla->cgi;
546
547     # Make sure the bug (and attachment, if given) exists and is accessible
548     # to the current user. Moreover, if an attachment object is passed,
549     # make sure it belongs to the given bug.
550     return if ($bug->error || ($attachment && $bug->bug_id != $attachment->bug_id));
551
552     my $bug_id = $bug->bug_id;
553     my $attach_id = $attachment ? $attachment->id : undef;
554
555     # Use the date/time we were given if possible (allowing calling code
556     # to synchronize the comment's timestamp with those of other records).
557     $timestamp ||= $dbh->selectrow_array('SELECT NOW()');
558
559     # Take a snapshot of flags before any changes.
560     my @old_summaries = $class->snapshot($bug_id, $attach_id);
561
562     # Cancel pending requests if we are obsoleting an attachment.
563     if ($attachment && $cgi->param('isobsolete')) {
564         $class->CancelRequests($bug, $attachment);
565     }
566
567     # Create new flags and update existing flags.
568     my $new_flags = FormToNewFlags($bug, $attachment, $cgi, $hr_vars);
569     foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
570     modify($bug, $attachment, $cgi, $timestamp);
571
572     # In case the bug's product/component has changed, clear flags that are
573     # no longer valid.
574     my $flag_ids = $dbh->selectcol_arrayref(
575         "SELECT DISTINCT flags.id
576            FROM flags
577      INNER JOIN bugs
578              ON flags.bug_id = bugs.bug_id
579       LEFT JOIN flaginclusions AS i
580              ON flags.type_id = i.type_id 
581             AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
582             AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
583           WHERE bugs.bug_id = ?
584             AND i.type_id IS NULL",
585         undef, $bug_id);
586
587     my $flags = Bugzilla::Flag->new_from_list($flag_ids);
588     foreach my $flag (@$flags) {
589         my $is_retargetted = retarget($flag, $bug);
590         unless ($is_retargetted) {
591             clear($flag, $bug, $flag->attachment);
592             $hr_vars->{'message'} = 'flag_cleared';
593         }
594     }
595
596     $flag_ids = $dbh->selectcol_arrayref(
597         "SELECT DISTINCT flags.id
598         FROM flags, bugs, flagexclusions e
599         WHERE bugs.bug_id = ?
600         AND flags.bug_id = bugs.bug_id
601         AND flags.type_id = e.type_id
602         AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
603         AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
604         undef, $bug_id);
605
606     $flags = Bugzilla::Flag->new_from_list($flag_ids);
607     foreach my $flag (@$flags) {
608         my $is_retargetted = retarget($flag, $bug);
609         clear($flag, $bug, $flag->attachment) unless $is_retargetted;
610     }
611
612     # Take a snapshot of flags after changes.
613     my @new_summaries = $class->snapshot($bug_id, $attach_id);
614
615     update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
616
617     Bugzilla::Hook::process('flag-end_of_update', { bug       => $bug,
618                                                     timestamp => $timestamp,
619                                                     old_flags => \@old_summaries,
620                                                     new_flags => \@new_summaries,
621                                                   });
622 }
623
624 sub update_activity {
625     my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
626     my $dbh = Bugzilla->dbh;
627
628     $old_summaries = join(", ", @$old_summaries);
629     $new_summaries = join(", ", @$new_summaries);
630     my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
631     if ($removed ne $added) {
632         my $field_id = get_field_id('flagtypes.name');
633         $dbh->do('INSERT INTO bugs_activity
634                   (bug_id, attach_id, who, bug_when, fieldid, removed, added)
635                   VALUES (?, ?, ?, ?, ?, ?, ?)',
636                   undef, ($bug_id, $attach_id, Bugzilla->user->id,
637                   $timestamp, $field_id, $removed, $added));
638
639         $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
640                   undef, ($timestamp, $bug_id));
641     }
642 }
643
644 =pod
645
646 =over
647
648 =item C<create($flag, $bug, $attachment, $timestamp)>
649
650 Creates a flag record in the database.
651
652 =back
653
654 =cut
655
656 sub create {
657     my ($flag, $bug, $attachment, $timestamp) = @_;
658     my $dbh = Bugzilla->dbh;
659
660     my $attach_id = $attachment ? $attachment->id : undef;
661     my $requestee_id;
662     # Be careful! At this point, $flag is *NOT* yet an object!
663     $requestee_id = $flag->{'requestee'}->id if $flag->{'requestee'};
664
665     $dbh->do('INSERT INTO flags (type_id, bug_id, attach_id, requestee_id,
666                                  setter_id, status, creation_date, modification_date)
667               VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
668               undef, ($flag->{'type'}->id, $bug->bug_id,
669                       $attach_id, $requestee_id, $flag->{'setter'}->id,
670                       $flag->{'status'}, $timestamp, $timestamp));
671
672     # Now that the new flag has been added to the DB, create a real flag object.
673     # This is required to call notify() correctly.
674     my $flag_id = $dbh->bz_last_key('flags', 'id');
675     $flag = new Bugzilla::Flag($flag_id);
676
677     # Send an email notifying the relevant parties about the flag creation.
678     if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
679         $flag->{'addressee'} = $flag->requestee;
680     }
681
682     notify($flag, $bug, $attachment);
683
684     # Return the new flag object.
685     return $flag;
686 }
687
688 =pod
689
690 =over
691
692 =item C<modify($bug, $attachment, $cgi, $timestamp)>
693
694 Modifies flags in the database when a user changes them.
695
696 =back
697
698 =cut
699
700 sub modify {
701     my ($bug, $attachment, $cgi, $timestamp) = @_;
702     my $setter = Bugzilla->user;
703     my $dbh = Bugzilla->dbh;
704
705     # Extract a list of flags from the form data.
706     my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
707
708     # Loop over flags and update their record in the database if necessary.
709     # Two kinds of changes can happen to a flag: it can be set to a different
710     # state, and someone else can be asked to set it.  We take care of both
711     # those changes.
712     my @flags;
713     foreach my $id (@ids) {
714         my $flag = new Bugzilla::Flag($id);
715         # If the flag no longer exists, ignore it.
716         next unless $flag;
717
718         my $status = $cgi->param("flag-$id");
719
720         # If the user entered more than one name into the requestee field
721         # (i.e. they want more than one person to set the flag) we can reuse
722         # the existing flag for the first person (who may well be the existing
723         # requestee), but we have to create new flags for each additional.
724         my @requestees = $cgi->param("requestee-$id");
725         my $requestee_email;
726         if ($status eq "?"
727             && scalar(@requestees) > 1
728             && $flag->type->is_multiplicable)
729         {
730             # The first person, for which we'll reuse the existing flag.
731             $requestee_email = shift(@requestees);
732
733             # Create new flags like the existing one for each additional person.
734             foreach my $login (@requestees) {
735                 create({ type      => $flag->type,
736                          setter    => $setter, 
737                          status    => "?",
738                          requestee => new Bugzilla::User({ name => $login }) },
739                        $bug, $attachment, $timestamp);
740             }
741         }
742         else {
743             $requestee_email = trim($cgi->param("requestee-$id") || '');
744         }
745
746         # Ignore flags the user didn't change. There are two components here:
747         # either the status changes (trivial) or the requestee changes.
748         # Change of either field will cause full update of the flag.
749
750         my $status_changed = ($status ne $flag->status);
751
752         # Requestee is considered changed, if all of the following apply:
753         # 1. Flag status is '?' (requested)
754         # 2. Flag can have a requestee
755         # 3. The requestee specified on the form is different from the 
756         #    requestee specified in the db.
757
758         my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
759
760         my $requestee_changed = 
761           ($status eq "?" && 
762            $flag->type->is_requesteeble &&
763            $old_requestee ne $requestee_email);
764
765         next unless ($status_changed || $requestee_changed);
766
767         # Since the status is validated, we know it's safe, but it's still
768         # tainted, so we have to detaint it before using it in a query.
769         trick_taint($status);
770
771         if ($status eq '+' || $status eq '-') {
772             $dbh->do('UPDATE flags
773                          SET setter_id = ?, requestee_id = NULL,
774                              status = ?, modification_date = ?
775                        WHERE id = ?',
776                        undef, ($setter->id, $status, $timestamp, $flag->id));
777
778             # If the status of the flag was "?", we have to notify
779             # the requester (if he wants to).
780             my $requester;
781             if ($flag->status eq '?') {
782                 $requester = $flag->setter;
783                 $flag->{'requester'} = $requester;
784             }
785             # Now update the flag object with its new values.
786             $flag->{'setter'} = $setter;
787             $flag->{'requestee'} = undef;
788             $flag->{'requestee_id'} = undef;
789             $flag->{'status'} = $status;
790
791             # Send an email notifying the relevant parties about the fulfillment,
792             # including the requester.
793             if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
794                 $flag->{'addressee'} = $requester;
795             }
796
797             notify($flag, $bug, $attachment);
798         }
799         elsif ($status eq '?') {
800             # If the one doing the change is the requestee, then this means he doesn't
801             # want to reply to the request and he simply reassigns the request to
802             # someone else. In this case, we keep the requester unaltered.
803             my $new_setter = $setter;
804             if ($flag->requestee && $flag->requestee->id == $setter->id) {
805                 $new_setter = $flag->setter;
806             }
807
808             # Get the requestee, if any.
809             my $requestee_id;
810             if ($requestee_email) {
811                 $requestee_id = login_to_id($requestee_email);
812                 $flag->{'requestee'} = new Bugzilla::User($requestee_id);
813                 $flag->{'requestee_id'} = $requestee_id;
814             }
815             else {
816                 # If the status didn't change but we only removed the
817                 # requestee, we have to clear the requestee field.
818                 $flag->{'requestee'} = undef;
819                 $flag->{'requestee_id'} = undef;
820             }
821
822             # Update the database with the changes.
823             $dbh->do('UPDATE flags
824                          SET setter_id = ?, requestee_id = ?,
825                              status = ?, modification_date = ?
826                        WHERE id = ?',
827                        undef, ($new_setter->id, $requestee_id, $status,
828                                $timestamp, $flag->id));
829
830             # Now update the flag object with its new values.
831             $flag->{'setter'} = $new_setter;
832             $flag->{'status'} = $status;
833
834             # Send an email notifying the relevant parties about the request.
835             if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
836                 $flag->{'addressee'} = $flag->requestee;
837             }
838
839             notify($flag, $bug, $attachment);
840         }
841         elsif ($status eq 'X') {
842             clear($flag, $bug, $attachment);
843         }
844
845         push(@flags, $flag);
846     }
847
848     return \@flags;
849 }
850
851 =pod
852
853 =over
854
855 =item C<retarget($flag, $bug)>
856
857 Change the type of the flag, if possible. The new flag type must have
858 the same name as the current flag type, must exist in the product and
859 component the bug is in, and the current settings of the flag must pass
860 validation. If no such flag type can be found, the type remains unchanged.
861
862 Retargetting flags is a good way to keep flags when moving bugs from one
863 product where a flag type is available to another product where the flag
864 type is unavailable, but another flag type having the same name exists.
865 Most of the time, if they have the same name, this means that they have
866 the same meaning, but with different settings.
867
868 =back
869
870 =cut
871
872 sub retarget {
873     my ($flag, $bug) = @_;
874     my $dbh = Bugzilla->dbh;
875
876     # We are looking for flagtypes having the same name as the flagtype
877     # to which the current flag belongs, and being in the new product and
878     # component of the bug.
879     my $flagtypes = Bugzilla::FlagType::match(
880                         {'name'         => $flag->name,
881                          'target_type'  => $flag->type->target_type,
882                          'is_active'    => 1,
883                          'product_id'   => $bug->product_id,
884                          'component_id' => $bug->component_id});
885
886     # If we found no flagtype, the flag will be deleted.
887     return 0 unless scalar(@$flagtypes);
888
889     # If we found at least one, change the type of the flag,
890     # assuming the setter/requester is allowed to set/request flags
891     # belonging to this flagtype.
892     my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
893     my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
894     my $is_retargetted = 0;
895
896     foreach my $flagtype (@$flagtypes) {
897         # Get the number of flags of this type already set for this target.
898         my $has_flags = __PACKAGE__->count(
899             { 'type_id'     => $flagtype->id,
900               'bug_id'      => $bug->bug_id,
901               'attach_id'   => $flag->attach_id });
902
903         # Do not create a new flag of this type if this flag type is
904         # not multiplicable and already has a flag set.
905         next if (!$flagtype->is_multiplicable && $has_flags);
906
907         # Check user privileges.
908         my $error_mode_cache = Bugzilla->error_mode;
909         Bugzilla->error_mode(ERROR_MODE_DIE);
910         eval {
911             _validate(undef, $flagtype, $flag->status, $flag->setter,
912                       $requestee, $is_private, $bug->bug_id, $flag->attach_id);
913         };
914         Bugzilla->error_mode($error_mode_cache);
915         # If the validation failed, then we cannot use this flagtype.
916         next if ($@);
917
918         # Checks are successful, we can retarget the flag to this flagtype.
919         $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
920                   undef, ($flagtype->id, $flag->id));
921
922         $is_retargetted = 1;
923         last;
924     }
925     return $is_retargetted;
926 }
927
928 =pod
929
930 =over
931
932 =item C<clear($flag, $bug, $attachment)>
933
934 Remove a flag from the DB.
935
936 =back
937
938 =cut
939
940 sub clear {
941     my ($flag, $bug, $attachment) = @_;
942     my $dbh = Bugzilla->dbh;
943
944     $dbh->do('DELETE FROM flags WHERE id = ?', undef, $flag->id);
945
946     # If we cancel a pending request, we have to notify the requester
947     # (if he wants to).
948     my $requester;
949     if ($flag->status eq '?') {
950         $requester = $flag->setter;
951         $flag->{'requester'} = $requester;
952     }
953
954     # Now update the flag object to its new values. The last
955     # requester/setter and requestee are kept untouched (for the
956     # record). Else we could as well delete the flag completely.
957     $flag->{'exists'} = 0;    
958     $flag->{'status'} = "X";
959
960     if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
961         $flag->{'addressee'} = $requester;
962     }
963
964     notify($flag, $bug, $attachment);
965 }
966
967
968 ######################################################################
969 # Utility Functions
970 ######################################################################
971
972 =pod
973
974 =over
975
976 =item C<FormToNewFlags($bug, $attachment, $cgi, $hr_vars)>
977
978 Checks whether or not there are new flags to create and returns an
979 array of flag objects. This array is then passed to Flag::create().
980
981 =back
982
983 =cut
984
985 sub FormToNewFlags {
986     my ($bug, $attachment, $cgi, $hr_vars) = @_;
987     my $dbh = Bugzilla->dbh;
988     my $setter = Bugzilla->user;
989     
990     # Extract a list of flag type IDs from field names.
991     my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
992     @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
993
994     return () unless scalar(@type_ids);
995
996     # Get a list of active flag types available for this product/component.
997     my $flag_types = Bugzilla::FlagType::match(
998         { 'product_id'   => $bug->{'product_id'},
999           'component_id' => $bug->{'component_id'},
1000           'is_active'    => 1 });
1001
1002     foreach my $type_id (@type_ids) {
1003         # Checks if there are unexpected flags for the product/component.
1004         if (!scalar(grep { $_->id == $type_id } @$flag_types)) {
1005             $hr_vars->{'message'} = 'unexpected_flag_types';
1006             last;
1007         }
1008     }
1009
1010     my @flags;
1011     foreach my $flag_type (@$flag_types) {
1012         my $type_id = $flag_type->id;
1013
1014         # Bug flags are only valid for bugs, and attachment flags are
1015         # only valid for attachments. So don't mix both.
1016         next unless ($flag_type->target_type eq 'bug' xor $attachment);
1017
1018         # We are only interested in flags the user tries to create.
1019         next unless scalar(grep { $_ == $type_id } @type_ids);
1020
1021         # Get the number of flags of this type already set for this target.
1022         my $has_flags = __PACKAGE__->count(
1023             { 'type_id'     => $type_id,
1024               'target_type' => $attachment ? 'attachment' : 'bug',
1025               'bug_id'      => $bug->bug_id,
1026               'attach_id'   => $attachment ? $attachment->id : undef });
1027
1028         # Do not create a new flag of this type if this flag type is
1029         # not multiplicable and already has a flag set.
1030         next if (!$flag_type->is_multiplicable && $has_flags);
1031
1032         my $status = $cgi->param("flag_type-$type_id");
1033         trick_taint($status);
1034
1035         my @logins = $cgi->param("requestee_type-$type_id");
1036         if ($status eq "?" && scalar(@logins) > 0) {
1037             foreach my $login (@logins) {
1038                 push (@flags, { type      => $flag_type ,
1039                                 setter    => $setter , 
1040                                 status    => $status ,
1041                                 requestee => 
1042                                     new Bugzilla::User({ name => $login }) });
1043                 last unless $flag_type->is_multiplicable;
1044             }
1045         }
1046         else {
1047             push (@flags, { type   => $flag_type ,
1048                             setter => $setter , 
1049                             status => $status });
1050         }
1051     }
1052
1053     # Return the list of flags.
1054     return \@flags;
1055 }
1056
1057 =pod
1058
1059 =over
1060
1061 =item C<notify($flag, $bug, $attachment)>
1062
1063 Sends an email notification about a flag being created, fulfilled
1064 or deleted.
1065
1066 =back
1067
1068 =cut
1069
1070 sub notify {
1071     my ($flag, $bug, $attachment) = @_;
1072
1073     #if WEBKIT_CHANGES
1074     # Don't send a notification when the flag is in-rietveld,
1075     # since it isn't a user visible flag, and that mail is spammy.
1076     return if ($flag->type->name eq 'in-rietveld');
1077     #endif // WEBKIT_CHANGES
1078
1079     # There is nobody to notify.
1080     return unless ($flag->{'addressee'} || $flag->type->cc_list);
1081
1082     # If the target bug is restricted to one or more groups, then we need
1083     # to make sure we don't send email about it to unauthorized users
1084     # on the request type's CC: list, so we have to trawl the list for users
1085     # not in those groups or email addresses that don't have an account.
1086     my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
1087     my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
1088
1089     my %recipients;
1090     foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) {
1091         my $ccuser = new Bugzilla::User({ name => $cc });
1092         next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
1093         next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
1094         # Prevent duplicated entries due to case sensitivity.
1095         $cc = $ccuser ? $ccuser->email : $cc;
1096         $recipients{$cc} = $ccuser;
1097     }
1098
1099     # Only notify if the addressee is allowed to receive the email.
1100     if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
1101         $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'};
1102     }
1103     # Process and send notification for each recipient.
1104     # If there are users in the CC list who don't have an account,
1105     # use the default language for email notifications.
1106     my $default_lang;
1107     if (grep { !$_ } values %recipients) {
1108         my $default_user = new Bugzilla::User();
1109         $default_lang = $default_user->settings->{'lang'}->{'value'};
1110     }
1111
1112     foreach my $to (keys %recipients) {
1113         # Add threadingmarker to allow flag notification emails to be the
1114         # threaded similar to normal bug change emails.
1115         my $user_id = $recipients{$to} ? $recipients{$to}->id : 0;
1116         my $threadingmarker = build_thread_marker($bug->id, $user_id);
1117     
1118         my $vars = { 'flag'            => $flag,
1119                      'to'              => $to,
1120                      'bug'             => $bug,
1121                      'attachment'      => $attachment,
1122                      'threadingmarker' => $threadingmarker };
1123
1124         my $lang = $recipients{$to} ?
1125           $recipients{$to}->settings->{'lang'}->{'value'} : $default_lang;
1126
1127         my $template = Bugzilla->template_inner($lang);
1128         my $message;
1129         $template->process("request/email.txt.tmpl", $vars, \$message)
1130           || ThrowTemplateError($template->error());
1131
1132         Bugzilla->template_inner("");
1133         MessageToMTA($message);
1134     }
1135 }
1136
1137 # Cancel all request flags from the attachment being obsoleted.
1138 sub CancelRequests {
1139     my ($class, $bug, $attachment, $timestamp) = @_;
1140     my $dbh = Bugzilla->dbh;
1141
1142     my $request_ids =
1143         $dbh->selectcol_arrayref("SELECT flags.id
1144                                   FROM flags
1145                                   LEFT JOIN attachments ON flags.attach_id = attachments.attach_id
1146                                   WHERE flags.attach_id = ?
1147                                   AND flags.status = '?'
1148                                   AND attachments.isobsolete = 0",
1149                                   undef, $attachment->id);
1150
1151     return if (!scalar(@$request_ids));
1152
1153     # Take a snapshot of flags before any changes.
1154     my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
1155         if ($timestamp);
1156     my $flags = Bugzilla::Flag->new_from_list($request_ids);
1157     foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
1158
1159     # If $timestamp is undefined, do not update the activity table
1160     return unless ($timestamp);
1161
1162     # Take a snapshot of flags after any changes.
1163     my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
1164     update_activity($bug->bug_id, $attachment->id, $timestamp,
1165                     \@old_summaries, \@new_summaries);
1166 }
1167
1168 =head1 SEE ALSO
1169
1170 =over
1171
1172 =item B<Bugzilla::FlagType>
1173
1174 =back
1175
1176
1177 =head1 CONTRIBUTORS
1178
1179 =over
1180
1181 =item Myk Melez <myk@mozilla.org>
1182
1183 =item Jouni Heikniemi <jouni@heikniemi.net>
1184
1185 =item Kevin Benton <kevin.benton@amd.com>
1186
1187 =item Frédéric Buclin <LpSolit@gmail.com>
1188
1189 =back
1190
1191 =cut
1192
1193 1;