Flag.pm 39.7 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
use Bugzilla::FlagType;
57
use Bugzilla::Hook;
58
use Bugzilla::User;
59
use Bugzilla::Util;
60
use Bugzilla::Error;
61
use Bugzilla::Mailer;
62
use Bugzilla::Constants;
63
use Bugzilla::Field;
64

65 66
use base qw(Bugzilla::Object Exporter);
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
67 68 69 70

###############################
####    Initialization     ####
###############################
71

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

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

85 86
use constant SKIP_REQUESTEE_ON_ERROR => 1;

87 88 89
###############################
####      Accessors      ######
###############################
90

91
=head2 METHODS
92

93 94
=over

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

Returns the ID of the flag.

=item C<name>

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

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

111 112 113
=item C<status>

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

115 116
=back

117
=cut
118

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

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

=pod

=over

=item C<type>

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

137
=item C<setter>
138

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

146 147 148 149 150
=item C<attachment>

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

151 152 153 154 155 156 157
=back

=cut

sub type {
    my $self = shift;

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

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

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

    require Bugzilla::Attachment;
183
    $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
184 185 186
    return $self->{'attachment'};
}

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

191 192 193 194
=pod

=over

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
=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

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

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

229 230 231
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (my $type = delete $criteria->{'target_type'}) {
232
        if ($type eq 'bug') {
233 234
            $criteria->{'attach_id'} = IS_NULL;
        }
235 236 237
        elsif (!defined $criteria->{'attach_id'}) {
            $criteria->{'attach_id'} = NOT_NULL;
        }
238
    }
239 240 241 242 243
    # 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;
    }
244

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

248 249 250 251 252 253
=pod

=over

=item C<count($criteria)>

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

=back
259

260 261 262
=cut

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

267
######################################################################
268
# Creating and Modifying
269
######################################################################
270

271 272 273 274
=pod

=over

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

277 278
Validates fields containing flag modifications.

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

282 283 284 285 286
=back

=cut

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

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

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

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    # 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 =
322 323 324 325 326 327
            $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);
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            # Throw an error if the user won't be allowed to set the flag.
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
            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);
489
        }
490
    }
491 492 493 494 495 496 497 498

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

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

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

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

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

525

526 527 528 529
=pod

=over

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

Processes changes to flags.

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

=back

=cut

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

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

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

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

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

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

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

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

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

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

612
    # Take a snapshot of flags after changes.
613
    my @new_summaries = $class->snapshot($bug_id, $attach_id);
614 615

    update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
616 617 618 619 620 621

    Bugzilla::Hook::process('flag-end_of_update', { bug       => $bug,
                                                    timestamp => $timestamp,
                                                    old_flags => \@old_summaries,
                                                    new_flags => \@new_summaries,
                                                  });
622 623 624 625 626 627 628 629
}

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);
630
    my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
631
    if ($removed ne $added) {
632
        my $field_id = get_field_id('flagtypes.name');
633
        $dbh->do('INSERT INTO bugs_activity
634
                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
635 636 637
                  VALUES (?, ?, ?, ?, ?, ?, ?)',
                  undef, ($bug_id, $attach_id, Bugzilla->user->id,
                  $timestamp, $field_id, $removed, $added));
638

639 640
        $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
                  undef, ($timestamp, $bug_id));
641 642 643
    }
}

644 645 646 647
=pod

=over

648
=item C<create($flag, $bug, $attachment, $timestamp)>
649 650

Creates a flag record in the database.
651

652 653 654 655 656
=back

=cut

sub create {
657
    my ($flag, $bug, $attachment, $timestamp) = @_;
658 659
    my $dbh = Bugzilla->dbh;

660
    my $attach_id = $attachment ? $attachment->id : undef;
661
    my $requestee_id;
662
    # Be careful! At this point, $flag is *NOT* yet an object!
663 664 665 666 667
    $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 (?, ?, ?, ?, ?, ?, ?, ?)',
668
              undef, ($flag->{'type'}->id, $bug->bug_id,
669 670
                      $attach_id, $requestee_id, $flag->{'setter'}->id,
                      $flag->{'status'}, $timestamp, $timestamp));
671

672 673 674 675 676
    # 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);

677
    # Send an email notifying the relevant parties about the flag creation.
678 679
    if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
        $flag->{'addressee'} = $flag->requestee;
680
    }
681

682
    notify($flag, $bug, $attachment);
683 684 685

    # Return the new flag object.
    return $flag;
686 687
}

688
=pod
689

690 691
=over

692
=item C<modify($bug, $attachment, $cgi, $timestamp)>
693

694 695 696 697 698 699 700
Modifies flags in the database when a user changes them.

=back

=cut

