Flag.pm 37.6 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
use base qw(Bugzilla::Object Exporter);
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
66 67 68 69

###############################
####    Initialization     ####
###############################
70

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

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

84 85
use constant SKIP_REQUESTEE_ON_ERROR => 1;

86 87 88
###############################
####      Accessors      ######
###############################
89

90
=head2 METHODS
91

92 93
=over

94 95 96 97 98 99 100
=item C<id>

Returns the ID of the flag.

=item C<name>

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

102 103 104 105 106 107 108 109
=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.

110 111 112
=item C<status>

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

114 115
=back

116
=cut
117

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

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

=pod

=over

=item C<type>

134
Returns the type of the flag, as a Bugzilla::FlagType object.
135

136
=item C<setter>
137

138 139 140 141 142 143 144
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.

145 146 147 148 149
=item C<attachment>

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

150 151 152 153 154 155 156
=back

=cut

sub type {
    my $self = shift;

157
    $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
158
    return $self->{'type'};
159 160
}

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

177 178 179 180 181 182 183 184 185
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'};
}

186 187 188 189
################################
## Searching/Retrieving Flags ##
################################

190 191 192 193
=pod

=over

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
=item C<has_flags>

Returns 1 if at least one flag exists in the DB, else 0. This subroutine
is mainly used to decide to display the "(My )Requests" link in the footer.

=back

=cut

sub has_flags {
    my $dbh = Bugzilla->dbh;

    my $has_flags = $dbh->selectrow_array('SELECT 1 FROM flags ' . $dbh->sql_limit(1));
    return $has_flags || 0;
}

=pod

=over

214
=item C<match($criteria)>
215

216 217 218 219 220 221 222 223 224
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 {
225
    my $class = shift;
226
    my ($criteria) = @_;
227

228 229 230 231 232 233 234 235 236 237
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (my $type = delete $criteria->{'target_type'}) {
        if ($type eq 'attachment') {
            $criteria->{'attach_id'} = NOT_NULL;
        }
        else {
            $criteria->{'attach_id'} = IS_NULL;
        }
    }
238 239 240 241 242
    # Flag->snapshot() calls Flag->match() with bug_id and attach_id
    # as hash keys, even if attach_id is undefined.
    if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
        $criteria->{'attach_id'} = IS_NULL;
    }
243

244
    return $class->SUPER::match(@_);
245 246
}

247 248 249 250 251 252
=pod

=over

=item C<count($criteria)>

253
Queries the database for flags matching the given criteria
254 255 256 257
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back
258

259 260 261
=cut

sub count {
262 263
    my $class = shift;
    return scalar @{$class->match(@_)};
264 265
}

266
######################################################################
267
# Creating and Modifying
268
######################################################################
269

270 271 272 273
=pod

=over

274
=item C<validate($bug_id, $attach_id, $skip_requestee_on_error)>
275

276 277
Validates fields containing flag modifications.

278 279 280
If the attachment is new, it has no ID yet and $attach_id is set
to -1 to force its check anyway.

281 282 283 284 285
=back

=cut

sub validate {
286 287
    my ($bug_id, $attach_id, $skip_requestee_on_error) = @_;
    my $cgi = Bugzilla->cgi;
288 289
    my $dbh = Bugzilla->dbh;

290 291
    # Get a list of flags to validate.  Uses the "map" function
    # to extract flag IDs from form field names by matching fields
292 293 294 295 296 297 298
    # 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));
299 300 301 302

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

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 =
321 322 323 324 325 326
            $dbh->selectrow_array(
                      "SELECT 1 FROM flags
                        WHERE " 
                       . $dbh->sql_in('id', \@flag_ids) 
                       . " AND ($field != ? OR attach_id IS $not NULL) "
                       . $dbh->sql_limit(1), undef, $field_id);
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

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

344 345
        # Make sure the flag type exists. If it doesn't, FormToNewFlags()
        # will ignore it, so it's safe to ignore it here.
346
        my $flag_type = new Bugzilla::FlagType($id);
347
        next unless $flag_type;
348

349 350 351 352 353
        # Make sure the flag type is active.
        unless ($flag_type->is_active) {
            ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
        }

354
        _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
355
                  $bug_id, $attach_id, $skip_requestee_on_error);
356 357
    }

358 359
    # Validate existing flags.
    foreach my $id (@flag_ids) {
360
        my $status = $cgi->param("flag-$id");
361
        my @requestees = $cgi->param("requestee-$id");
362
        my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
363

364 365
        # Make sure the flag exists. If it doesn't, process() will ignore it,
        # so it's safe to ignore it here.
366
        my $flag = new Bugzilla::Flag($id);
367
        next unless $flag;
368

369 370
        _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
                  undef, undef, $skip_requestee_on_error);
