Flag.pm 34.3 KB
Newer Older
1 2 3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
#
5 6
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
7

8 9
package Bugzilla::Flag;

10 11 12
use 5.10.1;
use strict;

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
=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 *

28
Import relevant functions from that script.
29 30 31 32

=item *

Use of private functions / variables outside this module may lead to
33
unexpected results after an upgrade.  Please avoid using private
34 35 36 37 38 39 40
functions in other files/modules.  Private functions are functions
whose names start with _ or a re specifically noted as being private.

=back

=cut

41
use Scalar::Util qw(blessed);
42
use Storable qw(dclone);
43

44
use Bugzilla::FlagType;
45
use Bugzilla::Hook;
46
use Bugzilla::User;
47
use Bugzilla::Util;
48
use Bugzilla::Error;
49
use Bugzilla::Mailer;
50
use Bugzilla::Constants;
51
use Bugzilla::Field;
52

53
use parent qw(Bugzilla::Object Exporter);
54
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
55 56 57 58

###############################
####    Initialization     ####
###############################
59

60 61
use constant DB_TABLE => 'flags';
use constant LIST_ORDER => 'id';
62
# Flags are tracked in bugs_activity.
63
use constant AUDIT_CREATES => 0;
64
use constant AUDIT_UPDATES => 0;
65
use constant AUDIT_REMOVES => 0;
66

67 68
use constant SKIP_REQUESTEE_ON_ERROR => 1;

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
sub DB_COLUMNS {
    my $dbh = Bugzilla->dbh;
    return qw(
        id
        type_id
        bug_id
        attach_id
        requestee_id
        setter_id
        status), 
        $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') .
                              ' AS creation_date', 
        $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') .
                              ' AS modification_date';
}
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

use constant UPDATE_COLUMNS => qw(
    requestee_id
    setter_id
    status
    type_id
);

use constant VALIDATORS => {
};

use constant UPDATE_VALIDATORS => {
    setter => \&_check_setter,
    status => \&_check_status,
};

100 101 102
###############################
####      Accessors      ######
###############################
103

104
=head2 METHODS
105

106 107
=over

108 109 110 111 112 113 114
=item C<id>

Returns the ID of the flag.

=item C<name>

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

116 117 118 119 120 121 122 123
=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.

124 125 126
=item C<status>

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

128 129 130 131 132 133 134 135
=item C<creation_date>

Returns the timestamp when the flag was created.

=item C<modification_date>

Returns the timestamp when the flag was last modified.

136 137
=back

138
=cut
139

140 141 142 143 144 145 146 147
sub id           { return $_[0]->{'id'};           }
sub name         { return $_[0]->type->name;       }
sub type_id      { return $_[0]->{'type_id'};      }
sub bug_id       { return $_[0]->{'bug_id'};       }
sub attach_id    { return $_[0]->{'attach_id'};    }
sub status       { return $_[0]->{'status'};       }
sub setter_id    { return $_[0]->{'setter_id'};    }
sub requestee_id { return $_[0]->{'requestee_id'}; }
148 149
sub creation_date     { return $_[0]->{'creation_date'};     }
sub modification_date { return $_[0]->{'modification_date'}; }
150 151 152 153 154 155 156 157 158 159 160

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

=pod

=over

=item C<type>

161
Returns the type of the flag, as a Bugzilla::FlagType object.
162

163
=item C<setter>
164

165 166 167 168 169 170 171
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.

172 173 174 175 176
=item C<attachment>

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

177 178 179 180 181 182 183
=back

=cut

sub type {
    my $self = shift;

184
    return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
185 186
}

187 188 189
sub setter {
    my $self = shift;

190
    return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 });
191 192 193 194 195 196
}

sub requestee {
    my $self = shift;

    if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
197
        $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 });
198 199 200 201
    }
    return $self->{'requestee'};
}

202 203 204 205 206
sub attachment {
    my $self = shift;
    return undef unless $self->attach_id;

    require Bugzilla::Attachment;
207 208
    return $self->{'attachment'}
      ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 });