sub modify {
701
    my ($bug, $attachment, $cgi, $timestamp) = @_;
702
    my $setter = Bugzilla->user;
703
    my $dbh = Bugzilla->dbh;
704 705

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

708 709 710 711
    # 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.
712 713
    my @flags;
    foreach my $id (@ids) {
714
        my $flag = new Bugzilla::Flag($id);
715 716
        # If the flag no longer exists, ignore it.
        next unless $flag;
717

718
        my $status = $cgi->param("flag-$id");
719

720 721 722 723 724 725 726 727
        # 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
728
            && $flag->type->is_multiplicable)
729 730 731
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
732

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

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

750
        my $status_changed = ($status ne $flag->status);
751

752 753 754 755 756
        # 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.
757

758
        my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
759 760 761

        my $requestee_changed = 
          ($status eq "?" && 
762
           $flag->type->is_requesteeble &&
763
           $old_requestee ne $requestee_email);
764

765 766
        next unless ($status_changed || $requestee_changed);

767 768
        # 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.
769 770
        trick_taint($status);

771
        if ($status eq '+' || $status eq '-') {
772 773 774 775
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = NULL,
                             status = ?, modification_date = ?
                       WHERE id = ?',
776
                       undef, ($setter->id, $status, $timestamp, $flag->id));
777 778 779 780

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

797
            notify($flag, $bug, $attachment);
798 799
        }
        elsif ($status eq '?') {
800 801 802 803 804 805 806 807
            # 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;
            }

808
            # Get the requestee, if any.
809
            my $requestee_id;
810
            if ($requestee_email) {
811
                $requestee_id = login_to_id($requestee_email);
812
                $flag->{'requestee'} = new Bugzilla::User($requestee_id);
813
                $flag->{'requestee_id'} = $requestee_id;
814
            }
815 816 817 818
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
819
                $flag->{'requestee_id'} = undef;
820
            }
821 822

            # Update the database with the changes.
823 824 825 826
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
827
                       undef, ($new_setter->id, $requestee_id, $status,
828
                               $timestamp, $flag->id));
829 830

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

834
            # Send an email notifying the relevant parties about the request.
835 836
            if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
                $flag->{'addressee'} = $flag->requestee;
837
            }
838

839
            notify($flag, $bug, $attachment);
840 841
        }
        elsif ($status eq 'X') {
842
            clear($flag, $bug, $attachment);
843
        }
844

845 846
        push(@flags, $flag);
    }
847

848 849 850
    return \@flags;
}

851 852 853 854
=pod

=over

855
=item C<retarget($flag, $bug)>
856 857 858

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

896 897
    foreach my $flagtype (@$flagtypes) {
        # Get the number of flags of this type already set for this target.
898
        my $has_flags = __PACKAGE__->count(
899
            { 'type_id'     => $flagtype->id,
900
              'bug_id'      => $bug->bug_id,
901
              'attach_id'   => $flag->attach_id });
902 903 904

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

        # Check user privileges.
        my $error_mode_cache = Bugzilla->error_mode;
        Bugzilla->error_mode(ERROR_MODE_DIE);
        eval {
911
            _validate(undef, $flagtype, $flag->status, $flag->setter,
912
                      $requestee, $is_private, $bug->bug_id, $flag->attach_id);
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
        };
        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

932
=item C<clear($flag, $bug, $attachment)>
933

934
Remove a flag from the DB.
935 936 937 938 939

=back

=cut

940
sub clear {
941
    my ($flag, $bug, $attachment) = @_;
942 943
    my $dbh = Bugzilla->dbh;

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

946 947 948
    # If we cancel a pending request, we have to notify the requester
    # (if he wants to).
    my $requester;
949 950
    if ($flag->status eq '?') {
        $requester = $flag->setter;
951
        $flag->{'requester'} = $requester;
952 953 954 955 956
    }

    # 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.
957
    $flag->{'exists'} = 0;    
958
    $flag->{'status'} = "X";
959 960 961 962 963

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

964
    notify($flag, $bug, $attachment);
965 966 967
}


968
######################################################################
969
# Utility Functions
970 971 972 973 974 975
######################################################################

=pod

=over

976
=item C<FormToNewFlags($bug, $attachment, $cgi, $hr_vars)>
977

978 979
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().
980 981 982 983

=back

=cut
984 985

sub FormToNewFlags {
986
    my ($bug, $attachment, $cgi, $hr_vars) = @_;
987
    my $dbh = Bugzilla->dbh;
988
    my $setter = Bugzilla->user;
989
    
990
    # Extract a list of flag type IDs from field names.
991 992
    my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
993

994
    return () unless scalar(@type_ids);
995

996
    # Get a list of active flag types available for this product/component.
997
    my $flag_types = Bugzilla::FlagType::match(
998
        { 'product_id'   => $bug->{'product_id'},
999
          'component_id' => $bug->{'component_id'},
1000 1001
          'is_active'    => 1 });

1002 1003 1004 1005 1006 1007 1008 1009
    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;
        }
    }

