Flag.pm 34.8 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 10 11
use strict;

package Bugzilla::Flag;

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

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

=item *

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

=back

=cut

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

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

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

###############################
####    Initialization     ####
###############################
58

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

66 67
use constant SKIP_REQUESTEE_ON_ERROR => 1;

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
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';
}
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

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

99 100 101
###############################
####      Accessors      ######
###############################
102

103
=head2 METHODS
104

105 106
=over

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

Returns the ID of the flag.

=item C<name>

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

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

123 124 125
=item C<status>

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

127 128 129 130 131 132 133 134
=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.

135 136
=back

137
=cut
138

139 140 141 142 143 144 145 146
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'}; }
147 148
sub creation_date     { return $_[0]->{'creation_date'};     }
sub modification_date { return $_[0]->{'modification_date'}; }
149 150 151 152 153 154 155 156 157 158 159

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

=pod

=over

=item C<type>

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

162
=item C<setter>
163

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

171 172 173 174 175
=item C<attachment>

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

176 177 178 179 180 181 182
=back

=cut

sub type {
    my $self = shift;

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

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

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

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

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

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

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

224 225 226 227 228
=pod

=over

=item C<match($criteria)>
229

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

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

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

261 262 263 264 265 266
=pod

=over

=item C<count($criteria)>

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

=back
272

273 274 275
=cut

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

280
######################################################################
281
# Creating and Modifying
282
######################################################################
283

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

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

299 300 301 302 303 304
    # Make sure the user can change flags
    my $privs;
    $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs)
        || ThrowUserError('illegal_change',
                          { field => 'flagtypes.name', privs => $privs });

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

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

355
        # Make sure the flag type is active.
356 357
        $flagtype->is_active
          || ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
358

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

362 363 364 365 366 367 368
        # 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 });
            }
        }
369

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

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

387
sub _validate {
388 389 390 391 392 393 394 395 396 397 398 399 400 401
    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});
402
    $obj_flag->_set_requestee($params->{requestee}, $bug, $attachment, $params->{skip_roe});
403

404 405 406
    # The requestee ID can be undefined.
    my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);

407 408
    # The setter field MUST NOT be updated if neither the status
    # nor the requestee fields changed.
409
    if (($obj_flag->status ne $old_status) || $requestee_changed) {
410
        $obj_flag->_set_setter($params->{setter});
411
    }
412

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

425
=pod
426

427
=over
428

429
=item C<create($flag, $timestamp)>
430

431
Creates a flag record in the database.
432

433
=back
434

435
=cut
436

437 438
sub create {
    my ($class, $flag, $timestamp) = @_;
439
    $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
440

441
    my $params = {};
442
    my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
443 444 445 446

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

447
    $params->{$_} = $flag->{$_} foreach @columns;
448

449
    $params->{creation_date} = $params->{modification_date} = $timestamp;
450

451 452 453 454 455 456 457 458
    $flag = $class->SUPER::create($params);
    return $flag;
}

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

460
    my $changes = $self->SUPER::update(@_);
461

462 463 464
    if (scalar(keys %$changes)) {
        $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
                 undef, ($timestamp, $self->id));
465
        $self->{'modification_date'} = format_time($timestamp, '%Y.%m.%d %T');
466 467
    }
    return $changes;
468 469
}

470
sub snapshot {
471
    my ($class, $flags) = @_;
472 473 474

    my @summaries;
    foreach my $flag (@$flags) {
475
        my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
476
        $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
477 478 479 480 481
        push(@summaries, $summary);
    }
    return @summaries;
}

482 483
sub update_activity {
    my ($class, $old_summaries, $new_summaries) = @_;
484

485 486 487 488
    my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
    if (scalar @$removed || scalar @$added) {
        # Remove flag requester/setter information
        foreach (@$removed, @$added) { s/^[^:]+:// }
489

490 491 492
        $removed = join(", ", @$removed);
        $added = join(", ", @$added);
        return ($removed, $added);
493
    }
494 495
    return ();
}
496

497 498
sub update_flags {
    my ($class, $self, $old_self, $timestamp) = @_;
499

500 501
    my @old_summaries = $class->snapshot($old_self->flags);
    my %old_flags = map { $_->id => $_ } @{$old_self->flags};
502

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

524 525 526 527 528 529 530 531 532
    # 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);
    }
533

534 535
    my @new_summaries = $class->snapshot($self->flags);
    my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
536

537
    Bugzilla::Hook::process('flag_end_of_update', { object    => $self,
538 539 540 541
                                                    timestamp => $timestamp,
                                                    old_flags => \@old_summaries,
                                                    new_flags => \@new_summaries,
                                                  });
542
    return @changes;
543 544
}