209 210
}

211 212 213 214
sub bug {
    my $self = shift;

    require Bugzilla::Bug;
215
    return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 });
216 217
}

218 219 220 221
################################
## Searching/Retrieving Flags ##
################################

222 223 224 225 226
=pod

=over

=item C<match($criteria)>
227

228 229 230 231 232 233 234 235 236
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 {
237
    my $class = shift;
238
    my ($criteria) = @_;
239

240 241 242
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (my $type = delete $criteria->{'target_type'}) {
243
        if ($type eq 'bug') {
244 245
            $criteria->{'attach_id'} = IS_NULL;
        }
246 247 248
        elsif (!defined $criteria->{'attach_id'}) {
            $criteria->{'attach_id'} = NOT_NULL;
        }
249
    }
250 251 252 253 254
    # 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;
    }
255

256
    return $class->SUPER::match(@_);
257 258
}

259 260 261 262 263 264
=pod

=over

=item C<count($criteria)>

265
Queries the database for flags matching the given criteria
266 267 268 269
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back
270

271 272 273
=cut

sub count {
274 275
    my $class = shift;
    return scalar @{$class->match(@_)};
276 277
}

278
######################################################################
279
# Creating and Modifying
280
######################################################################
281

282 283
sub set_flag {
    my ($class, $obj, $params) = @_;
284

285
    my ($bug, $attachment, $obj_flag, $requestee_changed);
286 287 288 289 290 291 292 293 294 295
    if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
        $attachment = $obj;
        $bug = $attachment->bug;
    }
    elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
        $bug = $obj;
    }
    else {
        ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
    }
296

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    # Update (or delete) an existing flag.
    if ($params->{id}) {
        my $flag = $class->check({ id => $params->{id} });

        # Security check: make sure the flag belongs to the bug/attachment.
        # We don't check that the user editing the flag can see
        # the bug/attachment. That's the job of the caller.
        ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id)
          || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id)
          || ThrowCodeError('invalid_flag_association',
                            { bug_id    => $bug->id,
                              attach_id => $attachment ? $attachment->id : undef });

        # Extract the current flag object from the object.
        my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
        # If no flagtype can be found for this flag, this means the bug is being
        # moved into a product/component where the flag is no longer valid.
        # So either we can attach the flag to another flagtype having the same
        # name, or we remove the flag.
        if (!$obj_flagtype) {
            my $success = $flag->retarget($obj);
            return unless $success;

            ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
            push(@{$obj_flagtype->{flags}}, $flag);
322
        }
323
        ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
324 325 326 327
        # If the flag has the correct type but cannot be found above, this means
        # the flag is going to be removed (e.g. because this is a pending request
        # and the attachment is being marked as obsolete).
        return unless $obj_flag;
328

329 330
        ($obj_flag, $requestee_changed) =
            $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
331
    }
332 333
    # Create a new flag.
    elsif ($params->{type_id}) {
334
        # Don't bother validating types the user didn't touch.
335 336 337 338 339 340 341 342 343 344 345
        return if $params->{status} eq 'X';

        my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} });
        # Security check: make sure the flag type belongs to the bug/attachment.
        ($attachment && $flagtype->target_type eq 'attachment'
          && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types}))
          || (!$attachment && $flagtype->target_type eq 'bug'
                && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types}))
          || ThrowCodeError('invalid_flag_association',
                            { bug_id    => $bug->id,
                              attach_id => $attachment ? $attachment->id : undef });
346

347
        # Make sure the flag type is active.
348 349
        $flagtype->is_active
          || ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
350

351 352
        # Extract the current flagtype object from the object.
        my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};
353

354 355 356 357 358 359 360
        # We cannot create a new flag if there is already one and this
        # flag type is not multiplicable.
        if (!$flagtype->is_multiplicable) {
            if (scalar @{$obj_flagtype->{flags}}) {
                ThrowUserError('flag_type_not_multiplicable', { type => $flagtype });
            }
        }
361

362 363
        ($obj_flag, $requestee_changed) =
            $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
