Flag.pm 36.3 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
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.

142 143 144 145 146
=item C<attachment>

Returns the attachment object the flag belongs to if the flag
is an attachment flag, else undefined.

147 148 149 150 151 152 153
=back

=cut

sub type {
    my $self = shift;

154
    $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
155
    return $self->{'type'};
156 157
}

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
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'};
}

174 175 176 177 178 179 180 181 182
sub attachment {
    my $self = shift;
    return undef unless $self->attach_id;

    require Bugzilla::Attachment;
    $self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
    return $self->{'attachment'};
}

183 184 185 186
################################
## Searching/Retrieving Flags ##
################################

187 188 189 190 191
=pod

=over

=item C<match($criteria)>
192

193 194 195 196 197 198 199 200 201
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 {
202
    my ($criteria) = @_;
203
    my $dbh = Bugzilla->dbh;
204 205

    my @criteria = sqlify_criteria($criteria);
206 207
    $criteria = join(' AND ', @criteria);

208 209
    my $flag_ids = $dbh->selectcol_arrayref("SELECT id FROM flags
                                             WHERE $criteria");
210

211
    return Bugzilla::Flag->new_from_list($flag_ids);
212 213
}

214 215 216 217 218 219
=pod

=over

=item C<count($criteria)>

220
Queries the database for flags matching the given criteria
221 222 223 224
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back
225

226 227 228
=cut

sub count {
229
    my ($criteria) = @_;
230
    my $dbh = Bugzilla->dbh;
231 232

    my @criteria = sqlify_criteria($criteria);
233 234 235
    $criteria = join(' AND ', @criteria);

    my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM flags WHERE $criteria");
236 237 238 239

    return $count;
}

240
######################################################################
241
# Creating and Modifying
242
######################################################################
243

244 245 246 247
=pod

=over

248
=item C<validate($cgi, $bug_id, $attach_id)>
249

250 251
Validates fields containing flag modifications.

252 253 254
If the attachment is new, it has no ID yet and $attach_id is set
to -1 to force its check anyway.

255 256 257 258 259
=back

=cut

sub validate {
260 261 262 263
    my ($cgi, $bug_id, $attach_id) = @_;

    my $dbh = Bugzilla->dbh;

264 265
    # Get a list of flags to validate.  Uses the "map" function
    # to extract flag IDs from form field names by matching fields
266 267 268 269 270 271 272
    # 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));
273 274 275 276

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

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 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    # 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 });

321 322 323 324 325
        # Make sure the flag type is active.
        unless ($flag_type->is_active) {
            ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
        }

326
        _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
327
                  $bug_id, $attach_id);
328 329
    }

330 331
    # Validate existing flags.
    foreach my $id (@flag_ids) {
332
        my $status = $cgi->param("flag-$id");
333
        my @requestees = $cgi->param("requestee-$id");
334
        my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
335

336
        # Make sure the flag exists.
337
        my $flag = new Bugzilla::Flag($id);
338
        $flag || ThrowCodeError("flag_nonexistent", { id => $id });
339

340
        _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment);
341 342
    }
}
343

344
sub _validate {
345
    my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
346 347
        $bug_id, $attach_id) = @_;

348 349
    # By default, the flag setter (or requester) is the current user.
    $setter ||= Bugzilla->user;
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369

    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 });
    }
370

371 372 373 374 375 376 377 378 379 380 381 382
    # 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 });
383
        }
384 385 386 387 388 389 390 391 392 393
    }

    # 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 });
    }
394

395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
    # 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 });
417
            }
418

419 420 421 422 423 424 425 426 427 428 429 430 431
            # 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 });
            }
432 433

            # Throw an error if the user won't be allowed to set the flag.
434 435 436 437
            $requestee->can_set_flag($flag_type)
              || ThrowUserError('flag_requestee_needs_privs',
                                {'requestee' => $requestee,
                                 'flagtype'  => $flag_type});
438
        }
439
    }
440 441 442 443 444 445 446 447

    # 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 '?')
448
               && $setter->can_request_flag($flag_type));
449 450

    # - User in the grant_group can set/clear flags, including "+" and "-".
451
    return if $setter->can_set_flag($flag_type);
452 453 454 455 456 457

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

460 461 462 463
sub snapshot {
    my ($bug_id, $attach_id) = @_;

    my $flags = match({ 'bug_id'    => $bug_id,
464
                        'attach_id' => $attach_id });
465 466
    my @summaries;
    foreach my $flag (@$flags) {
467
        my $summary = $flag->type->name . $flag->status;
468
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
469 470 471 472 473
        push(@summaries, $summary);
    }
    return @summaries;
}

474

475 476 477 478
=pod

=over

479
=item C<process($bug, $attachment, $timestamp, $cgi)>
480 481 482

Processes changes to flags.