371 372
    }
}
373

374
sub _validate {
375
    my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
376
        $bug_id, $attach_id, $skip_requestee_on_error) = @_;
377

378 379
    # By default, the flag setter (or requester) is the current user.
    $setter ||= Bugzilla->user;
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399

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

401 402 403 404 405 406 407 408 409 410 411 412
    # 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 });
413
        }
414 415 416 417 418 419 420 421 422 423
    }

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

425 426 427 428 429 430
    # 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 : '';
431 432

        my @legal_requestees;
433
        foreach my $login (@$requestees) {
434 435 436 437 438
            if ($login eq $old_requestee) {
                # This requestee was already set. Leave him alone.
                push(@legal_requestees, $login);
                next;
            }
439 440 441 442 443 444 445 446 447

            # 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)) {
448
                next if $skip_requestee_on_error;
449 450 451 452 453
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
454
            }
455

456 457 458 459 460 461 462
            # 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'}))
            {
463
                next if $skip_requestee_on_error;
464 465 466 467 468 469
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
            }
470 471

            # Throw an error if the user won't be allowed to set the flag.
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
            if (!$requestee->can_set_flag($flag_type)) {
                next if $skip_requestee_on_error;
                ThrowUserError('flag_requestee_needs_privs',
                               {'requestee' => $requestee,
                                'flagtype'  => $flag_type});
            }

            # This requestee can be set.
            push(@legal_requestees, $login);
        }

        # Update the requestee list for this flag.
        if (scalar(@legal_requestees) < scalar(@$requestees)) {
            my $field_name = 'requestee_type-' . $flag_type->id;
            Bugzilla->cgi->delete($field_name);
            Bugzilla->cgi->param(-name => $field_name, -value => \@legal_requestees);
488
        }
489
    }
490 491 492 493 494 495 496 497

    # 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 '?')
498
               && $setter->can_request_flag($flag_type));
499 500

    # - User in the grant_group can set/clear flags, including "+" and "-".
501
    return if $setter->can_set_flag($flag_type);
502 503 504 505 506 507

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

510
sub snapshot {
511
    my ($class, $bug_id, $attach_id) = @_;
512

513 514
    my $flags = $class->match({ 'bug_id'    => $bug_id,
                                'attach_id' => $attach_id });
515 516
    my @summaries;
    foreach my $flag (@$flags) {
517
        my $summary = $flag->type->name . $flag->status;
518
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
519 520 521 522 523
        push(@summaries, $summary);
    }
    return @summaries;
}

524

525 526 527 528
=pod

=over

529
=item C<process($bug, $attachment, $timestamp, $hr_vars)>
530 531 532

Processes changes to flags.

533 534
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
535
to the flag can be stamped with the same date/time).
536 537 538 539 540 541

=back

=cut

sub process {
542
    my ($class, $bug, $attachment, $timestamp, $hr_vars) = @_;
543
    my $dbh = Bugzilla->dbh;
544
    my $cgi = Bugzilla->cgi;
545 546 547 548 549 550 551 552

    # 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;
553 554 555

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

558
    # Take a snapshot of flags before any changes.
559
    my @old_summaries = $class->snapshot($bug_id, $attach_id);
560

561
    # Cancel pending requests if we are obsoleting an attachment.
562
    if ($attachment && $cgi->param('isobsolete')) {
563
        $class->CancelRequests($bug, $attachment);
564 565
    }

566
    # Create new flags and update existing flags.
567
    my $new_flags = FormToNewFlags($bug, $attachment, $cgi, $hr_vars);
568 569
    foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
    modify($bug, $attachment, $cgi, $timestamp);
570

571 572
    # In case the bug's product/component has changed, clear flags that are
    # no longer valid.
573
    my $flag_ids = $dbh->selectcol_arrayref(
574
        "SELECT DISTINCT flags.id
575 576 577 578 579
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
      LEFT JOIN flaginclusions AS i
             ON flags.type_id = i.type_id 
580
            AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
581 582 583
            AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ?
            AND i.type_id IS NULL",
584 585
        undef, $bug_id);

586 587 588
    my $flags = Bugzilla::Flag->new_from_list($flag_ids);
    foreach my $flag (@$flags) {
        my $is_retargetted = retarget($flag, $bug);
589 590 591 592
        unless ($is_retargetted) {
            clear($flag, $bug, $flag->attachment);
            $hr_vars->{'message'} = 'flag_cleared';
        }
593
    }
594 595

    $flag_ids = $dbh->selectcol_arrayref(
596
        "SELECT DISTINCT flags.id
597
        FROM flags, bugs, flagexclusions e
598
        WHERE bugs.bug_id = ?
599
        AND flags.bug_id = bugs.bug_id
600
        AND flags.type_id = e.type_id
601
        AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
602 603 604
        AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
        undef, $bug_id);

605 606 607 608
    $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;
609
    }