1010
    my @flags;
1011
    foreach my $flag_type (@$flag_types) {
1012
        my $type_id = $flag_type->id;
1013

1014 1015 1016 1017
        # 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);

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

1021
        # Get the number of flags of this type already set for this target.
1022
        my $has_flags = __PACKAGE__->count(
1023
            { 'type_id'     => $type_id,
1024 1025 1026
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
1027 1028

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

1032
        my $status = $cgi->param("flag_type-$type_id");
1033
        trick_taint($status);
1034

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

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

1057 1058 1059 1060
=pod

=over

1061
=item C<notify($flag, $bug, $attachment)>
1062

1063 1064
Sends an email notification about a flag being created, fulfilled
or deleted.
1065 1066 1067 1068 1069

=back

=cut

1070
sub notify {
1071
    my ($flag, $bug, $attachment) = @_;
1072

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

1076 1077 1078 1079
    # 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.
1080
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
1081
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
1082

1083 1084 1085 1086 1087 1088 1089 1090
    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;
1091 1092
    }

1093 1094
    # Only notify if the addressee is allowed to receive the email.
    if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
1095
        $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'};
1096
    }
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    # 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) {
1107 1108 1109 1110 1111 1112 1113 1114 1115
        # Add threadingmarker to allow flag notification emails to be the
        # threaded similar to normal bug change emails.
        my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
    
        my $vars = { 'flag'            => $flag,
                     'to'              => $to,
                     'bug'             => $bug,
                     'attachment'      => $attachment,
                     'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
1116 1117 1118 1119 1120

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

        my $template = Bugzilla->template_inner($lang);
1121
        my $message;
1122 1123
        $template->process("request/email.txt.tmpl", $vars, \$message)
          || ThrowTemplateError($template->error());
1124

1125
        Bugzilla->template_inner("");
1126
        MessageToMTA($message);
1127
    }
1128 1129
}

1130 1131
# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
1132
    my ($class, $bug, $attachment, $timestamp) = @_;
1133 1134 1135 1136 1137 1138 1139 1140 1141
    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",
1142
                                  undef, $attachment->id);
1143 1144 1145 1146

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

    # Take a snapshot of flags before any changes.
1147
    my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
1148
        if ($timestamp);
1149 1150
    my $flags = Bugzilla::Flag->new_from_list($request_ids);
    foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
1151 1152 1153 1154 1155

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

    # Take a snapshot of flags after any changes.
1156
    my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
1157 1158
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
1159 1160
}

1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198
# This is an internal function used by $bug->flag_types
# and $attachment->flag_types to collect data about available
# flag types and existing flags set on them. You should never
# call this function directly.
sub _flag_types {
    my $vars = shift;

    my $target_type = $vars->{target_type};
    my $flags;

    # Retrieve all existing flags for this bug/attachment.
    if ($target_type eq 'bug') {
        my $bug_id = delete $vars->{bug_id};
        $flags = Bugzilla::Flag->match({target_type => 'bug', bug_id => $bug_id});
    }
    elsif ($target_type eq 'attachment') {
        my $attach_id = delete $vars->{attach_id};
        $flags = Bugzilla::Flag->match({attach_id => $attach_id});
    }
    else {
        ThrowCodeError('bad_arg', {argument => 'target_type',
                                   function => 'Bugzilla::Flag::_flag_types'});
    }

    # Get all available flag types for the given product and component.
    my $flag_types = Bugzilla::FlagType::match($vars);

    $_->{flags} = [] foreach @$flag_types;
    my %flagtypes = map { $_->id => $_ } @$flag_types;

    # Group existing flags per type.
    # Call the internal 'type_id' variable instead of the method
    # to not create a flagtype object.
    push(@{$flagtypes{$_->{type_id}}->{flags}}, $_) foreach @$flags;

    return [sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
}

1199 1200 1201 1202 1203 1204 1205 1206
=head1 SEE ALSO

=over

=item B<Bugzilla::FlagType>

=back

1207

1208 1209 1210 1211 1212 1213 1214 1215 1216 1217
=head1 CONTRIBUTORS

=over

=item Myk Melez <myk@mozilla.org>

=item Jouni Heikniemi <jouni@heikniemi.net>

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

1218 1219
=item Frédéric Buclin <LpSolit@gmail.com>

1220 1221 1222 1223
=back

=cut

1224
1;