364 365 366 367
    }
    else {
        ThrowCodeError('param_required', { function => $class . '->set_flag',
                                           param    => 'id/type_id' });
368
    }
369 370 371 372 373 374 375 376

    if ($obj_flag
        && $requestee_changed
        && $obj_flag->requestee_id
        && $obj_flag->requestee->setting('requestee_cc') eq 'on')
    {
        $bug->add_cc($obj_flag->requestee);
    }
377
}
378

379
sub _validate {
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
    my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;

    # If it's a new flag, let's create it now.
    my $obj_flag = $flag || bless({ type_id   => $flag_type->id,
                                    status    => '',
                                    bug_id    => $bug->id,
                                    attach_id => $attachment ?
                                                   $attachment->id : undef},
                                    $class);

    my $old_status = $obj_flag->status;
    my $old_requestee_id = $obj_flag->requestee_id;

    $obj_flag->_set_status($params->{status});
    $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});

396 397 398
    # The requestee ID can be undefined.
    my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);

399 400
    # The setter field MUST NOT be updated if neither the status
    # nor the requestee fields changed.
401
    if (($obj_flag->status ne $old_status) || $requestee_changed) {
402
        $obj_flag->_set_setter($params->{setter});
403
    }
404

405 406 407
    # If the flag is deleted, remove it from the list.
    if ($obj_flag->status eq 'X') {
        @{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
408
        return;
409
    }
410 411 412
    # Add the newly created flag to the list.
    elsif (!$obj_flag->id) {
        push(@{$flag_type->{flags}}, $obj_flag);
413
    }
414
    return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
415
}
416

417
=pod
418

419
=over
420

421
=item C<create($flag, $timestamp)>
422

423
Creates a flag record in the database.
424

425
=back
426

427
=cut
428

429 430
sub create {
    my ($class, $flag, $timestamp) = @_;
431
    $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
432

433
    my $params = {};
434
    my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
435 436 437 438

    # Some columns use date formatting so use alias instead
    @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns;

439
    $params->{$_} = $flag->{$_} foreach @columns;
440

441
    $params->{creation_date} = $params->{modification_date} = $timestamp;
442

443 444 445 446 447 448 449 450
    $flag = $class->SUPER::create($params);
    return $flag;
}

sub update {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
451

452
    my $changes = $self->SUPER::update(@_);
453

454 455 456
    if (scalar(keys %$changes)) {
        $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
                 undef, ($timestamp, $self->id));
457
        $self->{'modification_date'} = format_time($timestamp, '%Y.%m.%d %T');
458 459
    }
    return $changes;
460 461
}

462
sub snapshot {
463
    my ($class, $flags) = @_;
464 465 466

    my @summaries;
    foreach my $flag (@$flags) {
467
        my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
468
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
469 470 471 472 473
        push(@summaries, $summary);
    }
    return @summaries;
}