610

611
    # Take a snapshot of flags after changes.
612
    my @new_summaries = $class->snapshot($bug_id, $attach_id);
613 614 615 616 617 618 619 620 621 622

    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);
623
    my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
624
    if ($removed ne $added) {
625
        my $field_id = get_field_id('flagtypes.name');
626
        $dbh->do('INSERT INTO bugs_activity
627
                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
628 629 630
                  VALUES (?, ?, ?, ?, ?, ?, ?)',
                  undef, ($bug_id, $attach_id, Bugzilla->user->id,
                  $timestamp, $field_id, $removed, $added));
631

632 633
        $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
                  undef, ($timestamp, $bug_id));
634 635 636
    }
}

637 638 639 640
=pod

=over

641
=item C<create($flag, $bug, $attachment, $timestamp)>
642 643

Creates a flag record in the database.
644

645 646 647 648 649
=back

=cut

sub create {
650
    my ($flag, $bug, $attachment, $timestamp) = @_;
651 652
    my $dbh = Bugzilla->dbh;

653
    my $attach_id = $attachment ? $attachment->id : undef;
654
    my $requestee_id;
655
    # Be careful! At this point, $flag is *NOT* yet an object!
656 657 658 659 660
    $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 (?, ?, ?, ?, ?, ?, ?, ?)',
661
              undef, ($flag->{'type'}->id, $bug->bug_id,
662 663
                      $attach_id, $requestee_id, $flag->{'setter'}->id,
                      $flag->{'status'}, $timestamp, $timestamp));
664

665 666 667 668 669
    # 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);

670
    # Send an email notifying the relevant parties about the flag creation.
671 672
    if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
        $flag->{'addressee'} = $flag->requestee;
673
    }
674

675
    notify($flag, $bug, $attachment);
676 677 678

    # Return the new flag object.
    return $flag;
679 680
}

681
=pod
682

683 684
=over

685
=item C<modify($bug, $attachment, $cgi, $timestamp)>
686

687 688 689 690 691 692 693
Modifies flags in the database when a user changes them.

=back

=cut

sub modify {
694
    my ($bug, $attachment, $cgi, $timestamp) = @_;
695
    my $setter = Bugzilla->user;
696
    my $dbh = Bugzilla->dbh;
697 698

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

701 702 703 704
    # 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.
705 706
    my @flags;
    foreach my $id (@ids) {
707
        my $flag = new Bugzilla::Flag($id);
708 709
        # If the flag no longer exists, ignore it.
        next unless $flag;
710

711
        my $status = $cgi->param("flag-$id");
712

713 714 715 716 717 718 719 720
        # 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
721
            && $flag->type->is_multiplicable)
722 723 724
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
725

726 727
            # Create new flags like the existing one for each additional person.
            foreach my $login (@requestees) {
728
                create({ type      => $flag->type,
729
                         setter    => $setter, 
730
                         status    => "?",
731
                         requestee => new Bugzilla::User({ name => $login }) },
732
                       $bug, $attachment, $timestamp);
733 734 735 736 737 738
            }
        }
        else {
            $requestee_email = trim($cgi->param("requestee-$id") || '');
        }

739 740 741 742
        # 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.

743
        my $status_changed = ($status ne $flag->status);
744

745 746 747 748 749
        # 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.
750

751
        my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
752 753 754

        my $requestee_changed = 
          ($status eq "?" && 
755
           $flag->type->is_requesteeble &&
756
           $old_requestee ne $requestee_email);
757

758 759
        next unless ($status_changed || $requestee_changed);

760 761
        # 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.
762 763
        trick_taint($status);

764
        if ($status eq '+' || $status eq '-') {
765 766 767 768
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = NULL,
                             status = ?, modification_date = ?
                       WHERE id = ?',
769
                       undef, ($setter->id, $status, $timestamp, $flag->id));
770 771 772 773

            # If the status of the flag was "?", we have to notify
            # the requester (if he wants to).
            my $requester;
774 775
            if ($flag->status eq '?') {
                $requester = $flag->setter;
776
                $flag->{'requester'} = $requester;
777
            }
778 779 780
            # Now update the flag object with its new values.
            $flag->{'setter'} = $setter;
            $flag->{'requestee'} = undef;
