Flag.pm 35.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Myk Melez <myk@mozilla.org>
21
#                 Jouni Heikniemi <jouni@heikniemi.net>
22
#                 Frédéric Buclin <LpSolit@gmail.com>
23

24 25 26 27
use strict;

package Bugzilla::Flag;

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
=head1 NAME

Bugzilla::Flag - A module to deal with Bugzilla flag values.

=head1 SYNOPSIS

Flag.pm provides an interface to flags as stored in Bugzilla.
See below for more information.

=head1 NOTES

=over

=item *

43
Import relevant functions from that script.
44 45 46 47

=item *

Use of private functions / variables outside this module may lead to
48
unexpected results after an upgrade.  Please avoid using private
49 50 51 52 53 54 55
functions in other files/modules.  Private functions are functions
whose names start with _ or a re specifically noted as being private.

=back

=cut

56 57
use Bugzilla::FlagType;
use Bugzilla::User;
58
use Bugzilla::Util;
59
use Bugzilla::Error;
60
use Bugzilla::Mailer;
61
use Bugzilla::Constants;
62
use Bugzilla::Field;
63

64 65 66 67 68
use base qw(Bugzilla::Object);

###############################
####    Initialization     ####
###############################
69

70 71 72 73 74 75 76 77 78
use constant DB_COLUMNS => qw(
    flags.id
    flags.type_id
    flags.bug_id
    flags.attach_id
    flags.requestee_id
    flags.setter_id
    flags.status
);
79

80 81
use constant DB_TABLE => 'flags';
use constant LIST_ORDER => 'id';
82

83 84 85
###############################
####      Accessors      ######
###############################
86

87
=head2 METHODS
88

89 90
=over

91 92 93 94 95 96 97
=item C<id>

Returns the ID of the flag.

=item C<name>

Returns the name of the flagtype the flag belongs to.
98

99 100 101 102 103 104 105 106
=item C<bug_id>

Returns the ID of the bug this flag belongs to.

=item C<attach_id>

Returns the ID of the attachment this flag belongs to, if any.

107 108 109
=item C<status>

Returns the status '+', '-', '?' of the flag.
110

111 112
=back

113
=cut
114

115 116
sub id     { return $_[0]->{'id'};     }
sub name   { return $_[0]->type->name; }
117 118
sub bug_id { return $_[0]->{'bug_id'}; }
sub attach_id { return $_[0]->{'attach_id'}; }
119
sub status { return $_[0]->{'status'}; }
120 121 122 123 124 125 126 127 128 129 130

###############################
####       Methods         ####
###############################

=pod

=over

=item C<type>

131
Returns the type of the flag, as a Bugzilla::FlagType object.
132

133
=item C<setter>
134

135 136 137 138 139 140 141 142 143 144 145 146 147 148
Returns the user who set the flag, as a Bugzilla::User object.

=item C<requestee>

Returns the user who has been requested to set the flag, as a
Bugzilla::User object.

=back

=cut

sub type {
    my $self = shift;

149
    $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
150
    return $self->{'type'};
151 152
}

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
sub setter {
    my $self = shift;

    $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
    return $self->{'setter'};
}

sub requestee {
    my $self = shift;

    if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
        $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
    }
    return $self->{'requestee'};
}

################################
## Searching/Retrieving Flags ##
################################

173 174 175 176 177
=pod

=over

=item C<match($criteria)>
178

179 180 181 182 183 184 185 186 187
Queries the database for flags matching the given criteria
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back

=cut