483 484 485 486
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.
487 488 489 490 491 492

=back

=cut

sub process {
493
    my ($bug, $attachment, $timestamp, $cgi) = @_;
494
    my $dbh = Bugzilla->dbh;
495 496 497 498 499 500 501 502

    # 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;
503 504 505

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

508
    # Take a snapshot of flags before any changes.
509
    my @old_summaries = snapshot($bug_id, $attach_id);
510

511
    # Cancel pending requests if we are obsoleting an attachment.
512 513
    if ($attachment && $cgi->param('isobsolete')) {
        CancelRequests($bug, $attachment);
514 515
    }

516
    # Create new flags and update existing flags.
517 518 519
    my $new_flags = FormToNewFlags($bug, $attachment, $cgi);
    foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
    modify($bug, $attachment, $cgi, $timestamp);
520

521 522
    # In case the bug's product/component has changed, clear flags that are
    # no longer valid.
523
    my $flag_ids = $dbh->selectcol_arrayref(
524
        "SELECT DISTINCT flags.id
525 526 527 528 529
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
      LEFT JOIN flaginclusions AS i
             ON flags.type_id = i.type_id 
530
            AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
531 532 533
            AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ?
            AND i.type_id IS NULL",
534 535
        undef, $bug_id);

536 537 538 539
    my $flags = Bugzilla::Flag->new_from_list($flag_ids);
    foreach my $flag (@$flags) {
        my $is_retargetted = retarget($flag, $bug);
        clear($flag, $bug, $flag->attachment) unless $is_retargetted;
540
    }
541 542

    $flag_ids = $dbh->selectcol_arrayref(
543
        "SELECT DISTINCT flags.id
544
        FROM flags, bugs, flagexclusions e
545
        WHERE bugs.bug_id = ?
546
        AND flags.bug_id = bugs.bug_id
547
        AND flags.type_id = e.type_id
548
        AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
549 550 551
        AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
        undef, $bug_id);

552 553 554 555
    $flags = Bugzilla::Flag->new_from_list($flag_ids);
    foreach my $flag (@$flags) {
        my $is_retargetted = retarget($flag, $bug);
        clear($flag, $bug, $flag->attachment) unless $is_retargetted;
556
    }
557

558 559 560 561 562 563 564 565 566 567 568 569
    # 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);
570
    my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
571
    if ($removed ne $added) {
572
        my $field_id = get_field_id('flagtypes.name');
573
        $dbh->do('INSERT INTO bugs_activity
574
                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
575 576 577
                  VALUES (?, ?, ?, ?, ?, ?, ?)',
                  undef, ($bug_id, $attach_id, Bugzilla->user->id,
                  $timestamp, $field_id, $removed, $added));
578

579 580
        $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
                  undef, ($timestamp, $bug_id));
581 582 583
    }
}

584 585 586 587
=pod

=over

588
=item C<create($flag, $bug, $attachment, $timestamp)>
589 590

Creates a flag record in the database.
591

592 593 594 595 596
=back

=cut

sub create {
597
    my ($flag, $bug, $attachment, $timestamp) = @_;
598 599
    my $dbh = Bugzilla->dbh;

600
    my $attach_id = $attachment ? $attachment->id : undef;
601
    my $requestee_id;
602
    # Be careful! At this point, $flag is *NOT* yet an object!
603 604 605 606 607
    $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 (?, ?, ?, ?, ?, ?, ?, ?)',
608
              undef, ($flag->{'type'}->id, $bug->bug_id,
609 610
                      $attach_id, $requestee_id, $flag->{'setter'}->id,
                      $flag->{'status'}, $timestamp, $timestamp));
611

612 613 614 615 616
    # 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);

617
    # Send an email notifying the relevant parties about the flag creation.
618 619
    if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
        $flag->{'addressee'} = $flag->requestee;
620
    }
621

622
    notify($flag, $bug, $attachment);
623 624 625

    # Return the new flag object.
    return $flag;
626 627
}

628
=pod
629

630 631
=over

632
=item C<modify($bug, $attachment, $cgi, $timestamp)>
633

634 635 636 637 638 639 640
Modifies flags in the database when a user changes them.

=back

=cut

sub modify {
641
    my ($bug, $attachment, $cgi, $timestamp) = @_;
642
    my $setter = Bugzilla->user;
643
    my $dbh = Bugzilla->dbh;
644 645

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

648 649 650 651
    # 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.
652 653
    my @flags;
    foreach my $id (@ids) {
654
        my $flag = new Bugzilla::Flag($id);
655 656
        # If the flag no longer exists, ignore it.
        next unless $flag;
657

658
        my $status = $cgi->param("flag-$id");
659

660 661 662 663 664 665 666 667
        # 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
668
            && $flag->type->is_multiplicable)
669 670 671
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
672

673 674
            # Create new flags like the existing one for each additional person.
            foreach my $login (@requestees) {
675
                create({ type      => $flag->type,
676
                         setter    => $setter, 
677
                         status    => "?",
678
                         requestee => new Bugzilla::User({ name => $login }) },
679
                       $bug, $attachment, $timestamp);
680 681 682 683 684 685
            }
        }
        else {
            $requestee_email = trim($cgi->param("requestee-$id") || '');
        }

