1233 lines
40 KiB
Perl
1233 lines
40 KiB
Perl
# 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/.
|
|
#
|
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
# defined by the Mozilla Public License, v. 2.0.
|
|
|
|
package Bugzilla::Flag;
|
|
|
|
use 5.10.1;
|
|
use strict;
|
|
use warnings;
|
|
|
|
=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 *
|
|
|
|
Import relevant functions from that script.
|
|
|
|
=item *
|
|
|
|
Use of private functions / variables outside this module may lead to
|
|
unexpected results after an upgrade. Please avoid using private
|
|
functions in other files/modules. Private functions are functions
|
|
whose names start with _ or a re specifically noted as being private.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
use Scalar::Util qw(blessed);
|
|
use Storable qw(dclone);
|
|
|
|
use Bugzilla::FlagType;
|
|
use Bugzilla::Hook;
|
|
use Bugzilla::User;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Mailer;
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Field;
|
|
|
|
use parent qw(Bugzilla::Object Exporter);
|
|
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
|
|
|
|
###############################
|
|
#### Initialization ####
|
|
###############################
|
|
|
|
use constant DB_TABLE => 'flags';
|
|
use constant LIST_ORDER => 'id';
|
|
# Flags are tracked in bugs_activity.
|
|
use constant AUDIT_CREATES => 0;
|
|
use constant AUDIT_UPDATES => 0;
|
|
use constant AUDIT_REMOVES => 0;
|
|
|
|
use constant SKIP_REQUESTEE_ON_ERROR => 1;
|
|
|
|
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';
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
###############################
|
|
#### Accessors ######
|
|
###############################
|
|
|
|
=head2 METHODS
|
|
|
|
=over
|
|
|
|
=item C<id>
|
|
|
|
Returns the ID of the flag.
|
|
|
|
=item C<name>
|
|
|
|
Returns the name of the flagtype the flag belongs to.
|
|
|
|
=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.
|
|
|
|
=item C<status>
|
|
|
|
Returns the status '+', '-', '?' of the flag.
|
|
|
|
=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.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
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'}; }
|
|
sub creation_date { return $_[0]->{'creation_date'}; }
|
|
sub modification_date { return $_[0]->{'modification_date'}; }
|
|
|
|
###############################
|
|
#### Methods ####
|
|
###############################
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<type>
|
|
|
|
Returns the type of the flag, as a Bugzilla::FlagType object.
|
|
|
|
=item C<setter>
|
|
|
|
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.
|
|
|
|
=item C<attachment>
|
|
|
|
Returns the attachment object the flag belongs to if the flag
|
|
is an attachment flag, else undefined.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub type {
|
|
my $self = shift;
|
|
|
|
return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
|
|
}
|
|
|
|
sub setter {
|
|
my $self = shift;
|
|
|
|
return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 });
|
|
}
|
|
|
|
sub requestee {
|
|
my $self = shift;
|
|
|
|
if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
|
|
$self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 });
|
|
}
|
|
return $self->{'requestee'};
|
|
}
|
|
|
|
sub attachment {
|
|
my $self = shift;
|
|
return undef unless $self->attach_id;
|
|
|
|
require Bugzilla::Attachment;
|
|
return $self->{'attachment'}
|
|
||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 });
|
|
}
|
|
|
|
sub bug {
|
|
my $self = shift;
|
|
|
|
require Bugzilla::Bug;
|
|
return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 });
|
|
}
|
|
|
|
################################
|
|
## Searching/Retrieving Flags ##
|
|
################################
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<match($criteria)>
|
|
|
|
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 {
|
|
my $class = shift;
|
|
my ($criteria) = @_;
|
|
|
|
# If the caller specified only bug or attachment flags,
|
|
# limit the query to those kinds of flags.
|
|
if (my $type = delete $criteria->{'target_type'}) {
|
|
if ($type eq 'bug') {
|
|
$criteria->{'attach_id'} = IS_NULL;
|
|
}
|
|
elsif (!defined $criteria->{'attach_id'}) {
|
|
$criteria->{'attach_id'} = NOT_NULL;
|
|
}
|
|
}
|
|
# 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;
|
|
}
|
|
|
|
return $class->SUPER::match(@_);
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<count($criteria)>
|
|
|
|
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 count {
|
|
my $class = shift;
|
|
return scalar @{$class->match(@_)};
|
|
}
|
|
|
|
######################################################################
|
|
# Creating and Modifying
|
|
######################################################################
|
|
|
|
sub set_flag {
|
|
my ($class, $obj, $params) = @_;
|
|
|
|
my ($bug, $attachment, $obj_flag, $requestee_changed);
|
|
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 });
|
|
}
|
|
|
|
# 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 });
|
|
|
|
# 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);
|
|
}
|
|
($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
|
|
# 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;
|
|
|
|
($obj_flag, $requestee_changed) =
|
|
$class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
|
|
}
|
|
# Create a new flag.
|
|
elsif ($params->{type_id}) {
|
|
# Don't bother validating types the user didn't touch.
|
|
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 });
|
|
|
|
# Make sure the flag type is active.
|
|
$flagtype->is_active
|
|
|| ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
|
|
|
|
# Extract the current flagtype object from the object.
|
|
my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};
|
|
|
|
# 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 });
|
|
}
|
|
}
|
|
|
|
($obj_flag, $requestee_changed) =
|
|
$class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
|
|
}
|
|
else {
|
|
ThrowCodeError('param_required', { function => $class . '->set_flag',
|
|
param => 'id/type_id' });
|
|
}
|
|
|
|
if ($obj_flag
|
|
&& $requestee_changed
|
|
&& $obj_flag->requestee_id
|
|
&& $obj_flag->requestee->setting('requestee_cc') eq 'on')
|
|
{
|
|
$bug->add_cc($obj_flag->requestee);
|
|
}
|
|
}
|
|
|
|
sub _validate {
|
|
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}, $bug, $attachment, $params->{skip_roe});
|
|
|
|
# The requestee ID can be undefined.
|
|
my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);
|
|
|
|
# The setter field MUST NOT be updated if neither the status
|
|
# nor the requestee fields changed.
|
|
if (($obj_flag->status ne $old_status) || $requestee_changed) {
|
|
$obj_flag->_set_setter($params->{setter});
|
|
}
|
|
|
|
# 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}};
|
|
return;
|
|
}
|
|
# Add the newly created flag to the list.
|
|
elsif (!$obj_flag->id) {
|
|
push(@{$flag_type->{flags}}, $obj_flag);
|
|
}
|
|
return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<create($flag, $timestamp)>
|
|
|
|
Creates a flag record in the database.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub create {
|
|
my ($class, $flag, $timestamp) = @_;
|
|
$timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
|
|
|
my $params = {};
|
|
my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
|
|
|
|
# Some columns use date formatting so use alias instead
|
|
@columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns;
|
|
|
|
$params->{$_} = $flag->{$_} foreach @columns;
|
|
|
|
$params->{creation_date} = $params->{modification_date} = $timestamp;
|
|
|
|
$flag = $class->SUPER::create($params);
|
|
return $flag;
|
|
}
|
|
|
|
sub update {
|
|
my $self = shift;
|
|
my $dbh = Bugzilla->dbh;
|
|
my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
|
|
|
my $changes = $self->SUPER::update(@_);
|
|
|
|
if (scalar(keys %$changes)) {
|
|
$dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
|
|
undef, ($timestamp, $self->id));
|
|
$self->{'modification_date'} =
|
|
format_time($timestamp, '%Y.%m.%d %T', Bugzilla->local_timezone);
|
|
Bugzilla->memcached->clear({ table => 'flags', id => $self->id });
|
|
}
|
|
return $changes;
|
|
}
|
|
|
|
sub snapshot {
|
|
my ($class, $flags) = @_;
|
|
|
|
my @summaries;
|
|
foreach my $flag (@$flags) {
|
|
my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
|
|
$summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
|
|
push(@summaries, $summary);
|
|
}
|
|
return @summaries;
|
|
}
|
|
|
|
sub update_activity {
|
|
my ($class, $old_summaries, $new_summaries) = @_;
|
|
|
|
my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
|
|
if (scalar @$removed || scalar @$added) {
|
|
# Remove flag requester/setter information
|
|
foreach (@$removed, @$added) { s/^[^:]+:// }
|
|
|
|
$removed = join(", ", @$removed);
|
|
$added = join(", ", @$added);
|
|
return ($removed, $added);
|
|
}
|
|
return ();
|
|
}
|
|
|
|
sub update_flags {
|
|
my ($class, $self, $old_self, $timestamp) = @_;
|
|
|
|
my @old_summaries = $class->snapshot($old_self->flags);
|
|
my %old_flags = map { $_->id => $_ } @{$old_self->flags};
|
|
|
|
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;
|
|
$class->notify($new_flag, undef, $self, $timestamp);
|
|
}
|
|
else {
|
|
my $changes = $new_flag->update($timestamp);
|
|
if (scalar(keys %$changes)) {
|
|
$class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp);
|
|
}
|
|
delete $old_flags{$new_flag->id};
|
|
}
|
|
}
|
|
# These flags have been deleted.
|
|
foreach my $old_flag (values %old_flags) {
|
|
$class->notify(undef, $old_flag, $self, $timestamp);
|
|
$old_flag->remove_from_db();
|
|
}
|
|
|
|
# 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);
|
|
}
|
|
|
|
my @new_summaries = $class->snapshot($self->flags);
|
|
my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
|
|
|
|
Bugzilla::Hook::process('flag_end_of_update', { object => $self,
|
|
timestamp => $timestamp,
|
|
old_flags => \@old_summaries,
|
|
new_flags => \@new_summaries,
|
|
});
|
|
return @changes;
|
|
}
|
|
|
|
sub retarget {
|
|
my ($self, $obj) = @_;
|
|
|
|
my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};
|
|
|
|
my $success = 0;
|
|
foreach my $flagtype (@flagtypes) {
|
|
next if !$flagtype->is_active;
|
|
next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}});
|
|
next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype))
|
|
|| $self->setter->can_set_flag($flagtype));
|
|
|
|
$self->{type_id} = $flagtype->id;
|
|
delete $self->{type};
|
|
$success = 1;
|
|
last;
|
|
}
|
|
return $success;
|
|
}
|
|
|
|
# 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;
|
|
|
|
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);
|
|
|
|
my @removed = $class->force_retarget($flag_ids, $bug);
|
|
|
|
$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;
|
|
}
|
|
|
|
sub force_retarget {
|
|
my ($class, $flag_ids, $bug) = @_;
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
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));
|
|
Bugzilla->memcached->clear({ table => 'flags', 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();
|
|
}
|
|
}
|
|
return @removed;
|
|
}
|
|
|
|
###############################
|
|
#### Validators ######
|
|
###############################
|
|
|
|
sub _set_requestee {
|
|
my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
|
|
|
|
$self->{requestee} =
|
|
$self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error);
|
|
|
|
$self->{requestee_id} =
|
|
$self->{requestee} ? $self->{requestee}->id : undef;
|
|
}
|
|
|
|
sub _set_setter {
|
|
my ($self, $setter) = @_;
|
|
|
|
$self->set('setter', $setter);
|
|
$self->{setter_id} = $self->setter->id;
|
|
}
|
|
|
|
sub _set_status {
|
|
my ($self, $status) = @_;
|
|
|
|
# Store the old flag status. It's needed by _check_setter().
|
|
$self->{_old_status} = $self->status;
|
|
$self->set('status', $status);
|
|
}
|
|
|
|
sub _check_requestee {
|
|
my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
|
|
|
|
# If the flag status is not "?", then no requestee can be defined.
|
|
return undef if ($self->status ne '?');
|
|
|
|
# Store this value before updating the flag object.
|
|
my $old_requestee = $self->requestee ? $self->requestee->login : '';
|
|
|
|
if ($self->status eq '?' && $requestee) {
|
|
$requestee = Bugzilla::User->check($requestee);
|
|
}
|
|
else {
|
|
undef $requestee;
|
|
}
|
|
|
|
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 them or leave them alone.
|
|
ThrowUserError('flag_type_requestee_disabled', { type => $self->type })
|
|
if !$self->type->is_requesteeble;
|
|
|
|
# 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;
|
|
|
|
# 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 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)))
|
|
{
|
|
if ($skip_requestee_on_error) {
|
|
undef $requestee;
|
|
}
|
|
else {
|
|
ThrowUserError('flag_requestee_unauthorized',
|
|
{ flag_type => $self->type,
|
|
requestee => $requestee,
|
|
bug_id => $self->bug_id,
|
|
attach_id => $self->attach_id });
|
|
}
|
|
}
|
|
# 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;
|
|
}
|
|
else {
|
|
ThrowUserError('flag_requestee_unauthorized_attachment',
|
|
{ flag_type => $self->type,
|
|
requestee => $requestee,
|
|
bug_id => $self->bug_id,
|
|
attach_id => $self->attach_id });
|
|
}
|
|
}
|
|
# 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});
|
|
}
|
|
}
|
|
}
|
|
return $requestee;
|
|
}
|
|
|
|
sub _check_setter {
|
|
my ($self, $setter) = @_;
|
|
|
|
# By default, the currently logged in user is the setter.
|
|
$setter ||= Bugzilla->user;
|
|
(blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
|
|
|| ThrowUserError('invalid_user');
|
|
|
|
# set_status() has already been called. So this refers
|
|
# to the new flag status.
|
|
my $status = $self->status;
|
|
|
|
# Make sure the user is authorized to modify flags, see bug 180879:
|
|
# - The flag exists and is unchanged.
|
|
# - The flag setter can unset flag.
|
|
# - 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})
|
|
|| ($status eq 'X' && $setter->id == Bugzilla->user->id)
|
|
|| (($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} });
|
|
}
|
|
|
|
# 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 '?') {
|
|
return $self->setter;
|
|
}
|
|
return $setter;
|
|
}
|
|
|
|
sub _check_status {
|
|
my ($self, $status) = @_;
|
|
|
|
# - 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))
|
|
{
|
|
ThrowUserError('flag_status_invalid', { id => $self->id,
|
|
status => $status });
|
|
}
|
|
return $status;
|
|
}
|
|
|
|
######################################################################
|
|
# Utility Functions
|
|
######################################################################
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<extract_flags_from_cgi($bug, $attachment, $hr_vars)>
|
|
|
|
Checks whether or not there are new flags to create and returns an
|
|
array of hashes. This array is then passed to Flag::create().
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub extract_flags_from_cgi {
|
|
my ($class, $bug, $attachment, $vars, $skip) = @_;
|
|
my $cgi = Bugzilla->cgi;
|
|
|
|
my $match_status = Bugzilla::User::match_field({
|
|
'^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
|
|
}, undef, $skip);
|
|
|
|
$vars->{'match_field'} = 'requestee';
|
|
if ($match_status == USER_MATCH_FAILED) {
|
|
$vars->{'message'} = 'user_match_failed';
|
|
}
|
|
elsif ($match_status == USER_MATCH_MULTIPLE) {
|
|
$vars->{'message'} = 'user_match_multiple';
|
|
}
|
|
|
|
# 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);
|
|
|
|
# Extract a list of existing flag IDs.
|
|
my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
|
|
|
|
return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));
|
|
|
|
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;
|
|
|
|
my $status = $cgi->param("flag-$flag_id");
|
|
|
|
# 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);
|
|
|
|
# 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") || '');
|
|
}
|
|
|
|
push(@flags, { id => $flag_id,
|
|
status => $status,
|
|
requestee => $requestee_email,
|
|
skip_roe => $skip });
|
|
}
|
|
|
|
# Get a list of active flag types available for this product/component.
|
|
my $flag_types = Bugzilla::FlagType::match(
|
|
{ 'product_id' => $bug->{'product_id'},
|
|
'component_id' => $bug->{'component_id'},
|
|
'is_active' => 1 });
|
|
|
|
foreach my $flagtype_id (@flagtype_ids) {
|
|
# Checks if there are unexpected flags for the product/component.
|
|
if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
|
|
$vars->{'message'} = 'unexpected_flag_types';
|
|
last;
|
|
}
|
|
}
|
|
|
|
foreach my $flag_type (@$flag_types) {
|
|
my $type_id = $flag_type->id;
|
|
|
|
# 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);
|
|
|
|
# We are only interested in flags the user tries to create.
|
|
next unless scalar(grep { $_ == $type_id } @flagtype_ids);
|
|
|
|
# Get the number of flags of this type already set for this target.
|
|
my $has_flags = $class->count(
|
|
{ 'type_id' => $type_id,
|
|
'target_type' => $attachment ? 'attachment' : 'bug',
|
|
'bug_id' => $bug->bug_id,
|
|
'attach_id' => $attachment ? $attachment->id : undef });
|
|
|
|
# Do not create a new flag of this type if this flag type is
|
|
# not multiplicable and already has a flag set.
|
|
next if (!$flag_type->is_multiplicable && $has_flags);
|
|
|
|
my $status = $cgi->param("flag_type-$type_id");
|
|
trick_taint($status);
|
|
|
|
my @logins = $cgi->param("requestee_type-$type_id");
|
|
if ($status eq "?" && scalar(@logins)) {
|
|
foreach my $login (@logins) {
|
|
push (@new_flags, { type_id => $type_id,
|
|
status => $status,
|
|
requestee => $login,
|
|
skip_roe => $skip });
|
|
last unless $flag_type->is_multiplicable;
|
|
}
|
|
}
|
|
else {
|
|
push (@new_flags, { type_id => $type_id,
|
|
status => $status });
|
|
}
|
|
}
|
|
|
|
# Return the list of flags to update and/or to create.
|
|
return (\@flags, \@new_flags);
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<multi_extract_flags_from_cgi($bug, $hr_vars)>
|
|
|
|
Checks whether or not there are new flags to create and returns an
|
|
array of hashes. This array is then passed to Flag::create(). This differs
|
|
from the previous sub-routine as it is called for changing multiple bugs
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub multi_extract_flags_from_cgi {
|
|
my ($class, $bug, $vars, $skip) = @_;
|
|
my $cgi = Bugzilla->cgi;
|
|
|
|
my $match_status = Bugzilla::User::match_field({
|
|
'^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
|
|
}, undef, $skip);
|
|
|
|
$vars->{'match_field'} = 'requestee';
|
|
if ($match_status == USER_MATCH_FAILED) {
|
|
$vars->{'message'} = 'user_match_failed';
|
|
}
|
|
elsif ($match_status == USER_MATCH_MULTIPLE) {
|
|
$vars->{'message'} = 'user_match_multiple';
|
|
}
|
|
|
|
# Extract a list of flag type IDs from field names.
|
|
my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
|
|
|
|
my (@new_flags, @flags);
|
|
|
|
# Get a list of active flag types available for this product/component.
|
|
my $flag_types = Bugzilla::FlagType::match(
|
|
{ 'product_id' => $bug->{'product_id'},
|
|
'component_id' => $bug->{'component_id'},
|
|
'is_active' => 1 });
|
|
|
|
foreach my $flagtype_id (@flagtype_ids) {
|
|
# Checks if there are unexpected flags for the product/component.
|
|
if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
|
|
$vars->{'message'} = 'unexpected_flag_types';
|
|
last;
|
|
}
|
|
}
|
|
|
|
foreach my $flag_type (@$flag_types) {
|
|
my $type_id = $flag_type->id;
|
|
|
|
# Bug flags are only valid for bugs
|
|
next unless ($flag_type->target_type eq 'bug');
|
|
|
|
# We are only interested in flags the user tries to create.
|
|
next unless scalar(grep { $_ == $type_id } @flagtype_ids);
|
|
|
|
# Get the flags of this type already set for this bug.
|
|
my $current_flags = $class->match(
|
|
{ 'type_id' => $type_id,
|
|
'target_type' => 'bug',
|
|
'bug_id' => $bug->bug_id });
|
|
|
|
# We will update existing flags (instead of creating new ones)
|
|
# if the flag exists and the user has not chosen the 'always add'
|
|
# option
|
|
my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id");
|
|
|
|
my $status = $cgi->param("flag_type-$type_id");
|
|
trick_taint($status);
|
|
|
|
my @logins = $cgi->param("requestee_type-$type_id");
|
|
if ($status eq "?" && scalar(@logins)) {
|
|
foreach my $login (@logins) {
|
|
if ($update) {
|
|
foreach my $current_flag (@$current_flags) {
|
|
push (@flags, { id => $current_flag->id,
|
|
status => $status,
|
|
requestee => $login,
|
|
skip_roe => $skip });
|
|
}
|
|
}
|
|
else {
|
|
push (@new_flags, { type_id => $type_id,
|
|
status => $status,
|
|
requestee => $login,
|
|
skip_roe => $skip });
|
|
}
|
|
|
|
last unless $flag_type->is_multiplicable;
|
|
}
|
|
}
|
|
else {
|
|
if ($update) {
|
|
foreach my $current_flag (@$current_flags) {
|
|
push (@flags, { id => $current_flag->id,
|
|
status => $status });
|
|
}
|
|
}
|
|
else {
|
|
push (@new_flags, { type_id => $type_id,
|
|
status => $status });
|
|
}
|
|
}
|
|
}
|
|
|
|
# Return the list of flags to update and/or to create.
|
|
return (\@flags, \@new_flags);
|
|
}
|
|
|
|
=pod
|
|
|
|
=over
|
|
|
|
=item C<notify($flag, $old_flag, $object, $timestamp)>
|
|
|
|
Sends an email notification about a flag being created, fulfilled
|
|
or deleted.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub notify {
|
|
my ($class, $flag, $old_flag, $obj, $timestamp) = @_;
|
|
|
|
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);
|
|
|
|
# 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');
|
|
|
|
# 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.
|
|
my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
|
|
my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
|
|
|
|
my %recipients;
|
|
foreach my $cc (split(/[, ]+/, $cc_list)) {
|
|
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;
|
|
}
|
|
|
|
# Only notify if the addressee is allowed to receive the email.
|
|
if ($addressee && $addressee->email_enabled) {
|
|
$recipients{$addressee->email} = $addressee;
|
|
}
|
|
# 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) {
|
|
$default_lang = Bugzilla::User->new()->setting('lang');
|
|
}
|
|
|
|
# Get comments on the bug
|
|
my $all_comments = $bug->comments({ after => $bug->lastdiffed });
|
|
@$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments;
|
|
|
|
# Get public only comments
|
|
my $public_comments = [ grep { !$_->is_private } @$all_comments ];
|
|
|
|
foreach my $to (keys %recipients) {
|
|
# 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;
|
|
|
|
# We only want to show private comments to users in the is_insider group
|
|
my $comments = $recipients{$to} && $recipients{$to}->is_insider
|
|
? $all_comments : $public_comments;
|
|
|
|
my $vars = {
|
|
flag => $flag,
|
|
old_flag => $old_flag,
|
|
to => $to,
|
|
date => $timestamp,
|
|
bug => $bug,
|
|
attachment => $attachment,
|
|
threadingmarker => build_thread_marker($bug->id, $thread_user_id),
|
|
new_comments => $comments,
|
|
};
|
|
|
|
my $lang = $recipients{$to} ?
|
|
$recipients{$to}->setting('lang') : $default_lang;
|
|
|
|
my $template = Bugzilla->template_inner($lang);
|
|
my $message;
|
|
$template->process("email/flagmail.txt.tmpl", $vars, \$message)
|
|
|| ThrowTemplateError($template->error());
|
|
|
|
MessageToMTA($message);
|
|
}
|
|
}
|
|
|
|
# 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 {
|
|
my ($class, $vars) = @_;
|
|
|
|
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};
|
|
$flags = $class->match({target_type => 'bug', bug_id => $bug_id});
|
|
}
|
|
elsif ($target_type eq 'attachment') {
|
|
my $attach_id = delete $vars->{attach_id};
|
|
$flags = $class->match({attach_id => $attach_id});
|
|
}
|
|
else {
|
|
ThrowCodeError('bad_arg', {argument => 'target_type',
|
|
function => $class . '->_flag_types'});
|
|
}
|
|
|
|
# Get all available flag types for the given product and component.
|
|
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);
|
|
|
|
$_->{flags} = [] foreach @$flag_types;
|
|
my %flagtypes = map { $_->id => $_ } @$flag_types;
|
|
|
|
# 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;
|
|
return $flag_types;
|
|
}
|
|
|
|
1;
|
|
|
|
=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
|