sub match {
188
    my ($criteria) = @_;
189
    my $dbh = Bugzilla->dbh;
190 191

    my @criteria = sqlify_criteria($criteria);
192 193
    $criteria = join(' AND ', @criteria);

194 195
    my $flag_ids = $dbh->selectcol_arrayref("SELECT id FROM flags
                                             WHERE $criteria");
196

197
    return Bugzilla::Flag->new_from_list($flag_ids);
198 199
}

200 201 202 203 204 205
=pod

=over

=item C<count($criteria)>

206
Queries the database for flags matching the given criteria
207 208 209 210
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back
211

212 213 214
=cut

sub count {
215
    my ($criteria) = @_;
216
    my $dbh = Bugzilla->dbh;
217 218

    my @criteria = sqlify_criteria($criteria);
219 220 221
    $criteria = join(' AND ', @criteria);

    my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM flags WHERE $criteria");
222 223 224 225

    return $count;
}

226
######################################################################
227
# Creating and Modifying
228
######################################################################
229

230 231 232 233
=pod

=over

234
=item C<validate($cgi, $bug_id, $attach_id)>
235

236 237
Validates fields containing flag modifications.

238 239 240
If the attachment is new, it has no ID yet and $attach_id is set
to -1 to force its check anyway.

241 242 243 244 245
=back

=cut

sub validate {
246 247 248 249
    my ($cgi, $bug_id, $attach_id) = @_;

    my $dbh = Bugzilla->dbh;

250 251
    # Get a list of flags to validate.  Uses the "map" function
    # to extract flag IDs from form field names by matching fields
252 253 254 255 256 257 258
    # whose name looks like "flag_type-nnn" (new flags) or "flag-nnn"
    # (existing flags), where "nnn" is the ID, and returning just
    # the ID portion of matching field names.
    my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());

    return unless (scalar(@flagtype_ids) || scalar(@flag_ids));
259 260 261 262

    # No flag reference should exist when changing several bugs at once.
    ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    # We don't check that these new flags are valid for this bug/attachment,
    # because the bug may be moved into another product meanwhile.
    # This check will be done later when creating new flags, see FormToNewFlags().

    if (scalar(@flag_ids)) {
        # No reference to existing flags should exist when creating a new
        # attachment.
        if ($attach_id && ($attach_id < 0)) {
            ThrowCodeError('flags_not_available', { type => 'a' });
        }

        # Make sure all existing flags belong to the bug/attachment
        # they pretend to be.
        my $field = ($attach_id) ? "attach_id" : "bug_id";
        my $field_id = $attach_id || $bug_id;
        my $not = ($attach_id) ? "" : "NOT";

        my $invalid_data =
            $dbh->selectrow_array("SELECT 1 FROM flags
                                   WHERE id IN (" . join(',', @flag_ids) . ")
                                   AND ($field != ? OR attach_id IS $not NULL) " .
                                   $dbh->sql_limit(1),
                                   undef, $field_id);

        if ($invalid_data) {
            ThrowCodeError('invalid_flag_association',
                           { bug_id    => $bug_id,
                             attach_id => $attach_id });
        }
    }

    # Validate new flags.
    foreach my $id (@flagtype_ids) {
        my $status = $cgi->param("flag_type-$id");
        my @requestees = $cgi->param("requestee_type-$id");
        my $private_attachment = $cgi->param('isprivate') ? 1 : 0;

        # Don't bother validating types the user didn't touch.
        next if $status eq 'X';

        # Make sure the flag type exists.
        my $flag_type = new Bugzilla::FlagType($id);
        $flag_type || ThrowCodeError('flag_type_nonexistent', { id => $id });

307 308 309 310 311
        # Make sure the flag type is active.
        unless ($flag_type->is_active) {
            ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
        }

312 313
        _validate(undef, $flag_type, $status, \@requestees, $private_attachment,
                  $bug_id, $attach_id);
314 315
    }

316 317
    # Validate existing flags.
    foreach my $id (@flag_ids) {
318
        my $status = $cgi->param("flag-$id");
319
        my @requestees = $cgi->param("requestee-$id");
320
        my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
321

322
        # Make sure the flag exists.
323
        my $flag = new Bugzilla::Flag($id);
324
        $flag || ThrowCodeError("flag_nonexistent", { id => $id });
325

326 327 328
        _validate($flag, $flag->type, $status, \@requestees, $private_attachment);
    }
}
329

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
sub _validate {
    my ($flag, $flag_type, $status, $requestees, $private_attachment,
        $bug_id, $attach_id) = @_;

    my $user = Bugzilla->user;

    my $id = $flag ? $flag->id : $flag_type->id; # Used in the error messages below.
    $bug_id ||= $flag->bug_id;
    $attach_id ||= $flag->attach_id if $flag; # Maybe it's a bug flag.

    # Make sure the user chose a valid status.
    grep($status eq $_, qw(X + - ?))
      || ThrowCodeError('flag_status_invalid',
                        { id => $id, status => $status });

    # Make sure the user didn't request the flag unless it's requestable.
    # If the flag existed and was requested before it became unrequestable,
    # leave it as is.
    if ($status eq '?'
        && (!$flag || $flag->status ne '?')
        && !$flag_type->is_requestable)
    {
        ThrowCodeError('flag_status_invalid',
                       { id => $id, status => $status });
    }
355

356 357 358 359 360 361 362 363 364 365 366 367
    # Make sure the user didn't specify a requestee unless the flag
    # is specifically requestable. For existing flags, if the requestee
    # was set before the flag became specifically unrequestable, don't
    # let the user change the requestee, but let the user remove it by
    # entering an empty string for the requestee.
    if ($status eq '?' && !$flag_type->is_requesteeble) {
        my $old_requestee = ($flag && $flag->requestee) ?
                                $flag->requestee->login : '';
        my $new_requestee = join('', @$requestees);
        if ($new_requestee && $new_requestee ne $old_requestee) {
            ThrowCodeError('flag_requestee_disabled',
                           { type => $flag_type });
368
        }
369 370 371 372 373 374 375 376 377 378
    }

    # Make sure the user didn't enter multiple requestees for a flag
    # that can't be requested from more than one person at a time.
    if ($status eq '?'
        && !$flag_type->is_multiplicable
        && scalar(@$requestees) > 1)
    {
        ThrowUserError('flag_not_multiplicable', { type => $flag_type });
    }
379

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    # Make sure the requestees are authorized to access the bug
    # (and attachment, if this installation is using the "insider group"
    # feature and the attachment is marked private).
    if ($status eq '?' && $flag_type->is_requesteeble) {
        my $old_requestee = ($flag && $flag->requestee) ?
                                $flag->requestee->login : '';
        foreach my $login (@$requestees) {
            next if $login eq $old_requestee;

            # We know the requestee exists because we ran
            # Bugzilla::User::match_field before getting here.
            my $requestee = new Bugzilla::User({ name => $login });

            # Throw an error if the user can't see the bug.
            # Note that if permissions on this bug are changed,
            # can_see_bug() will refer to old settings.
            if (!$requestee->can_see_bug($bug_id)) {
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
402
            }
403

404 405 406 407 408 409 410 411 412 413 414 415 416
            # Throw an error if the target is a private attachment and
            # the requestee isn't in the group of insiders who can see it.
            if ($attach_id
                && $private_attachment
                && Bugzilla->params->{'insidergroup'}
                && !$requestee->in_group(Bugzilla->params->{'insidergroup'}))
            {
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
            }
417 418

            # Throw an error if the user won't be allowed to set the flag.
419 420 421 422
            $requestee->can_set_flag($flag_type)
              || ThrowUserError('flag_requestee_needs_privs',
                                {'requestee' => $requestee,
                                 'flagtype'  => $flag_type});
423
        }
424
    }
425 426 427 428 429 430 431 432

    # Make sure the user is authorized to modify flags, see bug 180879
    # - The flag exists and is unchanged.
    return if ($flag && ($status eq $flag->status));

    # - User in the request_group can clear pending requests and set flags
    #   and can rerequest set flags.
    return if (($status eq 'X' || $status eq '?')
433
               && $user->can_request_flag($flag_type));
434 435

    # - User in the grant_group can set/clear flags, including "+" and "-".
436
    return if $user->can_set_flag($flag_type);
437 438 439 440 441 442

    # - Any other flag modification is denied
    ThrowUserError('flag_update_denied',
                    { name       => $flag_type->name,
                      status     => $status,
                      old_status => $flag ? $flag->status : 'X' });
443 444
}

