Flag.pm 34.1 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 54
use base qw(Bugzilla::Object Exporter);
@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
    $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
185
    return $self->{'type'};
186 187
}

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
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'};
}

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

    require Bugzilla::Attachment;
209
    $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
210 211 212
    return $self->{'attachment'};
}

213 214 215 216 217 218 219 220
sub bug {
    my $self = shift;

    require Bugzilla::Bug;
    $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
    return $self->{'bug'};
}

221 222 223 224
################################
## Searching/Retrieving Flags ##
################################

225 226 227 228 229
=pod

=over

=item C<match($criteria)>
230

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

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

259
    return $class->SUPER::match(@_);
260 261
}

262 263 264 265 266 267
=pod

=over

=item C<count($criteria)>

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

=back
273

274 275 276
=cut

sub count {
277 278
    my $class = shift;
    return scalar @{$class->match(@_)};
279 280
}

281
######################################################################
282
# Creating and Modifying
283
######################################################################
284

285 286
sub set_flag {
    my ($class, $obj, $params) = @_;
287

288
    my ($bug, $attachment, $obj_flag, $requestee_changed);
289 290 291 292 293 294 295 296 297 298
    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 });
    }
299

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
    # 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);
325
        }
326
        ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
327 328 329 330
        # 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;
331

332 333
        ($obj_flag, $requestee_changed) =
            $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
334
    }
335 336
    # Create a new flag.
    elsif ($params->{type_id}) {
337
        # Don't bother validating types the user didn't touch.
338 339 340 341 342 343 344 345 346 347 348
        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 });
349

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

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

357 358 359 360 361 362 363
        # 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 });
            }
        }
364

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

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

382
sub _validate {
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
    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});

399 400 401
    # The requestee ID can be undefined.
    my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);

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

408 409 410
    # 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}};
411
        return;
412
    }
413 414 415
    # Add the newly created flag to the list.
    elsif (!$obj_flag->id) {
        push(@{$flag_type->{flags}}, $obj_flag);
416
    }
417
    return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
418
}
419

420
=pod
421

422
=over
423

424
=item C<create($flag, $timestamp)>
425

426
Creates a flag record in the database.
427

428
=back
429

430
=cut
431

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

436
    my $params = {};
437
    my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
438 439 440 441

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

442
    $params->{$_} = $flag->{$_} foreach @columns;
443

444
    $params->{creation_date} = $params->{modification_date} = $timestamp;
445

446 447 448 449 450 451 452 453
    $flag = $class->SUPER::create($params);
    return $flag;
}

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

455
    my $changes = $self->SUPER::update(@_);
456

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

465
sub snapshot {
466
    my ($class, $flags) = @_;
467 468 469

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

477 478
sub update_activity {
    my ($class, $old_summaries, $new_summaries) = @_;
479

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

485 486 487
        $removed = join(", ", @$removed);
        $added = join(", ", @$added);
        return ($removed, $added);
488
    }
489 490
    return ();
}
491

492 493
sub update_flags {
    my ($class, $self, $old_self, $timestamp) = @_;
494

495 496
    my @old_summaries = $class->snapshot($old_self->flags);
    my %old_flags = map { $_->id => $_ } @{$old_self->flags};
497

498 499 500 501 502
    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;
503
            $class->notify($new_flag, undef, $self, $timestamp);
504 505
        }
        else {
506 507
            my $changes = $new_flag->update($timestamp);
            if (scalar(keys %$changes)) {
508
                $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
509
            }
510
            delete $old_flags{$new_flag->id};
511
        }
512
    }
513 514
    # These flags have been deleted.
    foreach my $old_flag (values %old_flags) {
515
        $class->notify(undef, $old_flag, $self, $timestamp);
516
        $old_flag->remove_from_db();
517
    }
518

519 520 521 522 523 524 525 526 527
    # 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);
    }
528

529 530
    my @new_summaries = $class->snapshot($self->flags);
    my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
531

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

540 541
sub retarget {
    my ($self, $obj) = @_;
542

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

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

        $self->{type_id} = $flagtype->id;
        delete $self->{type};
        $success = 1;
        last;
556
    }
557
    return $success;
558 559
}

560 561 562 563 564
# 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;
565

566 567 568 569 570 571 572 573 574 575 576
    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);
577

578
    my @removed = $class->force_retarget($flag_ids, $bug);
579

580 581 582 583 584 585 586 587 588 589 590 591 592
    $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;
}
593