781
            $flag->{'requestee_id'} = undef;
782 783 784 785 786 787 788 789
            $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;
            }

790
            notify($flag, $bug, $attachment);
791 792
        }
        elsif ($status eq '?') {
793 794 795 796 797 798 799 800
            # If the one doing the change is the requestee, then this means he doesn't
            # want to reply to the request and he simply reassigns the request to
            # someone else. In this case, we keep the requester unaltered.
            my $new_setter = $setter;
            if ($flag->requestee && $flag->requestee->id == $setter->id) {
                $new_setter = $flag->setter;
            }

801
            # Get the requestee, if any.
802
            my $requestee_id;
803
            if ($requestee_email) {
804
                $requestee_id = login_to_id($requestee_email);
805
                $flag->{'requestee'} = new Bugzilla::User($requestee_id);
806
                $flag->{'requestee_id'} = $requestee_id;
807
            }
808 809 810 811
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
812
                $flag->{'requestee_id'} = undef;
813
            }
814 815

            # Update the database with the changes.
816 817 818 819
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
820
                       undef, ($new_setter->id, $requestee_id, $status,
821
                               $timestamp, $flag->id));
822 823

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

827
            # Send an email notifying the relevant parties about the request.
828 829
            if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
                $flag->{'addressee'} = $flag->requestee;
830
            }
831

832
            notify($flag, $bug, $attachment);
833 834
        }
        elsif ($status eq 'X') {
835
            clear($flag, $bug, $attachment);
836
        }
837

838 839
        push(@flags, $flag);
    }
840

841 842 843
    return \@flags;
}

844 845 846 847
=pod

=over

