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

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

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

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

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

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

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

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

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

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

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

419
=pod
420

421
=over
422

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

425
Creates a flag record in the database.
426

427
=back
428

429
=cut
430

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

617 618 619
###############################
####      Validators     ######
###############################
620

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

769 770 771 772
######################################################################
# Utility Functions
######################################################################

773 774 775 776
=pod

=over

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

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

=back

=cut

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

911 912 913 914
=pod

=over

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

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

=back

=cut

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1060 1061 1062 1063 1064 1065 1066 1067
=head1 SEE ALSO

=over

=item B<Bugzilla::FlagType>

=back

1068

1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
=head1 CONTRIBUTORS

=over

=item Myk Melez <myk@mozilla.org>

=item Jouni Heikniemi <jouni@heikniemi.net>

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

1079 1080
=item Frédéric Buclin <LpSolit@gmail.com>

1081 1082 1083 1084
=back

=cut

1085
1;