445 446 447 448
sub snapshot {
    my ($bug_id, $attach_id) = @_;

    my $flags = match({ 'bug_id'    => $bug_id,
449
                        'attach_id' => $attach_id });
450 451
    my @summaries;
    foreach my $flag (@$flags) {
452
        my $summary = $flag->type->name . $flag->status;
453
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
454 455 456 457 458
        push(@summaries, $summary);
    }
    return @summaries;
}

459

460 461 462 463
=pod

=over

464
=item C<process($bug, $attachment, $timestamp, $cgi)>
465 466 467

Processes changes to flags.

468 469 470 471
The bug and/or the attachment objects are the ones this flag is about,
the timestamp is the date/time the bug was last touched (so that changes
to the flag can be stamped with the same date/time), the cgi is the CGI
object used to obtain the flag fields that the user submitted.
472 473 474 475 476 477

=back

=cut

sub process {
478
    my ($bug, $attachment, $timestamp, $cgi) = @_;
479
    my $dbh = Bugzilla->dbh;
480 481 482 483 484 485 486 487

    # Make sure the bug (and attachment, if given) exists and is accessible
    # to the current user. Moreover, if an attachment object is passed,
    # make sure it belongs to the given bug.
    return if ($bug->error || ($attachment && $bug->bug_id != $attachment->bug_id));

    my $bug_id = $bug->bug_id;
    my $attach_id = $attachment ? $attachment->id : undef;
488 489 490

    # Use the date/time we were given if possible (allowing calling code
    # to synchronize the comment's timestamp with those of other records).
491 492
    $timestamp ||= $dbh->selectrow_array('SELECT NOW()');

493
    # Take a snapshot of flags before any changes.
494
    my @old_summaries = snapshot($bug_id, $attach_id);
495

496
    # Cancel pending requests if we are obsoleting an attachment.
497 498
    if ($attachment && $cgi->param('isobsolete')) {
        CancelRequests($bug, $attachment);
499 500
    }

501
    # Create new flags and update existing flags.
502 503 504
    my $new_flags = FormToNewFlags($bug, $attachment, $cgi);
    foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
    modify($bug, $attachment, $cgi, $timestamp);
505

506 507
    # In case the bug's product/component has changed, clear flags that are
    # no longer valid.
508
    my $flag_ids = $dbh->selectcol_arrayref(
509
        "SELECT DISTINCT flags.id
510 511 512 513 514
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
      LEFT JOIN flaginclusions AS i
             ON flags.type_id = i.type_id 
515
            AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
516 517 518
            AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ?
            AND i.type_id IS NULL",
519 520
        undef, $bug_id);

521 522 523 524
    foreach my $flag_id (@$flag_ids) {
        my $is_retargetted = retarget($flag_id, $bug, $attachment);
        clear($flag_id, $bug, $attachment) unless $is_retargetted;
    }
525 526

    $flag_ids = $dbh->selectcol_arrayref(
527
        "SELECT DISTINCT flags.id
528
        FROM flags, bugs, flagexclusions e
529
        WHERE bugs.bug_id = ?
530
        AND flags.bug_id = bugs.bug_id
531
        AND flags.type_id = e.type_id
532
        AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
533 534 535
        AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
        undef, $bug_id);

536 537 538 539
    foreach my $flag_id (@$flag_ids) {
        my $is_retargetted = retarget($flag_id, $bug, $attachment);
        clear($flag_id, $bug, $attachment) unless $is_retargetted;
    }
540

541 542 543 544 545 546 547 548 549 550 551 552
    # Take a snapshot of flags after changes.
    my @new_summaries = snapshot($bug_id, $attach_id);

    update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
}

