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 194
=pod

=over

=item C<match($criteria)>
195

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

    my @criteria = sqlify_criteria($criteria);
209 210
    $criteria = join(' AND ', @criteria);

211 212
    my $flag_ids = $dbh->selectcol_arrayref("SELECT id FROM flags
                                             WHERE $criteria");
213

214
    return Bugzilla::Flag->new_from_list($flag_ids);
215 216
}

217 218 219 220 221 222
=pod

=over

=item C<count($criteria)>

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

=back
228

229 230 231
=cut

sub count {
232
    my ($criteria) = @_;
233
    my $dbh = Bugzilla->dbh;
234 235

    my @criteria = sqlify_criteria($criteria);
236 237 238
    $criteria = join(' AND ', @criteria);

    my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM flags WHERE $criteria");
239 240 241 242

    return $count;
}

243
######################################################################
244
# Creating and Modifying
245
######################################################################
246

247 248 249 250
=pod

=over

251
=item C<validate($cgi, $bug_id, $attach_id, $skip_requestee_on_error)>
252

253 254
Validates fields containing flag modifications.

255 256 257
If the attachment is new, it has no ID yet and $attach_id is set
to -1 to force its check anyway.

258 259 260 261 262
=back

=cut

sub validate {
263
    my ($cgi, $bug_id, $attach_id, $skip_requestee_on_error) = @_;
264 265 266

    my $dbh = Bugzilla->dbh;

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

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

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

320 321
        # Make sure the flag type exists. If it doesn't, FormToNewFlags()
        # will ignore it, so it's safe to ignore it here.
322
        my $flag_type = new Bugzilla::FlagType($id);
323
        next unless $flag_type;
324

325 326 327 328 329
        # Make sure the flag type is active.
        unless ($flag_type->is_active) {
            ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
        }

330
        _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
331
                  $bug_id, $attach_id, $skip_requestee_on_error);
332 333
    }

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

340 341
        # Make sure the flag exists. If it doesn't, process() will ignore it,
        # so it's safe to ignore it here.
342
        my $flag = new Bugzilla::Flag($id);
343
        next unless $flag;
344

345 346
        _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
                  undef, undef, $skip_requestee_on_error);
347 348
    }
}
349

350
sub _validate {
351
    my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
352
        $bug_id, $attach_id, $skip_requestee_on_error) = @_;
353

354 355
    # By default, the flag setter (or requester) is the current user.
    $setter ||= Bugzilla->user;
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375

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

377 378 379 380 381 382 383 384 385 386 387 388
    # 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 });
389
        }
390 391 392 393 394 395 396 397 398 399
    }

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

401 402 403 404 405 406
    # 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 : '';
407 408

        my @legal_requestees;
409
        foreach my $login (@$requestees) {
410 411 412 413 414
            if ($login eq $old_requestee) {
                # This requestee was already set. Leave him alone.
                push(@legal_requestees, $login);
                next;
            }
415 416 417 418 419 420 421 422 423

            # 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)) {
424
                next if $skip_requestee_on_error;
425 426 427 428 429
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
430
            }
431

432 433 434 435 436 437 438
            # 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'}))
            {
439
                next if $skip_requestee_on_error;
440 441 442 443 444 445
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $flag_type,
                                 requestee  => $requestee,
                                 bug_id     => $bug_id,
                                 attach_id  => $attach_id });
            }
446 447

            # Throw an error if the user won't be allowed to set the flag.
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
            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);
464
        }
465
    }
466 467 468 469 470 471 472 473

    # 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 '?')
474
               && $setter->can_request_flag($flag_type));
475 476

    # - User in the grant_group can set/clear flags, including "+" and "-".
477
    return if $setter->can_set_flag($flag_type);
478 479 480 481 482 483

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

486 487 488 489
sub snapshot {
    my ($bug_id, $attach_id) = @_;

    my $flags = match({ 'bug_id'    => $bug_id,
490
                        'attach_id' => $attach_id });
491 492
    my @summaries;
    foreach my $flag (@$flags) {
493
        my $summary = $flag->type->name . $flag->status;
494
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
495 496 497 498 499
        push(@summaries, $summary);
    }
    return @summaries;
}

500

501 502 503 504
=pod

=over

505
=item C<process($bug, $attachment, $timestamp, $cgi)>
506 507 508

Processes changes to flags.

509 510 511 512
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.
513 514 515 516 517 518

=back

=cut