474 475
sub update_activity {
    my ($class, $old_summaries, $new_summaries) = @_;
476

477 478 479 480
    my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
    if (scalar @$removed || scalar @$added) {
        # Remove flag requester/setter information
        foreach (@$removed, @$added) { s/^[^:]+:// }
481

482 483 484
        $removed = join(", ", @$removed);
        $added = join(", ", @$added);
        return ($removed, $added);
485
    }
486 487
    return ();
}
488

489 490
sub update_flags {
    my ($class, $self, $old_self, $timestamp) = @_;
491

492 493
    my @old_summaries = $class->snapshot($old_self->flags);
    my %old_flags = map { $_->id => $_ } @{$old_self->flags};
494

495 496 497 498 499
    foreach my $new_flag (@{$self->flags}) {
        if (!$new_flag->id) {
            # This is a new flag.
            my $flag = $class->create($new_flag, $timestamp);
            $new_flag->{id} = $flag->id;
500
            $class->notify($new_flag, undef, $self, $timestamp);
501 502
        }
        else {
503 504
            my $changes = $new_flag->update($timestamp);
            if (scalar(keys %$changes)) {
505
                $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
506
            }
507
            delete $old_flags{$new_flag->id};
508
        }
509
    }
510 511
    # These flags have been deleted.
    foreach my $old_flag (values %old_flags) {
512
        $class->notify(undef, $old_flag, $self, $timestamp);
513
        $old_flag->remove_from_db();
514
    }
515

516 517 518 519 520 521 522 523 524
    # If the bug has been moved into another product or component,
    # we must also take care of attachment flags which are no longer valid,
    # as well as all bug flags which haven't been forgotten above.
    if ($self->isa('Bugzilla::Bug')
        && ($self->{_old_product_name} || $self->{_old_component_name}))
    {
        my @removed = $class->force_cleanup($self);
        push(@old_summaries, @removed);
    }
525

526 527
    my @new_summaries = $class->snapshot($self->flags);
    my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
528

529
    Bugzilla::Hook::process('flag_end_of_update', { object    => $self,
530 531 532 533
                                                    timestamp => $timestamp,
                                                    old_flags => \@old_summaries,
                                                    new_flags => \@new_summaries,
                                                  });
534
    return @changes;
535 536
}

537 538
sub retarget {
    my ($self, $obj) = @_;
539

540
    my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};
541

542 543 544 545
    my $success = 0;
    foreach my $flagtype (@flagtypes) {
        next if !$flagtype->is_active;
        next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}});
546 547
        next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype))
                     || $self->setter->can_set_flag($flagtype));
548 549 550 551 552

        $self->{type_id} = $flagtype->id;
        delete $self->{type};
        $success = 1;
        last;
553
    }
554
    return $success;
555 556
}

557 558 559 560 561
# In case the bug's product/component has changed, clear flags that are
# no longer valid.
sub force_cleanup {
    my ($class, $bug) = @_;
    my $dbh = Bugzilla->dbh;
562

563 564 565 566 567 568 569 570 571 572 573
    my $flag_ids = $dbh->selectcol_arrayref(
        'SELECT DISTINCT flags.id
           FROM flags
          INNER JOIN bugs
                ON flags.bug_id = bugs.bug_id
           LEFT JOIN flaginclusions AS i
                ON flags.type_id = i.type_id
                AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
                AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
          WHERE bugs.bug_id = ? AND i.type_id IS NULL',
         undef, $bug->id);
574

575
    my @removed = $class->force_retarget($flag_ids, $bug);
576

577 578 579 580 581 582 583 584 585 586 587 588 589
    $flag_ids = $dbh->selectcol_arrayref(
        'SELECT DISTINCT flags.id
           FROM flags, bugs, flagexclusions e
          WHERE bugs.bug_id = ?
                AND flags.bug_id = bugs.bug_id
                AND flags.type_id = e.type_id
                AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
                AND (bugs.component_id = e.component_id OR e.component_id IS NULL)',
         undef, $bug->id);

    push(@removed , $class->force_retarget($flag_ids, $bug));
    return @removed;
}
590

591 592
sub force_retarget {
    my ($class, $flag_ids, $bug) = @_;
593 594
    my $dbh = Bugzilla->dbh;

595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
    my $flags = $class->new_from_list($flag_ids);
    my @removed;
    foreach my $flag (@$flags) {
        # $bug is undefined when e.g. editing inclusion and exclusion lists.
        my $obj = $flag->attachment || $bug || $flag->bug;
        my $is_retargetted = $flag->retarget($obj);
        if ($is_retargetted) {
            $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
                     undef, ($flag->type_id, $flag->id));
        }
        else {
            # Track deleted attachment flags.
            push(@removed, $class->snapshot([$flag])) if $flag->attach_id;
            $class->notify(undef, $flag, $bug || $flag->bug);
            $flag->remove_from_db();
        }
611
    }
612
    return @removed;
613 614
}

615 616 617
###############################
####      Validators     ######
###############################
618

619 620
sub _set_requestee {
    my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
621

622 623
    $self->{requestee} =
      $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
624

625 626 627
    $self->{requestee_id} =
      $self->{requestee} ? $self->{requestee}->id : undef;
}
628