sub update_activity {
    my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
    my $dbh = Bugzilla->dbh;

    $old_summaries = join(", ", @$old_summaries);
    $new_summaries = join(", ", @$new_summaries);
553
    my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
554
    if ($removed ne $added) {
555
        my $field_id = get_field_id('flagtypes.name');
556
        $dbh->do('INSERT INTO bugs_activity
557
                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
558 559 560
                  VALUES (?, ?, ?, ?, ?, ?, ?)',
                  undef, ($bug_id, $attach_id, Bugzilla->user->id,
                  $timestamp, $field_id, $removed, $added));
561

562 563
        $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
                  undef, ($timestamp, $bug_id));
564 565 566
    }
}

567 568 569 570
=pod

=over

571
=item C<create($flag, $bug, $attachment, $timestamp)>
572 573

Creates a flag record in the database.
574

575 576 577 578 579
=back

=cut

sub create {
580
    my ($flag, $bug, $attachment, $timestamp) = @_;
581 582
    my $dbh = Bugzilla->dbh;

583
    my $attach_id = $attachment ? $attachment->id : undef;
584
    my $requestee_id;
585
    # Be careful! At this point, $flag is *NOT* yet an object!
586 587 588 589 590
    $requestee_id = $flag->{'requestee'}->id if $flag->{'requestee'};

    $dbh->do('INSERT INTO flags (type_id, bug_id, attach_id, requestee_id,
                                 setter_id, status, creation_date, modification_date)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
591
              undef, ($flag->{'type'}->id, $bug->bug_id,
592 593
                      $attach_id, $requestee_id, $flag->{'setter'}->id,
                      $flag->{'status'}, $timestamp, $timestamp));
