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

8 9
package Bugzilla::Flag;

10 11 12
use 5.10.1;
use strict;

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
=head1 NAME

Bugzilla::Flag - A module to deal with Bugzilla flag values.

=head1 SYNOPSIS

Flag.pm provides an interface to flags as stored in Bugzilla.
See below for more information.

=head1 NOTES

=over

=item *

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

=item *

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

=back

=cut

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

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

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

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

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

67 68
use constant SKIP_REQUESTEE_ON_ERROR => 1;

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

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

use constant VALIDATORS => {
};

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

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

104
=head2 METHODS
105

106 107
=over

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

Returns the ID of the flag.

=item C<name>

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

116 117 118 119 120 121 122 123
=item C<bug_id>

Returns the ID of the bug this flag belongs to.

=item C<attach_id>

Returns the ID of the attachment this flag belongs to, if any.

124 125 126
=item C<status>

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

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

Returns the timestamp when the flag was created.

=item C<modification_date>

Returns the timestamp when the flag was last modified.

136 137
=back

138
=cut
139

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

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

=pod

=over

=item C<type>

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

163
=item C<setter>
164

165 166 167 168 169 170 171
Returns the user who set the flag, as a Bugzilla::User object.

=item C<requestee>

Returns the user who has been requested to set the flag, as a
Bugzilla::User object.

172 173 174 175 176
=item C<attachment>

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

177 178 179 180 181 182 183
=back

=cut

sub type {
    my $self = shift;

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

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

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

sub requestee {
    my $self = shift;

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

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

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

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

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

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

222 223 224 225 226
=pod

=over

=item C<match($criteria)>
227

228 229 230 231 232 233 234 235 236
Queries the database for flags matching the given criteria
(specified as a hash of field names and their matching values)
and returns an array of matching records.

=back

=cut

sub match {
237
    my $class = shift;
238
    my ($criteria) = @_;
239

240 241 242
    # If the caller specified only bug or attachment flags,
    # limit the query to those kinds of flags.
    if (my $type = delete $criteria->{'target_type'}) {
243
        if ($type eq 'bug') {
244 245
            $criteria->{'attach_id'} = IS_NULL;
        }
246 247 248
        elsif (!defined $criteria->{'attach_id'}) {
            $criteria->{'attach_id'} = NOT_NULL;
        }
249
    }
250 251 252 253 254
    # Flag->snapshot() calls Flag->match() with bug_id and attach_id
    # as hash keys, even if attach_id is undefined.
    if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
        $criteria->{'attach_id'} = IS_NULL;
    }
255

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

259 260 261 262 263 264
=pod

=over

=item C<count($criteria)>

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

=back
270

271 272 273
=cut

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

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

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

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

297 298 299 300 301 302
    # 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 });

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

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

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

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

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

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

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

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

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

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

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

423
=pod
424

425
=over
426

427
=item C<create($flag, $timestamp)>
428

429
Creates a flag record in the database.
430

431
=back
432

433
=cut
434

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

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

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

445
    $params->{$_} = $flag->{$_} foreach @columns;
446

447
    $params->{creation_date} = $params->{modification_date} = $timestamp;
448

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

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

458
    my $changes = $self->SUPER::update(@_);
459

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

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

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

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

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

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

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

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

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

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

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

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

543 544
sub retarget {
    my ($self, $obj) = @_;
545

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

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

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

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

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

581
    my @removed = $class->force_retarget($flag_ids, $bug);
582

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

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

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

621 622 623
###############################
####      Validators     ######
###############################
624

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

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

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

635 636
sub _set_setter {
    my ($self, $setter) = @_;
637

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

642 643
sub _set_status {
    my ($self, $status) = @_;
644

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

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

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

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

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

666 667 668 669 670
    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.
671
        ThrowUserError('flag_type_requestee_disabled', { type => $self->type })
672 673
          if !$self->type->is_requesteeble;

674 675 676 677 678
        # You can't ask a disabled account, as they don't have the ability to
        # set the flag.
        ThrowUserError('flag_requestee_disabled', { requestee => $requestee })
          if !$requestee->is_enabled;

679 680 681 682
        # 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
683 684 685 686 687 688 689 690 691 692
        # 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)))
        {
693 694
            if ($skip_requestee_on_error) {
                undef $requestee;
695
            }
696 697 698 699 700 701
            else {
                ThrowUserError('flag_requestee_unauthorized',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
702
            }
703
        }
704 705 706 707
        # 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;
708
            }
709
            else {
710 711 712 713 714
                ThrowUserError('flag_requestee_unauthorized_attachment',
                               { flag_type  => $self->type,
                                 requestee  => $requestee,
                                 bug_id     => $self->bug_id,
                                 attach_id  => $self->attach_id });
715
            }
716
        }
717 718 719 720 721 722 723 724 725 726
        # 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});
            }
727 728
        }
    }
729
    return $requestee;
730 731
}

732 733
sub _check_setter {
    my ($self, $setter) = @_;
734

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

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

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

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

770 771
sub _check_status {
    my ($self, $status) = @_;
772

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

786 787 788 789
######################################################################
# Utility Functions
######################################################################

790 791 792 793
=pod

=over

794
=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
795

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

=back

=cut

803 804 805
sub extract_flags_from_cgi {
    my ($class, $bug, $attachment, $vars, $skip) = @_;
    my $cgi = Bugzilla->cgi;
806

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

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

819 820 821
    # 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);
822

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

826
    return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
827

828 829 830 831 832
    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;
833

834
        my $status = $cgi->param("flag-$flag_id");
835

836 837 838 839 840 841 842 843 844 845 846 847
        # 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);
848

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

864 865 866 867 868
        push(@flags, { id        => $flag_id,
                       status    => $status,
                       requestee => $requestee_email,
                       skip_roe  => $skip });
    }
869

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

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

884
    foreach my $flag_type (@$flag_types) {
885
        my $type_id = $flag_type->id;
886

887 888 889 890
        # 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);

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

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

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

905
        my $status = $cgi->param("flag_type-$type_id");
906
        trick_taint($status);
907

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

924 925
    # Return the list of flags to update and/or to create.
    return (\@flags, \@new_flags);
926 927
}

928 929 930 931
=pod

=over

932
=item C<notify($flag, $old_flag, $object, $timestamp)>
933

934 935
Sends an email notification about a flag being created, fulfilled
or deleted.
936 937 938 939 940

=back

=cut

941
sub notify {
942
    my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
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 974 975 976
    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);
977

978 979 980 981 982
    # 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');

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

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

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

    foreach my $to (keys %recipients) {
1013 1014 1015
        # 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;
1016

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

        my $lang = $recipients{$to} ?
1026
          $recipients{$to}->setting('lang') : $default_lang;
1027 1028

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

1033
        MessageToMTA($message);
1034
    }
1035 1036
}

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

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

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

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

1069 1070 1071 1072 1073
    # 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;
1074
    return $flag_types;
1075 1076
}

1077
1;
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103

=head1 B<Methods in need of POD>

=over

=item update_activity

=item setter_id

=item bug

=item requestee_id

=item DB_COLUMNS

=item set_flag

=item type_id

=item snapshot

=item update_flags

=item update

=back