686 687 688 689
        # 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.

690
        my $status_changed = ($status ne $flag->status);
691

692 693 694 695 696
        # 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.
697

698
        my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
699 700 701

        my $requestee_changed = 
          ($status eq "?" && 
702
           $flag->type->is_requesteeble &&
703
           $old_requestee ne $requestee_email);
704

705 706
        next unless ($status_changed || $requestee_changed);

707 708
        # 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.
709 710
        trick_taint($status);

711
        if ($status eq '+' || $status eq '-') {
712 713 714 715
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = NULL,
                             status = ?, modification_date = ?
                       WHERE id = ?',
716
                       undef, ($setter->id, $status, $timestamp, $flag->id));
717 718 719 720

            # If the status of the flag was "?", we have to notify
            # the requester (if he wants to).
            my $requester;
721 722
            if ($flag->status eq '?') {
                $requester = $flag->setter;
723
            }
724 725 726 727 728 729 730 731 732 733 734
            # 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;
            }

735
            notify($flag, $bug, $attachment);
736 737
        }
        elsif ($status eq '?') {
738
            # Get the requestee, if any.
739
            my $requestee_id;
740
            if ($requestee_email) {
741
                $requestee_id = login_to_id($requestee_email);
742 743
                $flag->{'requestee'} = new Bugzilla::User($requestee_id);
            }
744 745 746 747 748
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
            }
749 750

            # Update the database with the changes.
751 752 753 754 755
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
                       undef, ($setter->id, $requestee_id, $status,
756
                               $timestamp, $flag->id));
757 758 759 760 761

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

762
            # Send an email notifying the relevant parties about the request.
763 764
            if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
                $flag->{'addressee'} = $flag->requestee;
765
            }
766

767
            notify($flag, $bug, $attachment);
768 769
        }
        elsif ($status eq 'X') {
770
            clear($flag, $bug, $attachment);
771
        }
772

773 774
        push(@flags, $flag);
    }
775

776 777 778
    return \@flags;
}

779 780 781 782
=pod

=over