629 630
sub _set_setter {
    my ($self, $setter) = @_;
631

632 633 634
    $self->set('setter', $setter);
    $self->{setter_id} = $self->setter->id;
}
635

636 637
sub _set_status {
    my ($self, $status) = @_;
638

639 640 641 642
    # Store the old flag status. It's needed by _check_setter().
    $self->{_old_status} = $self->status;
    $self->set('status', $status);
}
643

644 645
sub _check_requestee {
    my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
646

647 648
    # If the flag status is not "?", then no requestee can be defined.
    return undef if ($self->status ne '?');
649

650 651
    # Store this value before updating the flag object.
    my $old_requestee = $self->requestee ? $self->requestee->login : '';
652

653 654 655 656 657 658
    if ($self->status eq '?' && $requestee) {
        $requestee = Bugzilla::User->check($requestee);
    }
    else {
        undef $requestee;
    }
659

660 661 662 663 664
    if ($requestee && $requestee->login ne $old_requestee) {
        # 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, the
        # user can either remove him or leave him alone.
665
        ThrowUserError('flag_requestee_disabled', { type => $self->type })
666 667 668 669 670 671 672 673 674 675
          if !$self->type->is_requesteeble;

        # Make sure the requestee can see the bug.
        # Note that can_see_bug() will query the DB, so if the bug
        # is being added/removed from some groups and these changes
        # haven't been committed to the DB yet, they won't be taken
        # into account here. In this case, old restrictions matters.
        if (!$requestee->can_see_bug($self->bug_id)) {
            if ($skip_requestee_on_error) {
                undef $requestee;
676
            }
677 678 679 680 681 682
            else {
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
683
            }
684
        }
685 686 687 688
        # Make sure the requestee can see the private attachment.
        elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) {
            if ($skip_requestee_on_error) {
                undef $requestee;
689
            }
690
            else {
691 692 693 694 695
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
696
            }
697
        }
698 699 700 701 702 703 704 705 706 707
        # Make sure the user is allowed to set the flag.
        elsif (!$requestee->can_set_flag($self->type)) {
            if ($skip_requestee_on_error) {
                undef $requestee;
            }
            else {
                ThrowUserError('flag_requestee_needs_privs',
                               {'requestee' => $requestee,
                                'flagtype'  => $self->type});
            }
708 709
        }
    }
710
    return $requestee;
711 712
}

713 714
sub _check_setter {
    my ($self, $setter) = @_;
715

716 717 718
    # By default, the currently logged in user is the setter.
    $setter ||= Bugzilla->user;
    (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
719
      || ThrowUserError('invalid_user');
720

721 722 723
    # set_status() has already been called. So this refers
    # to the new flag status.
    my $status = $self->status;
724

725 726
    # Make sure the user is authorized to modify flags, see bug 180879:
    # - The flag exists and is unchanged.
727
    # - The flag setter can unset flag.
728 729 730 731
    # - Users in the request_group can clear pending requests and set flags
    #   and can rerequest set flags.
    # - Users in the grant_group can set/clear flags, including "+" and "-".
    unless (($status eq $self->{_old_status})
732
            || ($status eq 'X' && $setter->id == Bugzilla->user->id)
733 734 735 736 737 738 739 740 741
            || (($status eq 'X' || $status eq '?')
                && $setter->can_request_flag($self->type))
            || $setter->can_set_flag($self->type))
    {
        ThrowUserError('flag_update_denied',
                        { name       => $self->type->name,
                          status     => $status,
                          old_status => $self->{_old_status} });
    }
742

743 744 745
    # If the request is being retargetted, we don't update
    # the setter, so that the setter gets the notification.
    if ($status eq '?' && $self->{_old_status} eq '?') {
746 747 748 749
        return $self->setter;
    }
    return $setter;
}
750

751 752
sub _check_status {
    my ($self, $status) = @_;
753

754 755 756 757 758 759 760
    # - Make sure the status is valid.
    # - 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 (!grep($status eq $_ , qw(X + - ?))
        || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable))
    {
761
        ThrowUserError('flag_status_invalid', { id     => $self->id,
762
                                                status => $status });
763
    }