594

595 596 597 598 599
    # Now that the new flag has been added to the DB, create a real flag object.
    # This is required to call notify() correctly.
    my $flag_id = $dbh->bz_last_key('flags', 'id');
    $flag = new Bugzilla::Flag($flag_id);

600
    # Send an email notifying the relevant parties about the flag creation.
601 602
    if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
        $flag->{'addressee'} = $flag->requestee;
603
    }
604

605
    notify($flag, $bug, $attachment);
606 607 608

    # Return the new flag object.
    return $flag;
609 610
}

611
=pod
612

613 614
=over

615
=item C<modify($bug, $attachment, $cgi, $timestamp)>
616

617 618 619 620 621 622 623
Modifies flags in the database when a user changes them.

=back

=cut

sub modify {
624
    my ($bug, $attachment, $cgi, $timestamp) = @_;
625
    my $setter = Bugzilla->user;
626
    my $dbh = Bugzilla->dbh;
627 628

    # Extract a list of flags from the form data.
629
    my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
630

631 632 633 634
    # Loop over flags and update their record in the database if necessary.
    # Two kinds of changes can happen to a flag: it can be set to a different
    # state, and someone else can be asked to set it.  We take care of both
    # those changes.
635 636
    my @flags;
    foreach my $id (@ids) {
637
        my $flag = new Bugzilla::Flag($id);
638 639
        # If the flag no longer exists, ignore it.
        next unless $flag;
640

641
        my $status = $cgi->param("flag-$id");
642

643 644 645 646 647 648 649 650
        # If the user entered more than one name into the requestee field
        # (i.e. they want more than one person to set the flag) we can reuse
        # the existing flag for the first person (who may well be the existing
        # requestee), but we have to create new flags for each additional.
        my @requestees = $cgi->param("requestee-$id");
        my $requestee_email;
        if ($status eq "?"
            && scalar(@requestees) > 1
651
            && $flag->type->is_multiplicable)
652 653 654
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
655

656 657
            # Create new flags like the existing one for each additional person.
            foreach my $login (@requestees) {
658
                create({ type      => $flag->type,
659
                         setter    => $setter, 
660
                         status    => "?",
661
                         requestee => new Bugzilla::User({ name => $login }) },
662
                       $bug, $attachment, $timestamp);
663 664 665 666 667 668
            }
        }
        else {
            $requestee_email = trim($cgi->param("requestee-$id") || '');
        }

669 670 671 672
        # Ignore flags the user didn't change. There are two components here:
        # either the status changes (trivial) or the requestee changes.
        # Change of either field will cause full update of the flag.

673
        my $status_changed = ($status ne $flag->status);
674

675 676 677 678 679
        # Requestee is considered changed, if all of the following apply:
        # 1. Flag status is '?' (requested)
        # 2. Flag can have a requestee
        # 3. The requestee specified on the form is different from the 
        #    requestee specified in the db.
680

681
        my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
682 683 684

        my $requestee_changed = 
          ($status eq "?" && 
685
           $flag->type->is_requesteeble &&
686
           $old_requestee ne $requestee_email);
687

688 689
        next unless ($status_changed || $requestee_changed);

690 691
        # Since the status is validated, we know it's safe, but it's still
        # tainted, so we have to detaint it before using it in a query.
692 693
        trick_taint($status);

694
        if ($status eq '+' || $status eq '-') {
695 696 697 698
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = NULL,
                             status = ?, modification_date = ?
                       WHERE id = ?',
699
                       undef, ($setter->id, $status, $timestamp, $flag->id));
700 701 702 703

            # If the status of the flag was "?", we have to notify
            # the requester (if he wants to).
            my $requester;
704 705
            if ($flag->status eq '?') {
                $requester = $flag->setter;
706
            }
707 708 709 710 711 712 713 714 715 716 717
            # Now update the flag object with its new values.
            $flag->{'setter'} = $setter;
            $flag->{'requestee'} = undef;
            $flag->{'status'} = $status;

            # Send an email notifying the relevant parties about the fulfillment,
            # including the requester.
            if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
                $flag->{'addressee'} = $requester;
            }