sub process {
519
    my ($bug, $attachment, $timestamp, $cgi) = @_;
520
    my $dbh = Bugzilla->dbh;
521 522 523 524 525 526 527 528

    # 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;
529 530 531

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

534
    # Take a snapshot of flags before any changes.
535
    my @old_summaries = snapshot($bug_id, $attach_id);
536

537
    # Cancel pending requests if we are obsoleting an attachment.
538 539
    if ($attachment && $cgi->param('isobsolete')) {
        CancelRequests($bug, $attachment);
540 541
    }

542
    # Create new flags and update existing flags.
543 544 545
    my $new_flags = FormToNewFlags($bug, $attachment, $cgi);
    foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
    modify($bug, $attachment, $cgi, $timestamp);
546

547 548
    # In case the bug's product/component has changed, clear flags that are
    # no longer valid.
549
    my $flag_ids = $dbh->selectcol_arrayref(
550
        "SELECT DISTINCT flags.id
551 552 553 554 555
           FROM flags
     INNER JOIN bugs
             ON flags.bug_id = bugs.bug_id
      LEFT JOIN flaginclusions AS i
             ON flags.type_id = i.type_id 
556
            AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
557 558 559
            AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ?
            AND i.type_id IS NULL",
560 561
        undef, $bug_id);

562 563 564 565
    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;
566
    }
567 568

    $flag_ids = $dbh->selectcol_arrayref(
569
        "SELECT DISTINCT flags.id
570
        FROM flags, bugs, flagexclusions e
571
        WHERE bugs.bug_id = ?
572
        AND flags.bug_id = bugs.bug_id
573
        AND flags.type_id = e.type_id
574
        AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
575 576 577
        AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
        undef, $bug_id);

578 579 580 581
    $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;
582
    }
583

584 585 586 587 588 589 590 591 592 593 594 595
    # 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);
596
    my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
597
    if ($removed ne $added) {
598
        my $field_id = get_field_id('flagtypes.name');
599
        $dbh->do('INSERT INTO bugs_activity
600
                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
601 602 603
                  VALUES (?, ?, ?, ?, ?, ?, ?)',
                  undef, ($bug_id, $attach_id, Bugzilla->user->id,
                  $timestamp, $field_id, $removed, $added));
604

605 606
        $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
                  undef, ($timestamp, $bug_id));
607 608 609
    }
}

610 611 612 613
=pod

=over

614
=item C<create($flag, $bug, $attachment, $timestamp)>
615 616

Creates a flag record in the database.
617

618 619 620 621 622
=back

=cut

sub create {
623
    my ($flag, $bug, $attachment, $timestamp) = @_;
624 625
    my $dbh = Bugzilla->dbh;

626
    my $attach_id = $attachment ? $attachment->id : undef;
627
    my $requestee_id;
628
    # Be careful! At this point, $flag is *NOT* yet an object!
629 630 631 632 633
    $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 (?, ?, ?, ?, ?, ?, ?, ?)',
634
              undef, ($flag->{'type'}->id, $bug->bug_id,
635 636
                      $attach_id, $requestee_id, $flag->{'setter'}->id,
                      $flag->{'status'}, $timestamp, $timestamp));
637

638 639 640 641 642
    # 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);

643
    # Send an email notifying the relevant parties about the flag creation.
644 645
    if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
        $flag->{'addressee'} = $flag->requestee;
646
    }
647

648
    notify($flag, $bug, $attachment);
649 650 651

    # Return the new flag object.
    return $flag;
652 653
}

654
=pod
655

656 657
=over

658
=item C<modify($bug, $attachment, $cgi, $timestamp)>
659

660 661 662 663 664 665 666
Modifies flags in the database when a user changes them.

=back

=cut

sub modify {
667
    my ($bug, $attachment, $cgi, $timestamp) = @_;
668
    my $setter = Bugzilla->user;
669
    my $dbh = Bugzilla->dbh;
670 671

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

674 675 676 677
    # 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.
678 679
    my @flags;
    foreach my $id (@ids) {
680
        my $flag = new Bugzilla::Flag($id);
681 682
        # If the flag no longer exists, ignore it.
        next unless $flag;
683

684
        my $status = $cgi->param("flag-$id");
685

686 687 688 689 690 691 692 693
        # 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
694
            && $flag->type->is_multiplicable)
695 696 697
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
698

699 700
            # Create new flags like the existing one for each additional person.
            foreach my $login (@requestees) {
701
                create({ type      => $flag->type,
702
                         setter    => $setter, 
703
                         status    => "?",
704
                         requestee => new Bugzilla::User({ name => $login }) },
705
                       $bug, $attachment, $timestamp);
706 707 708 709 710 711
            }
        }
        else {
            $requestee_email = trim($cgi->param("requestee-$id") || '');
        }

712 713 714 715
        # 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.

716
        my $status_changed = ($status ne $flag->status);
717