764
    return $status;
765 766
}

767 768 769 770
######################################################################
# Utility Functions
######################################################################

771 772 773 774
=pod

=over

775
=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
776

777 778
Checks whether or not there are new flags to create and returns an
array of hashes. This array is then passed to Flag::create().
779 780 781 782 783

=back

=cut

784 785 786
sub extract_flags_from_cgi {
    my ($class, $bug, $attachment, $vars, $skip) = @_;
    my $cgi = Bugzilla->cgi;
787

788
    my $match_status = Bugzilla::User::match_field({
789
        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
790
    }, undef, $skip);
791

792 793 794
    $vars->{'match_field'} = 'requestee';
    if ($match_status == USER_MATCH_FAILED) {
        $vars->{'message'} = 'user_match_failed';
795
    }
796 797
    elsif ($match_status == USER_MATCH_MULTIPLE) {
        $vars->{'message'} = 'user_match_multiple';
798 799
    }

800 801 802
    # Extract a list of flag type IDs from field names.
    my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
    @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids);
803

804 805
    # Extract a list of existing flag IDs.
    my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
806

807
    return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
808

809 810 811 812 813
    my (@new_flags, @flags);
    foreach my $flag_id (@flag_ids) {
        my $flag = $class->new($flag_id);
        # If the flag no longer exists, ignore it.
        next unless $flag;
814

815
        my $status = $cgi->param("flag-$flag_id");
816

817 818 819 820 821 822 823 824 825 826 827 828
        # 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 requestee.
        my @requestees = $cgi->param("requestee-$flag_id");
        my $requestee_email;
        if ($status eq "?"
            && scalar(@requestees) > 1
            && $flag->type->is_multiplicable)
        {
            # The first person, for which we'll reuse the existing flag.
            $requestee_email = shift(@requestees);
829

830 831 832 833 834 835 836 837 838 839 840 841 842 843
            # Create new flags like the existing one for each additional person.
            foreach my $login (@requestees) {
                push(@new_flags, { type_id   => $flag->type_id,
                                   status    => "?",
                                   requestee => $login,
                                   skip_roe  => $skip });
            }
        }
        elsif ($status eq "?" && scalar(@requestees)) {
            # If there are several requestees and the flag type is not multiplicable,
            # this will fail. But that's the job of the validator to complain. All
            # we do here is to extract and convert data from the CGI.
            $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
        }
844

845 846 847 848 849
        push(@flags, { id        => $flag_id,
                       status    => $status,
                       requestee => $requestee_email,
                       skip_roe  => $skip });
    }
850

851
    # Get a list of active flag types available for this product/component.
852
    my $flag_types = Bugzilla::FlagType::match(
853
        { 'product_id'   => $bug->{'product_id'},
854
          'component_id' => $bug->{'component_id'},
855 856
          'is_active'    => 1 });

857
    foreach my $flagtype_id (@flagtype_ids) {
858
        # Checks if there are unexpected flags for the product/component.
859 860
        if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
            $vars->{'message'} = 'unexpected_flag_types';
861 862 863 864
            last;
        }
    }