718
            notify($flag, $bug, $attachment);
719 720
        }
        elsif ($status eq '?') {
721
            # Get the requestee, if any.
722
            my $requestee_id;
723
            if ($requestee_email) {
724
                $requestee_id = login_to_id($requestee_email);
725 726
                $flag->{'requestee'} = new Bugzilla::User($requestee_id);
            }
727 728 729 730 731
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
            }
732 733

            # Update the database with the changes.
734 735 736 737 738
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
                       undef, ($setter->id, $requestee_id, $status,
739
                               $timestamp, $flag->id));
740 741 742 743 744

            # Now update the flag object with its new values.
            $flag->{'setter'} = $setter;
            $flag->{'status'} = $status;

745
            # Send an email notifying the relevant parties about the request.
746 747
            if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
                $flag->{'addressee'} = $flag->requestee;
748
            }
749

750
            notify($flag, $bug, $attachment);
751 752
        }
        elsif ($status eq 'X') {
753
            clear($flag->id, $bug, $attachment);
754
        }
755

756 757
        push(@flags, $flag);
    }
758

759 760 761
    return \@flags;
}

762 763 764 765
=pod

=over

766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843
=item C<retarget($flag_id, $bug, $attachment)>

Change the type of the flag, if possible. The new flag type must have
the same name as the current flag type, must exist in the product and
attachment the bug is in, and the current settings of the flag must pass
validation. If no such flag type can be found, the type remains unchanged.

Retargetting flags is a good way to keep flags when moving bugs from one
product where a flag type is available to another product where the flag
type is unavailable, but another flag type having the same name exists.
Most of the time, if they have the same name, this means that they have
the same meaning, but with different settings.

=back

=cut

sub retarget {
    my ($flag_id, $bug, $attachment) = @_;
    my $dbh = Bugzilla->dbh;

    my $flag = new Bugzilla::Flag($flag_id);
    # We are looking for flagtypes having the same name as the flagtype
    # to which the current flag belongs, and being in the new product and
    # component of the bug.
    my $flagtypes = Bugzilla::FlagType::match(
                        {'name'         => $flag->name,
                         'target_type'  => $attachment ? 'attachment' : 'bug',
                         'is_active'    => 1,
                         'product_id'   => $bug->product_id,
                         'component_id' => $bug->component_id});

    # If we found no flagtype, the flag will be deleted.
    return 0 unless scalar(@$flagtypes);

    # If we found at least one, change the type of the flag,
    # assuming the setter/requester is allowed to set/request flags
    # belonging to this flagtype.
    my $is_retargetted = 0;
    foreach my $flagtype (@$flagtypes) {
        # Get the number of flags of this type already set for this target.
        my $has_flags = count(
            { 'type_id'     => $flag->type->id,
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });

        # Do not create a new flag of this type if this flag type is
        # not multiplicable and already has a flag set.
        next if (!$flag->type->is_multiplicable && $has_flags);

        # Check user privileges.
        my $error_mode_cache = Bugzilla->error_mode;
        Bugzilla->error_mode(ERROR_MODE_DIE);
        eval {
            my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
            my $is_private = $attachment ? $attachment->isprivate : 0;
            _validate($flag, $flag->type, $flag->status, $flag->setter,
                      $requestee, $is_private);
        };
        Bugzilla->error_mode($error_mode_cache);
        # If the validation failed, then we cannot use this flagtype.
        next if ($@);

        # Checks are successful, we can retarget the flag to this flagtype.
        $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
                  undef, ($flagtype->id, $flag->id));

        $is_retargetted = 1;
        last;
    }
    return $is_retargetted;
}

=pod

=over

844
=item C<clear($id, $bug, $attachment)>
845

846
Remove a flag from the DB.
847 848 849 850 851

=back

=cut