718 719 720 721 722
        # 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.
723

724
        my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
725 726 727

        my $requestee_changed = 
          ($status eq "?" && 
728
           $flag->type->is_requesteeble &&
729
           $old_requestee ne $requestee_email);
730

731 732
        next unless ($status_changed || $requestee_changed);

733 734
        # 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.
735 736
        trick_taint($status);

737
        if ($status eq '+' || $status eq '-') {
738 739 740 741
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = NULL,
                             status = ?, modification_date = ?
                       WHERE id = ?',
742
                       undef, ($setter->id, $status, $timestamp, $flag->id));
743 744 745 746

            # If the status of the flag was "?", we have to notify
            # the requester (if he wants to).
            my $requester;
747 748
            if ($flag->status eq '?') {
                $requester = $flag->setter;
749
                $flag->{'requester'} = $requester;
750
            }
751 752 753
            # Now update the flag object with its new values.
            $flag->{'setter'} = $setter;
            $flag->{'requestee'} = undef;
754
            $flag->{'requestee_id'} = undef;
755 756 757 758 759 760 761 762
            $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;
            }

763
            notify($flag, $bug, $attachment);
764 765
        }
        elsif ($status eq '?') {
766
            # Get the requestee, if any.
767
            my $requestee_id;
768
            if ($requestee_email) {
769
                $requestee_id = login_to_id($requestee_email);
770
                $flag->{'requestee'} = new Bugzilla::User($requestee_id);
771
                $flag->{'requestee_id'} = $requestee_id;
772
            }
773 774 775 776
            else {
                # If the status didn't change but we only removed the
                # requestee, we have to clear the requestee field.
                $flag->{'requestee'} = undef;
777
                $flag->{'requestee_id'} = undef;
778
            }
779 780

            # Update the database with the changes.
781 782 783 784 785
            $dbh->do('UPDATE flags
                         SET setter_id = ?, requestee_id = ?,
                             status = ?, modification_date = ?
                       WHERE id = ?',
                       undef, ($setter->id, $requestee_id, $status,
786
                               $timestamp, $flag->id));
787 788 789 790 791

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

792
            # Send an email notifying the relevant parties about the request.
793 794
            if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
                $flag->{'addressee'} = $flag->requestee;
795
            }
796

797
            notify($flag, $bug, $attachment);
798 799
        }
        elsif ($status eq 'X') {
800
            clear($flag, $bug, $attachment);
801
        }
802

803 804
        push(@flags, $flag);
    }
805

806 807 808
    return \@flags;
}

809 810 811 812
=pod

=over