865
    foreach my $flag_type (@$flag_types) {
866
        my $type_id = $flag_type->id;
867

868 869 870 871
        # 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);

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

875
        # Get the number of flags of this type already set for this target.
876
        my $has_flags = $class->count(
877
            { 'type_id'     => $type_id,
878 879 880
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
881 882

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

886
        my $status = $cgi->param("flag_type-$type_id");
887
        trick_taint($status);
888

889
        my @logins = $cgi->param("requestee_type-$type_id");
890
        if ($status eq "?" && scalar(@logins)) {
891
            foreach my $login (@logins) {
892 893 894 895
                push (@new_flags, { type_id   => $type_id,
                                    status    => $status,
                                    requestee => $login,
                                    skip_roe  => $skip });
896
                last unless $flag_type->is_multiplicable;
897
            }
898
        }
899
        else {
900 901
            push (@new_flags, { type_id => $type_id,
                                status  => $status });
902
        }
903 904
    }

905 906
    # Return the list of flags to update and/or to create.
    return (\@flags, \@new_flags);
907 908
}

909 910 911 912
=pod

=over

913
=item C<notify($flag, $old_flag, $object, $timestamp)>
914

915 916
Sends an email notification about a flag being created, fulfilled
or deleted.
917 918 919 920 921

=back

=cut

922
sub notify {
923
    my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
924

925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957
    my ($bug, $attachment);
    if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
        $attachment = $obj;
        $bug = $attachment->bug;
    }
    elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
        $bug = $obj;
    }
    else {
        # Not a good time to throw an error.
        return;
    }

    my $addressee;
    # If the flag is set to '?', maybe the requestee wants a notification.
    if ($flag && $flag->requestee_id
        && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id))
    {
        if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
            $addressee = $flag->requestee;
        }
    }
    elsif ($old_flag && $old_flag->status eq '?'
           && (!$flag || $flag->status ne '?'))
    {
        if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
            $addressee = $old_flag->setter;
        }
    }

    my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list;
    # Is there someone to notify?
    return unless ($addressee || $cc_list);
958

959 960 961 962 963
    # The email client will display the Date: header in the desired timezone,
    # so we can always use UTC here.
    $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
    $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC');

964 965 966 967
    # 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.
968
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
969
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
970

971
    my %recipients;
972
    foreach my $cc (split(/[, ]+/, $cc_list)) {
973 974 975 976 977 978
        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;
979 980
    }

981
    # Only notify if the addressee is allowed to receive the email.
982 983
    if ($addressee && $addressee->email_enabled) {
        $recipients{$addressee->email} = $addressee;
984
    }
985 986 987 988 989
    # 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) {
990
        $default_lang = Bugzilla::User->new()->setting('lang');
991 992 993
    }

    foreach my $to (keys %recipients) {
994 995 996
        # 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;
997

998
        my $vars = { 'flag'            => $flag,
999
                     'old_flag'        => $old_flag,
1000
                     'to'              => $to,
1001
                     'date'            => $timestamp,
1002 1003 1004
                     'bug'             => $bug,
                     'attachment'      => $attachment,
                     'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
1005 1006

        my $lang = $recipients{$to} ?
1007
          $recipients{$to}->setting('lang') : $default_lang;
1008 1009

        my $template = Bugzilla->template_inner($lang);
1010
        my $message;
1011
        $template->process("email/flagmail.txt.tmpl", $vars, \$message)
1012
          || ThrowTemplateError($template->error());
1013

1014
        MessageToMTA($message);
1015
    }
1016 1017
}

1018 1019 1020 1021 1022
# 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 {
1023
    my ($class, $vars) = @_;
1024 1025 1026 1027 1028 1029 1030

    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};
1031
        $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
1032 1033 1034
    }
    elsif ($target_type eq 'attachment') {
        my $attach_id = delete $vars->{attach_id};
1035
        $flags = $class->match({attach_id => $attach_id});
1036 1037 1038
    }
    else {
        ThrowCodeError('bad_arg', {argument => 'target_type',
1039
                                   function => $class . '->_flag_types'});
1040 1041 1042
    }

    # Get all available flag types for the given product and component.
1043 1044 1045
    my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {};
    my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars);
    my $flag_types = dclone($flag_data);
1046 1047 1048 1049

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

1050 1051 1052 1053 1054
    # Group existing flags per type, and skip those becoming invalid
    # (which can happen when a bug is being moved into a new product
    # or component).
    @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
    push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
1055
    return $flag_types;
1056 1057
}

1058
1;
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084

=head1 B<Methods in need of POD>

=over

=item update_activity

=item setter_id

=item bug

=item requestee_id

=item DB_COLUMNS

=item set_flag

=item type_id

=item snapshot

=item update_flags

=item update

=back