545 546
sub retarget {
    my ($self, $obj) = @_;
547

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

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

        $self->{type_id} = $flagtype->id;
        delete $self->{type};
        $success = 1;
        last;
561
    }
562
    return $success;
563 564
}

565 566 567 568 569
# 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;
570

571 572 573 574 575 576 577 578 579 580 581
    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);
582

583
    my @removed = $class->force_retarget($flag_ids, $bug);
584

585 586 587 588 589 590 591 592 593 594 595 596 597
    $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;
}
598

599 600
sub force_retarget {
    my ($class, $flag_ids, $bug) = @_;
601 602
    my $dbh = Bugzilla->dbh;

603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    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();
        }
619
    }
620
    return @removed;
621 622
}

623 624 625
###############################
####      Validators     ######
###############################
626

627
sub _set_requestee {
628
    my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
629

630
    $self->{requestee} =
631
      $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error);
632

633 634 635
    $self->{requestee_id} =
      $self->{requestee} ? $self->{requestee}->id : undef;
}
636

637 638
sub _set_setter {
    my ($self, $setter) = @_;
639

640 641 642
    $self->set('setter', $setter);
    $self->{setter_id} = $self->setter->id;
}
643

644 645
sub _set_status {
    my ($self, $status) = @_;
646

647 648 649 650
    # Store the old flag status. It's needed by _check_setter().
    $self->{_old_status} = $self->status;
    $self->set('status', $status);
}
651

652
sub _check_requestee {
653
    my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
654

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

658 659
    # Store this value before updating the flag object.
    my $old_requestee = $self->requestee ? $self->requestee->login : '';
660

661 662 663 664 665 666
    if ($self->status eq '?' && $requestee) {
        $requestee = Bugzilla::User->check($requestee);
    }
    else {
        undef $requestee;
    }
667

668 669 670 671 672
    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.
673
        ThrowUserError('flag_requestee_disabled', { type => $self->type })
674 675 676 677 678 679
          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
680 681 682 683 684 685 686 687 688 689
        # into account here. In this case, old group restrictions matter.
        # However, if the user has just been changed to the assignee,
        # qa_contact, or added to the cc list of the bug and the bug
        # is cclist_accessible, the requestee is allowed.
        if (!$requestee->can_see_bug($self->bug_id)
            && (!$bug->cclist_accessible
                || !grep($_->id == $requestee->id, @{ $bug->cc_users })
            && $requestee->id != $bug->assigned_to->id
            && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)))
        {
690 691
            if ($skip_requestee_on_error) {
                undef $requestee;
692
            }
693 694 695 696 697 698
            else {
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
699
            }
700
        }
701 702 703 704
        # 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;
705
            }
706
            else {
707 708 709 710 711
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
712
            }
713
        }
714 715 716 717 718 719 720 721 722 723
        # 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});
            }
724 725
        }
    }
726
    return $requestee;
727 728
}

729 730
sub _check_setter {
    my ($self, $setter) = @_;
731

732 733 734
    # By default, the currently logged in user is the setter.
    $setter ||= Bugzilla->user;
    (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
735
      || ThrowUserError('invalid_user');
736

737 738 739
    # set_status() has already been called. So this refers
    # to the new flag status.
    my $status = $self->status;
740

741 742
    # Make sure the user is authorized to modify flags, see bug 180879:
    # - The flag exists and is unchanged.
743
    # - The flag setter can unset flag.
744 745 746 747
    # - 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})
748
            || ($status eq 'X' && $setter->id == Bugzilla->user->id)
749 750 751 752 753 754 755 756 757
            || (($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} });
    }
758

759 760 761
    # 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 '?') {
762 763 764 765
        return $self->setter;
    }
    return $setter;
}
766

767 768
sub _check_status {
    my ($self, $status) = @_;
769

770 771 772 773 774 775 776
    # - 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))
    {
777
        ThrowUserError('flag_status_invalid', { id     => $self->id,
778
                                                status => $status });
779
    }
780
    return $status;
781 782
}

783 784 785 786
######################################################################
# Utility Functions
######################################################################

787 788 789 790
=pod

=over

791
=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
792

793 794
Checks whether or not there are new flags to create and returns an
array of hashes. This array is then passed to Flag::create().
795 796 797 798 799

=back

=cut

800 801 802
sub extract_flags_from_cgi {
    my ($class, $bug, $attachment, $vars, $skip) = @_;
    my $cgi = Bugzilla->cgi;
803

804
    my $match_status = Bugzilla::User::match_field({
805
        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
806
    }, undef, $skip);
807

808 809 810
    $vars->{'match_field'} = 'requestee';
    if ($match_status == USER_MATCH_FAILED) {
        $vars->{'message'} = 'user_match_failed';
811
    }
812 813
    elsif ($match_status == USER_MATCH_MULTIPLE) {
        $vars->{'message'} = 'user_match_multiple';
814 815
    }

816 817 818
    # 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);
819

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

823
    return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
824

825 826 827 828 829
    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;
830

831
        my $status = $cgi->param("flag-$flag_id");
832

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

846 847 848 849 850 851 852 853 854 855 856 857 858 859
            # 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") || '');
        }