783
=item C<retarget($flag, $bug)>
784 785 786

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
787
component the bug is in, and the current settings of the flag must pass
788 789 790 791 792 793 794 795 796 797 798 799 800
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 {
801
    my ($flag, $bug) = @_;
802 803 804 805 806 807 808
    my $dbh = Bugzilla->dbh;

    # 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,
809
                         'target_type'  => $flag->type->target_type,
810 811 812 813 814 815 816 817 818 819
                         '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.
820
    my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
821
    my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
822
    my $is_retargetted = 0;
823

824 825 826
    foreach my $flagtype (@$flagtypes) {
        # Get the number of flags of this type already set for this target.
        my $has_flags = count(
827
            { 'type_id'     => $flagtype->id,
828
              'bug_id'      => $bug->bug_id,
829
              'attach_id'   => $flag->attach_id });
830 831 832

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

        # Check user privileges.
        my $error_mode_cache = Bugzilla->error_mode;
        Bugzilla->error_mode(ERROR_MODE_DIE);
        eval {
839
            _validate(undef, $flagtype, $flag->status, $flag->setter,
840
                      $requestee, $is_private, $bug->bug_id, $flag->attach_id);
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859
        };
        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

860
=item C<clear($flag, $bug, $attachment)>
861

862
Remove a flag from the DB.
863 864 865 866 867

=back

=cut

868
sub clear {
869
    my ($flag, $bug, $attachment) = @_;
870 871
    my $dbh = Bugzilla->dbh;

872
    $dbh->do('DELETE FROM flags WHERE id = ?', undef, $flag->id);
873

874 875 876
    # If we cancel a pending request, we have to notify the requester
    # (if he wants to).
    my $requester;
877 878
    if ($flag->status eq '?') {
        $requester = $flag->setter;
879 880 881 882 883
    }

    # 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.
884
    $flag->{'exists'} = 0;    
885
    $flag->{'status'} = "X";
886 887 888 889 890

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

891
    notify($flag, $bug, $attachment);
892 893 894
}


895
######################################################################
896
# Utility Functions
897 898 899 900 901 902
######################################################################

=pod

=over

903
=item C<FormToNewFlags($bug, $attachment, $cgi)>
904

905 906
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().
907 908 909 910

=back

=cut
911 912

sub FormToNewFlags {
913
    my ($bug, $attachment, $cgi) = @_;
914
    my $dbh = Bugzilla->dbh;
915
    my $setter = Bugzilla->user;
916
    
917
    # Extract a list of flag type IDs from field names.
918 919
    my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
920

921
    return () unless scalar(@type_ids);
922 923 924

    # Get a list of active flag types available for this target.
    my $flag_types = Bugzilla::FlagType::match(
925 926 927
        { 'target_type'  => $attachment ? 'attachment' : 'bug',
          'product_id'   => $bug->{'product_id'},
          'component_id' => $bug->{'component_id'},
928 929
          'is_active'    => 1 });

930
    my @flags;
931
    foreach my $flag_type (@$flag_types) {
932
        my $type_id = $flag_type->id;
933 934 935 936

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

937
        # Get the number of flags of this type already set for this target.
938 939
        my $has_flags = count(
            { 'type_id'     => $type_id,
940 941 942
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
943 944

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

948
        my $status = $cgi->param("flag_type-$type_id");
949
        trick_taint($status);
950

951 952 953
        my @logins = $cgi->param("requestee_type-$type_id");
        if ($status eq "?" && scalar(@logins) > 0) {
            foreach my $login (@logins) {
954 955 956
                push (@flags, { type      => $flag_type ,
                                setter    => $setter , 
                                status    => $status ,
957 958
                                requestee => 
                                    new Bugzilla::User({ name => $login }) });
959
                last unless $flag_type->is_multiplicable;
960
            }
961
        }
962 963 964 965 966
        else {
            push (@flags, { type   => $flag_type ,
                            setter => $setter , 
                            status => $status });
        }
967 968 969 970 971 972
    }

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

973 974 975 976
=pod

=over

977
=item C<notify($flag, $bug, $attachment)>
978

979 980
Sends an email notification about a flag being created, fulfilled
or deleted.
981 982 983 984 985

=back

=cut

986
sub notify {
987
    my ($flag, $bug, $attachment) = @_;
988

989
    my $template = Bugzilla->template;
990 991

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

994
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
995

996 997 998 999
    # 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.
1000 1001 1002
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};

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

1007
            next if (scalar(@bug_in_groups) && !$ccuser->can_see_bug($bug->bug_id));
1008
            next if $attachment_is_private
1009 1010
              && Bugzilla->params->{"insidergroup"}
              && !$ccuser->in_group(Bugzilla->params->{"insidergroup"});
1011
            push(@new_cc_list, $cc);
1012
        }
1013
        $flag->type->{'cc_list'} = join(", ", @new_cc_list);
1014 1015
    }

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

1019 1020
    # Process and send notification for each recipient
    foreach my $to ($flag->{'addressee'} ? $flag->{'addressee'}->email : '',
1021
                    split(/[, ]+/, $flag->type->cc_list))
1022 1023
    {
        next unless $to;
1024 1025 1026 1027
        my $vars = { 'flag'       => $flag,
                     'to'         => $to,
                     'bug'        => $bug,
                     'attachment' => $attachment};
1028
        my $message;
1029
        my $rv = $template->process("request/email.txt.tmpl", $vars, \$message);
1030 1031 1032 1033
        if (!$rv) {
            Bugzilla->cgi->header();
            ThrowTemplateError($template->error());
        }
1034

1035
        MessageToMTA($message);
1036
    }
1037 1038
}

1039 1040
# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
1041
    my ($bug, $attachment, $timestamp) = @_;
1042 1043 1044 1045 1046 1047 1048 1049 1050
    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",
1051
                                  undef, $attachment->id);
1052 1053 1054 1055

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

    # Take a snapshot of flags before any changes.
1056
    my @old_summaries = snapshot($bug->bug_id, $attachment->id) if ($timestamp);
1057 1058
    my $flags = Bugzilla::Flag->new_from_list($request_ids);
    foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
1059 1060 1061 1062 1063

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

    # Take a snapshot of flags after any changes.
1064 1065 1066
    my @new_summaries = snapshot($bug->bug_id, $attachment->id);
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
1067 1068
}

1069
######################################################################
1070
# Private Functions
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085
######################################################################

=begin private

=head1 PRIVATE FUNCTIONS

=over

=item C<sqlify_criteria($criteria)>

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

=back

=cut
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117

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;
}

1118 1119 1120 1121 1122 1123 1124 1125
=head1 SEE ALSO

=over

=item B<Bugzilla::FlagType>

=back

1126

1127 1128 1129 1130 1131 1132 1133 1134 1135 1136
=head1 CONTRIBUTORS

=over

=item Myk Melez <myk@mozilla.org>

=item Jouni Heikniemi <jouni@heikniemi.net>

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

1137 1138
=item Frédéric Buclin <LpSolit@gmail.com>

1139 1140 1141 1142
=back

=cut

1143
1;