852
sub clear {
853
    my ($id, $bug, $attachment) = @_;
854 855
    my $dbh = Bugzilla->dbh;

856
    my $flag = new Bugzilla::Flag($id);
857
    $dbh->do('DELETE FROM flags WHERE id = ?', undef, $id);
858

859 860 861
    # If we cancel a pending request, we have to notify the requester
    # (if he wants to).
    my $requester;
862 863
    if ($flag->status eq '?') {
        $requester = $flag->setter;
864 865 866 867 868
    }

    # Now update the flag object to its new values. The last
    # requester/setter and requestee are kept untouched (for the
    # record). Else we could as well delete the flag completely.
869
    $flag->{'exists'} = 0;    
870
    $flag->{'status'} = "X";
871 872 873 874 875

    if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
        $flag->{'addressee'} = $requester;
    }

876
    notify($flag, $bug, $attachment);
877 878 879
}


880
######################################################################
881
# Utility Functions
882 883 884 885 886 887
######################################################################

=pod

=over

888
=item C<FormToNewFlags($bug, $attachment, $cgi)>
889

890 891
Checks whether or not there are new flags to create and returns an
array of flag objects. This array is then passed to Flag::create().
892 893 894 895

=back

=cut
896 897

sub FormToNewFlags {
898
    my ($bug, $attachment, $cgi) = @_;
899
    my $dbh = Bugzilla->dbh;
900
    my $setter = Bugzilla->user;
901
    
902
    # Extract a list of flag type IDs from field names.
903 904
    my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
905

906
    return () unless scalar(@type_ids);
907 908 909

    # Get a list of active flag types available for this target.
    my $flag_types = Bugzilla::FlagType::match(
910 911 912
        { 'target_type'  => $attachment ? 'attachment' : 'bug',
          'product_id'   => $bug->{'product_id'},
          'component_id' => $bug->{'component_id'},
913 914
          'is_active'    => 1 });

915
    my @flags;
916
    foreach my $flag_type (@$flag_types) {
917
        my $type_id = $flag_type->id;
918 919 920 921

        # We are only interested in flags the user tries to create.
        next unless scalar(grep { $_ == $type_id } @type_ids);

922
        # Get the number of flags of this type already set for this target.
923 924
        my $has_flags = count(
            { 'type_id'     => $type_id,
925 926 927
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
928 929

        # Do not create a new flag of this type if this flag type is
930
        # not multiplicable and already has a flag set.
931
        next if (!$flag_type->is_multiplicable && $has_flags);
932

933
        my $status = $cgi->param("flag_type-$type_id");
934
        trick_taint($status);
935

936 937 938
        my @logins = $cgi->param("requestee_type-$type_id");
        if ($status eq "?" && scalar(@logins) > 0) {
            foreach my $login (@logins) {
939 940 941
                push (@flags, { type      => $flag_type ,
                                setter    => $setter , 
                                status    => $status ,
942 943
                                requestee => 
                                    new Bugzilla::User({ name => $login }) });
944
                last unless $flag_type->is_multiplicable;
945
            }
946
        }
947 948 949 950 951
        else {
            push (@flags, { type   => $flag_type ,
                            setter => $setter , 
                            status => $status });
        }
952 953 954 955 956 957
    }

    # Return the list of flags.
    return \@flags;
}

958 959 960 961
=pod

=over

962
=item C<notify($flag, $bug, $attachment)>
963

964 965
Sends an email notification about a flag being created, fulfilled
or deleted.
966 967 968 969 970

=back

=cut

971
sub notify {
972
    my ($flag, $bug, $attachment) = @_;
973

974
    my $template = Bugzilla->template;
975 976

    # There is nobody to notify.
977
    return unless ($flag->{'addressee'} || $flag->type->cc_list);
978

979
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
980

981 982 983 984
    # If the target bug is restricted to one or more groups, then we need
    # to make sure we don't send email about it to unauthorized users
    # on the request type's CC: list, so we have to trawl the list for users
    # not in those groups or email addresses that don't have an account.
985 986 987
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};

    if (scalar(@bug_in_groups) || $attachment_is_private) {
988
        my @new_cc_list;
989
        foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) {
990
            my $ccuser = new Bugzilla::User({ name => $cc }) || next;
991

992
            next if (scalar(@bug_in_groups) && !$ccuser->can_see_bug($bug->bug_id));
993
            next if $attachment_is_private
994 995
              && Bugzilla->params->{"insidergroup"}
              && !$ccuser->in_group(Bugzilla->params->{"insidergroup"});
996
            push(@new_cc_list, $cc);
997
        }
998
        $flag->type->{'cc_list'} = join(", ", @new_cc_list);
999 1000
    }

1001
    # If there is nobody left to notify, return.
1002
    return unless ($flag->{'addressee'} || $flag->type->cc_list);
1003

1004 1005
    # Process and send notification for each recipient
    foreach my $to ($flag->{'addressee'} ? $flag->{'addressee'}->email : '',
1006
                    split(/[, ]+/, $flag->type->cc_list))
1007 1008
    {
        next unless $to;
1009 1010 1011 1012
        my $vars = { 'flag'       => $flag,
                     'to'         => $to,
                     'bug'        => $bug,
                     'attachment' => $attachment};
1013
        my $message;
1014
        my $rv = $template->process("request/email.txt.tmpl", $vars, \$message);
1015 1016 1017 1018
        if (!$rv) {
            Bugzilla->cgi->header();
            ThrowTemplateError($template->error());
        }
1019

1020
        MessageToMTA($message);
1021
    }
1022 1023
}