860

861 862 863 864 865
        push(@flags, { id        => $flag_id,
                       status    => $status,
                       requestee => $requestee_email,
                       skip_roe  => $skip });
    }
866

867
    # Get a list of active flag types available for this product/component.
868
    my $flag_types = Bugzilla::FlagType::match(
869
        { 'product_id'   => $bug->{'product_id'},
870
          'component_id' => $bug->{'component_id'},
871 872
          'is_active'    => 1 });

873
    foreach my $flagtype_id (@flagtype_ids) {
874
        # Checks if there are unexpected flags for the product/component.
875 876
        if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
            $vars->{'message'} = 'unexpected_flag_types';
877 878 879 880
            last;
        }
    }

881
    foreach my $flag_type (@$flag_types) {
882
        my $type_id = $flag_type->id;
883

884 885 886 887
        # 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);

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

891
        # Get the number of flags of this type already set for this target.
892
        my $has_flags = $class->count(
893
            { 'type_id'     => $type_id,
894 895 896
              'target_type' => $attachment ? 'attachment' : 'bug',
              'bug_id'      => $bug->bug_id,
              'attach_id'   => $attachment ? $attachment->id : undef });
897 898

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

902
        my $status = $cgi->param("flag_type-$type_id");
903
        trick_taint($status);
904

905
        my @logins = $cgi->param("requestee_type-$type_id");
906
        if ($status eq "?" && scalar(@logins)) {
907
            foreach my $login (@logins) {
908 909 910 911
                push (@new_flags, { type_id   => $type_id,
                                    status    => $status,
                                    requestee => $login,
                                    skip_roe  => $skip });
912
                last unless $flag_type->is_multiplicable;
913
            }
914
        }
915
        else {
916 917
            push (@new_flags, { type_id => $type_id,
                                status  => $status });
918
        }
919 920
    }

921 922
    # Return the list of flags to update and/or to create.
    return (\@flags, \@new_flags);
923 924
}

925 926 927 928
=pod

=over

929
=item C<notify($flag, $old_flag, $object, $timestamp)>
930

931 932
Sends an email notification about a flag being created, fulfilled
or deleted.
933 934 935 936 937

=back

=cut

938
sub notify {
939
    my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
940

941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973
    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);
974

975 976 977 978 979
    # 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');

980 981 982 983
    # 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.
984
    my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
985
    my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
986

987
    my %recipients;
988
    foreach my $cc (split(/[, ]+/, $cc_list)) {
989 990 991 992 993 994
        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;
995 996
    }

997
    # Only notify if the addressee is allowed to receive the email.
998 999
    if ($addressee && $addressee->email_enabled) {
        $recipients{$addressee->email} = $addressee;
1000
    }
1001 1002 1003 1004 1005
    # 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) {
1006
        $default_lang = Bugzilla::User->new()->setting('lang');
1007 1008 1009
    }

    foreach my $to (keys %recipients) {
1010 1011 1012
        # 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;
1013

1014
        my $vars = { 'flag'            => $flag,
1015
                     'old_flag'        => $old_flag,
1016
                     'to'              => $to,
1017
                     'date'            => $timestamp,
1018 1019 1020
                     'bug'             => $bug,
                     'attachment'      => $attachment,
                     'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
1021 1022

        my $lang = $recipients{$to} ?
1023
          $recipients{$to}->setting('lang') : $default_lang;
1024 1025

        my $template = Bugzilla->template_inner($lang);
1026
        my $message;
1027
        $template->process("email/flagmail.txt.tmpl", $vars, \$message)
1028
          || ThrowTemplateError($template->error());
1029

1030
        MessageToMTA($message);
1031
    }
1032 1033
}

1034 1035 1036 1037 1038
# 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 {
1039
    my ($class, $vars) = @_;
1040 1041 1042 1043 1044 1045 1046

    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};
1047
        $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
1048 1049 1050
    }
    elsif ($target_type eq 'attachment') {
        my $attach_id = delete $vars->{attach_id};
1051
        $flags = $class->match({attach_id => $attach_id});
1052 1053 1054
    }
    else {
        ThrowCodeError('bad_arg', {argument => 'target_type',
1055
                                   function => $class . '->_flag_types'});
1056 1057 1058
    }

    # Get all available flag types for the given product and component.
1059 1060 1061
    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);
1062 1063 1064 1065

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

1066 1067 1068 1069 1070
    # 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;
1071
    return $flag_types;
1072 1073
}

1074
1;