813
=item C<retarget($flag, $bug)>
814 815 816

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
817
component the bug is in, and the current settings of the flag must pass
818 819 820 821 822 823 824 825 826 827 828 829 830
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 {
831
    my ($flag, $bug) = @_;
832 833 834 835 836 837 838
    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,
839
                         'target_type'  => $flag->type->target_type,
840 841 842 843 844 845 846 847 848 849
                         '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.
850
    my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
851
    my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
852
    my $is_retargetted = 0;
853

854 855 856
    foreach my $flagtype (@$flagtypes) {
        # Get the number of flags of this type already set for this target.
        my $has_flags = count(
857
            { 'type_id'     => $flagtype->id,
858
              'bug_id'      => $bug->bug_id,
859
              'attach_id'   => $flag->attach_id });
860 861 862

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

        # Check user privileges.
        my $error_mode_cache = Bugzilla->error_mode;
        Bugzilla->error_mode(ERROR_MODE_DIE);
        eval {
869
            _validate(undef, $flagtype, $flag->status, $flag->setter,
870
                      $requestee, $is_private, $bug->bug_id, $flag->attach_id);
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889
        };
        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

890
=item C<clear($flag, $bug, $attachment)>
891

892
Remove a flag from the DB.
893 894 895 896 897

=back

=cut

898
sub clear {
899
    my ($flag, $bug, $attachment) = @_;
900 901
    my $dbh = Bugzilla->dbh;

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

904 905 906
    # If we cancel a pending request, we have to notify the requester
    # (if he wants to).
    my $requester;
907 908
    if ($flag->status eq '?') {
        $requester = $flag->setter;
909
        $flag->{'requester'} = $requester;
910 911 912 913 914
    }

    # 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.
915
    $flag->{'exists'} = 0;    
916
    $flag->{'status'} = "X";
917 918 919 920 921

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

922
    notify($flag, $bug, $attachment);
923 924 925
}


926
######################################################################
927
# Utility Functions
928 929 930 931 932 933
######################################################################

=pod

=over

934
=item C<FormToNewFlags($bug, $attachment, $cgi)>
935

936 937
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().
938 939 940 941

=back

=cut
942 943

sub FormToNewFlags {
944
    my ($bug, $attachment, $cgi) = @_;
945
    my $dbh = Bugzilla->dbh;
946
    my $setter = Bugzilla->user;
947
    
948
    # Extract a list of flag type IDs from field names.
949 950
    my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
951

952
    return () unless scalar(@type_ids);
953 954 955

    # Get a list of active flag types available for this target.
    my $flag_types = Bugzilla::FlagType::match(
956 957 958
        { 'target_type'  => $attachment ? 'attachment' : 'bug',
          'product_id'   => $bug->{'product_id'},
          'component_id' => $bug->{'component_id'},
959 960
          'is_active'    => 1 });

961
    my @flags;
962
    foreach my $flag_type (@$flag_types) {
963
        my $type_id = $flag_type->id;
964 965 966 967

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

968
        # Get the number of flags of this type already set for this target.
969 970
        my $has_flags = count(
            { 'type_id'     => $type_id,
971 972 973
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
974 975

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

979
        my $status = $cgi->param("flag_type-$type_id");
980
        trick_taint($status);
981

982 983 984
        my @logins = $cgi->param("requestee_type-$type_id");
        if ($status eq "?" && scalar(@logins) > 0) {
            foreach my $login (@logins) {
985 986 987
                push (@flags, { type      => $flag_type ,
                                setter    => $setter , 
                                status    => $status ,
988 989
                                requestee => 
                                    new Bugzilla::User({ name => $login }) });
990
                last unless $flag_type->is_multiplicable;
991
            }
992
        }
993 994 995 996 997
        else {
            push (@flags, { type   => $flag_type ,
                            setter => $setter , 
                            status => $status });
        }
998 999 1000 1001 1002 1003
    }

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

1004 1005 1006 1007
=pod

=over

1008
=item C<notify($flag, $bug, $attachment)>
1009

1010 1011
Sends an email notification about a flag being created, fulfilled
or deleted.
1012 1013 1014 1015 1016

=back

=cut

1017
sub notify {
1018
    my ($flag, $bug, $attachment) = @_;
1019

1020
    my $template = Bugzilla->template;
1021 1022

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

1025
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
1026

1027 1028 1029 1030
    # 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.
1031 1032 1033
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};

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

1038
            next if (scalar(@bug_in_groups) && !$ccuser->can_see_bug($bug->bug_id));
1039
            next if $attachment_is_private
1040 1041
              && Bugzilla->params->{"insidergroup"}
              && !$ccuser->in_group(Bugzilla->params->{"insidergroup"});
1042
            push(@new_cc_list, $cc);
1043
        }
1044
        $flag->type->{'cc_list'} = join(", ", @new_cc_list);
1045 1046
    }

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

1050 1051
    # Process and send notification for each recipient
    foreach my $to ($flag->{'addressee'} ? $flag->{'addressee'}->email : '',
1052
                    split(/[, ]+/, $flag->type->cc_list))
1053 1054
    {
        next unless $to;
1055 1056 1057 1058
        my $vars = { 'flag'       => $flag,
                     'to'         => $to,
                     'bug'        => $bug,
                     'attachment' => $attachment};
1059
        my $message;
1060
        my $rv = $template->process("request/email.txt.tmpl", $vars, \$message);
1061 1062 1063 1064
        if (!$rv) {
            Bugzilla->cgi->header();
            ThrowTemplateError($template->error());
        }
1065

1066
        MessageToMTA($message);
1067
    }
1068 1069
}

1070 1071
# Cancel all request flags from the attachment being obsoleted.
sub CancelRequests {
1072
    my ($bug, $attachment, $timestamp) = @_;
1073 1074 1075 1076 1077 1078 1079 1080 1081
    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",
1082
                                  undef, $attachment->id);
1083 1084 1085 1086

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

    # Take a snapshot of flags before any changes.
1087
    my @old_summaries = snapshot($bug->bug_id, $attachment->id) if ($timestamp);
1088 1089
    my $flags = Bugzilla::Flag->new_from_list($request_ids);
    foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
1090 1091 1092 1093 1094

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

    # Take a snapshot of flags after any changes.
1095 1096 1097
    my @new_summaries = snapshot($bug->bug_id, $attachment->id);
    update_activity($bug->bug_id, $attachment->id, $timestamp,
                    \@old_summaries, \@new_summaries);
1098 1099
}

1100
######################################################################
1101
# Private Functions
1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
######################################################################

=begin private

=head1 PRIVATE FUNCTIONS

=over

=item C<sqlify_criteria($criteria)>

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

=back

=cut
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148

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

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;