1024 1025
# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
1026
    my ($bug, $attachment, $timestamp) = @_;
1027 1028 1029 1030 1031 1032 1033 1034 1035
    my $dbh = Bugzilla->dbh;

    my $request_ids =
        $dbh->selectcol_arrayref("SELECT flags.id
                                  FROM flags
                                  LEFT JOIN attachments ON flags.attach_id = attachments.attach_id
                                  WHERE flags.attach_id = ?
                                  AND flags.status = '?'
                                  AND attachments.isobsolete = 0",
1036
                                  undef, $attachment->id);
1037 1038 1039 1040

    return if (!scalar(@$request_ids));

    # Take a snapshot of flags before any changes.
1041 1042
    my @old_summaries = snapshot($bug->bug_id, $attachment->id) if ($timestamp);
    foreach my $flag (@$request_ids) { clear($flag, $bug, $attachment) }
1043 1044 1045 1046 1047

    # If $timestamp is undefined, do not update the activity table
    return unless ($timestamp);

    # Take a snapshot of flags after any changes.
1048 1049 1050
    my @new_summaries = snapshot($bug->bug_id, $attachment->id);
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
1051 1052
}

1053
######################################################################
1054
# Private Functions
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
######################################################################

=begin private

=head1 PRIVATE FUNCTIONS

=over

=item C<sqlify_criteria($criteria)>

Converts a hash of criteria into a list of SQL criteria.

=back

=cut
1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101

sub sqlify_criteria {
    # a reference to a hash containing the criteria (field => value)
    my ($criteria) = @_;

    # the generated list of SQL criteria; "1=1" is a clever way of making sure
    # there's something in the list so calling code doesn't have to check list
    # size before building a WHERE clause out of it
    my @criteria = ("1=1");
    
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (defined($criteria->{'target_type'})) {
        if    ($criteria->{'target_type'} eq 'bug')        { push(@criteria, "attach_id IS NULL") }
        elsif ($criteria->{'target_type'} eq 'attachment') { push(@criteria, "attach_id IS NOT NULL") }
    }
    
    # Go through each criterion from the calling code and add it to the query.
    foreach my $field (keys %$criteria) {
        my $value = $criteria->{$field};
        next unless defined($value);
        if    ($field eq 'type_id')      { push(@criteria, "type_id      = $value") }
        elsif ($field eq 'bug_id')       { push(@criteria, "bug_id       = $value") }
        elsif ($field eq 'attach_id')    { push(@criteria, "attach_id    = $value") }
        elsif ($field eq 'requestee_id') { push(@criteria, "requestee_id = $value") }
        elsif ($field eq 'setter_id')    { push(@criteria, "setter_id    = $value") }
        elsif ($field eq 'status')       { push(@criteria, "status       = '$value'") }
    }
    
    return @criteria;
}

1102 1103 1104 1105 1106 1107 1108 1109
=head1 SEE ALSO

=over

=item B<Bugzilla::FlagType>

=back

1110

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
=head1 CONTRIBUTORS

=over

=item Myk Melez <myk@mozilla.org>

=item Jouni Heikniemi <jouni@heikniemi.net>

=item Kevin Benton <kevin.benton@amd.com>

1121 1122
=item Frédéric Buclin <LpSolit@gmail.com>

1123 1124 1125 1126
=back

=cut

1127
1;