594 595
sub force_retarget {
    my ($class, $flag_ids, $bug) = @_;
596 597
    my $dbh = Bugzilla->dbh;

598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
    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();
        }
614
    }
615
    return @removed;
616 617
}

618 619 620
###############################
####      Validators     ######
###############################
621

622 623
sub _set_requestee {
    my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
624

625 626
    $self->{requestee} =
      $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
627

628 629 630
    $self->{requestee_id} =
      $self->{requestee} ? $self->{requestee}->id : undef;
}
631

632 633
sub _set_setter {
    my ($self, $setter) = @_;
634

635 636 637
    $self->set('setter', $setter);
    $self->{setter_id} = $self->setter->id;
}
638

639 640
sub _set_status {
    my ($self, $status) = @_;
641

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

647 648
sub _check_requestee {
    my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
649

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

653 654
    # Store this value before updating the flag object.
    my $old_requestee = $self->requestee ? $self->requestee->login : '';
655

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

663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
    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.
        ThrowCodeError('flag_requestee_disabled', { type => $self->type })
          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;
679
            }
680 681 682 683 684 685
            else {
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
686
            }
687
        }
688 689 690 691
        # 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;
692
            }
693
            else {
694 695 696 697 698
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
699
            }
700
        }
701 702 703 704 705 706 707 708 709 710
        # 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});
            }
711 712
        }
    }
713
    return $requestee;
714 715
}

716 717
sub _check_setter {
    my ($self, $setter) = @_;
718

719 720 721 722
    # By default, the currently logged in user is the setter.
    $setter ||= Bugzilla->user;
    (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
      || ThrowCodeError('invalid_user');
723

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

728 729
    # Make sure the user is authorized to modify flags, see bug 180879:
    # - The flag exists and is unchanged.
730
    # - The flag setter can unset flag.
731 732 733 734
    # - 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})
735
            || ($status eq 'X' && $setter->id == Bugzilla->user->id)
736 737 738 739 740 741 742 743 744
            || (($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} });
    }
745

746 747 748
    # 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 '?') {
749 750 751 752
        return $self->setter;
    }
    return $setter;
}
753

754 755
sub _check_status {
    my ($self, $status) = @_;
756

757 758 759 760 761 762 763
    # - 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))
    {
764
        ThrowUserError('flag_status_invalid', { id     => $self->id,
765
                                                status => $status });
766
    }
767
    return $status;
768 769
}

770 771 772 773
######################################################################
# Utility Functions
######################################################################

774 775 776 777
=pod

=over

778
=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
779

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

=back

=cut

787 788 789
sub extract_flags_from_cgi {
    my ($class, $bug, $attachment, $vars, $skip) = @_;
    my $cgi = Bugzilla->cgi;
790

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

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

803 804 805
    # 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);
806

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

810
    return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
811

812 813 814 815 816
    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;
817

818
        my $status = $cgi->param("flag-$flag_id");
819

820 821 822 823 824 825 826 827 828 829 830 831
        # 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);
832

833 834 835 836 837 838 839 840 841 842 843 844 845 846
            # 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") || '');
        }
847

848 849 850 851 852
        push(@flags, { id        => $flag_id,
                       status    => $status,
                       requestee => $requestee_email,
                       skip_roe  => $skip });
    }
853

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

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

868
    foreach my $flag_type (@$flag_types) {
869
        my $type_id = $flag_type->id;
870

871 872 873 874
        # 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);

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

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

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

889
        my $status = $cgi->param("flag_type-$type_id");
890
        trick_taint($status);
891

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

908 909
    # Return the list of flags to update and/or to create.
    return (\@flags, \@new_flags);
910 911
}

912 913 914 915
=pod

=over

916
=item C<notify($flag, $old_flag, $object, $timestamp)>
917

918 919
Sends an email notification about a flag being created, fulfilled
or deleted.
920 921 922 923 924

=back

=cut

925
sub notify {
926
    my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
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 958 959 960
    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);
961

962 963 964 965 966
    # 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');

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

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

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

    foreach my $to (keys %recipients) {
997 998 999
        # 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;
1000

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

        my $lang = $recipients{$to} ?
1010
          $recipients{$to}->setting('lang') : $default_lang;
1011 1012

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

1017
        MessageToMTA($message);
1018
    }
1019 1020
}

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

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

    # Get all available flag types for the given product and component.
1046 1047 1048
    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);
1049 1050 1051 1052

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

1053 1054 1055 1056 1057
    # 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;
1058
    return $flag_types;
1059 1060
}

1061
1;