848
=item C<retarget($flag, $bug)>
849 850 851

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
852
component the bug is in, and the current settings of the flag must pass
853 854 855 856 857 858 859 860 861 862 863 864 865
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 {
866
    my ($flag, $bug) = @_;
867 868 869 870 871 872 873
    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,
874
                         'target_type'  => $flag->type->target_type,
875 876 877 878 879 880 881 882 883 884
                         '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.
885
    my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
886
    my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
887
    my $is_retargetted = 0;
888

889 890
    foreach my $flagtype (@$flagtypes) {
        # Get the number of flags of this type already set for this target.
891
        my $has_flags = __PACKAGE__->count(
892
            { 'type_id'     => $flagtype->id,
893
              'bug_id'      => $bug->bug_id,
894
              'attach_id'   => $flag->attach_id });
895 896 897

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

        # Check user privileges.
        my $error_mode_cache = Bugzilla->error_mode;
        Bugzilla->error_mode(ERROR_MODE_DIE);
        eval {
904
            _validate(undef, $flagtype, $flag->status, $flag->setter,
905
                      $requestee, $is_private, $bug->bug_id, $flag->attach_id);
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
        };
        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

925
=item C<clear($flag, $bug, $attachment)>
926

927
Remove a flag from the DB.
928 929 930 931 932

=back

=cut

933
sub clear {
934
    my ($flag, $bug, $attachment) = @_;
935 936
    my $dbh = Bugzilla->dbh;

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

939 940 941
    # If we cancel a pending request, we have to notify the requester
    # (if he wants to).
    my $requester;
942 943
    if ($flag->status eq '?') {
        $requester = $flag->setter;
944
        $flag->{'requester'} = $requester;
945 946 947 948 949
    }

    # 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.
950
    $flag->{'exists'} = 0;    
951
    $flag->{'status'} = "X";
952 953 954 955 956

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

957
    notify($flag, $bug, $attachment);
958 959 960
}


961
######################################################################
962
# Utility Functions
963 964 965 966 967 968
######################################################################

=pod

=over

969
=item C<FormToNewFlags($bug, $attachment, $cgi, $hr_vars)>
970

971 972
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().
973 974 975 976

=back

=cut
977 978

sub FormToNewFlags {
979
    my ($bug, $attachment, $cgi, $hr_vars) = @_;
980
    my $dbh = Bugzilla->dbh;
981
    my $setter = Bugzilla->user;
982
    
983
    # Extract a list of flag type IDs from field names.
984 985
    my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
986

987
    return () unless scalar(@type_ids);
988

989
    # Get a list of active flag types available for this product/component.
990
    my $flag_types = Bugzilla::FlagType::match(
991
        { 'product_id'   => $bug->{'product_id'},
992
          'component_id' => $bug->{'component_id'},
993 994
          'is_active'    => 1 });

995 996 997 998 999 1000 1001 1002
    foreach my $type_id (@type_ids) {
        # Checks if there are unexpected flags for the product/component.
        if (!scalar(grep { $_->id == $type_id } @$flag_types)) {
            $hr_vars->{'message'} = 'unexpected_flag_types';
            last;
        }
    }

1003
    my @flags;
1004
    foreach my $flag_type (@$flag_types) {
1005
        my $type_id = $flag_type->id;
1006

1007 1008 1009 1010
        # Bug flags are only valid for bugs, and attachment flags are
        # only valid for attachments. So don't mix both.
        next unless ($flag_type->target_type eq 'bug' xor $attachment);

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

1014
        # Get the number of flags of this type already set for this target.
1015
        my $has_flags = __PACKAGE__->count(
1016
            { 'type_id'     => $type_id,
1017 1018 1019
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
1020 1021

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

1025
        my $status = $cgi->param("flag_type-$type_id");
1026
        trick_taint($status);
1027

1028 1029 1030
        my @logins = $cgi->param("requestee_type-$type_id");
        if ($status eq "?" && scalar(@logins) > 0) {
            foreach my $login (@logins) {
1031 1032 1033
                push (@flags, { type      => $flag_type ,
                                setter    => $setter , 
                                status    => $status ,
1034 1035
                                requestee => 
                                    new Bugzilla::User({ name => $login }) });
1036
                last unless $flag_type->is_multiplicable;
1037
            }
1038
        }
1039 1040 1041 1042 1043
        else {
            push (@flags, { type   => $flag_type ,
                            setter => $setter , 
                            status => $status });
        }
1044 1045 1046 1047 1048 1049
    }

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

1050 1051 1052 1053
=pod

=over

1054
=item C<notify($flag, $bug, $attachment)>
1055

1056 1057
Sends an email notification about a flag being created, fulfilled
or deleted.
1058 1059 1060 1061 1062

=back

=cut

1063
sub notify {
1064
    my ($flag, $bug, $attachment) = @_;
1065

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

1069 1070 1071 1072
    # 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.
1073
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
1074
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
1075

1076 1077 1078 1079 1080 1081 1082 1083
    my %recipients;
    foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) {
        my $ccuser = new Bugzilla::User({ name => $cc });
        next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
        next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
        # Prevent duplicated entries due to case sensitivity.
        $cc = $ccuser ? $ccuser->email : $cc;
        $recipients{$cc} = $ccuser;
1084 1085
    }

1086 1087
    # Only notify if the addressee is allowed to receive the email.
    if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
1088
        $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'};
1089
    }
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
    # Process and send notification for each recipient.
    # If there are users in the CC list who don't have an account,
    # use the default language for email notifications.
    my $default_lang;
    if (grep { !$_ } values %recipients) {
        my $default_user = new Bugzilla::User();
        $default_lang = $default_user->settings->{'lang'}->{'value'};
    }

    foreach my $to (keys %recipients) {
1100 1101 1102 1103
        my $vars = { 'flag'       => $flag,
                     'to'         => $to,
                     'bug'        => $bug,
                     'attachment' => $attachment};
1104 1105 1106 1107 1108

        my $lang = $recipients{$to} ?
          $recipients{$to}->settings->{'lang'}->{'value'} : $default_lang;

        my $template = Bugzilla->template_inner($lang);
1109
        my $message;
1110 1111
        $template->process("request/email.txt.tmpl", $vars, \$message)
          || ThrowTemplateError($template->error());
1112

1113
        Bugzilla->template_inner("");
1114
        MessageToMTA($message);
1115
    }
1116 1117
}

1118 1119
# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
1120
    my ($class, $bug, $attachment, $timestamp) = @_;
1121 1122 1123 1124 1125 1126 1127 1128 1129
    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",
1130
                                  undef, $attachment->id);
1131 1132 1133 1134

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

    # Take a snapshot of flags before any changes.
1135
    my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
1136
        if ($timestamp);
1137 1138
    my $flags = Bugzilla::Flag->new_from_list($request_ids);
    foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
1139 1140 1141 1142 1143

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

    # Take a snapshot of flags after any changes.
1144
    my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
1145 1146
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
1147 1148
}

1149 1150 1151 1152 1153 1154 1155 1156
=head1 SEE ALSO

=over

=item B<Bugzilla::FlagType>

=back

1157

1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
=head1 CONTRIBUTORS

=over

=item Myk Melez <myk@mozilla.org>

=item Jouni Heikniemi <jouni@heikniemi.net>

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

1168 1169
=item Frédéric Buclin <LpSolit@gmail.com>

1170 1171 1172 1173
=back

=cut

1174
1;