2008-12-15 15:53:33 +03:00
|
|
|
|
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
|
|
|
|
#
|
|
|
|
|
# The contents of this file are subject to the Mozilla Public
|
|
|
|
|
# License Version 1.1 (the "License"); you may not use this file
|
|
|
|
|
# except in compliance with the License. You may obtain a copy of
|
|
|
|
|
# the License at http://www.mozilla.org/MPL/
|
|
|
|
|
#
|
|
|
|
|
# Software distributed under the License is distributed on an "AS
|
|
|
|
|
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
|
|
|
|
# implied. See the License for the specific language governing
|
|
|
|
|
# rights and limitations under the License.
|
|
|
|
|
#
|
|
|
|
|
# The Original Code is the Bugzilla Bug Tracking System.
|
|
|
|
|
#
|
|
|
|
|
# The Initial Developer of the Original Code is Netscape Communications
|
|
|
|
|
# Corporation. Portions created by Netscape are
|
|
|
|
|
# Copyright (C) 1998 Netscape Communications Corporation. All
|
|
|
|
|
# Rights Reserved.
|
|
|
|
|
#
|
|
|
|
|
# Contributor(s): Dawn Endico <endico@mozilla.org>
|
|
|
|
|
# Terry Weissman <terry@mozilla.org>
|
|
|
|
|
# Chris Yeh <cyeh@bluemartini.com>
|
|
|
|
|
# Bradley Baetz <bbaetz@acm.org>
|
|
|
|
|
# Dave Miller <justdave@bugzilla.org>
|
|
|
|
|
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
|
|
|
|
# Frédéric Buclin <LpSolit@gmail.com>
|
|
|
|
|
# Lance Larsh <lance.larsh@oracle.com>
|
|
|
|
|
|
|
|
|
|
package Bugzilla::Bug;
|
|
|
|
|
|
2009-09-01 16:10:01 +04:00
|
|
|
|
use utf8;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
use strict;
|
|
|
|
|
|
|
|
|
|
use Bugzilla::Attachment;
|
|
|
|
|
use Bugzilla::Constants;
|
|
|
|
|
use Bugzilla::Field;
|
|
|
|
|
use Bugzilla::Flag;
|
|
|
|
|
use Bugzilla::FlagType;
|
2009-12-18 20:14:04 +03:00
|
|
|
|
use Bugzilla::FlagType::UserList;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
use Bugzilla::Hook;
|
|
|
|
|
use Bugzilla::Keyword;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
use Bugzilla::Milestone;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
use Bugzilla::User;
|
|
|
|
|
use Bugzilla::Util;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
use Bugzilla::Version;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
use Bugzilla::Error;
|
|
|
|
|
use Bugzilla::Product;
|
|
|
|
|
use Bugzilla::Component;
|
|
|
|
|
use Bugzilla::Group;
|
|
|
|
|
use Bugzilla::Status;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
use Bugzilla::Comment;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
use List::Util qw(min);
|
|
|
|
|
use Storable qw(dclone);
|
2009-07-29 15:21:49 +04:00
|
|
|
|
use URI;
|
|
|
|
|
use URI::QueryParam;
|
2009-12-17 15:20:04 +03:00
|
|
|
|
use Date::Format qw(time2str);
|
2010-02-01 16:33:39 +03:00
|
|
|
|
use POSIX qw(floor);
|
2010-11-24 20:22:45 +03:00
|
|
|
|
use Scalar::Util qw(blessed);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
use base qw(Bugzilla::Object Exporter);
|
|
|
|
|
@Bugzilla::Bug::EXPORT = qw(
|
2009-07-29 15:21:49 +04:00
|
|
|
|
bug_alias_to_id
|
2008-12-15 15:53:33 +03:00
|
|
|
|
RemoveVotes CheckIfVotedConfirmed
|
|
|
|
|
LogActivityEntry
|
|
|
|
|
editable_bug_fields
|
|
|
|
|
);
|
2009-08-25 18:43:01 +04:00
|
|
|
|
@Bugzilla::Bug::EXPORT_OK = @Bugzilla::Bug::EXPORT;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
#####################################################################
|
|
|
|
|
# Constants
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
use constant DB_TABLE => 'bugs';
|
|
|
|
|
use constant ID_FIELD => 'bug_id';
|
|
|
|
|
use constant NAME_FIELD => 'alias';
|
|
|
|
|
use constant LIST_ORDER => ID_FIELD;
|
|
|
|
|
|
|
|
|
|
# This is a sub because it needs to call other subroutines.
|
2011-09-13 21:00:58 +04:00
|
|
|
|
sub DB_COLUMNS
|
|
|
|
|
{
|
|
|
|
|
my $cache = Bugzilla->cache_fields;
|
|
|
|
|
return @{$cache->{bug_columns}} if defined $cache->{bug_columns};
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
|
|
|
|
|
Bugzilla->active_custom_fields;
|
|
|
|
|
my @custom_names = map {$_->name} @custom;
|
2009-07-29 15:21:49 +04:00
|
|
|
|
|
|
|
|
|
my @columns = (qw(
|
2008-12-15 15:53:33 +03:00
|
|
|
|
alias
|
|
|
|
|
assigned_to
|
|
|
|
|
bug_file_loc
|
|
|
|
|
bug_id
|
|
|
|
|
bug_severity
|
|
|
|
|
bug_status
|
|
|
|
|
cclist_accessible
|
|
|
|
|
component_id
|
|
|
|
|
delta_ts
|
|
|
|
|
estimated_time
|
|
|
|
|
everconfirmed
|
|
|
|
|
priority
|
|
|
|
|
product_id
|
|
|
|
|
qa_contact
|
|
|
|
|
remaining_time
|
|
|
|
|
reporter_accessible
|
|
|
|
|
resolution
|
|
|
|
|
short_desc
|
|
|
|
|
status_whiteboard
|
|
|
|
|
target_milestone
|
|
|
|
|
version
|
|
|
|
|
),
|
2011-09-13 21:00:58 +04:00
|
|
|
|
# FIXME kill op_sys and rep_platform completely, make them custom fields
|
|
|
|
|
(Bugzilla->params->{useopsys} ? 'op_sys' : ()),
|
|
|
|
|
(Bugzilla->params->{useplatform} ? 'rep_platform' : ()),
|
2008-12-15 15:53:33 +03:00
|
|
|
|
'reporter AS reporter_id',
|
|
|
|
|
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
|
|
|
|
|
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
|
2009-07-29 15:21:49 +04:00
|
|
|
|
@custom_names);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
Bugzilla::Hook::process("bug_columns", { columns => \@columns });
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2011-09-13 21:00:58 +04:00
|
|
|
|
$cache->{bug_columns} = \@columns;
|
2009-07-29 15:21:49 +04:00
|
|
|
|
return @columns;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use constant REQUIRED_CREATE_FIELDS => qw(
|
|
|
|
|
component
|
|
|
|
|
product
|
|
|
|
|
short_desc
|
|
|
|
|
version
|
|
|
|
|
);
|
|
|
|
|
|
2010-07-29 17:25:07 +04:00
|
|
|
|
our $CUSTOM_FIELD_VALIDATORS = {
|
|
|
|
|
FIELD_TYPE_UNKNOWN() => \&_check_default_field,
|
|
|
|
|
FIELD_TYPE_FREETEXT() => \&_check_freetext_field,
|
|
|
|
|
FIELD_TYPE_SINGLE_SELECT() => \&_check_select_field,
|
|
|
|
|
FIELD_TYPE_MULTI_SELECT() => \&_check_multi_select_field,
|
|
|
|
|
FIELD_TYPE_TEXTAREA() => \&_check_default_field,
|
|
|
|
|
FIELD_TYPE_DATETIME() => \&_check_datetime_field,
|
|
|
|
|
FIELD_TYPE_BUG_ID() => \&_check_bugid_field,
|
|
|
|
|
FIELD_TYPE_BUG_URLS() => \&_check_default_field,
|
|
|
|
|
FIELD_TYPE_NUMERIC() => \&_check_numeric_field,
|
|
|
|
|
};
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# There are also other, more complex validators that are called
|
|
|
|
|
# from run_create_validators.
|
|
|
|
|
sub VALIDATORS {
|
2011-09-13 21:00:58 +04:00
|
|
|
|
my $cache = Bugzilla->cache_fields;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $cache->{bug_validators} if defined $cache->{bug_validators};
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $validators = {
|
|
|
|
|
alias => \&_check_alias,
|
|
|
|
|
bug_file_loc => \&_check_bug_file_loc,
|
2010-05-15 00:02:34 +04:00
|
|
|
|
bug_severity => \&_check_select_field,
|
2008-12-15 15:53:33 +03:00
|
|
|
|
comment => \&_check_comment,
|
|
|
|
|
commentprivacy => \&_check_commentprivacy,
|
|
|
|
|
deadline => \&_check_deadline,
|
|
|
|
|
estimated_time => \&_check_estimated_time,
|
|
|
|
|
priority => \&_check_priority,
|
|
|
|
|
product => \&_check_product,
|
|
|
|
|
remaining_time => \&_check_remaining_time,
|
|
|
|
|
short_desc => \&_check_short_desc,
|
|
|
|
|
status_whiteboard => \&_check_status_whiteboard,
|
|
|
|
|
};
|
|
|
|
|
|
2011-09-13 21:00:58 +04:00
|
|
|
|
$validators->{op_sys} = \&_check_select_field if Bugzilla->params->{useopsys};
|
|
|
|
|
$validators->{rep_platform} = \&_check_select_field if Bugzilla->params->{useplatform};
|
|
|
|
|
|
2010-07-29 17:25:07 +04:00
|
|
|
|
# Set up validators for custom fields.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
foreach my $field (Bugzilla->active_custom_fields) {
|
2010-07-29 17:25:07 +04:00
|
|
|
|
$validators->{$field->name} = $CUSTOM_FIELD_VALIDATORS->{$field->type};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$cache->{bug_validators} = $validators;
|
|
|
|
|
return $cache->{bug_validators};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use constant UPDATE_VALIDATORS => {
|
2009-06-03 21:15:55 +04:00
|
|
|
|
reporter => \&_check_reporter,
|
2008-12-15 15:53:33 +03:00
|
|
|
|
assigned_to => \&_check_assigned_to,
|
|
|
|
|
bug_status => \&_check_bug_status,
|
|
|
|
|
cclist_accessible => \&Bugzilla::Object::check_boolean,
|
|
|
|
|
dup_id => \&_check_dup_id,
|
2009-07-13 17:54:45 +04:00
|
|
|
|
everconfirmed => \&Bugzilla::Object::check_boolean,
|
2008-12-15 15:53:33 +03:00
|
|
|
|
qa_contact => \&_check_qa_contact,
|
|
|
|
|
reporter_accessible => \&Bugzilla::Object::check_boolean,
|
|
|
|
|
resolution => \&_check_resolution,
|
|
|
|
|
target_milestone => \&_check_target_milestone,
|
|
|
|
|
version => \&_check_version,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sub UPDATE_COLUMNS {
|
|
|
|
|
my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
|
|
|
|
|
Bugzilla->active_custom_fields;
|
|
|
|
|
my @custom_names = map {$_->name} @custom;
|
|
|
|
|
my @columns = qw(
|
|
|
|
|
alias
|
|
|
|
|
assigned_to
|
|
|
|
|
bug_file_loc
|
|
|
|
|
bug_severity
|
|
|
|
|
bug_status
|
|
|
|
|
cclist_accessible
|
|
|
|
|
component_id
|
|
|
|
|
deadline
|
|
|
|
|
estimated_time
|
|
|
|
|
everconfirmed
|
|
|
|
|
priority
|
|
|
|
|
product_id
|
|
|
|
|
qa_contact
|
|
|
|
|
remaining_time
|
|
|
|
|
reporter_accessible
|
|
|
|
|
resolution
|
|
|
|
|
short_desc
|
|
|
|
|
status_whiteboard
|
|
|
|
|
target_milestone
|
|
|
|
|
version
|
|
|
|
|
);
|
2011-09-13 21:00:58 +04:00
|
|
|
|
push @columns, 'op_sys' if Bugzilla->params->{useopsys};
|
|
|
|
|
push @columns, 'rep_platform' if Bugzilla->params->{useplatform};
|
|
|
|
|
push @columns, @custom_names;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return @columns;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use constant NUMERIC_COLUMNS => qw(
|
|
|
|
|
estimated_time
|
|
|
|
|
remaining_time
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
sub DATE_COLUMNS {
|
|
|
|
|
my @fields = Bugzilla->get_fields(
|
|
|
|
|
{ custom => 1, type => FIELD_TYPE_DATETIME });
|
|
|
|
|
return map { $_->name } @fields;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# This is used by add_comment to know what we validate before putting in
|
|
|
|
|
# the DB.
|
|
|
|
|
use constant UPDATE_COMMENT_COLUMNS => qw(
|
|
|
|
|
thetext
|
|
|
|
|
work_time
|
|
|
|
|
type
|
|
|
|
|
extra_data
|
|
|
|
|
isprivate
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Used in LogActivityEntry(). Gives the max length of lines in the
|
|
|
|
|
# activity table.
|
|
|
|
|
use constant MAX_LINE_LENGTH => 254;
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# This maps the names of internal Bugzilla bug fields to things that would
|
|
|
|
|
# make sense to somebody who's not intimately familiar with the inner workings
|
|
|
|
|
# of Bugzilla. (These are the field names that the WebService and email_in.pl
|
|
|
|
|
# use.)
|
|
|
|
|
use constant FIELD_MAP => {
|
|
|
|
|
creation_time => 'creation_ts',
|
|
|
|
|
description => 'comment',
|
|
|
|
|
id => 'bug_id',
|
|
|
|
|
last_change_time => 'delta_ts',
|
|
|
|
|
platform => 'rep_platform',
|
|
|
|
|
severity => 'bug_severity',
|
|
|
|
|
status => 'bug_status',
|
|
|
|
|
summary => 'short_desc',
|
|
|
|
|
url => 'bug_file_loc',
|
|
|
|
|
whiteboard => 'status_whiteboard',
|
|
|
|
|
|
|
|
|
|
# These are special values for the WebService Bug.search method.
|
|
|
|
|
limit => 'LIMIT',
|
|
|
|
|
offset => 'OFFSET',
|
|
|
|
|
};
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
sub new {
|
|
|
|
|
my $invocant = shift;
|
|
|
|
|
my $class = ref($invocant) || $invocant;
|
|
|
|
|
my $param = shift;
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# Remove leading "#" mark if we've just been passed an id.
|
|
|
|
|
if (!ref $param && $param =~ /^#(\d+)$/) {
|
|
|
|
|
$param = $1;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# If we get something that looks like a word (not a number),
|
|
|
|
|
# make it the "name" param.
|
|
|
|
|
if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
|
|
|
|
|
# But only if aliases are enabled.
|
|
|
|
|
if (Bugzilla->params->{'usebugaliases'} && $param) {
|
|
|
|
|
$param = { name => $param };
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# Aliases are off, and we got something that's not a number.
|
|
|
|
|
my $error_self = {};
|
|
|
|
|
bless $error_self, $class;
|
|
|
|
|
$error_self->{'bug_id'} = $param;
|
|
|
|
|
$error_self->{'error'} = 'InvalidBugId';
|
|
|
|
|
return $error_self;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unshift @_, $param;
|
|
|
|
|
my $self = $class->SUPER::new(@_);
|
|
|
|
|
|
|
|
|
|
# Bugzilla::Bug->new always returns something, but sets $self->{error}
|
|
|
|
|
# if the bug wasn't found in the database.
|
|
|
|
|
if (!$self) {
|
|
|
|
|
my $error_self = {};
|
2009-07-29 15:21:49 +04:00
|
|
|
|
if (ref $param) {
|
|
|
|
|
$error_self->{bug_id} = $param->{name};
|
|
|
|
|
$error_self->{error} = 'InvalidBugId';
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$error_self->{bug_id} = $param;
|
|
|
|
|
$error_self->{error} = 'NotFound';
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
bless $error_self, $class;
|
|
|
|
|
return $error_self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $self;
|
|
|
|
|
}
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
sub check {
|
|
|
|
|
my $class = shift;
|
|
|
|
|
my ($id, $field) = @_;
|
|
|
|
|
|
|
|
|
|
ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
|
|
|
|
|
|
|
|
|
|
# Bugzilla::Bug throws lots of special errors, so we don't call
|
|
|
|
|
# SUPER::check, we just call our new and do our own checks.
|
|
|
|
|
my $self = $class->new(trim($id));
|
|
|
|
|
# For error messages, use the id that was returned by new(), because
|
|
|
|
|
# it's cleaned up.
|
|
|
|
|
$id = $self->id;
|
|
|
|
|
|
|
|
|
|
if ($self->{error}) {
|
|
|
|
|
if ($self->{error} eq 'NotFound') {
|
|
|
|
|
ThrowUserError("bug_id_does_not_exist", { bug_id => $id });
|
|
|
|
|
}
|
|
|
|
|
if ($self->{error} eq 'InvalidBugId') {
|
|
|
|
|
ThrowUserError("improper_bug_id_field_value",
|
|
|
|
|
{ bug_id => $id,
|
|
|
|
|
field => $field });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
|
|
|
|
|
$self->check_is_visible;
|
|
|
|
|
}
|
|
|
|
|
return $self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub check_is_visible {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
|
|
|
|
|
if (!$user->can_see_bug($self->id)) {
|
|
|
|
|
# The error the user sees depends on whether or not they are
|
|
|
|
|
# logged in (i.e. $user->id contains the user's positive integer ID).
|
2011-04-12 21:04:43 +04:00
|
|
|
|
my $err_args = { bug_id => $self->id };
|
2009-07-29 15:21:49 +04:00
|
|
|
|
if ($user->id) {
|
2011-04-12 21:04:43 +04:00
|
|
|
|
if (Bugzilla->params->{unauth_bug_details}) {
|
|
|
|
|
$err_args->{product} = $self->product;
|
|
|
|
|
}
|
|
|
|
|
ThrowUserError("bug_access_denied", $err_args);
|
2009-07-29 15:21:49 +04:00
|
|
|
|
} else {
|
2011-04-12 21:04:43 +04:00
|
|
|
|
ThrowUserError("bug_access_query", $err_args);
|
2009-07-29 15:21:49 +04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub match {
|
|
|
|
|
my $class = shift;
|
|
|
|
|
my ($params) = @_;
|
|
|
|
|
|
|
|
|
|
# Allow matching certain fields by name (in addition to matching by ID).
|
|
|
|
|
my %translate_fields = (
|
|
|
|
|
assigned_to => 'Bugzilla::User',
|
|
|
|
|
qa_contact => 'Bugzilla::User',
|
|
|
|
|
reporter => 'Bugzilla::User',
|
|
|
|
|
product => 'Bugzilla::Product',
|
|
|
|
|
component => 'Bugzilla::Component',
|
|
|
|
|
);
|
|
|
|
|
my %translated;
|
|
|
|
|
|
|
|
|
|
foreach my $field (keys %translate_fields) {
|
|
|
|
|
my @ids;
|
|
|
|
|
# Convert names to ids. We use "exists" everywhere since people can
|
|
|
|
|
# legally specify "undef" to mean IS NULL (even though most of these
|
|
|
|
|
# fields can't be NULL, people can still specify it...).
|
|
|
|
|
if (exists $params->{$field}) {
|
|
|
|
|
my $names = $params->{$field};
|
|
|
|
|
my $type = $translate_fields{$field};
|
|
|
|
|
my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name';
|
|
|
|
|
# We call Bugzilla::Object::match directly to avoid the
|
|
|
|
|
# Bugzilla::User::match implementation which is different.
|
|
|
|
|
my $objects = Bugzilla::Object::match($type, { $param => $names });
|
|
|
|
|
push(@ids, map { $_->id } @$objects);
|
|
|
|
|
}
|
|
|
|
|
# You can also specify ids directly as arguments to this function,
|
|
|
|
|
# so include them in the list if they have been specified.
|
|
|
|
|
if (exists $params->{"${field}_id"}) {
|
|
|
|
|
my $current_ids = $params->{"${field}_id"};
|
|
|
|
|
my @id_array = ref $current_ids ? @$current_ids : ($current_ids);
|
|
|
|
|
push(@ids, @id_array);
|
|
|
|
|
}
|
|
|
|
|
# We do this "or" instead of a "scalar(@ids)" to handle the case
|
|
|
|
|
# when people passed only invalid object names. Otherwise we'd
|
|
|
|
|
# end up with a SUPER::match call with zero criteria (which dies).
|
|
|
|
|
if (exists $params->{$field} or exists $params->{"${field}_id"}) {
|
|
|
|
|
$translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The user fields don't have an _id on the end of them in the database,
|
|
|
|
|
# but the product & component fields do, so we have to have separate
|
|
|
|
|
# code to deal with the different sets of fields here.
|
|
|
|
|
foreach my $field (qw(assigned_to qa_contact reporter)) {
|
|
|
|
|
delete $params->{"${field}_id"};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$params->{$field} = $translated{$field}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
if exists $translated{$field};
|
|
|
|
|
}
|
|
|
|
|
foreach my $field (qw(product component)) {
|
|
|
|
|
delete $params->{$field};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$params->{"${field}_id"} = $translated{$field}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
if exists $translated{$field};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $class->SUPER::match(@_);
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Docs for create() (there's no POD in this file yet, but we very
|
|
|
|
|
# much need this documented right now):
|
|
|
|
|
#
|
|
|
|
|
# The same as Bugzilla::Object->create. Parameters are only required
|
|
|
|
|
# if they say so below.
|
|
|
|
|
#
|
|
|
|
|
# Params:
|
|
|
|
|
#
|
|
|
|
|
# C<product> - B<Required> The name of the product this bug is being
|
|
|
|
|
# filed against.
|
|
|
|
|
# C<component> - B<Required> The name of the component this bug is being
|
|
|
|
|
# filed against.
|
|
|
|
|
#
|
|
|
|
|
# C<bug_severity> - B<Required> The severity for the bug, a string.
|
|
|
|
|
# C<creation_ts> - B<Required> A SQL timestamp for when the bug was created.
|
|
|
|
|
# C<short_desc> - B<Required> A summary for the bug.
|
2009-10-22 15:42:07 +04:00
|
|
|
|
# C<op_sys> - The OS the bug was found against.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# C<priority> - B<Required> The initial priority for the bug.
|
2009-10-22 15:42:07 +04:00
|
|
|
|
# C<rep_platform> - The platform the bug was found against.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# C<version> - B<Required> The version of the product the bug was found in.
|
|
|
|
|
#
|
|
|
|
|
# C<alias> - An alias for this bug. Will be ignored if C<usebugaliases>
|
|
|
|
|
# is off.
|
|
|
|
|
# C<target_milestone> - When this bug is expected to be fixed.
|
|
|
|
|
# C<status_whiteboard> - A string.
|
|
|
|
|
# C<bug_status> - The initial status of the bug, a string.
|
|
|
|
|
# C<bug_file_loc> - The URL field.
|
|
|
|
|
#
|
|
|
|
|
# C<assigned_to> - The full login name of the user who the bug is
|
|
|
|
|
# initially assigned to.
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# C<qa_contact> - The full login name of the QA Contact for this bug.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Will be ignored if C<useqacontact> is off.
|
|
|
|
|
#
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# C<estimated_time> - For time-tracking. Will be ignored if
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# C<timetrackinggroup> is not set, or if the current
|
|
|
|
|
# user is not a member of the timetrackinggroup.
|
2009-08-25 21:08:44 +04:00
|
|
|
|
# C<work_time> - For time-tracking. Will be ignored for the same
|
|
|
|
|
# reasons as C<estimated_time>.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# C<deadline> - For time-tracking. Will be ignored for the same
|
|
|
|
|
# reasons as C<estimated_time>.
|
|
|
|
|
sub create {
|
|
|
|
|
my ($class, $params) = @_;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
|
|
$dbh->bz_start_transaction();
|
|
|
|
|
|
|
|
|
|
# These fields have default values which we can use if they are undefined.
|
|
|
|
|
$params->{bug_severity} = Bugzilla->params->{defaultseverity}
|
2011-09-13 21:00:58 +04:00
|
|
|
|
if !defined $params->{bug_severity};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{priority} = Bugzilla->params->{defaultpriority}
|
2011-09-13 21:00:58 +04:00
|
|
|
|
if !defined $params->{priority};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{op_sys} = Bugzilla->params->{defaultopsys}
|
2011-09-13 21:00:58 +04:00
|
|
|
|
if Bugzilla->params->{useopsys} && !defined $params->{op_sys};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{rep_platform} = Bugzilla->params->{defaultplatform}
|
2011-09-13 21:00:58 +04:00
|
|
|
|
if Bugzilla->params->{useplatform} && !defined $params->{rep_platform};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Make sure a comment is always defined.
|
|
|
|
|
$params->{comment} = '' unless defined $params->{comment};
|
|
|
|
|
|
|
|
|
|
$class->check_required_create_fields($params);
|
|
|
|
|
$params = $class->run_create_validators($params);
|
|
|
|
|
|
|
|
|
|
# These are not a fields in the bugs table, so we don't pass them to
|
|
|
|
|
# insert_create_data.
|
|
|
|
|
my $cc_ids = delete $params->{cc};
|
|
|
|
|
my $groups = delete $params->{groups};
|
|
|
|
|
my $depends_on = delete $params->{dependson};
|
|
|
|
|
my $blocked = delete $params->{blocked};
|
2009-08-25 21:08:44 +04:00
|
|
|
|
my ($work_time, $comment, $privacy) = ($params->{work_time}, $params->{comment}, $params->{commentprivacy});
|
|
|
|
|
delete $params->{work_time};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
delete $params->{comment};
|
|
|
|
|
delete $params->{commentprivacy};
|
|
|
|
|
|
|
|
|
|
# Set up the keyword cache for bug creation.
|
|
|
|
|
my $keywords = $params->{keywords};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$params->{keywords} = join(', ', sort {lc($a) cmp lc($b)}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
map($_->name, @$keywords));
|
|
|
|
|
|
|
|
|
|
# We don't want the bug to appear in the system until it's correctly
|
|
|
|
|
# protected by groups.
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my $timestamp = delete $params->{creation_ts};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
my $ms_values = $class->_extract_multi_selects($params);
|
|
|
|
|
my $bug = $class->insert_create_data($params);
|
|
|
|
|
|
2010-10-05 21:40:17 +04:00
|
|
|
|
# CustIS Bug 38616 - CC list restriction
|
|
|
|
|
if ($bug->product_obj->cc_restrict_group)
|
|
|
|
|
{
|
|
|
|
|
$bug->{restricted_cc} = $bug->product_obj->restrict_cc($cc_ids, 'id');
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Add the group restrictions
|
|
|
|
|
my $sth_group = $dbh->prepare(
|
|
|
|
|
'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
|
|
|
|
|
foreach my $group_id (@$groups) {
|
|
|
|
|
$sth_group->execute($bug->bug_id, $group_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef,
|
|
|
|
|
$timestamp, $bug->bug_id);
|
|
|
|
|
# Update the bug instance as well
|
|
|
|
|
$bug->{creation_ts} = $timestamp;
|
|
|
|
|
|
|
|
|
|
# Add the CCs
|
|
|
|
|
my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)');
|
2009-06-03 20:28:38 +04:00
|
|
|
|
foreach my $user_id (@$cc_ids)
|
|
|
|
|
{
|
2009-06-03 21:15:55 +04:00
|
|
|
|
$sth_cc->execute($bug->bug_id, $user_id);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Add in keywords
|
|
|
|
|
my $sth_keyword = $dbh->prepare(
|
|
|
|
|
'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)');
|
|
|
|
|
foreach my $keyword_id (map($_->id, @$keywords)) {
|
|
|
|
|
$sth_keyword->execute($bug->bug_id, $keyword_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Set up dependencies (blocked/dependson)
|
|
|
|
|
my $sth_deps = $dbh->prepare(
|
|
|
|
|
'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
|
|
|
|
|
my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
|
|
|
|
|
|
|
|
|
|
foreach my $depends_on_id (@$depends_on) {
|
|
|
|
|
$sth_deps->execute($bug->bug_id, $depends_on_id);
|
|
|
|
|
# Log the reverse action on the other bug.
|
|
|
|
|
LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
|
|
|
|
|
$bug->{reporter_id}, $timestamp);
|
|
|
|
|
$sth_bug_time->execute($timestamp, $depends_on_id);
|
|
|
|
|
}
|
|
|
|
|
foreach my $blocked_id (@$blocked) {
|
|
|
|
|
$sth_deps->execute($blocked_id, $bug->bug_id);
|
|
|
|
|
# Log the reverse action on the other bug.
|
|
|
|
|
LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
|
|
|
|
|
$bug->{reporter_id}, $timestamp);
|
|
|
|
|
$sth_bug_time->execute($timestamp, $blocked_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Insert the values into the multiselect value tables
|
|
|
|
|
foreach my $field (keys %$ms_values) {
|
|
|
|
|
$dbh->do("DELETE FROM bug_$field where bug_id = ?",
|
|
|
|
|
undef, $bug->bug_id);
|
|
|
|
|
foreach my $value ( @{$ms_values->{$field}} ) {
|
|
|
|
|
$dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)",
|
|
|
|
|
undef, $bug->bug_id, $value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# And insert the comment. We always insert a comment on bug creation,
|
|
|
|
|
# but sometimes it's blank.
|
2009-08-25 21:08:44 +04:00
|
|
|
|
my @columns = qw(bug_id who bug_when thetext work_time);
|
|
|
|
|
my @values = ($bug->bug_id, $bug->{reporter_id}, $timestamp, $comment, $work_time);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# We don't include the "isprivate" column unless it was specified.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# This allows it to fall back to its database default.
|
|
|
|
|
if (defined $privacy) {
|
|
|
|
|
push(@columns, 'isprivate');
|
|
|
|
|
push(@values, $privacy);
|
|
|
|
|
}
|
|
|
|
|
my $qmarks = "?," x @columns;
|
|
|
|
|
chop($qmarks);
|
|
|
|
|
$dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
|
|
|
|
|
VALUES ($qmarks)", undef, @values);
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
|
2009-07-29 15:21:49 +04:00
|
|
|
|
timestamp => $timestamp,
|
|
|
|
|
});
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$dbh->bz_commit_transaction();
|
|
|
|
|
|
|
|
|
|
# Because MySQL doesn't support transactions on the fulltext table,
|
|
|
|
|
# we do this after we've committed the transaction. That way we're
|
|
|
|
|
# sure we're inserting a good Bug ID.
|
|
|
|
|
$bug->_sync_fulltext('new bug');
|
|
|
|
|
|
|
|
|
|
return $bug;
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
my $dependent_validators;
|
|
|
|
|
sub run_create_validators
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $class = shift;
|
|
|
|
|
my $params = $class->SUPER::run_create_validators(@_);
|
|
|
|
|
|
|
|
|
|
my $product = $params->{product};
|
|
|
|
|
$params->{product_id} = $product->id;
|
|
|
|
|
|
|
|
|
|
($params->{bug_status}, $params->{everconfirmed})
|
|
|
|
|
= $class->_check_bug_status($params->{bug_status}, $product,
|
2008-12-18 15:49:01 +03:00
|
|
|
|
$params->{comment}, $params->{assigned_to});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2009-09-02 17:39:24 +04:00
|
|
|
|
if ($params->{bug_status} eq 'RESOLVED')
|
|
|
|
|
{
|
|
|
|
|
check_field('resolution', trim($params->{resolution}));
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{target_milestone} = $class->_check_target_milestone(
|
|
|
|
|
$params->{target_milestone}, $product);
|
|
|
|
|
|
|
|
|
|
$params->{version} = $class->_check_version($params->{version}, $product);
|
|
|
|
|
|
|
|
|
|
$params->{keywords} = $class->_check_keywords($params->{keywords}, $product);
|
|
|
|
|
|
|
|
|
|
$params->{groups} = $class->_check_groups($product,
|
|
|
|
|
$params->{groups});
|
|
|
|
|
|
|
|
|
|
my $component = $class->_check_component($params->{component}, $product);
|
|
|
|
|
|
2011-01-26 20:08:25 +03:00
|
|
|
|
if ($component)
|
|
|
|
|
{
|
|
|
|
|
$params->{component_id} = $component->id;
|
|
|
|
|
$params->{assigned_to} =
|
|
|
|
|
$class->_check_assigned_to($params->{assigned_to}, $component);
|
|
|
|
|
$params->{qa_contact} =
|
|
|
|
|
$class->_check_qa_contact($params->{qa_contact}, $component);
|
|
|
|
|
$params->{cc} = $class->_check_cc($component, $params->{cc});
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Callers cannot set reporter, creation_ts, or delta_ts.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{reporter} = $class->_check_reporter();
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$params->{creation_ts} =
|
2010-05-15 00:02:34 +04:00
|
|
|
|
Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$params->{delta_ts} = $params->{creation_ts};
|
|
|
|
|
|
|
|
|
|
if ($params->{estimated_time}) {
|
|
|
|
|
$params->{remaining_time} = $params->{estimated_time};
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-25 21:08:44 +04:00
|
|
|
|
$params->{work_time} = $class->_check_time($params->{work_time}, 'work_time');
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
|
|
|
|
|
$params->{qa_contact}, $product);
|
|
|
|
|
|
2010-10-05 20:47:44 +04:00
|
|
|
|
($params->{dependson}, $params->{blocked}) =
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$class->_check_dependencies($params->{dependson}, $params->{blocked},
|
|
|
|
|
$product);
|
|
|
|
|
|
|
|
|
|
# You can't set these fields on bug creation (or sometimes ever).
|
2011-01-26 20:10:24 +03:00
|
|
|
|
delete $params->{bug_id};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
delete $params->{votes};
|
|
|
|
|
delete $params->{lastdiffed};
|
2011-01-26 20:08:25 +03:00
|
|
|
|
delete $params->{resolution} if $params->{bug_status} ne 'RESOLVED';
|
|
|
|
|
|
2011-01-26 20:10:24 +03:00
|
|
|
|
# Check dependent field values and die on errors
|
|
|
|
|
check_dependent_fields($dependent_validators, $params);
|
|
|
|
|
|
2011-01-26 20:08:25 +03:00
|
|
|
|
# These are converted into IDs
|
|
|
|
|
delete $params->{product};
|
|
|
|
|
delete $params->{component};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
Bugzilla::Hook::process('bug_end_of_create_validators',
|
|
|
|
|
{ params => $params });
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $params;
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# Check values of the dependent fields
|
|
|
|
|
sub check_dependent_fields
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
my ($validators, $params) = @_;
|
|
|
|
|
my $incorrect_fields = {};
|
|
|
|
|
foreach my $field_name (keys %{$validators || {}})
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2011-09-05 17:46:27 +04:00
|
|
|
|
next if $field_name eq 'dependencies'; # this is not really a field, see add_to_deps
|
2010-11-24 20:22:45 +03:00
|
|
|
|
my $value = $validators->{$field_name};
|
|
|
|
|
my $field = Bugzilla->get_field($field_name);
|
|
|
|
|
# Check field visibility
|
|
|
|
|
if (!$field->check_visibility($params))
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# Field is invisible and must be cleared
|
|
|
|
|
if ($field->type == FIELD_TYPE_SINGLE_SELECT)
|
|
|
|
|
{
|
|
|
|
|
$params->{$field_name} = '---';
|
|
|
|
|
}
|
|
|
|
|
elsif ($field->type == FIELD_TYPE_MULTI_SELECT)
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
$params->{$field_name} = [];
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
2010-11-24 20:22:45 +03:00
|
|
|
|
elsif ($field->type == FIELD_TYPE_BUG_ID)
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
$params->{$field_name} = undef;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
$params->{$field_name} = '';
|
|
|
|
|
}
|
2011-05-24 19:59:11 +04:00
|
|
|
|
next;
|
2010-11-24 20:22:45 +03:00
|
|
|
|
}
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# CustIS Bug 73054, Bug 75690 (TODO maybe move to hooks)
|
|
|
|
|
# Add field value to dependency tree when FIELD_TYPE_BUG_ID with add_to_deps
|
|
|
|
|
# "Add to dependency tree" means add, if current bug isn't depending on / blocked by
|
|
|
|
|
# added bug directly or through arbitrary number of other bugs.
|
|
|
|
|
# This allows to easily view BUG_ID custom fields as the part of dependency tree,
|
|
|
|
|
# without redundant dependencies.
|
2010-12-13 20:33:33 +03:00
|
|
|
|
if ($field->type == FIELD_TYPE_BUG_ID && $field->add_to_deps &&
|
|
|
|
|
$value != $params->{bug_id})
|
2010-12-13 20:08:25 +03:00
|
|
|
|
{
|
2011-09-05 17:46:27 +04:00
|
|
|
|
if (!$validators->{dependencies})
|
2010-12-13 20:08:25 +03:00
|
|
|
|
{
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# Check if ValidateDependencies wasn't called yet
|
|
|
|
|
ValidateDependencies($params, $params->{dependson}, $params->{blocked});
|
|
|
|
|
}
|
|
|
|
|
my $blk = $field->add_to_deps == BUG_ID_ADD_TO_BLOCKED;
|
|
|
|
|
my $to = $blk ? 'blocked' : 'dependson';
|
|
|
|
|
# Add the bug if it isn't already in dependency tree
|
|
|
|
|
if (!$validators->{dependencies}->{blocked}->{$value} &&
|
|
|
|
|
!$validators->{dependencies}->{dependson}->{$value})
|
|
|
|
|
{
|
|
|
|
|
if (ref $params->{$to})
|
|
|
|
|
{
|
|
|
|
|
push @{$params->{$to}}, $value;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
$params->{$to} .= " $value";
|
|
|
|
|
}
|
2010-12-13 20:08:25 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# Check field value visibility for select fields
|
|
|
|
|
if (my $f = $field->value_field)
|
|
|
|
|
{
|
|
|
|
|
my $n = $f->name;
|
|
|
|
|
$value = [ $value ] if !ref $value || blessed $value;
|
|
|
|
|
foreach (@$value)
|
|
|
|
|
{
|
|
|
|
|
if (!ref $_ || !$_->check_visibility($params))
|
|
|
|
|
{
|
|
|
|
|
# Field value is incorrect for the value of controlling field and must be modified
|
|
|
|
|
if (!$incorrect_fields->{$field_name})
|
|
|
|
|
{
|
|
|
|
|
my $controller = blessed $params ? $params->$n : $params->{$n};
|
|
|
|
|
if (!blessed $controller)
|
|
|
|
|
{
|
|
|
|
|
$controller = Bugzilla::Field::Choice->type($f)->new({ name => $controller });
|
|
|
|
|
}
|
|
|
|
|
$incorrect_fields->{$field_name} = {
|
|
|
|
|
field => $field,
|
|
|
|
|
options => [ map { $_->name } @{ $field->restricted_legal_values($controller) } ],
|
|
|
|
|
values => [],
|
|
|
|
|
value_names => [],
|
|
|
|
|
controller => $controller,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
push @{$incorrect_fields->{$field_name}->{values}}, $_;
|
|
|
|
|
push @{$incorrect_fields->{$field_name}->{value_names}}, ref $_ ? $_->name : $_;
|
|
|
|
|
}
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2010-11-24 20:22:45 +03:00
|
|
|
|
}
|
|
|
|
|
# When moving bugs between products, verify groups
|
|
|
|
|
my $verify_bug_groups = undef;
|
|
|
|
|
if (blessed $params && $params->{product_changed})
|
|
|
|
|
{
|
|
|
|
|
# FIXME do not refer to cgi here?
|
|
|
|
|
if (!Bugzilla->cgi->param('verify_bug_groups') &&
|
|
|
|
|
Bugzilla->cgi->param('id'))
|
|
|
|
|
{
|
|
|
|
|
# Display group verification message only for single bug changes
|
|
|
|
|
# Get the ID of groups which are no longer valid in the new product.
|
|
|
|
|
my $gids = Bugzilla->dbh->selectcol_arrayref(
|
|
|
|
|
'SELECT bgm.group_id
|
|
|
|
|
FROM bug_group_map AS bgm
|
|
|
|
|
WHERE bgm.bug_id=?
|
|
|
|
|
AND bgm.group_id NOT IN
|
|
|
|
|
(SELECT gcm.group_id
|
|
|
|
|
FROM group_control_map AS gcm
|
|
|
|
|
WHERE gcm.product_id = ?
|
|
|
|
|
AND ( (gcm.membercontrol != ?
|
|
|
|
|
AND gcm.group_id IN ('
|
|
|
|
|
. Bugzilla->user->groups_as_string . '))
|
|
|
|
|
OR gcm.othercontrol != ?) )',
|
|
|
|
|
undef, $params->id, $params->product_obj->id, CONTROLMAPNA, CONTROLMAPNA);
|
|
|
|
|
$verify_bug_groups = Bugzilla::Group->new_from_list($gids);
|
|
|
|
|
}
|
|
|
|
|
# Remove groups that aren't valid in the new product. This will also
|
|
|
|
|
# have the side effect of removing the bug from groups that aren't
|
|
|
|
|
# active anymore.
|
|
|
|
|
#
|
|
|
|
|
# We copy this array because the original array is modified while we're
|
|
|
|
|
# working, and that confuses "foreach".
|
|
|
|
|
my @current_groups = @{$params->groups_in};
|
|
|
|
|
foreach my $group (@current_groups) {
|
|
|
|
|
if (!grep($group->id == $_->id, @{$params->product_obj->groups_valid})) {
|
|
|
|
|
$params->remove_group($group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
# Make sure the bug is in all the mandatory groups for the new product.
|
|
|
|
|
foreach my $group (@{$params->product_obj->groups_mandatory_for(Bugzilla->user)}) {
|
|
|
|
|
$params->add_group($group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If we're not in browser, throw an error
|
2011-01-26 20:08:25 +03:00
|
|
|
|
if ((Bugzilla->usage_mode != USAGE_MODE_BROWSER || !blessed $params) && %$incorrect_fields)
|
2010-11-24 20:22:45 +03:00
|
|
|
|
{
|
|
|
|
|
ThrowUserError('incorrect_field_values', {
|
2011-01-31 15:11:49 +03:00
|
|
|
|
bug_id => blessed $params ? $params->bug_id : undef,
|
2010-11-24 20:22:45 +03:00
|
|
|
|
incorrect_fields => [ values %$incorrect_fields ],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2011-01-31 15:11:49 +03:00
|
|
|
|
# Else display UI for value checking
|
|
|
|
|
if (Bugzilla->usage_mode == USAGE_MODE_BROWSER && (%$incorrect_fields || $verify_bug_groups) && blessed $params)
|
2010-11-24 20:22:45 +03:00
|
|
|
|
{
|
|
|
|
|
Bugzilla->template->process('bug/process/verify-field-values.html.tmpl', {
|
|
|
|
|
product => $verify_bug_groups && $params->product_obj,
|
|
|
|
|
old_groups => $verify_bug_groups,
|
|
|
|
|
verify_bug_groups => $verify_bug_groups && 1,
|
|
|
|
|
incorrect_fields => [ values %$incorrect_fields ],
|
|
|
|
|
incorrect_field_descs => [ map { $_->{field}->description } values %$incorrect_fields ],
|
|
|
|
|
exclude_params_re => '^(' . join('|', keys %$incorrect_fields) . ')$',
|
|
|
|
|
});
|
|
|
|
|
Bugzilla->dbh->rollback;
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Clear $validators
|
|
|
|
|
$_[0] = undef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub update
|
|
|
|
|
{
|
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
|
|
# First check dependent field values
|
|
|
|
|
if ($self->{dependent_validators})
|
|
|
|
|
{
|
|
|
|
|
check_dependent_fields($self->{dependent_validators}, $self);
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
2008-12-22 18:43:16 +03:00
|
|
|
|
my $user = Bugzilla->user;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# XXX This is just a temporary hack until all updating happens
|
|
|
|
|
# inside this function.
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-10-22 20:57:14 +04:00
|
|
|
|
Bugzilla::Hook::process('bug_pre_update', { bug => $self });
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my ($changes, $old_bug) = $self->SUPER::update(@_);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# Certain items in $changes have to be fixed so that they hold
|
|
|
|
|
# a name instead of an ID.
|
2010-05-15 00:02:34 +04:00
|
|
|
|
foreach my $field (qw(product_id component_id)) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $change = delete $changes->{$field};
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if ($change) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $new_field = $field;
|
|
|
|
|
$new_field =~ s/_id$//;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$changes->{$new_field} =
|
2008-12-15 15:53:33 +03:00
|
|
|
|
[$self->{"_old_${new_field}_name"}, $self->$new_field];
|
|
|
|
|
}
|
|
|
|
|
}
|
2008-12-29 23:01:06 +03:00
|
|
|
|
|
|
|
|
|
foreach my $field (qw(qa_contact assigned_to))
|
|
|
|
|
{
|
|
|
|
|
if ($changes->{$field})
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($from, $to) = @{ $changes->{$field} };
|
2008-12-29 23:01:06 +03:00
|
|
|
|
if ($from)
|
|
|
|
|
{
|
|
|
|
|
$from = $old_bug->$field->login;
|
|
|
|
|
# Add previous Assignee and QA to CC list
|
|
|
|
|
$self->add_cc($from);
|
|
|
|
|
}
|
|
|
|
|
if ($to)
|
|
|
|
|
{
|
|
|
|
|
$to = $self->$field->login;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$changes->{$field} = [$from, $to];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# CC
|
|
|
|
|
my @old_cc = map {$_->id} @{$old_bug->cc_users};
|
2009-06-03 20:28:38 +04:00
|
|
|
|
my @new_cc = @{$self->cc_users};
|
2010-10-05 21:40:17 +04:00
|
|
|
|
|
|
|
|
|
# CustIS Bug 38616 - CC list restriction
|
|
|
|
|
if ($self->product_obj->cc_restrict_group)
|
2009-12-02 16:39:36 +03:00
|
|
|
|
{
|
2010-10-05 21:40:17 +04:00
|
|
|
|
$self->{restricted_cc} = $self->product_obj->restrict_cc(\@new_cc);
|
2009-12-02 16:39:36 +03:00
|
|
|
|
}
|
2010-10-05 21:40:17 +04:00
|
|
|
|
|
2009-06-03 20:28:38 +04:00
|
|
|
|
@new_cc = map {$_->id} @new_cc;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if (scalar @$removed_cc) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$dbh->do('DELETE FROM cc WHERE bug_id = ? AND '
|
2008-12-15 15:53:33 +03:00
|
|
|
|
. $dbh->sql_in('who', $removed_cc), undef, $self->id);
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
foreach my $user_id (@$added_cc) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)',
|
|
|
|
|
undef, $self->id, $user_id);
|
|
|
|
|
}
|
|
|
|
|
# If any changes were found, record it in the activity log
|
|
|
|
|
if (scalar @$removed_cc || scalar @$added_cc) {
|
|
|
|
|
my $removed_users = Bugzilla::User->new_from_list($removed_cc);
|
|
|
|
|
my $added_users = Bugzilla::User->new_from_list($added_cc);
|
|
|
|
|
my $removed_names = join(', ', (map {$_->login} @$removed_users));
|
|
|
|
|
my $added_names = join(', ', (map {$_->login} @$added_users));
|
|
|
|
|
$changes->{cc} = [$removed_names, $added_names];
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Keywords
|
|
|
|
|
my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects};
|
|
|
|
|
my @new_kw_ids = map { $_->id } @{$self->keyword_objects};
|
|
|
|
|
|
|
|
|
|
my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids);
|
|
|
|
|
|
|
|
|
|
if (scalar @$removed_kw) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$dbh->do('DELETE FROM keywords WHERE bug_id = ? AND '
|
2008-12-15 15:53:33 +03:00
|
|
|
|
. $dbh->sql_in('keywordid', $removed_kw), undef, $self->id);
|
|
|
|
|
}
|
|
|
|
|
foreach my $keyword_id (@$added_kw) {
|
|
|
|
|
$dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)',
|
|
|
|
|
undef, $self->id, $keyword_id);
|
|
|
|
|
}
|
|
|
|
|
$dbh->do('UPDATE bugs SET keywords = ? WHERE bug_id = ?', undef,
|
|
|
|
|
$self->keywords, $self->id);
|
|
|
|
|
# If any changes were found, record it in the activity log
|
|
|
|
|
if (scalar @$removed_kw || scalar @$added_kw) {
|
|
|
|
|
my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw);
|
|
|
|
|
my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw);
|
|
|
|
|
my $removed_names = join(', ', (map {$_->name} @$removed_keywords));
|
|
|
|
|
my $added_names = join(', ', (map {$_->name} @$added_keywords));
|
|
|
|
|
$changes->{keywords} = [$removed_names, $added_names];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Dependencies
|
|
|
|
|
foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) {
|
|
|
|
|
my ($type, $other) = @$pair;
|
|
|
|
|
my $old = $old_bug->$type;
|
|
|
|
|
my $new = $self->$type;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($removed, $added) = diff_arrays($old, $new);
|
|
|
|
|
foreach my $removed_id (@$removed) {
|
|
|
|
|
$dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?",
|
|
|
|
|
undef, $removed_id, $self->id);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Add an activity entry for the other bug.
|
|
|
|
|
LogActivityEntry($removed_id, $other, $self->id, '',
|
2008-12-22 18:43:16 +03:00
|
|
|
|
$user->id, $delta_ts);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Update delta_ts on the other bug so that we trigger mid-airs.
|
|
|
|
|
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
|
|
|
|
|
undef, $delta_ts, $removed_id);
|
|
|
|
|
}
|
|
|
|
|
foreach my $added_id (@$added) {
|
|
|
|
|
$dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
|
|
|
|
|
undef, $added_id, $self->id);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Add an activity entry for the other bug.
|
|
|
|
|
LogActivityEntry($added_id, $other, '', $self->id,
|
2008-12-22 18:43:16 +03:00
|
|
|
|
$user->id, $delta_ts);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Update delta_ts on the other bug so that we trigger mid-airs.
|
|
|
|
|
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
|
|
|
|
|
undef, $delta_ts, $added_id);
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if (scalar(@$removed) || scalar(@$added)) {
|
|
|
|
|
$changes->{$type} = [join(', ', @$removed), join(', ', @$added)];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Groups
|
|
|
|
|
my %old_groups = map {$_->id => $_} @{$old_bug->groups_in};
|
|
|
|
|
my %new_groups = map {$_->id => $_} @{$self->groups_in};
|
|
|
|
|
my ($removed_gr, $added_gr) = diff_arrays([keys %old_groups],
|
|
|
|
|
[keys %new_groups]);
|
|
|
|
|
if (scalar @$removed_gr || scalar @$added_gr) {
|
|
|
|
|
if (@$removed_gr) {
|
|
|
|
|
my $qmarks = join(',', ('?') x @$removed_gr);
|
|
|
|
|
$dbh->do("DELETE FROM bug_group_map
|
|
|
|
|
WHERE bug_id = ? AND group_id IN ($qmarks)", undef,
|
|
|
|
|
$self->id, @$removed_gr);
|
|
|
|
|
}
|
|
|
|
|
my $sth_insert = $dbh->prepare(
|
|
|
|
|
'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)');
|
|
|
|
|
foreach my $gid (@$added_gr) {
|
|
|
|
|
$sth_insert->execute($self->id, $gid);
|
|
|
|
|
}
|
|
|
|
|
my @removed_names = map { $old_groups{$_}->name } @$removed_gr;
|
|
|
|
|
my @added_names = map { $new_groups{$_}->name } @$added_gr;
|
|
|
|
|
$changes->{'bug_group'} = [join(', ', @removed_names),
|
|
|
|
|
join(', ', @added_names)];
|
|
|
|
|
}
|
2009-12-02 16:39:36 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Flags
|
|
|
|
|
my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
|
|
|
|
|
if ($removed || $added) {
|
|
|
|
|
$changes->{'flagtypes.name'} = [$removed, $added];
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Comments
|
2009-12-17 15:20:04 +03:00
|
|
|
|
foreach my $comment (@{$self->{added_comments} || []})
|
|
|
|
|
{
|
|
|
|
|
if (Bugzilla->cgi->param('commentsilent'))
|
|
|
|
|
{
|
2011-10-04 18:01:24 +04:00
|
|
|
|
# Log silent comments
|
2009-12-17 15:20:04 +03:00
|
|
|
|
SilentLog($self->bug_id, $comment->{thetext});
|
|
|
|
|
}
|
2010-10-01 18:20:33 +04:00
|
|
|
|
$comment->{bug_id} = $self->bug_id;
|
|
|
|
|
$comment->{who} ||= $user->id;
|
|
|
|
|
$comment->{bug_when} = $delta_ts if !$comment->{bug_when} || $comment->{bug_when} gt $delta_ts;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $columns = join(',', keys %$comment);
|
|
|
|
|
my @values = values %$comment;
|
|
|
|
|
my $qmarks = join(',', ('?') x @values);
|
2010-10-01 18:20:33 +04:00
|
|
|
|
$dbh->do("INSERT INTO longdescs ($columns) VALUES ($qmarks)", undef, @values);
|
2010-12-24 20:00:25 +03:00
|
|
|
|
if (0+$comment->{work_time} != 0)
|
2010-10-01 18:20:33 +04:00
|
|
|
|
{
|
2011-10-04 18:01:24 +04:00
|
|
|
|
# Log worktime
|
2010-10-25 16:15:17 +04:00
|
|
|
|
$changes->{work_time} ||= [ '', 0 ];
|
|
|
|
|
$changes->{work_time}->[1] += $comment->{work_time};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-01 18:20:33 +04:00
|
|
|
|
|
2011-10-04 18:01:24 +04:00
|
|
|
|
foreach my $comment_id (keys %{$self->{comment_isprivate} || {}})
|
|
|
|
|
{
|
2010-03-01 17:35:18 +03:00
|
|
|
|
$dbh->do("UPDATE longdescs SET isprivate = ? WHERE comment_id = ?",
|
|
|
|
|
undef, $self->{comment_isprivate}->{$comment_id}, $comment_id);
|
2011-10-04 18:01:24 +04:00
|
|
|
|
# FIXME It'd be nice to track this in the bug activity.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach my $comment_id (keys %{$self->{comment_type} || {}})
|
|
|
|
|
{
|
|
|
|
|
$dbh->do("UPDATE longdescs SET type = ? WHERE comment_id = ?",
|
|
|
|
|
undef, $self->{comment_type}->{$comment_id}, $comment_id);
|
|
|
|
|
# FIXME It'd be nice to track this in the bug activity.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Insert the values into the multiselect value tables
|
|
|
|
|
my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT}
|
|
|
|
|
Bugzilla->active_custom_fields;
|
|
|
|
|
foreach my $field (@multi_selects) {
|
|
|
|
|
my $name = $field->name;
|
|
|
|
|
my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name);
|
|
|
|
|
if (scalar @$removed || scalar @$added) {
|
|
|
|
|
$changes->{$name} = [join(', ', @$removed), join(', ', @$added)];
|
|
|
|
|
|
|
|
|
|
$dbh->do("DELETE FROM bug_$name where bug_id = ?",
|
|
|
|
|
undef, $self->id);
|
|
|
|
|
foreach my $value (@{$self->$name}) {
|
|
|
|
|
$dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)",
|
|
|
|
|
undef, $self->id, $value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# See Also
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my ($removed_see, $added_see) =
|
2009-07-29 15:21:49 +04:00
|
|
|
|
diff_arrays($old_bug->see_also, $self->see_also);
|
|
|
|
|
|
|
|
|
|
if (scalar @$removed_see) {
|
|
|
|
|
$dbh->do('DELETE FROM bug_see_also WHERE bug_id = ? AND '
|
|
|
|
|
. $dbh->sql_in('value', [('?') x @$removed_see]),
|
|
|
|
|
undef, $self->id, @$removed_see);
|
|
|
|
|
}
|
|
|
|
|
foreach my $url (@$added_see) {
|
|
|
|
|
$dbh->do('INSERT INTO bug_see_also (bug_id, value) VALUES (?,?)',
|
|
|
|
|
undef, $self->id, $url);
|
|
|
|
|
}
|
|
|
|
|
# If any changes were found, record it in the activity log
|
|
|
|
|
if (scalar @$removed_see || scalar @$added_see) {
|
|
|
|
|
$changes->{see_also} = [join(', ', @$removed_see),
|
|
|
|
|
join(', ', @$added_see)];
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Log bugs_activity items
|
|
|
|
|
# XXX Eventually, when bugs_activity is able to track the dupe_id,
|
|
|
|
|
# this code should go below the duplicates-table-updating code below.
|
|
|
|
|
foreach my $field (keys %$changes) {
|
|
|
|
|
my $change = $changes->{$field};
|
|
|
|
|
my $from = defined $change->[0] ? $change->[0] : '';
|
|
|
|
|
my $to = defined $change->[1] ? $change->[1] : '';
|
|
|
|
|
LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id,
|
|
|
|
|
$delta_ts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check if we have to update the duplicates table and the other bug.
|
|
|
|
|
my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0);
|
|
|
|
|
if ($old_dup != $cur_dup) {
|
|
|
|
|
$dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id);
|
|
|
|
|
if ($cur_dup) {
|
|
|
|
|
$dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)',
|
|
|
|
|
undef, $self->id, $cur_dup);
|
|
|
|
|
if (my $update_dup = delete $self->{_dup_for_update}) {
|
|
|
|
|
$update_dup->update();
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If any change occurred, refresh the timestamp of the bug.
|
|
|
|
|
if (scalar(keys %$changes) || $self->{added_comments}) {
|
|
|
|
|
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
|
|
|
|
|
undef, ($delta_ts, $self->id));
|
|
|
|
|
$self->{delta_ts} = $delta_ts;
|
|
|
|
|
}
|
|
|
|
|
|
2010-10-22 20:57:14 +04:00
|
|
|
|
Bugzilla::Hook::process('bug_end_of_update',
|
|
|
|
|
{ bug => $self, timestamp => $delta_ts, changes => $changes,
|
|
|
|
|
old_bug => $old_bug });
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# The only problem with this here is that update() is often called
|
|
|
|
|
# in the middle of a transaction, and if that transaction is rolled
|
|
|
|
|
# back, this change will *not* be rolled back. As we expect rollbacks
|
|
|
|
|
# to be extremely rare, that is OK for us.
|
|
|
|
|
$self->_sync_fulltext()
|
2010-08-10 15:15:39 +04:00
|
|
|
|
if $self->{added_comments} || $changes->{short_desc}
|
|
|
|
|
|| $self->{comment_isprivate};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# Remove obsolete internal variables.
|
|
|
|
|
delete $self->{'_old_assigned_to'};
|
|
|
|
|
delete $self->{'_old_qa_contact'};
|
2011-03-29 12:55:21 +04:00
|
|
|
|
delete $self->{added_comments};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# Also flush the visible_bugs cache for this bug as the user's
|
|
|
|
|
# relationship with this bug may have changed.
|
|
|
|
|
delete Bugzilla->user->{_visible_bugs_cache}->{$self->id};
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $changes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Used by create().
|
|
|
|
|
# We need to handle multi-select fields differently than normal fields,
|
|
|
|
|
# because they're arrays and don't go into the bugs table.
|
|
|
|
|
sub _extract_multi_selects {
|
|
|
|
|
my ($invocant, $params) = @_;
|
|
|
|
|
|
|
|
|
|
my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT}
|
|
|
|
|
Bugzilla->active_custom_fields;
|
|
|
|
|
my %ms_values;
|
|
|
|
|
foreach my $field (@multi_selects) {
|
|
|
|
|
my $name = $field->name;
|
|
|
|
|
if (exists $params->{$name}) {
|
|
|
|
|
my $array = delete($params->{$name}) || [];
|
|
|
|
|
$ms_values{$name} = $array;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return \%ms_values;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Should be called any time you update short_desc or change a comment.
|
2009-01-30 20:10:49 +03:00
|
|
|
|
sub _sync_fulltext
|
|
|
|
|
{
|
2011-08-23 19:27:12 +04:00
|
|
|
|
use utf8;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($self, $new_bug) = @_;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
2009-01-30 20:10:49 +03:00
|
|
|
|
my ($short_desc) = $dbh->selectrow_array(
|
|
|
|
|
"SELECT short_desc FROM bugs WHERE bug_id=?", undef, $self->id
|
|
|
|
|
);
|
2011-08-23 19:27:12 +04:00
|
|
|
|
my ($nopriv, $priv) = ([], []);
|
|
|
|
|
for (@{ $dbh->selectall_arrayref(
|
2009-01-30 20:10:49 +03:00
|
|
|
|
"SELECT thetext, isprivate FROM longdescs WHERE bug_id=?",
|
|
|
|
|
undef, $self->id
|
2011-08-23 19:27:12 +04:00
|
|
|
|
) || [] })
|
2009-01-30 20:10:49 +03:00
|
|
|
|
{
|
2011-08-23 19:27:12 +04:00
|
|
|
|
$_->[1] ? push @$priv, $_->[0] : push @$nopriv, $_->[0];
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2011-08-23 19:27:12 +04:00
|
|
|
|
$nopriv = join "\n", @$nopriv;
|
|
|
|
|
$priv = join "\n", @$priv;
|
|
|
|
|
my $row = [ $short_desc, $nopriv, $priv ];
|
|
|
|
|
$_ = $dbh->quote_fulltext($_) for @$row;
|
|
|
|
|
## O_o Don't know how can it be tainted here, sometimes it was. Checking if it goes away.
|
|
|
|
|
#trick_taint($row);
|
2010-12-02 21:33:22 +03:00
|
|
|
|
my $sql;
|
|
|
|
|
if ($new_bug)
|
|
|
|
|
{
|
2011-08-23 19:27:12 +04:00
|
|
|
|
$sql = "INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_private)".
|
|
|
|
|
" VALUES (".join(',', $self->id, @$row).")";
|
2010-12-02 21:33:22 +03:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2011-08-23 19:27:12 +04:00
|
|
|
|
$sql = "UPDATE bugs_fulltext SET short_desc=$row->[0],".
|
|
|
|
|
" comments=$row->[1], comments_private=$row->[2] WHERE bug_id=".$self->id;
|
2010-12-02 21:33:22 +03:00
|
|
|
|
}
|
2011-08-23 19:27:12 +04:00
|
|
|
|
return $dbh->do($sql);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# This is the correct way to delete bugs from the DB.
|
|
|
|
|
# No bug should be deleted from anywhere else except from here.
|
|
|
|
|
#
|
|
|
|
|
sub remove_from_db {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
|
|
if ($self->{'error'}) {
|
|
|
|
|
ThrowCodeError("bug_error", { bug => $self });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $bug_id = $self->{'bug_id'};
|
|
|
|
|
|
|
|
|
|
# tables having 'bugs.bug_id' as a foreign key:
|
|
|
|
|
# - attachments
|
|
|
|
|
# - bug_group_map
|
|
|
|
|
# - bugs
|
|
|
|
|
# - bugs_activity
|
|
|
|
|
# - bugs_fulltext
|
|
|
|
|
# - cc
|
|
|
|
|
# - dependencies
|
|
|
|
|
# - duplicates
|
|
|
|
|
# - flags
|
|
|
|
|
# - keywords
|
|
|
|
|
# - longdescs
|
|
|
|
|
# - votes
|
|
|
|
|
|
|
|
|
|
# Also, the attach_data table uses attachments.attach_id as a foreign
|
|
|
|
|
# key, and so indirectly depends on a bug deletion too.
|
|
|
|
|
|
|
|
|
|
$dbh->bz_start_transaction();
|
|
|
|
|
|
|
|
|
|
$dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
|
|
|
|
|
undef, ($bug_id, $bug_id));
|
|
|
|
|
$dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
|
|
|
|
|
undef, ($bug_id, $bug_id));
|
|
|
|
|
$dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM votes WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
|
|
|
|
|
# The attach_data table doesn't depend on bugs.bug_id directly.
|
|
|
|
|
my $attach_ids =
|
|
|
|
|
$dbh->selectcol_arrayref("SELECT attach_id FROM attachments
|
|
|
|
|
WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
|
|
|
|
|
if (scalar(@$attach_ids)) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$dbh->do("DELETE FROM attach_data WHERE "
|
2008-12-15 15:53:33 +03:00
|
|
|
|
. $dbh->sql_in('id', $attach_ids));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Several of the previous tables also depend on attach_id.
|
|
|
|
|
$dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
$dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
|
|
|
|
|
$dbh->bz_commit_transaction();
|
|
|
|
|
|
|
|
|
|
# The bugs_fulltext table doesn't support transactions.
|
|
|
|
|
$dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id);
|
|
|
|
|
|
|
|
|
|
# Now this bug no longer exists
|
|
|
|
|
$self->DESTROY;
|
|
|
|
|
return $self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#####################################################################
|
|
|
|
|
# Validators
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
sub _check_alias {
|
|
|
|
|
my ($invocant, $alias) = @_;
|
|
|
|
|
$alias = trim($alias);
|
|
|
|
|
return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
|
|
|
|
|
|
|
|
|
|
# Make sure the alias isn't too long.
|
|
|
|
|
if (length($alias) > 20) {
|
|
|
|
|
ThrowUserError("alias_too_long");
|
|
|
|
|
}
|
|
|
|
|
# Make sure the alias isn't just a number.
|
|
|
|
|
if ($alias =~ /^\d+$/) {
|
|
|
|
|
ThrowUserError("alias_is_numeric", { alias => $alias });
|
|
|
|
|
}
|
|
|
|
|
# Make sure the alias has no commas or spaces.
|
|
|
|
|
if ($alias =~ /[, ]/) {
|
|
|
|
|
ThrowUserError("alias_has_comma_or_space", { alias => $alias });
|
|
|
|
|
}
|
|
|
|
|
# Make sure the alias is unique, or that it's already our alias.
|
|
|
|
|
my $other_bug = new Bugzilla::Bug($alias);
|
|
|
|
|
if (!$other_bug->{error}
|
|
|
|
|
&& (!ref $invocant || $other_bug->id != $invocant->id))
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("alias_in_use", { alias => $alias,
|
|
|
|
|
bug_id => $other_bug->id });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $alias;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_assigned_to {
|
|
|
|
|
my ($invocant, $assignee, $component) = @_;
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
|
|
|
|
|
# Default assignee is the component owner.
|
|
|
|
|
my $id;
|
|
|
|
|
# If this is a new bug, you can only set the assignee if you have editbugs.
|
|
|
|
|
# If you didn't specify the assignee, we use the default assignee.
|
|
|
|
|
if (!ref $invocant
|
|
|
|
|
&& (!$user->in_group('editbugs', $component->product_id) || !$assignee))
|
|
|
|
|
{
|
|
|
|
|
$id = $component->default_assignee->id;
|
|
|
|
|
} else {
|
|
|
|
|
if (!ref $assignee) {
|
|
|
|
|
$assignee = trim($assignee);
|
|
|
|
|
# When updating a bug, assigned_to can't be empty.
|
|
|
|
|
ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee;
|
|
|
|
|
$assignee = Bugzilla::User->check($assignee);
|
|
|
|
|
}
|
|
|
|
|
$id = $assignee->id;
|
|
|
|
|
# create() checks this another way, so we don't have to run this
|
|
|
|
|
# check during create().
|
|
|
|
|
$invocant->_check_strict_isolation_for_user($assignee) if ref $invocant;
|
2009-06-03 21:15:55 +04:00
|
|
|
|
my $prod = ref $invocant ? $invocant->product_obj : $component->product;
|
2009-09-02 17:39:24 +04:00
|
|
|
|
my ($ccg) = $prod->description =~ /\[[CС]{2}:\s*([^\]]+)\s*\]/iso;
|
2009-06-03 21:15:55 +04:00
|
|
|
|
if ($ccg && !$assignee->in_group($ccg))
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("cc_group_restriction", { user => $assignee->login });
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
return $id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_bug_file_loc {
|
|
|
|
|
my ($invocant, $url) = @_;
|
|
|
|
|
$url = '' if !defined($url);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# On bug entry, if bug_file_loc is "http://", the default, use an
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# empty value instead. However, on bug editing people can set that
|
|
|
|
|
# back if they *really* want to.
|
|
|
|
|
if (!ref $invocant && $url eq 'http://') {
|
|
|
|
|
$url = '';
|
|
|
|
|
}
|
|
|
|
|
return $url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_bug_status {
|
2008-12-18 15:49:01 +03:00
|
|
|
|
my ($invocant, $new_status, $product, $comment, $assigned_to) = @_;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
my @valid_statuses;
|
|
|
|
|
my $old_status; # Note that this is undef for new bugs.
|
|
|
|
|
|
|
|
|
|
if (ref $invocant) {
|
2010-05-15 00:02:34 +04:00
|
|
|
|
@valid_statuses = @{$invocant->statuses_available};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$product = $invocant->product_obj;
|
|
|
|
|
$old_status = $invocant->status;
|
|
|
|
|
my $comments = $invocant->{added_comments} || [];
|
|
|
|
|
$comment = $comments->[-1];
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
@valid_statuses = @{Bugzilla::Status->can_change_to()};
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if (!$product->allows_unconfirmed) {
|
|
|
|
|
@valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2008-12-22 18:43:16 +03:00
|
|
|
|
if ($assigned_to && $user->email ne $assigned_to)
|
2008-12-18 15:49:01 +03:00
|
|
|
|
{
|
|
|
|
|
# You can not assign bugs to other people
|
|
|
|
|
@valid_statuses = grep {$_->name ne 'ASSIGNED'} @valid_statuses;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Check permissions for users filing new bugs.
|
|
|
|
|
if (!ref $invocant) {
|
|
|
|
|
if ($user->in_group('editbugs', $product->id)
|
|
|
|
|
|| $user->in_group('canconfirm', $product->id)) {
|
|
|
|
|
# If the user with privs hasn't selected another status,
|
|
|
|
|
# select the first one of the list.
|
|
|
|
|
unless ($new_status) {
|
|
|
|
|
if (scalar(@valid_statuses) == 1) {
|
|
|
|
|
$new_status = $valid_statuses[0];
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ?
|
|
|
|
|
$valid_statuses[0] : $valid_statuses[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# A user with no privs cannot choose the initial status.
|
|
|
|
|
# If UNCONFIRMED is valid for this product, use it; else
|
|
|
|
|
# use the first bug status available.
|
|
|
|
|
if (grep {$_->name eq 'UNCONFIRMED'} @valid_statuses) {
|
|
|
|
|
$new_status = 'UNCONFIRMED';
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$new_status = $valid_statuses[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Time to validate the bug status.
|
|
|
|
|
$new_status = Bugzilla::Status->check($new_status) unless ref($new_status);
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# We skip this check if we are changing from a status to itself.
|
|
|
|
|
if ( (!$old_status || $old_status->id != $new_status->id)
|
2010-10-05 20:47:44 +04:00
|
|
|
|
&& !grep {$_->name eq $new_status->name} @valid_statuses)
|
2010-05-15 00:02:34 +04:00
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError('illegal_bug_status_transition',
|
|
|
|
|
{ old => $old_status, new => $new_status });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check if a comment is required for this change.
|
|
|
|
|
if ($new_status->comment_required_on_change_from($old_status) && !$comment)
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('comment_required', { old => $old_status,
|
|
|
|
|
new => $new_status });
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if (ref $invocant && $new_status->name eq 'ASSIGNED'
|
|
|
|
|
&& Bugzilla->params->{"usetargetmilestone"}
|
|
|
|
|
&& Bugzilla->params->{"musthavemilestoneonaccept"}
|
|
|
|
|
# musthavemilestoneonaccept applies only if at least two
|
|
|
|
|
# target milestones are defined for the product.
|
|
|
|
|
&& scalar(@{ $product->milestones }) > 1
|
|
|
|
|
&& $invocant->target_milestone eq $product->default_milestone)
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("milestone_required", { bug => $invocant });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $new_status->name if ref $invocant;
|
|
|
|
|
return ($new_status->name, $new_status->name eq 'UNCONFIRMED' ? 0 : 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_cc {
|
|
|
|
|
my ($invocant, $component, $ccs) = @_;
|
|
|
|
|
return [map {$_->id} @{$component->initial_cc}] unless $ccs;
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Allow comma-separated input as well as arrayrefs.
|
|
|
|
|
$ccs = [split(/[\s,]+/, $ccs)] if !ref $ccs;
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my %cc_ids;
|
2009-09-02 17:39:24 +04:00
|
|
|
|
my ($ccg) = $component->product->description =~ /\[[CС]{2}:\s*([^\]]+)\s*\]/iso;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
foreach my $person (@$ccs) {
|
|
|
|
|
next unless $person;
|
|
|
|
|
my $id = login_to_id($person, THROW_ERROR);
|
2009-06-03 21:15:55 +04:00
|
|
|
|
my $user = Bugzilla::User->new($id);
|
2009-09-04 16:25:23 +04:00
|
|
|
|
next if $ccg && !$user->in_group($ccg);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$cc_ids{$id} = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2009-10-21 18:46:47 +04:00
|
|
|
|
# CustIS Bug 55095: Don't enforce default CC
|
|
|
|
|
## Enforce Default CC
|
|
|
|
|
#$cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
return [keys %cc_ids];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_comment {
|
|
|
|
|
my ($invocant, $comment) = @_;
|
|
|
|
|
|
|
|
|
|
$comment = '' unless defined $comment;
|
|
|
|
|
|
|
|
|
|
# Remove any trailing whitespace. Leading whitespace could be
|
|
|
|
|
# a valid part of the comment.
|
|
|
|
|
$comment =~ s/\s*$//s;
|
|
|
|
|
$comment =~ s/\r\n?/\n/g; # Get rid of \r.
|
|
|
|
|
|
|
|
|
|
ThrowUserError('comment_too_long') if length($comment) > MAX_COMMENT_LENGTH;
|
|
|
|
|
return $comment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_commentprivacy {
|
|
|
|
|
my ($invocant, $comment_privacy) = @_;
|
2009-07-29 15:21:49 +04:00
|
|
|
|
if ($comment_privacy && !Bugzilla->user->is_insider) {
|
|
|
|
|
ThrowUserError('user_not_insider');
|
|
|
|
|
}
|
|
|
|
|
return $comment_privacy ? 1 : 0;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_comment_type {
|
|
|
|
|
my ($invocant, $type) = @_;
|
|
|
|
|
detaint_natural($type)
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|| ThrowCodeError('bad_arg', { argument => 'type',
|
2008-12-15 15:53:33 +03:00
|
|
|
|
function => caller });
|
|
|
|
|
return $type;
|
|
|
|
|
}
|
|
|
|
|
|
2011-01-26 19:56:34 +03:00
|
|
|
|
sub _check_component
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($invocant, $name, $product) = @_;
|
|
|
|
|
$name = trim($name);
|
|
|
|
|
$name || ThrowUserError("require_component");
|
|
|
|
|
($product = $invocant->product_obj) if ref $invocant;
|
2010-11-26 16:30:24 +03:00
|
|
|
|
my $obj = Bugzilla::Component->new({ product => $product, name => $name });
|
2010-11-24 20:22:45 +03:00
|
|
|
|
if (!$obj)
|
|
|
|
|
{
|
|
|
|
|
$invocant->dependent_validators->{component} = $name;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $obj;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_deadline {
|
|
|
|
|
my ($invocant, $date) = @_;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Check time-tracking permissions.
|
|
|
|
|
# deadline() returns '' instead of undef if no deadline is set.
|
|
|
|
|
my $current = ref $invocant ? ($invocant->deadline || undef) : undef;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $current unless Bugzilla->user->is_timetracker;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Validate entered deadline
|
|
|
|
|
$date = trim($date);
|
|
|
|
|
return undef if !$date;
|
|
|
|
|
validate_date($date)
|
|
|
|
|
|| ThrowUserError('illegal_date', { date => $date,
|
|
|
|
|
format => 'YYYY-MM-DD' });
|
|
|
|
|
return $date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Takes two comma/space-separated strings and returns arrayrefs
|
|
|
|
|
# of valid bug IDs.
|
|
|
|
|
sub _check_dependencies {
|
|
|
|
|
my ($invocant, $depends_on, $blocks, $product) = @_;
|
|
|
|
|
|
|
|
|
|
if (!ref $invocant) {
|
|
|
|
|
# Only editbugs users can set dependencies on bug entry.
|
|
|
|
|
return ([], []) unless Bugzilla->user->in_group('editbugs',
|
|
|
|
|
$product->id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
|
|
|
|
|
|
2010-11-03 20:13:12 +03:00
|
|
|
|
foreach my $type (qw(dependson blocked)) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my @bug_ids = ref($deps_in{$type})
|
|
|
|
|
? @{$deps_in{$type}}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
: split(/[\s,]+/, $deps_in{$type});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Eliminate nulls.
|
|
|
|
|
@bug_ids = grep {$_} @bug_ids;
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# We do this up here to make sure all aliases are converted to IDs.
|
|
|
|
|
@bug_ids = map { $invocant->check($_, $type)->id } @bug_ids;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my @check_access = @bug_ids;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# When we're updating a bug, only added or removed bug_ids are
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# checked for whether or not we can see/edit those bugs.
|
|
|
|
|
if (ref $invocant) {
|
|
|
|
|
my $old = $invocant->$type;
|
|
|
|
|
my ($removed, $added) = diff_arrays($old, \@bug_ids);
|
|
|
|
|
@check_access = (@$added, @$removed);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Check field permissions if we've changed anything.
|
|
|
|
|
if (@check_access) {
|
|
|
|
|
my $privs;
|
|
|
|
|
if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) {
|
|
|
|
|
ThrowUserError('illegal_change', { field => $type,
|
|
|
|
|
privs => $privs });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
foreach my $modified_id (@check_access) {
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my $delta_bug = $invocant->check($modified_id);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Under strict isolation, you can't modify a bug if you can't
|
|
|
|
|
# edit it, even if you can see it.
|
|
|
|
|
if (Bugzilla->params->{"strict_isolation"}) {
|
2010-06-09 19:10:11 +04:00
|
|
|
|
if (!$user->can_edit_bug($delta_bug)) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError("illegal_change_deps", {field => $type});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$deps_in{$type} = \@bug_ids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# And finally, check for dependency loops.
|
2011-09-05 17:46:27 +04:00
|
|
|
|
ValidateDependencies($invocant, $deps_in{dependson}, $deps_in{blocked});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
return ($deps_in{dependson}, $deps_in{blocked});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_dup_id {
|
|
|
|
|
my ($self, $dupe_of) = @_;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$dupe_of = trim($dupe_of);
|
|
|
|
|
$dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' });
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# Validate the bug ID. The second argument will force check() to only
|
|
|
|
|
# make sure that the bug exists, and convert the alias to the bug ID
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# if a string is passed. Group restrictions are checked below.
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my $dupe_of_bug = $self->check($dupe_of, 'dup_id');
|
|
|
|
|
$dupe_of = $dupe_of_bug->id;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# If the dupe is unchanged, we have nothing more to check.
|
|
|
|
|
return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of);
|
|
|
|
|
|
|
|
|
|
# If we come here, then the duplicate is new. We have to make sure
|
|
|
|
|
# that we can view/change it (issue A on bug 96085).
|
2009-07-29 15:21:49 +04:00
|
|
|
|
$dupe_of_bug->check_is_visible;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# Make sure a loop isn't created when marking this bug
|
|
|
|
|
# as duplicate.
|
|
|
|
|
my %dupes;
|
|
|
|
|
my $this_dup = $dupe_of;
|
|
|
|
|
my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?');
|
|
|
|
|
|
|
|
|
|
while ($this_dup) {
|
|
|
|
|
if ($this_dup == $self->id) {
|
|
|
|
|
ThrowUserError('dupe_loop_detected', { bug_id => $self->id,
|
|
|
|
|
dupe_of => $dupe_of });
|
|
|
|
|
}
|
|
|
|
|
# If $dupes{$this_dup} is already set to 1, then a loop
|
|
|
|
|
# already exists which does not involve this bug.
|
|
|
|
|
# As the user is not responsible for this loop, do not
|
|
|
|
|
# prevent him from marking this bug as a duplicate.
|
|
|
|
|
last if exists $dupes{$this_dup};
|
|
|
|
|
$dupes{$this_dup} = 1;
|
|
|
|
|
$this_dup = $dbh->selectrow_array($sth, undef, $this_dup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $cur_dup = $self->dup_id || 0;
|
|
|
|
|
if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'}
|
|
|
|
|
&& !$self->{added_comments})
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('comment_required');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Should we add the reporter to the CC list of the new bug?
|
|
|
|
|
# If he can see the bug...
|
|
|
|
|
if ($self->reporter->can_see_bug($dupe_of)) {
|
|
|
|
|
# We only add him if he's not the reporter of the other bug.
|
|
|
|
|
$self->{_add_dup_cc} = 1
|
|
|
|
|
if $dupe_of_bug->reporter->id != $self->reporter->id;
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# What if the reporter currently can't see the new bug? In the browser
|
|
|
|
|
# interface, we prompt the user. In other interfaces, we default to
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# not adding the user, as the safest option.
|
|
|
|
|
elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
|
|
|
|
|
# If we've already confirmed whether the user should be added...
|
|
|
|
|
my $cgi = Bugzilla->cgi;
|
|
|
|
|
my $add_confirmed = $cgi->param('confirm_add_duplicate');
|
|
|
|
|
if (defined $add_confirmed) {
|
|
|
|
|
$self->{_add_dup_cc} = $add_confirmed;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# Note that here we don't check if he user is already the reporter
|
|
|
|
|
# of the dupe_of bug, since we already checked if he can *see*
|
|
|
|
|
# the bug, above. People might have reporter_accessible turned
|
|
|
|
|
# off, but cclist_accessible turned on, so they might want to
|
|
|
|
|
# add the reporter even though he's already the reporter of the
|
|
|
|
|
# dup_of bug.
|
|
|
|
|
my $vars = {};
|
|
|
|
|
my $template = Bugzilla->template;
|
|
|
|
|
# Ask the user what they want to do about the reporter.
|
2009-07-29 15:21:49 +04:00
|
|
|
|
$vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$vars->{'original_bug_id'} = $dupe_of;
|
|
|
|
|
$vars->{'duplicate_bug_id'} = $self->id;
|
2010-06-11 21:31:17 +04:00
|
|
|
|
$cgi->send_header();
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
|
|
|
|
|
|| ThrowTemplateError($template->error());
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $dupe_of;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_estimated_time {
|
|
|
|
|
return $_[0]->_check_time($_[1], 'estimated_time');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_groups {
|
|
|
|
|
my ($invocant, $product, $group_ids) = @_;
|
|
|
|
|
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
|
|
|
|
|
my %add_groups;
|
|
|
|
|
my $controls = $product->group_controls;
|
|
|
|
|
|
|
|
|
|
foreach my $id (@$group_ids) {
|
|
|
|
|
my $group = new Bugzilla::Group($id)
|
|
|
|
|
|| ThrowUserError("invalid_group_ID");
|
|
|
|
|
|
|
|
|
|
# This can only happen if somebody hacked the enter_bug form.
|
|
|
|
|
ThrowCodeError("inactive_group", { name => $group->name })
|
|
|
|
|
unless $group->is_active;
|
|
|
|
|
|
|
|
|
|
my $membercontrol = $controls->{$id}
|
|
|
|
|
&& $controls->{$id}->{membercontrol};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my $othercontrol = $controls->{$id}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
&& $controls->{$id}->{othercontrol};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $permit = ($membercontrol && $user->in_group($group->name))
|
|
|
|
|
|| $othercontrol;
|
|
|
|
|
|
|
|
|
|
$add_groups{$id} = 1 if $permit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach my $id (keys %$controls) {
|
|
|
|
|
next unless $controls->{$id}->{'group'}->is_active;
|
|
|
|
|
my $membercontrol = $controls->{$id}->{membercontrol} || 0;
|
|
|
|
|
my $othercontrol = $controls->{$id}->{othercontrol} || 0;
|
|
|
|
|
|
|
|
|
|
# Add groups required
|
|
|
|
|
if ($membercontrol == CONTROLMAPMANDATORY
|
|
|
|
|
|| ($othercontrol == CONTROLMAPMANDATORY
|
2010-10-05 20:47:44 +04:00
|
|
|
|
&& !$user->in_group_id($id)))
|
2008-12-15 15:53:33 +03:00
|
|
|
|
{
|
|
|
|
|
# User had no option, bug needs to be in this group.
|
|
|
|
|
$add_groups{$id} = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my @add_groups = keys %add_groups;
|
|
|
|
|
return \@add_groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_keywords {
|
|
|
|
|
my ($invocant, $keyword_string, $product) = @_;
|
|
|
|
|
$keyword_string = trim($keyword_string);
|
|
|
|
|
return [] if !$keyword_string;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# On creation, only editbugs users can set keywords.
|
|
|
|
|
if (!ref $invocant) {
|
|
|
|
|
return [] if !Bugzilla->user->in_group('editbugs', $product->id);
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my %keywords;
|
|
|
|
|
foreach my $keyword (split(/[\s,]+/, $keyword_string)) {
|
|
|
|
|
next unless $keyword;
|
|
|
|
|
my $obj = new Bugzilla::Keyword({ name => $keyword });
|
|
|
|
|
ThrowUserError("unknown_keyword", { keyword => $keyword }) if !$obj;
|
|
|
|
|
$keywords{$obj->id} = $obj;
|
|
|
|
|
}
|
|
|
|
|
return [values %keywords];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_product {
|
|
|
|
|
my ($invocant, $name) = @_;
|
|
|
|
|
$name = trim($name);
|
|
|
|
|
# If we're updating the bug and they haven't changed the product,
|
|
|
|
|
# always allow it.
|
|
|
|
|
if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) {
|
|
|
|
|
return $invocant->product_obj;
|
|
|
|
|
}
|
|
|
|
|
# Check that the product exists and that the user
|
|
|
|
|
# is allowed to enter bugs into this product.
|
|
|
|
|
Bugzilla->user->can_enter_product($name, THROW_ERROR);
|
|
|
|
|
# can_enter_product already does everything that check_product
|
|
|
|
|
# would do for us, so we don't need to use it.
|
|
|
|
|
return new Bugzilla::Product({ name => $name });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_priority {
|
|
|
|
|
my ($invocant, $priority) = @_;
|
|
|
|
|
if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) {
|
|
|
|
|
$priority = Bugzilla->params->{'defaultpriority'};
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $invocant->_check_select_field($priority, 'priority');
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_qa_contact {
|
|
|
|
|
my ($invocant, $qa_contact, $component) = @_;
|
|
|
|
|
$qa_contact = trim($qa_contact) if !ref $qa_contact;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $id;
|
|
|
|
|
if (!ref $invocant) {
|
|
|
|
|
# Bugs get no QA Contact on creation if useqacontact is off.
|
|
|
|
|
return undef if !Bugzilla->params->{useqacontact};
|
|
|
|
|
# Set the default QA Contact if one isn't specified or if the
|
|
|
|
|
# user doesn't have editbugs.
|
|
|
|
|
if (!Bugzilla->user->in_group('editbugs', $component->product_id)
|
|
|
|
|
|| !$qa_contact)
|
|
|
|
|
{
|
|
|
|
|
$id = $component->default_qa_contact->id;
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# If a QA Contact was specified or if we're updating, check
|
|
|
|
|
# the QA Contact for validity.
|
|
|
|
|
if (!defined $id && $qa_contact) {
|
|
|
|
|
$qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact;
|
|
|
|
|
$id = $qa_contact->id;
|
|
|
|
|
# create() checks this another way, so we don't have to run this
|
|
|
|
|
# check during create().
|
|
|
|
|
# If there is no QA contact, this check is not required.
|
|
|
|
|
$invocant->_check_strict_isolation_for_user($qa_contact)
|
|
|
|
|
if (ref $invocant && $id);
|
2009-06-03 21:15:55 +04:00
|
|
|
|
my $prod = ref $invocant ? $invocant->product_obj : $component->product;
|
2009-09-02 17:39:24 +04:00
|
|
|
|
my ($ccg) = $prod->description =~ /\[[CС]{2}:\s*([^\]]+)\s*\]/iso;
|
2009-06-03 21:15:55 +04:00
|
|
|
|
if ($ccg && !$qa_contact->in_group($ccg))
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("cc_group_restriction", { user => $qa_contact->login });
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# "0" always means "undef", for QA Contact.
|
|
|
|
|
return $id || undef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_remaining_time {
|
|
|
|
|
return $_[0]->_check_time($_[1], 'remaining_time');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_reporter {
|
|
|
|
|
my $invocant = shift;
|
|
|
|
|
my $reporter;
|
|
|
|
|
if (ref $invocant) {
|
|
|
|
|
# You cannot change the reporter of a bug.
|
|
|
|
|
$reporter = $invocant->reporter->id;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# On bug creation, the reporter is the logged in user
|
|
|
|
|
# (meaning that he must be logged in first!).
|
|
|
|
|
$reporter = Bugzilla->user->id;
|
|
|
|
|
$reporter || ThrowCodeError('invalid_user');
|
|
|
|
|
}
|
2009-06-03 21:15:55 +04:00
|
|
|
|
if ($reporter && ref $invocant)
|
|
|
|
|
{
|
|
|
|
|
# Custis Bug 38616
|
|
|
|
|
# For situations of moving external bugs into internal
|
|
|
|
|
my $prod = $invocant->product_obj;
|
2009-09-02 17:39:24 +04:00
|
|
|
|
my ($ccg) = $prod->description =~ /\[[CС]{2}:\s*([^\]]+)\s*\]/iso;
|
2009-06-03 21:15:55 +04:00
|
|
|
|
my $user = Bugzilla::User->new($reporter);
|
|
|
|
|
if ($ccg && !$user->in_group($ccg))
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("cc_group_restriction", { user => $user->login });
|
|
|
|
|
}
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $reporter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_resolution {
|
|
|
|
|
my ($self, $resolution) = @_;
|
|
|
|
|
$resolution = trim($resolution);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Throw a special error for resolving bugs without a resolution
|
|
|
|
|
# (or trying to change the resolution to '' on a closed bug without
|
|
|
|
|
# using clear_resolution).
|
|
|
|
|
ThrowUserError('missing_resolution', { status => $self->status->name })
|
|
|
|
|
if !$resolution && !$self->status->is_open;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Make sure this is a valid resolution.
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$resolution = $self->_check_select_field($resolution, 'resolution');
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# Don't allow open bugs to have resolutions.
|
|
|
|
|
ThrowUserError('resolution_not_allowed') if $self->status->is_open;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Check noresolveonopenblockers.
|
2010-06-25 20:04:36 +04:00
|
|
|
|
if (Bugzilla->params->{"noresolveonopenblockers"}
|
|
|
|
|
&& $resolution eq 'FIXED'
|
|
|
|
|
&& (!$self->resolution || $resolution ne $self->resolution))
|
2008-12-15 15:53:33 +03:00
|
|
|
|
{
|
|
|
|
|
my @dependencies = CountOpenDependencies($self->id);
|
|
|
|
|
if (@dependencies) {
|
|
|
|
|
ThrowUserError("still_unresolved_bugs",
|
|
|
|
|
{ dependencies => \@dependencies,
|
|
|
|
|
dependency_count => scalar @dependencies });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check if they're changing the resolution and need to comment.
|
2010-10-05 20:47:44 +04:00
|
|
|
|
if (Bugzilla->params->{'commentonchange_resolution'}
|
|
|
|
|
&& $self->resolution && $resolution ne $self->resolution
|
2008-12-15 15:53:33 +03:00
|
|
|
|
&& !$self->{added_comments})
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('comment_required');
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $resolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_short_desc {
|
|
|
|
|
my ($invocant, $short_desc) = @_;
|
|
|
|
|
# Set the parameter to itself, but cleaned up
|
|
|
|
|
$short_desc = clean_text($short_desc) if $short_desc;
|
|
|
|
|
|
|
|
|
|
if (!defined $short_desc || $short_desc eq '') {
|
|
|
|
|
ThrowUserError("require_summary");
|
|
|
|
|
}
|
|
|
|
|
return $short_desc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; }
|
|
|
|
|
|
|
|
|
|
# Unlike other checkers, this one doesn't return anything.
|
|
|
|
|
sub _check_strict_isolation {
|
|
|
|
|
my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_;
|
|
|
|
|
return unless Bugzilla->params->{'strict_isolation'};
|
|
|
|
|
|
|
|
|
|
if (ref $invocant) {
|
|
|
|
|
my $original = $invocant->new($invocant->id);
|
|
|
|
|
|
|
|
|
|
# We only check people if they've been added. This way, if
|
|
|
|
|
# strict_isolation is turned on when there are invalid users
|
|
|
|
|
# on bugs, people can still add comments and so on.
|
|
|
|
|
my @old_cc = map { $_->id } @{$original->cc_users};
|
|
|
|
|
my @new_cc = map { $_->id } @{$invocant->cc_users};
|
|
|
|
|
my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc);
|
|
|
|
|
$ccs = Bugzilla::User->new_from_list($added);
|
|
|
|
|
|
|
|
|
|
$assignee = $invocant->assigned_to
|
|
|
|
|
if $invocant->assigned_to->id != $original->assigned_to->id;
|
|
|
|
|
if ($invocant->qa_contact
|
|
|
|
|
&& (!$original->qa_contact
|
|
|
|
|
|| $invocant->qa_contact->id != $original->qa_contact->id))
|
|
|
|
|
{
|
|
|
|
|
$qa_contact = $invocant->qa_contact;
|
|
|
|
|
}
|
|
|
|
|
$product = $invocant->product_obj;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my @related_users = @$ccs;
|
|
|
|
|
push(@related_users, $assignee) if $assignee;
|
|
|
|
|
|
|
|
|
|
if (Bugzilla->params->{'useqacontact'} && $qa_contact) {
|
|
|
|
|
push(@related_users, $qa_contact);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@related_users = @{Bugzilla::User->new_from_list(\@related_users)}
|
|
|
|
|
if !ref $invocant;
|
|
|
|
|
|
|
|
|
|
# For each unique user in @related_users...(assignee and qa_contact
|
|
|
|
|
# could be duplicates of users in the CC list)
|
|
|
|
|
my %unique_users = map {$_->id => $_} @related_users;
|
|
|
|
|
my @blocked_users;
|
|
|
|
|
foreach my $id (keys %unique_users) {
|
|
|
|
|
my $related_user = $unique_users{$id};
|
|
|
|
|
if (!$related_user->can_edit_product($product->id) ||
|
|
|
|
|
!$related_user->can_see_product($product->name)) {
|
|
|
|
|
push (@blocked_users, $related_user->login);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (scalar(@blocked_users)) {
|
|
|
|
|
my %vars = ( users => \@blocked_users,
|
|
|
|
|
product => $product->name );
|
|
|
|
|
if (ref $invocant) {
|
|
|
|
|
$vars{'bug_id'} = $invocant->id;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$vars{'new'} = 1;
|
|
|
|
|
}
|
|
|
|
|
ThrowUserError("invalid_user_group", \%vars);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# This is used by various set_ checkers, to make their code simpler.
|
|
|
|
|
sub _check_strict_isolation_for_user {
|
|
|
|
|
my ($self, $user) = @_;
|
|
|
|
|
return unless Bugzilla->params->{"strict_isolation"};
|
2010-06-09 19:10:11 +04:00
|
|
|
|
if (!$user->can_edit_bug($self)) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError('invalid_user_group',
|
|
|
|
|
{ users => $user->login,
|
|
|
|
|
product => $self->product,
|
|
|
|
|
bug_id => $self->id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
sub _check_target_milestone
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($invocant, $target, $product) = @_;
|
|
|
|
|
$product = $invocant->product_obj if ref $invocant;
|
|
|
|
|
$target = trim($target);
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# Reporters can move bugs between products but not set the TM.
|
|
|
|
|
# So reset it to the default value.
|
|
|
|
|
if (!defined $target || !Bugzilla->params->{usetargetmilestone} ||
|
|
|
|
|
blessed $invocant && !$invocant->check_can_change_field('target_milestone', 0, 1))
|
|
|
|
|
{
|
|
|
|
|
$target = $product->default_milestone;
|
|
|
|
|
}
|
|
|
|
|
my $object = Bugzilla::Milestone->new({ product => $product, name => $target });
|
|
|
|
|
if (!$object)
|
|
|
|
|
{
|
|
|
|
|
$invocant->dependent_validators->{target_milestone} = $target;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $object->name;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_time {
|
|
|
|
|
my ($invocant, $time, $field) = @_;
|
|
|
|
|
|
|
|
|
|
my $current = 0;
|
|
|
|
|
if (ref $invocant && $field ne 'work_time') {
|
|
|
|
|
$current = $invocant->$field;
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $current unless Bugzilla->user->is_timetracker;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2009-12-10 19:28:19 +03:00
|
|
|
|
$time = ValidateTime($time, $field);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $time;
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
sub _check_version
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($invocant, $version, $product) = @_;
|
|
|
|
|
$version = trim($version);
|
|
|
|
|
($product = $invocant->product_obj) if ref $invocant;
|
2011-01-26 19:56:34 +03:00
|
|
|
|
my $object = Bugzilla::Version->new({ product => $product, name => $version }) if length $version;
|
2010-11-24 20:22:45 +03:00
|
|
|
|
if (!$object)
|
|
|
|
|
{
|
|
|
|
|
$invocant->dependent_validators->{version} = $version;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $object->name;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_work_time {
|
|
|
|
|
return $_[0]->_check_time($_[1], 'work_time');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Custom Field Validators
|
|
|
|
|
|
|
|
|
|
sub _check_datetime_field {
|
|
|
|
|
my ($invocant, $date_time) = @_;
|
|
|
|
|
|
|
|
|
|
# Empty datetimes are empty strings or strings only containing
|
|
|
|
|
# 0's, whitespace, and punctuation.
|
|
|
|
|
if ($date_time =~ /^[\s0[:punct:]]*$/) {
|
|
|
|
|
return undef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$date_time = trim($date_time);
|
|
|
|
|
my ($date, $time) = split(' ', $date_time);
|
|
|
|
|
if ($date && !validate_date($date)) {
|
|
|
|
|
ThrowUserError('illegal_date', { date => $date,
|
|
|
|
|
format => 'YYYY-MM-DD' });
|
|
|
|
|
}
|
|
|
|
|
if ($time && !validate_time($time)) {
|
|
|
|
|
ThrowUserError('illegal_time', { 'time' => $time,
|
|
|
|
|
format => 'HH:MM:SS' });
|
|
|
|
|
}
|
|
|
|
|
return $date_time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; }
|
|
|
|
|
|
2010-07-29 17:25:07 +04:00
|
|
|
|
sub _check_numeric_field
|
|
|
|
|
{
|
|
|
|
|
my ($invocant, $text) = @_;
|
|
|
|
|
($text) = $text =~ /^(-?\d+(\.\d+)?)$/so;
|
|
|
|
|
return $text || 0;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub _check_freetext_field {
|
|
|
|
|
my ($invocant, $text) = @_;
|
|
|
|
|
|
|
|
|
|
$text = (defined $text) ? trim($text) : '';
|
|
|
|
|
if (length($text) > MAX_FREETEXT_LENGTH) {
|
|
|
|
|
ThrowUserError('freetext_too_long', { text => $text });
|
|
|
|
|
}
|
|
|
|
|
return $text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _check_multi_select_field {
|
|
|
|
|
my ($invocant, $values, $field) = @_;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|
|
|
|
|
# Allow users (mostly email_in.pl) to specify multi-selects as
|
|
|
|
|
# comma-separated values.
|
|
|
|
|
if (defined $values and !ref $values) {
|
|
|
|
|
# We don't split on spaces because multi-select values can and often
|
|
|
|
|
# do have spaces in them. (Theoretically they can have commas in them
|
|
|
|
|
# too, but that's much less common and people should be able to work
|
|
|
|
|
# around it pretty cleanly, if they want to use email_in.pl.)
|
|
|
|
|
$values = [split(',', $values)];
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return [] if !$values;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my @checked_values;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
foreach my $value (@$values) {
|
2010-05-15 00:02:34 +04:00
|
|
|
|
push(@checked_values, $invocant->_check_select_field($value, $field));
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return \@checked_values;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
sub dependent_validators
|
|
|
|
|
{
|
|
|
|
|
my $invocant = shift;
|
|
|
|
|
my $tmp = ref $invocant
|
|
|
|
|
? ($invocant->{dependent_validators} ||= {})
|
|
|
|
|
: ($dependent_validators ||= {});
|
|
|
|
|
return $tmp;
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 16:41:09 +03:00
|
|
|
|
sub _check_select_field
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($invocant, $value, $field) = @_;
|
2010-11-24 16:41:09 +03:00
|
|
|
|
$field = Bugzilla->get_field($field);
|
|
|
|
|
# Check dependent field values
|
|
|
|
|
if ($field->visibility_field_id || $field->value_field_id)
|
|
|
|
|
{
|
2011-05-24 19:59:11 +04:00
|
|
|
|
my $t = Bugzilla::Field::Choice->type($field);
|
|
|
|
|
my $object = $t->new({ name => $value }) || $value;
|
2010-11-24 20:22:45 +03:00
|
|
|
|
my $tmp = $invocant->dependent_validators;
|
|
|
|
|
# Remember the call and perform check later
|
|
|
|
|
if ($field->type == FIELD_TYPE_MULTI_SELECT)
|
2010-11-24 16:41:09 +03:00
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
push @{$tmp->{$field->name}}, $object;
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2010-11-24 20:22:45 +03:00
|
|
|
|
$tmp->{$field->name} = $object;
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
2011-05-24 19:59:11 +04:00
|
|
|
|
return $value;
|
2010-11-24 16:41:09 +03:00
|
|
|
|
}
|
2011-05-24 19:59:11 +04:00
|
|
|
|
my $object = Bugzilla::Field::Choice->type($field)->check($value);
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return $object->name;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
sub _check_bugid_field
|
|
|
|
|
{
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my ($invocant, $value, $field) = @_;
|
|
|
|
|
return undef if !$value;
|
2010-12-13 20:37:18 +03:00
|
|
|
|
my $r;
|
|
|
|
|
if (ref $invocant && $invocant->{$field} eq $value)
|
|
|
|
|
{
|
|
|
|
|
# If there is no change, do not check the bug id, as it may be invisible for current user
|
|
|
|
|
$r = $invocant->{$field};
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
$r = $invocant->check($value, $field)->id;
|
|
|
|
|
}
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# Check if the field is not visible anymore
|
2010-12-13 20:08:25 +03:00
|
|
|
|
# + Optionally add to dependencies
|
2010-11-24 20:22:45 +03:00
|
|
|
|
# FIXME probably move it somewhere
|
2010-12-13 20:08:25 +03:00
|
|
|
|
if (Bugzilla->get_field($field)->visibility_field_id ||
|
|
|
|
|
Bugzilla->get_field($field)->add_to_deps)
|
2010-11-24 20:22:45 +03:00
|
|
|
|
{
|
2010-12-13 20:37:18 +03:00
|
|
|
|
$invocant->dependent_validators->{$field} = $r;
|
2010-11-24 20:22:45 +03:00
|
|
|
|
}
|
2010-12-13 20:37:18 +03:00
|
|
|
|
return $r;
|
2009-07-29 15:21:49 +04:00
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#####################################################################
|
|
|
|
|
# Class Accessors
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
sub fields {
|
|
|
|
|
my $class = shift;
|
|
|
|
|
|
2011-08-24 02:17:45 +04:00
|
|
|
|
# FIXME bugzilla.dtd should be autogenerated
|
|
|
|
|
# FIXME change useplatform/useopsys/useqacontact to "field(xxx).is_obsolete"
|
2010-07-29 17:25:07 +04:00
|
|
|
|
my @fields =
|
|
|
|
|
(
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Standard Fields
|
|
|
|
|
# Keep this ordering in sync with bugzilla.dtd.
|
|
|
|
|
qw(bug_id alias creation_ts short_desc delta_ts
|
|
|
|
|
reporter_accessible cclist_accessible
|
|
|
|
|
classification_id classification
|
2009-10-22 15:42:07 +04:00
|
|
|
|
product component version),
|
|
|
|
|
# Use platform?
|
|
|
|
|
Bugzilla->params->{useplatform} ? "rep_platform" : (),
|
|
|
|
|
# Use OS?
|
|
|
|
|
Bugzilla->params->{useopsys} ? "op_sys" : (),
|
|
|
|
|
# Standard Fields continued...
|
|
|
|
|
qw(bug_status resolution dup_id see_also
|
2008-12-15 15:53:33 +03:00
|
|
|
|
bug_file_loc status_whiteboard keywords
|
|
|
|
|
priority bug_severity target_milestone
|
|
|
|
|
dependson blocked votes everconfirmed
|
|
|
|
|
reporter assigned_to cc estimated_time
|
|
|
|
|
remaining_time actual_time deadline),
|
|
|
|
|
|
2009-10-22 15:42:07 +04:00
|
|
|
|
# Use QA contact?
|
|
|
|
|
Bugzilla->params->{useqacontact} ? "qa_contact" : (),
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Custom Fields
|
|
|
|
|
map { $_->name } Bugzilla->active_custom_fields
|
|
|
|
|
);
|
2010-05-15 00:02:34 +04:00
|
|
|
|
Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} );
|
2010-07-29 17:25:07 +04:00
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
return @fields;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#####################################################################
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# Mutators
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
# To run check_can_change_field.
|
|
|
|
|
sub _set_global_validator {
|
|
|
|
|
my ($self, $value, $field) = @_;
|
|
|
|
|
my $current = $self->$field;
|
|
|
|
|
my $privs;
|
|
|
|
|
|
|
|
|
|
if (ref $current && ref($current) ne 'ARRAY'
|
|
|
|
|
&& $current->isa('Bugzilla::Object')) {
|
|
|
|
|
$current = $current->id ;
|
|
|
|
|
}
|
|
|
|
|
if (ref $value && ref($value) ne 'ARRAY'
|
|
|
|
|
&& $value->isa('Bugzilla::Object')) {
|
|
|
|
|
$value = $value->id ;
|
|
|
|
|
}
|
|
|
|
|
my $can = $self->check_can_change_field($field, $current, $value, \$privs);
|
|
|
|
|
if (!$can) {
|
|
|
|
|
if ($field eq 'assigned_to' || $field eq 'qa_contact') {
|
|
|
|
|
$value = user_id_to_login($value);
|
|
|
|
|
$current = user_id_to_login($current);
|
|
|
|
|
}
|
|
|
|
|
ThrowUserError('illegal_change', { field => $field,
|
|
|
|
|
oldvalue => $current,
|
|
|
|
|
newvalue => $value,
|
|
|
|
|
privs => $privs });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#################
|
|
|
|
|
# "Set" Methods #
|
|
|
|
|
#################
|
|
|
|
|
|
|
|
|
|
sub set_alias { $_[0]->set('alias', $_[1]); }
|
|
|
|
|
sub set_assigned_to {
|
|
|
|
|
my ($self, $value) = @_;
|
|
|
|
|
$self->set('assigned_to', $value);
|
|
|
|
|
# Store the old assignee. check_can_change_field() needs it.
|
|
|
|
|
$self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id;
|
|
|
|
|
delete $self->{'assigned_to_obj'};
|
|
|
|
|
}
|
|
|
|
|
sub reset_assigned_to {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my $comp = $self->component_obj;
|
|
|
|
|
$self->set_assigned_to($comp->default_assignee);
|
|
|
|
|
}
|
|
|
|
|
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
|
|
|
|
|
sub set_comment_is_private {
|
|
|
|
|
my ($self, $comment_id, $isprivate) = @_;
|
|
|
|
|
return unless Bugzilla->user->is_insider;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my ($comment) = grep($comment_id == $_->id, @{ $self->comments });
|
2010-10-05 20:47:44 +04:00
|
|
|
|
ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if !$comment;
|
|
|
|
|
|
|
|
|
|
$isprivate = $isprivate ? 1 : 0;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if ($isprivate != $comment->is_private) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->{comment_isprivate} ||= {};
|
|
|
|
|
$self->{comment_isprivate}->{$comment_id} = $isprivate;
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-10-04 18:01:24 +04:00
|
|
|
|
sub set_comment_worktimeonly
|
|
|
|
|
{
|
|
|
|
|
my ($self, $comment_id, $type) = @_;
|
|
|
|
|
my ($comment) = grep $comment_id == $_->id, @{ $self->comments };
|
2011-10-04 18:28:36 +04:00
|
|
|
|
if (!$comment || $comment->type != CMT_NORMAL && $comment->type != CMT_WORKTIME ||
|
|
|
|
|
$comment->who != Bugzilla->user->id && !Bugzilla->user->in_group('worktimeadmin'))
|
2011-10-04 18:01:24 +04:00
|
|
|
|
{
|
|
|
|
|
ThrowUserError('comment_invalid_worktimeonly', { id => $comment_id })
|
|
|
|
|
}
|
|
|
|
|
$type = $type ? CMT_WORKTIME : CMT_NORMAL;
|
|
|
|
|
if ($type != $comment->type)
|
|
|
|
|
{
|
|
|
|
|
$self->{comment_type}->{$comment_id} = $type;
|
|
|
|
|
}
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub set_component {
|
|
|
|
|
my ($self, $name) = @_;
|
|
|
|
|
my $old_comp = $self->component_obj;
|
|
|
|
|
my $component = $self->_check_component($name);
|
2010-11-26 16:30:24 +03:00
|
|
|
|
if ($component && $old_comp->id != $component->id) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->{component_id} = $component->id;
|
|
|
|
|
$self->{component} = $component->name;
|
|
|
|
|
$self->{component_obj} = $component;
|
|
|
|
|
# For update()
|
|
|
|
|
$self->{_old_component_name} = $old_comp->name;
|
|
|
|
|
# Add in the Default CC of the new Component;
|
|
|
|
|
foreach my $cc (@{$component->initial_cc}) {
|
|
|
|
|
$self->add_cc($cc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub set_custom_field {
|
|
|
|
|
my ($self, $field, $value) = @_;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) {
|
|
|
|
|
$value = $value->[0];
|
|
|
|
|
}
|
|
|
|
|
ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom;
|
|
|
|
|
$self->set($field->name, $value);
|
|
|
|
|
}
|
|
|
|
|
sub set_deadline { $_[0]->set('deadline', $_[1]); }
|
|
|
|
|
sub set_dependencies {
|
|
|
|
|
my ($self, $dependson, $blocked) = @_;
|
|
|
|
|
($dependson, $blocked) = $self->_check_dependencies($dependson, $blocked);
|
|
|
|
|
# These may already be detainted, but all setters are supposed to
|
|
|
|
|
# detaint their input if they've run a validator (just as though
|
|
|
|
|
# we had used Bugzilla::Object::set), so we do that here.
|
|
|
|
|
detaint_natural($_) foreach (@$dependson, @$blocked);
|
|
|
|
|
$self->{'dependson'} = $dependson;
|
|
|
|
|
$self->{'blocked'} = $blocked;
|
|
|
|
|
}
|
|
|
|
|
sub _clear_dup_id { $_[0]->{dup_id} = undef; }
|
|
|
|
|
sub set_dup_id {
|
|
|
|
|
my ($self, $dup_id) = @_;
|
|
|
|
|
my $old = $self->dup_id || 0;
|
|
|
|
|
$self->set('dup_id', $dup_id);
|
|
|
|
|
my $new = $self->dup_id;
|
|
|
|
|
return if $old == $new;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Update the other bug.
|
|
|
|
|
my $dupe_of = new Bugzilla::Bug($self->dup_id);
|
|
|
|
|
if (delete $self->{_add_dup_cc}) {
|
|
|
|
|
$dupe_of->add_cc($self->reporter);
|
|
|
|
|
}
|
|
|
|
|
$dupe_of->add_comment("", { type => CMT_HAS_DUPE,
|
|
|
|
|
extra_data => $self->id });
|
|
|
|
|
$self->{_dup_for_update} = $dupe_of;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Now make sure that we add a duplicate comment on *this* bug.
|
|
|
|
|
# (Change an existing comment into a dup comment, if there is one,
|
|
|
|
|
# or add an empty dup comment.)
|
2011-11-09 20:44:29 +04:00
|
|
|
|
if ($self->{added_comments} && @{$self->{added_comments}}) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL }
|
|
|
|
|
@{ $self->{added_comments} };
|
|
|
|
|
# Turn the last one into a dup comment.
|
|
|
|
|
$normal[-1]->{type} = CMT_DUPE_OF;
|
|
|
|
|
$normal[-1]->{extra_data} = $self->dup_id;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$self->add_comment('', { type => CMT_DUPE_OF,
|
|
|
|
|
extra_data => $self->dup_id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); }
|
|
|
|
|
sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
|
2010-05-15 00:02:34 +04:00
|
|
|
|
sub set_flags {
|
|
|
|
|
my ($self, $flags, $new_flags) = @_;
|
|
|
|
|
|
|
|
|
|
Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags);
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub set_op_sys { $_[0]->set('op_sys', $_[1]); }
|
|
|
|
|
sub set_platform { $_[0]->set('rep_platform', $_[1]); }
|
|
|
|
|
sub set_priority { $_[0]->set('priority', $_[1]); }
|
2010-11-24 20:22:45 +03:00
|
|
|
|
|
|
|
|
|
sub set_product
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($self, $name, $params) = @_;
|
|
|
|
|
my $old_product = $self->product_obj;
|
|
|
|
|
my $product = $self->_check_product($name);
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
if ($old_product->id != $product->id)
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->{product_id} = $product->id;
|
|
|
|
|
$self->{product} = $product->name;
|
|
|
|
|
$self->{product_obj} = $product;
|
|
|
|
|
# For update()
|
|
|
|
|
$self->{_old_product_name} = $old_product->name;
|
|
|
|
|
# Delete fields that depend upon the old Product value.
|
|
|
|
|
delete $self->{choices};
|
2010-11-24 20:22:45 +03:00
|
|
|
|
$self->{product_changed} = 1;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2010-11-24 20:22:45 +03:00
|
|
|
|
return $self->{product_changed};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub set_qa_contact {
|
|
|
|
|
my ($self, $value) = @_;
|
|
|
|
|
$self->set('qa_contact', $value);
|
|
|
|
|
# Store the old QA contact. check_can_change_field() needs it.
|
|
|
|
|
if ($self->{'qa_contact_obj'}) {
|
|
|
|
|
$self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id;
|
|
|
|
|
}
|
|
|
|
|
delete $self->{'qa_contact_obj'};
|
|
|
|
|
}
|
|
|
|
|
sub reset_qa_contact {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my $comp = $self->component_obj;
|
|
|
|
|
$self->set_qa_contact($comp->default_qa_contact);
|
|
|
|
|
}
|
|
|
|
|
sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); }
|
|
|
|
|
# Used only when closing a bug or moving between closed states.
|
2009-01-12 19:33:05 +03:00
|
|
|
|
# sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; }
|
|
|
|
|
sub _zero_remaining_time { }
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); }
|
|
|
|
|
sub set_resolution {
|
|
|
|
|
my ($self, $value, $params) = @_;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $old_res = $self->resolution;
|
|
|
|
|
$self->set('resolution', $value);
|
2010-05-15 00:02:34 +04:00
|
|
|
|
delete $self->{choices};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $new_res = $self->resolution;
|
|
|
|
|
|
|
|
|
|
if ($new_res ne $old_res) {
|
|
|
|
|
# MOVED has a special meaning and can only be used when
|
|
|
|
|
# really moving bugs to another installation.
|
|
|
|
|
ThrowCodeError('no_manual_moved') if ($new_res eq 'MOVED' && !$params->{moving});
|
|
|
|
|
|
|
|
|
|
# Clear the dup_id if we're leaving the dup resolution.
|
|
|
|
|
if ($old_res eq 'DUPLICATE') {
|
|
|
|
|
$self->_clear_dup_id();
|
|
|
|
|
}
|
|
|
|
|
# Duplicates should have no remaining time left.
|
|
|
|
|
elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) {
|
|
|
|
|
$self->_zero_remaining_time();
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# We don't check if we're entering or leaving the dup resolution here,
|
|
|
|
|
# because we could be moving from being a dup of one bug to being a dup
|
|
|
|
|
# of another, theoretically. Note that this code block will also run
|
|
|
|
|
# when going between different closed states.
|
|
|
|
|
if ($self->resolution eq 'DUPLICATE') {
|
|
|
|
|
if ($params->{dupe_of}) {
|
|
|
|
|
$self->set_dup_id($params->{dupe_of});
|
|
|
|
|
}
|
|
|
|
|
elsif (!$self->dup_id) {
|
|
|
|
|
ThrowUserError('dupe_id_required');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub clear_resolution {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
if (!$self->status->is_open) {
|
|
|
|
|
ThrowUserError('resolution_cant_clear', { bug_id => $self->id });
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$self->{'resolution'} = '';
|
|
|
|
|
$self->_clear_dup_id;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
sub set_severity { $_[0]->set('bug_severity', $_[1]); }
|
|
|
|
|
sub set_status {
|
|
|
|
|
my ($self, $status, $params) = @_;
|
|
|
|
|
my $old_status = $self->status;
|
|
|
|
|
$self->set('bug_status', $status);
|
|
|
|
|
delete $self->{'status'};
|
2010-05-15 00:02:34 +04:00
|
|
|
|
delete $self->{'statuses_available'};
|
|
|
|
|
delete $self->{'choices'};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $new_status = $self->status;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if ($new_status->is_open) {
|
|
|
|
|
# Check for the everconfirmed transition
|
2009-07-13 17:54:45 +04:00
|
|
|
|
$self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->clear_resolution();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# We do this here so that we can make sure closed statuses have
|
|
|
|
|
# resolutions.
|
|
|
|
|
my $resolution = delete $params->{resolution} || $self->resolution;
|
|
|
|
|
$self->set_resolution($resolution, $params);
|
|
|
|
|
|
|
|
|
|
# Changing between closed statuses zeros the remaining time.
|
|
|
|
|
if ($new_status->id != $old_status->id && $self->remaining_time != 0) {
|
|
|
|
|
$self->_zero_remaining_time();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); }
|
|
|
|
|
sub set_summary { $_[0]->set('short_desc', $_[1]); }
|
|
|
|
|
sub set_target_milestone { $_[0]->set('target_milestone', $_[1]); }
|
|
|
|
|
sub set_url { $_[0]->set('bug_file_loc', $_[1]); }
|
|
|
|
|
sub set_version { $_[0]->set('version', $_[1]); }
|
2009-01-13 18:23:11 +03:00
|
|
|
|
sub depscompletedpercent { $_[0]->checkdepsinfo; $_[0]->{depscompletedpercent}; }
|
|
|
|
|
sub lastchangeddeps { $_[0]->checkdepsinfo; $_[0]->{lastchangeddeps}; }
|
|
|
|
|
|
|
|
|
|
sub checkdepsinfo
|
|
|
|
|
{
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my $dep = $self->dependson;
|
|
|
|
|
return if defined $self->{lastchangeddeps} || !$dep || !@$dep;
|
|
|
|
|
my $where = "bug_id IN (" . join(",", ("?") x @$dep) . ")";
|
|
|
|
|
my ($last, $rem) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
"SELECT MAX(delta_ts), SUM(remaining_time)" .
|
|
|
|
|
" FROM bugs WHERE $where", undef, @$dep
|
|
|
|
|
);
|
|
|
|
|
my ($work) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
"SELECT SUM(work_time) FROM longdescs WHERE $where",
|
|
|
|
|
undef, @$dep
|
|
|
|
|
);
|
|
|
|
|
$self->{lastchangeddeps} = $last;
|
|
|
|
|
$self->{depscompletedpercent} = int(100*$work/($work+$rem || 1));
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
########################
|
|
|
|
|
# "Add/Remove" Methods #
|
|
|
|
|
########################
|
|
|
|
|
|
|
|
|
|
# These are in alphabetical order by field name.
|
|
|
|
|
|
|
|
|
|
# Accepts a User object or a username. Adds the user only if they
|
|
|
|
|
# don't already exist as a CC on the bug.
|
|
|
|
|
sub add_cc {
|
|
|
|
|
my ($self, $user_or_name) = @_;
|
|
|
|
|
return if !$user_or_name;
|
|
|
|
|
my $user = ref $user_or_name ? $user_or_name
|
|
|
|
|
: Bugzilla::User->check($user_or_name);
|
|
|
|
|
$self->_check_strict_isolation_for_user($user);
|
|
|
|
|
my $cc_users = $self->cc_users;
|
|
|
|
|
push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Accepts a User object or a username. Removes the User if they exist
|
|
|
|
|
# in the list, but doesn't throw an error if they don't exist.
|
|
|
|
|
sub remove_cc {
|
|
|
|
|
my ($self, $user_or_name) = @_;
|
|
|
|
|
my $user = ref $user_or_name ? $user_or_name
|
|
|
|
|
: Bugzilla::User->check($user_or_name);
|
|
|
|
|
my $cc_users = $self->cc_users;
|
|
|
|
|
@$cc_users = grep { $_->id != $user->id } @$cc_users;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5,
|
|
|
|
|
# type => CMT_NORMAL, extra_data => $data});
|
|
|
|
|
sub add_comment {
|
|
|
|
|
my ($self, $comment, $params) = @_;
|
|
|
|
|
|
|
|
|
|
$comment = $self->_check_comment($comment);
|
|
|
|
|
|
|
|
|
|
$params ||= {};
|
|
|
|
|
if (exists $params->{work_time}) {
|
|
|
|
|
$params->{work_time} = $self->_check_work_time($params->{work_time});
|
2011-11-22 20:36:23 +04:00
|
|
|
|
if ($comment eq '' && $params->{work_time} != 0 &&
|
|
|
|
|
(!exists $params->{type} ||
|
|
|
|
|
$params->{type} != CMT_WORKTIME &&
|
|
|
|
|
$params->{type} != CMT_BACKDATED_WORKTIME))
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('comment_required');
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
if (exists $params->{type}) {
|
|
|
|
|
$params->{type} = $self->_check_comment_type($params->{type});
|
|
|
|
|
}
|
|
|
|
|
if (exists $params->{isprivate}) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$params->{isprivate} =
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->_check_commentprivacy($params->{isprivate});
|
|
|
|
|
}
|
|
|
|
|
# XXX We really should check extra_data, too.
|
|
|
|
|
|
|
|
|
|
if ($comment eq '' && !($params->{type} || $params->{work_time})) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# So we really want to comment. Make sure we are allowed to do so.
|
|
|
|
|
my $privs;
|
|
|
|
|
$self->check_can_change_field('longdesc', 0, 1, \$privs)
|
|
|
|
|
|| ThrowUserError('illegal_change', { field => 'longdesc', privs => $privs });
|
|
|
|
|
|
|
|
|
|
$self->{added_comments} ||= [];
|
|
|
|
|
my $add_comment = dclone($params);
|
|
|
|
|
$add_comment->{thetext} = $comment;
|
|
|
|
|
|
|
|
|
|
# We only want to trick_taint fields that we know about--we don't
|
|
|
|
|
# want to accidentally let somebody set some field that's not OK
|
|
|
|
|
# to set!
|
|
|
|
|
foreach my $field (UPDATE_COMMENT_COLUMNS) {
|
|
|
|
|
trick_taint($add_comment->{$field}) if defined $add_comment->{$field};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
push(@{$self->{added_comments}}, $add_comment);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# There was a lot of duplicate code when I wrote this as three separate
|
|
|
|
|
# functions, so I just combined them all into one. This is also easier for
|
|
|
|
|
# process_bug to use.
|
|
|
|
|
sub modify_keywords {
|
|
|
|
|
my ($self, $keywords, $action) = @_;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$action ||= "makeexact";
|
|
|
|
|
if (!grep($action eq $_, qw(add delete makeexact))) {
|
|
|
|
|
$action = "makeexact";
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$keywords = $self->_check_keywords($keywords);
|
|
|
|
|
|
|
|
|
|
my (@result, $any_changes);
|
|
|
|
|
if ($action eq 'makeexact') {
|
|
|
|
|
@result = @$keywords;
|
|
|
|
|
# Check if anything was added or removed.
|
|
|
|
|
my @old_ids = map { $_->id } @{$self->keyword_objects};
|
|
|
|
|
my @new_ids = map { $_->id } @result;
|
|
|
|
|
my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
|
|
|
|
|
$any_changes = scalar @$removed || scalar @$added;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
# We're adding or deleting specific keywords.
|
|
|
|
|
my %keys = map {$_->id => $_} @{$self->keyword_objects};
|
|
|
|
|
if ($action eq 'add') {
|
|
|
|
|
$keys{$_->id} = $_ foreach @$keywords;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
delete $keys{$_->id} foreach @$keywords;
|
|
|
|
|
}
|
|
|
|
|
@result = values %keys;
|
|
|
|
|
$any_changes = scalar @$keywords;
|
|
|
|
|
}
|
|
|
|
|
# Make sure we retain the sort order.
|
|
|
|
|
@result = sort {lc($a->name) cmp lc($b->name)} @result;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if ($any_changes) {
|
|
|
|
|
my $privs;
|
|
|
|
|
my $new = join(', ', (map {$_->name} @result));
|
|
|
|
|
my $check = $self->check_can_change_field('keywords', 0, 1, \$privs)
|
|
|
|
|
|| ThrowUserError('illegal_change', { field => 'keywords',
|
|
|
|
|
oldvalue => $self->keywords,
|
|
|
|
|
newvalue => $new,
|
|
|
|
|
privs => $privs });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->{'keyword_objects'} = \@result;
|
|
|
|
|
return $any_changes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub add_group {
|
|
|
|
|
my ($self, $group) = @_;
|
|
|
|
|
# Invalid ids are silently ignored. (We can't tell people whether
|
|
|
|
|
# or not a group exists.)
|
|
|
|
|
$group = new Bugzilla::Group($group) unless ref $group;
|
|
|
|
|
return unless $group;
|
|
|
|
|
|
2009-11-20 16:07:55 +03:00
|
|
|
|
return if !$group->is_active or !$group->is_bug_group;
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Make sure that bugs in this product can actually be restricted
|
|
|
|
|
# to this group.
|
|
|
|
|
grep($group->id == $_->id, @{$self->product_obj->groups_valid})
|
2010-12-13 17:04:09 +03:00
|
|
|
|
# But during product change, verification happens anyway in update().
|
|
|
|
|
|| $self->{_old_product_name}
|
|
|
|
|
|| ThrowUserError('group_invalid_restriction',
|
2008-12-15 15:53:33 +03:00
|
|
|
|
{ product => $self->product, group_id => $group->id });
|
|
|
|
|
|
|
|
|
|
# OtherControl people can add groups only during a product change,
|
|
|
|
|
# and only when the group is not NA for them.
|
|
|
|
|
if (!Bugzilla->user->in_group($group->name)) {
|
|
|
|
|
my $controls = $self->product_obj->group_controls->{$group->id};
|
|
|
|
|
if (!$self->{_old_product_name}
|
|
|
|
|
|| $controls->{othercontrol} == CONTROLMAPNA)
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('group_change_denied',
|
|
|
|
|
{ bug => $self, group_id => $group->id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $current_groups = $self->groups_in;
|
|
|
|
|
if (!grep($group->id == $_->id, @$current_groups)) {
|
|
|
|
|
push(@$current_groups, $group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub remove_group {
|
|
|
|
|
my ($self, $group) = @_;
|
|
|
|
|
$group = new Bugzilla::Group($group) unless ref $group;
|
|
|
|
|
return unless $group;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# First, check if this is a valid group for this product.
|
|
|
|
|
# You can *always* remove a group that is not valid for this product, so
|
|
|
|
|
# we don't do any other checks if that's the case. (set_product does this.)
|
|
|
|
|
#
|
|
|
|
|
# This particularly happens when isbuggroup is no longer 1, and we're
|
|
|
|
|
# moving a bug to a new product.
|
2010-10-05 20:47:44 +04:00
|
|
|
|
if (grep($_->id == $group->id, @{$self->product_obj->groups_valid})) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $controls = $self->product_obj->group_controls->{$group->id};
|
|
|
|
|
|
|
|
|
|
# Nobody can ever remove a Mandatory group.
|
2010-12-13 17:04:09 +03:00
|
|
|
|
# But during product change, verification happens anyway in update().
|
|
|
|
|
if (!$self->{_old_product_name} &&
|
|
|
|
|
$controls->{membercontrol} == CONTROLMAPMANDATORY) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError('group_invalid_removal',
|
|
|
|
|
{ product => $self->product, group_id => $group->id,
|
|
|
|
|
bug => $self });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# OtherControl people can remove groups only during a product change,
|
|
|
|
|
# and only when they are non-Mandatory and non-NA.
|
|
|
|
|
if (!Bugzilla->user->in_group($group->name)) {
|
|
|
|
|
if (!$self->{_old_product_name}
|
|
|
|
|
|| $controls->{othercontrol} == CONTROLMAPMANDATORY
|
|
|
|
|
|| $controls->{othercontrol} == CONTROLMAPNA)
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('group_change_denied',
|
|
|
|
|
{ bug => $self, group_id => $group->id });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $current_groups = $self->groups_in;
|
|
|
|
|
@$current_groups = grep { $_->id != $group->id } @$current_groups;
|
|
|
|
|
}
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
sub add_see_also {
|
|
|
|
|
my ($self, $input) = @_;
|
|
|
|
|
$input = trim($input);
|
|
|
|
|
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# We assume that the URL is an HTTP URL if there is no (something)://
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# in front.
|
|
|
|
|
my $uri = new URI($input);
|
|
|
|
|
if (!$uri->scheme) {
|
|
|
|
|
# This works better than setting $uri->scheme('http'), because
|
|
|
|
|
# that creates URLs like "http:domain.com" and doesn't properly
|
|
|
|
|
# differentiate the path from the domain.
|
|
|
|
|
$uri = new URI("http://$input");
|
|
|
|
|
}
|
|
|
|
|
elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
|
|
|
|
|
ThrowUserError('bug_url_invalid', { url => $input, reason => 'http' });
|
|
|
|
|
}
|
|
|
|
|
|
2010-11-03 20:13:12 +03:00
|
|
|
|
# This stops the following edge cases from being accepted:
|
|
|
|
|
# * show_bug.cgi?id=1
|
|
|
|
|
# * /show_bug.cgi?id=1
|
|
|
|
|
# * http:///show_bug.cgi?id=1
|
|
|
|
|
if (!$uri->authority or $uri->path !~ m{/}) {
|
|
|
|
|
ThrowUserError('bug_url_invalid',
|
|
|
|
|
{ url => $input, reason => 'path_only' });
|
|
|
|
|
}
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my $result;
|
|
|
|
|
# Launchpad URLs
|
|
|
|
|
if ($uri->authority =~ /launchpad.net$/) {
|
|
|
|
|
# Launchpad bug URLs can look like various things:
|
|
|
|
|
# https://bugs.launchpad.net/ubuntu/+bug/1234
|
|
|
|
|
# https://launchpad.net/bugs/1234
|
|
|
|
|
# All variations end with either "/bugs/1234" or "/+bug/1234"
|
|
|
|
|
if ($uri->path =~ m|bugs?/(\d+)$|) {
|
|
|
|
|
# This is the shortest standard URL form for Launchpad bugs,
|
|
|
|
|
# and so we reduce all URLs to this.
|
|
|
|
|
$result = "https://launchpad.net/bugs/$1";
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
ThrowUserError('bug_url_invalid',
|
|
|
|
|
{ url => $input, reason => 'id' });
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Google Code URLs
|
|
|
|
|
elsif ($uri->authority =~ /^code.google.com$/i) {
|
|
|
|
|
# Google Code URLs only have one form:
|
|
|
|
|
# http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
|
|
|
|
|
my $project_name;
|
|
|
|
|
if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
|
|
|
|
|
$project_name = $1;
|
|
|
|
|
} else {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
ThrowUserError('bug_url_invalid',
|
2010-05-15 00:02:34 +04:00
|
|
|
|
{ url => $input });
|
|
|
|
|
}
|
|
|
|
|
my $bug_id = $uri->query_param('id');
|
|
|
|
|
detaint_natural($bug_id);
|
|
|
|
|
if (!$bug_id) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
ThrowUserError('bug_url_invalid',
|
2010-05-15 00:02:34 +04:00
|
|
|
|
{ url => $input, reason => 'id' });
|
|
|
|
|
}
|
|
|
|
|
# While Google Code URLs can be either HTTP or HTTPS,
|
|
|
|
|
# always go with the HTTP scheme, as that's the default.
|
|
|
|
|
$result = "http://code.google.com/p/" . $project_name .
|
|
|
|
|
"/issues/detail?id=" . $bug_id;
|
|
|
|
|
}
|
|
|
|
|
# Debian BTS URLs
|
|
|
|
|
elsif ($uri->authority =~ /^bugs.debian.org$/i) {
|
|
|
|
|
# Debian BTS URLs can look like various things:
|
|
|
|
|
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
|
|
|
|
|
# http://bugs.debian.org/1234
|
|
|
|
|
my $bug_id;
|
|
|
|
|
if ($uri->path =~ m|^/(\d+)$|) {
|
|
|
|
|
$bug_id = $1;
|
|
|
|
|
}
|
|
|
|
|
elsif ($uri->path =~ /bugreport\.cgi$/) {
|
|
|
|
|
$bug_id = $uri->query_param('bug');
|
|
|
|
|
detaint_natural($bug_id);
|
|
|
|
|
}
|
|
|
|
|
if (!$bug_id) {
|
|
|
|
|
ThrowUserError('bug_url_invalid',
|
|
|
|
|
{ url => $input, reason => 'id' });
|
|
|
|
|
}
|
|
|
|
|
# This is the shortest standard URL form for Debian BTS URLs,
|
|
|
|
|
# and so we reduce all URLs to this.
|
|
|
|
|
$result = "http://bugs.debian.org/" . $bug_id;
|
|
|
|
|
}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# Bugzilla URLs
|
|
|
|
|
else {
|
|
|
|
|
if ($uri->path !~ /show_bug\.cgi$/) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
ThrowUserError('bug_url_invalid',
|
2009-07-29 15:21:49 +04:00
|
|
|
|
{ url => $input, reason => 'show_bug' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $bug_id = $uri->query_param('id');
|
|
|
|
|
# We don't currently allow aliases, because we can't check to see
|
|
|
|
|
# if somebody's putting both an alias link and a numeric ID link.
|
|
|
|
|
# When we start validating the URL by accessing the other Bugzilla,
|
|
|
|
|
# we can allow aliases.
|
|
|
|
|
detaint_natural($bug_id);
|
|
|
|
|
if (!$bug_id) {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
ThrowUserError('bug_url_invalid',
|
2009-07-29 15:21:49 +04:00
|
|
|
|
{ url => $input, reason => 'id' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Make sure that "id" is the only query parameter.
|
|
|
|
|
$uri->query("id=$bug_id");
|
|
|
|
|
# And remove any # part if there is one.
|
|
|
|
|
$uri->fragment(undef);
|
|
|
|
|
$result = $uri->canonical->as_string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (length($result) > MAX_BUG_URL_LENGTH) {
|
|
|
|
|
ThrowUserError('bug_url_too_long', { url => $result });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# We only add the new URI if it hasn't been added yet. URIs are
|
|
|
|
|
# case-sensitive, but most of our DBs are case-insensitive, so we do
|
|
|
|
|
# this check case-insensitively.
|
|
|
|
|
if (!grep { lc($_) eq lc($result) } @{ $self->see_also }) {
|
|
|
|
|
my $privs;
|
|
|
|
|
my $can = $self->check_can_change_field('see_also', '', $result, \$privs);
|
|
|
|
|
if (!$can) {
|
|
|
|
|
ThrowUserError('illegal_change', { field => 'see_also',
|
|
|
|
|
newvalue => $result,
|
|
|
|
|
privs => $privs });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
push(@{ $self->see_also }, $result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub remove_see_also {
|
|
|
|
|
my ($self, $url) = @_;
|
|
|
|
|
my $see_also = $self->see_also;
|
|
|
|
|
my @new_see_also = grep { lc($_) ne lc($url) } @$see_also;
|
|
|
|
|
my $privs;
|
|
|
|
|
my $can = $self->check_can_change_field('see_also', $see_also, \@new_see_also, \$privs);
|
|
|
|
|
if (!$can) {
|
|
|
|
|
ThrowUserError('illegal_change', { field => 'see_also',
|
|
|
|
|
oldvalue => $url,
|
|
|
|
|
privs => $privs });
|
|
|
|
|
}
|
|
|
|
|
$self->{see_also} = \@new_see_also;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#####################################################################
|
|
|
|
|
# Instance Accessors
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
# These subs are in alphabetical order, as much as possible.
|
|
|
|
|
# If you add a new sub, please try to keep it in alphabetical order
|
|
|
|
|
# with the other ones.
|
|
|
|
|
|
|
|
|
|
# Note: If you add a new method, remember that you must check the error
|
|
|
|
|
# state of the bug before returning any data. If $self->{error} is
|
|
|
|
|
# defined, then return something empty. Otherwise you risk potential
|
|
|
|
|
# security holes.
|
|
|
|
|
|
|
|
|
|
sub dup_id {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'dup_id'} if exists $self->{'dup_id'};
|
|
|
|
|
|
|
|
|
|
$self->{'dup_id'} = undef;
|
|
|
|
|
return if $self->{'error'};
|
|
|
|
|
|
2010-10-05 20:47:44 +04:00
|
|
|
|
if ($self->{'resolution'} eq 'DUPLICATE') {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
$self->{'dup_id'} =
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$dbh->selectrow_array(q{SELECT dupe_of
|
2008-12-15 15:53:33 +03:00
|
|
|
|
FROM duplicates
|
|
|
|
|
WHERE dupe = ?},
|
|
|
|
|
undef,
|
|
|
|
|
$self->{'bug_id'});
|
|
|
|
|
}
|
|
|
|
|
return $self->{'dup_id'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub actual_time {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'actual_time'} if exists $self->{'actual_time'};
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$self->{'actual_time'} = undef;
|
|
|
|
|
return $self->{'actual_time'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time)
|
2010-10-05 20:47:44 +04:00
|
|
|
|
FROM longdescs
|
2008-12-15 15:53:33 +03:00
|
|
|
|
WHERE longdescs.bug_id=?");
|
|
|
|
|
$sth->execute($self->{bug_id});
|
|
|
|
|
$self->{'actual_time'} = $sth->fetchrow_array();
|
|
|
|
|
return $self->{'actual_time'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub any_flags_requesteeble {
|
|
|
|
|
my ($self) = @_;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
return $self->{'any_flags_requesteeble'}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if exists $self->{'any_flags_requesteeble'};
|
|
|
|
|
return 0 if $self->{'error'};
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my $any_flags_requesteeble =
|
|
|
|
|
grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types};
|
|
|
|
|
# Useful in case a flagtype is no longer requestable but a requestee
|
|
|
|
|
# has been set before we turned off that bit.
|
|
|
|
|
$any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags};
|
|
|
|
|
$self->{'any_flags_requesteeble'} = $any_flags_requesteeble;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
return $self->{'any_flags_requesteeble'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub attachments {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'attachments'} if exists $self->{'attachments'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
$self->{'attachments'} =
|
|
|
|
|
Bugzilla::Attachment->get_attachments_by_bug($self->bug_id, {preload => 1});
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $self->{'attachments'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub assigned_to {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'};
|
|
|
|
|
$self->{'assigned_to'} = 0 if $self->{'error'};
|
|
|
|
|
$self->{'assigned_to_obj'} ||= new Bugzilla::User($self->{'assigned_to'});
|
|
|
|
|
return $self->{'assigned_to_obj'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub blocked {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'blocked'} if exists $self->{'blocked'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
$self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id);
|
|
|
|
|
return $self->{'blocked'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Even bugs in an error state always have a bug_id.
|
|
|
|
|
sub bug_id { $_[0]->{'bug_id'}; }
|
|
|
|
|
|
2010-10-22 20:57:14 +04:00
|
|
|
|
sub failed_checkers { $_[0]->{failed_checkers} }
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub cc {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'cc'} if exists $self->{'cc'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
$self->{'cc'} = $dbh->selectcol_arrayref(
|
|
|
|
|
q{SELECT profiles.login_name FROM cc, profiles
|
|
|
|
|
WHERE bug_id = ?
|
|
|
|
|
AND cc.who = profiles.userid
|
|
|
|
|
ORDER BY profiles.login_name},
|
|
|
|
|
undef, $self->bug_id);
|
|
|
|
|
|
|
|
|
|
$self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
|
|
|
|
|
|
|
|
|
|
return $self->{'cc'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# XXX Eventually this will become the standard "cc" method used everywhere.
|
|
|
|
|
sub cc_users {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return $self->{'cc_users'} if exists $self->{'cc_users'};
|
|
|
|
|
return [] if $self->{'error'};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my $cc_ids = $dbh->selectcol_arrayref(
|
|
|
|
|
'SELECT who FROM cc WHERE bug_id = ?', undef, $self->id);
|
|
|
|
|
$self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids);
|
|
|
|
|
return $self->{'cc_users'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub component {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{component} if exists $self->{component};
|
|
|
|
|
return '' if $self->{error};
|
|
|
|
|
($self->{component}) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
'SELECT name FROM components WHERE id = ?',
|
|
|
|
|
undef, $self->{component_id});
|
|
|
|
|
return $self->{component};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# XXX Eventually this will replace component()
|
|
|
|
|
sub component_obj {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{component_obj} if defined $self->{component_obj};
|
|
|
|
|
return {} if $self->{error};
|
|
|
|
|
$self->{component_obj} = new Bugzilla::Component($self->{component_id});
|
|
|
|
|
return $self->{component_obj};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub classification_id {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{classification_id} if exists $self->{classification_id};
|
|
|
|
|
return 0 if $self->{error};
|
|
|
|
|
($self->{classification_id}) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
'SELECT classification_id FROM products WHERE id = ?',
|
|
|
|
|
undef, $self->{product_id});
|
|
|
|
|
return $self->{classification_id};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub classification {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{classification} if exists $self->{classification};
|
|
|
|
|
return '' if $self->{error};
|
|
|
|
|
($self->{classification}) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
'SELECT name FROM classifications WHERE id = ?',
|
|
|
|
|
undef, $self->classification_id);
|
|
|
|
|
return $self->{classification};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub dependson {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'dependson'} if exists $self->{'dependson'};
|
|
|
|
|
return [] if $self->{'error'};
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$self->{'dependson'} =
|
2008-12-15 15:53:33 +03:00
|
|
|
|
EmitDependList("blocked", "dependson", $self->bug_id);
|
|
|
|
|
return $self->{'dependson'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub flag_types {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'flag_types'} if exists $self->{'flag_types'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
my $vars = { target_type => 'bug',
|
|
|
|
|
product_id => $self->{product_id},
|
|
|
|
|
component_id => $self->{component_id},
|
2010-05-26 20:03:17 +04:00
|
|
|
|
bug_id => $self->bug_id,
|
|
|
|
|
bug_obj => $self,
|
|
|
|
|
};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars);
|
2009-11-27 20:41:12 +03:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $self->{'flag_types'};
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
sub flags {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $self = shift;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|
|
|
|
|
# Don't cache it as it must be in sync with ->flag_types.
|
|
|
|
|
$self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}];
|
|
|
|
|
return $self->{flags};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
sub isopened {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $self = shift;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
return is_open_state($self->{bug_status}) ? 1 : 0;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub keywords {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return join(', ', (map { $_->name } @{$self->keyword_objects}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# XXX At some point, this should probably replace the normal "keywords" sub.
|
|
|
|
|
sub keyword_objects {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return $self->{'keyword_objects'} if defined $self->{'keyword_objects'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my $ids = $dbh->selectcol_arrayref(
|
|
|
|
|
"SELECT keywordid FROM keywords WHERE bug_id = ?", undef, $self->id);
|
|
|
|
|
$self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids);
|
|
|
|
|
return $self->{'keyword_objects'};
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
sub comments {
|
|
|
|
|
my ($self, $params) = @_;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return [] if $self->{'error'};
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$params ||= {};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if (!defined $self->{'comments'}) {
|
|
|
|
|
$self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id });
|
|
|
|
|
my $count = 0;
|
|
|
|
|
foreach my $comment (@{ $self->{'comments'} }) {
|
2011-10-05 18:06:14 +04:00
|
|
|
|
$comment->{count} = $count++ if $comment->type != CMT_BACKDATED_WORKTIME;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$comment->{bug} = $self;
|
|
|
|
|
}
|
|
|
|
|
Bugzilla::Comment->preload($self->{'comments'});
|
|
|
|
|
}
|
|
|
|
|
my @comments = @{ $self->{'comments'} };
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my $order = $params->{order}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|| Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
|
|
|
|
|
if ($order ne 'oldest_to_newest') {
|
|
|
|
|
@comments = reverse @comments;
|
|
|
|
|
if ($order eq 'newest_to_oldest_desc_first') {
|
|
|
|
|
unshift(@comments, pop @comments);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($params->{after}) {
|
|
|
|
|
my $from = datetime_from($params->{after});
|
|
|
|
|
@comments = grep { datetime_from($_->creation_ts) > $from } @comments;
|
|
|
|
|
}
|
2010-12-20 22:05:49 +03:00
|
|
|
|
elsif ($params->{start_at}) {
|
|
|
|
|
splice(@comments, 0, $params->{start_at});
|
|
|
|
|
}
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if ($params->{to}) {
|
|
|
|
|
my $to = datetime_from($params->{to});
|
|
|
|
|
@comments = grep { datetime_from($_->creation_ts) <= $to } @comments;
|
|
|
|
|
}
|
|
|
|
|
return \@comments;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub product {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{product} if exists $self->{product};
|
|
|
|
|
return '' if $self->{error};
|
|
|
|
|
($self->{product}) = Bugzilla->dbh->selectrow_array(
|
|
|
|
|
'SELECT name FROM products WHERE id = ?',
|
|
|
|
|
undef, $self->{product_id});
|
|
|
|
|
return $self->{product};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# XXX This should eventually replace the "product" subroutine.
|
|
|
|
|
sub product_obj {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return {} if $self->{error};
|
|
|
|
|
$self->{product_obj} ||= new Bugzilla::Product($self->{product_id});
|
|
|
|
|
return $self->{product_obj};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub qa_contact {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'};
|
|
|
|
|
return undef if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
|
|
|
|
|
$self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
|
|
|
|
|
} else {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# XXX - This is somewhat inconsistent with the assignee/reporter
|
|
|
|
|
# methods, which will return an empty User if they get a 0.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# However, we're keeping it this way now, for backwards-compatibility.
|
|
|
|
|
$self->{'qa_contact_obj'} = undef;
|
|
|
|
|
}
|
|
|
|
|
return $self->{'qa_contact_obj'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub reporter {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return $self->{'reporter'} if exists $self->{'reporter'};
|
|
|
|
|
$self->{'reporter_id'} = 0 if $self->{'error'};
|
|
|
|
|
$self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'});
|
|
|
|
|
return $self->{'reporter'};
|
|
|
|
|
}
|
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
|
sub see_also {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
$self->{'see_also'} ||= Bugzilla->dbh->selectcol_arrayref(
|
|
|
|
|
'SELECT value FROM bug_see_also WHERE bug_id = ?', undef, $self->id);
|
|
|
|
|
return $self->{'see_also'};
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub status {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return undef if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
$self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}});
|
|
|
|
|
return $self->{'status'};
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
sub statuses_available {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
return $self->{'statuses_available'}
|
|
|
|
|
if defined $self->{'statuses_available'};
|
|
|
|
|
|
|
|
|
|
my @statuses = @{ $self->status->can_change_to };
|
|
|
|
|
|
|
|
|
|
# UNCONFIRMED is only a valid status if it is enabled in this product.
|
|
|
|
|
if (!$self->product_obj->allows_unconfirmed) {
|
|
|
|
|
@statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my @available;
|
|
|
|
|
foreach my $status (@statuses) {
|
|
|
|
|
# Make sure this is a legal status transition
|
|
|
|
|
next if !$self->check_can_change_field(
|
|
|
|
|
'bug_status', $self->status->name, $status->name);
|
|
|
|
|
push(@available, $status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If this bug has an inactive status set, it should still be in the list.
|
|
|
|
|
if (!grep($_->name eq $self->status->name, @available)) {
|
|
|
|
|
unshift(@available, $self->status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->{'statuses_available'} = \@available;
|
|
|
|
|
return $self->{'statuses_available'};
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub show_attachment_flags {
|
|
|
|
|
my ($self) = @_;
|
2010-10-05 20:47:44 +04:00
|
|
|
|
return $self->{'show_attachment_flags'}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
if exists $self->{'show_attachment_flags'};
|
|
|
|
|
return 0 if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
# The number of types of flags that can be set on attachments to this bug
|
|
|
|
|
# and the number of flags on those attachments. One of these counts must be
|
|
|
|
|
# greater than zero in order for the "flags" column to appear in the table
|
|
|
|
|
# of attachments.
|
|
|
|
|
my $num_attachment_flag_types = Bugzilla::FlagType::count(
|
|
|
|
|
{ 'target_type' => 'attachment',
|
|
|
|
|
'product_id' => $self->{'product_id'},
|
|
|
|
|
'component_id' => $self->{'component_id'} });
|
|
|
|
|
my $num_attachment_flags = Bugzilla::Flag->count(
|
|
|
|
|
{ 'target_type' => 'attachment',
|
|
|
|
|
'bug_id' => $self->bug_id });
|
|
|
|
|
|
|
|
|
|
$self->{'show_attachment_flags'} =
|
|
|
|
|
($num_attachment_flag_types || $num_attachment_flags);
|
|
|
|
|
|
|
|
|
|
return $self->{'show_attachment_flags'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub use_votes {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return 0 if $self->{'error'};
|
|
|
|
|
|
2010-10-05 20:47:44 +04:00
|
|
|
|
return Bugzilla->params->{'usevotes'}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
&& $self->product_obj->votes_per_user > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub groups {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return $self->{'groups'} if exists $self->{'groups'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my @groups;
|
|
|
|
|
|
|
|
|
|
# Some of this stuff needs to go into Bugzilla::User
|
|
|
|
|
|
|
|
|
|
# For every group, we need to know if there is ANY bug_group_map
|
|
|
|
|
# record putting the current bug in that group and if there is ANY
|
|
|
|
|
# user_group_map record putting the user in that group.
|
|
|
|
|
# The LEFT JOINs are checking for record existence.
|
|
|
|
|
#
|
|
|
|
|
my $grouplist = Bugzilla->user->groups_as_string;
|
|
|
|
|
my $sth = $dbh->prepare(
|
|
|
|
|
"SELECT DISTINCT groups.id, name, description," .
|
|
|
|
|
" CASE WHEN bug_group_map.group_id IS NOT NULL" .
|
|
|
|
|
" THEN 1 ELSE 0 END," .
|
|
|
|
|
" CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," .
|
|
|
|
|
" isactive, membercontrol, othercontrol" .
|
2010-10-05 20:47:44 +04:00
|
|
|
|
" FROM groups" .
|
2008-12-15 15:53:33 +03:00
|
|
|
|
" LEFT JOIN bug_group_map" .
|
|
|
|
|
" ON bug_group_map.group_id = groups.id" .
|
|
|
|
|
" AND bug_id = ?" .
|
|
|
|
|
" LEFT JOIN group_control_map" .
|
|
|
|
|
" ON group_control_map.group_id = groups.id" .
|
|
|
|
|
" AND group_control_map.product_id = ? " .
|
|
|
|
|
" WHERE isbuggroup = 1" .
|
|
|
|
|
" ORDER BY description");
|
|
|
|
|
$sth->execute($self->{'bug_id'},
|
|
|
|
|
$self->{'product_id'});
|
|
|
|
|
|
|
|
|
|
while (my ($groupid, $name, $description, $ison, $ingroup, $isactive,
|
|
|
|
|
$membercontrol, $othercontrol) = $sth->fetchrow_array()) {
|
|
|
|
|
|
|
|
|
|
$membercontrol ||= 0;
|
|
|
|
|
|
|
|
|
|
# For product groups, we only want to use the group if either
|
|
|
|
|
# (1) The bit is set and not required, or
|
|
|
|
|
# (2) The group is Shown or Default for members and
|
|
|
|
|
# the user is a member of the group.
|
|
|
|
|
if ($ison ||
|
|
|
|
|
($isactive && $ingroup
|
|
|
|
|
&& (($membercontrol == CONTROLMAPDEFAULT)
|
|
|
|
|
|| ($membercontrol == CONTROLMAPSHOWN))
|
|
|
|
|
))
|
|
|
|
|
{
|
|
|
|
|
my $ismandatory = $isactive
|
|
|
|
|
&& ($membercontrol == CONTROLMAPMANDATORY);
|
|
|
|
|
|
|
|
|
|
push (@groups, { "bit" => $groupid,
|
|
|
|
|
"name" => $name,
|
|
|
|
|
"ison" => $ison,
|
|
|
|
|
"ingroup" => $ingroup,
|
|
|
|
|
"mandatory" => $ismandatory,
|
|
|
|
|
"description" => $description });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$self->{'groups'} = \@groups;
|
|
|
|
|
|
|
|
|
|
return $self->{'groups'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub groups_in {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return $self->{'groups_in'} if exists $self->{'groups_in'};
|
|
|
|
|
return [] if $self->{'error'};
|
|
|
|
|
my $group_ids = Bugzilla->dbh->selectcol_arrayref(
|
|
|
|
|
'SELECT group_id FROM bug_group_map WHERE bug_id = ?',
|
|
|
|
|
undef, $self->id);
|
|
|
|
|
$self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids);
|
|
|
|
|
return $self->{'groups_in'};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub user {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
return $self->{'user'} if exists $self->{'user'};
|
|
|
|
|
return {} if $self->{'error'};
|
|
|
|
|
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
my $canmove = Bugzilla->params->{'move-enabled'} && $user->is_mover;
|
|
|
|
|
|
|
|
|
|
my $prod_id = $self->{'product_id'};
|
|
|
|
|
|
|
|
|
|
my $unknown_privileges = $user->in_group('editbugs', $prod_id);
|
|
|
|
|
my $canedit = $unknown_privileges
|
|
|
|
|
|| $user->id == $self->{'assigned_to'}
|
|
|
|
|
|| (Bugzilla->params->{'useqacontact'}
|
|
|
|
|
&& $self->{'qa_contact'}
|
|
|
|
|
&& $user->id == $self->{'qa_contact'});
|
|
|
|
|
my $canconfirm = $unknown_privileges
|
|
|
|
|
|| $user->in_group('canconfirm', $prod_id);
|
|
|
|
|
my $isreporter = $user->id
|
|
|
|
|
&& $user->id == $self->{reporter_id};
|
|
|
|
|
|
|
|
|
|
$self->{'user'} = {canmove => $canmove,
|
|
|
|
|
canconfirm => $canconfirm,
|
|
|
|
|
canedit => $canedit,
|
|
|
|
|
isreporter => $isreporter};
|
|
|
|
|
return $self->{'user'};
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# This is intended to get values that can be selected by the user in the
|
|
|
|
|
# UI. It should not be used for security or validation purposes.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub choices {
|
|
|
|
|
my $self = shift;
|
2009-10-22 15:48:20 +04:00
|
|
|
|
return $self->{choices} if exists $self->{'choices'};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return {} if $self->{'error'};
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my $user = Bugzilla->user;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my @products = @{ $user->get_enterable_products };
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# The current product is part of the popup, even if new bugs are no longer
|
|
|
|
|
# allowed for that product
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if (!grep($_->name eq $self->product_obj->name, @products)) {
|
|
|
|
|
unshift(@products, $self->product_obj);
|
|
|
|
|
}
|
2009-10-22 15:42:07 +04:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my %choices = (
|
|
|
|
|
bug_status => $self->statuses_available,
|
|
|
|
|
product => \@products,
|
2010-09-10 19:13:28 +04:00
|
|
|
|
component => $self->product_obj->active_components,
|
2010-05-15 00:02:34 +04:00
|
|
|
|
version => $self->product_obj->versions,
|
|
|
|
|
target_milestone => $self->product_obj->milestones,
|
|
|
|
|
);
|
2009-10-22 15:42:07 +04:00
|
|
|
|
|
2010-10-14 14:31:30 +04:00
|
|
|
|
my $resolution_field = Bugzilla->get_field('resolution');
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Don't include the empty resolution in drop-downs.
|
|
|
|
|
my @resolutions = grep($_->name, @{ $resolution_field->legal_values });
|
|
|
|
|
# And don't include MOVED in the list unless the bug is already MOVED.
|
|
|
|
|
if ($self->resolution ne 'MOVED') {
|
|
|
|
|
@resolutions= grep { $_->name ne 'MOVED' } @resolutions;
|
|
|
|
|
}
|
|
|
|
|
$choices{'resolution'} = \@resolutions;
|
2009-10-22 15:42:07 +04:00
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$self->{'choices'} = \%choices;
|
|
|
|
|
return $self->{'choices'};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub votes {
|
|
|
|
|
my ($self) = @_;
|
|
|
|
|
return 0 if $self->{error};
|
|
|
|
|
return $self->{votes} if defined $self->{votes};
|
|
|
|
|
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
$self->{votes} = $dbh->selectrow_array(
|
|
|
|
|
'SELECT SUM(vote_count) FROM votes
|
|
|
|
|
WHERE bug_id = ? ' . $dbh->sql_group_by('bug_id'),
|
|
|
|
|
undef, $self->bug_id);
|
|
|
|
|
$self->{votes} ||= 0;
|
|
|
|
|
return $self->{votes};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Convenience Function. If you need speed, use this. If you need
|
|
|
|
|
# other Bug fields in addition to this, just create a new Bug with
|
|
|
|
|
# the alias.
|
|
|
|
|
# Queries the database for the bug with a given alias, and returns
|
|
|
|
|
# the ID of the bug if it exists or the undefined value if it doesn't.
|
|
|
|
|
sub bug_alias_to_id {
|
|
|
|
|
my ($alias) = @_;
|
|
|
|
|
return undef unless Bugzilla->params->{"usebugaliases"};
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
trick_taint($alias);
|
|
|
|
|
return $dbh->selectrow_array(
|
|
|
|
|
"SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias);
|
|
|
|
|
}
|
|
|
|
|
|
2009-01-27 18:51:58 +03:00
|
|
|
|
sub get_test_case_count {
|
2010-11-10 21:21:19 +03:00
|
|
|
|
my $self = shift;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my $row_count = $dbh->selectall_arrayref(
|
|
|
|
|
"SELECT DISTINCT case_id FROM test_case_bugs WHERE bug_id = ?",
|
|
|
|
|
undef, $self->bug_id);
|
|
|
|
|
return scalar @$row_count;
|
2009-01-27 18:51:58 +03:00
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#####################################################################
|
|
|
|
|
# Subroutines
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
2010-11-10 21:21:19 +03:00
|
|
|
|
# NB // Vitaliy Filippov <vitalif@mail.ru> 2010-02-01 19:23
|
2010-02-02 19:57:14 +03:00
|
|
|
|
# editable_bug_fields() is one more example of incorrect and unused generalization.
|
|
|
|
|
# It does not represent which fields from the bugs table are handled by process_bug.cgi,
|
|
|
|
|
# because process_bug.cgi itself does not use it at any point. In fact, it was used only
|
|
|
|
|
# in 2 places: 1) the field list for boolean charts 2) in BugMail.pm to avoid 'SELECT *'.
|
|
|
|
|
# Both of them are not related to "editable" at all.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# XXX - When Bug::update() will be implemented, we should make this routine
|
|
|
|
|
# a private method.
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# Join with bug_status and bugs tables to show bugs with open statuses first,
|
|
|
|
|
# and then the others
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub EmitDependList {
|
|
|
|
|
my ($myfield, $targetfield, $bug_id) = (@_);
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
my $list_ref = $dbh->selectcol_arrayref(
|
2010-11-10 21:21:19 +03:00
|
|
|
|
"SELECT $targetfield
|
|
|
|
|
FROM dependencies
|
|
|
|
|
INNER JOIN bugs ON dependencies.$targetfield = bugs.bug_id
|
|
|
|
|
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
|
|
|
|
|
WHERE $myfield = ?
|
|
|
|
|
ORDER BY is_open DESC, $targetfield",
|
|
|
|
|
undef, $bug_id);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
return $list_ref;
|
|
|
|
|
}
|
|
|
|
|
|
2009-12-10 19:28:19 +03:00
|
|
|
|
sub ValidateTime
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($time, $field) = @_;
|
|
|
|
|
|
2009-12-10 19:28:19 +03:00
|
|
|
|
$time =~ tr/,/./;
|
|
|
|
|
$time = trim($time) || 0;
|
|
|
|
|
|
2010-02-01 16:33:39 +03:00
|
|
|
|
if ($time =~ /^(-?)(\d+):(\d+)$/so)
|
2009-12-10 19:28:19 +03:00
|
|
|
|
{
|
2010-02-01 16:33:39 +03:00
|
|
|
|
# HH:MM
|
2009-12-11 14:48:15 +03:00
|
|
|
|
$time = $1 . ($2 + $3/60 + ($4||0)/3600);
|
2010-02-01 16:33:39 +03:00
|
|
|
|
$time = floor($time*100+0.5)/100;
|
2009-12-11 14:48:15 +03:00
|
|
|
|
}
|
|
|
|
|
elsif ($time =~ /^(-?\d+(?:\.\d+)?)d$/so)
|
|
|
|
|
{
|
|
|
|
|
# days
|
2010-02-01 16:33:39 +03:00
|
|
|
|
$time = $1 * 8;
|
2009-12-10 19:28:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# regexp verifies one or more digits, optionally followed by a period and
|
|
|
|
|
# zero or more digits, OR we have a period followed by one or more digits
|
|
|
|
|
# (allow negatives, though, so people can back out errors in time reporting)
|
2009-12-10 19:28:19 +03:00
|
|
|
|
if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/so) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError("number_not_numeric",
|
|
|
|
|
{field => "$field", num => "$time"});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Only the "work_time" field is allowed to contain a negative value.
|
2009-12-10 19:28:19 +03:00
|
|
|
|
if ($time < 0 && $field ne "work_time") {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ThrowUserError("number_too_small",
|
|
|
|
|
{field => "$field", num => "$time", min_num => "0"});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($time > 99999.99) {
|
|
|
|
|
ThrowUserError("number_too_large",
|
|
|
|
|
{field => "$field", num => "$time", max_num => "99999.99"});
|
|
|
|
|
}
|
2009-12-10 19:28:19 +03:00
|
|
|
|
|
|
|
|
|
return $time;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get the activity of a bug, starting from $starttime (if given).
|
2009-07-29 15:21:49 +04:00
|
|
|
|
# This routine assumes Bugzilla::Bug->check has been previously called.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
sub GetBugActivity {
|
|
|
|
|
my ($bug_id, $attach_id, $starttime) = @_;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
|
|
# Arguments passed to the SQL query.
|
|
|
|
|
my @args = ($bug_id);
|
|
|
|
|
|
|
|
|
|
# Only consider changes since $starttime, if given.
|
|
|
|
|
my $datepart = "";
|
|
|
|
|
if (defined $starttime) {
|
|
|
|
|
trick_taint($starttime);
|
|
|
|
|
push (@args, $starttime);
|
|
|
|
|
$datepart = "AND bugs_activity.bug_when > ?";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $attachpart = "";
|
|
|
|
|
if ($attach_id) {
|
|
|
|
|
push(@args, $attach_id);
|
|
|
|
|
$attachpart = "AND bugs_activity.attach_id = ?";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Only includes attachments the user is allowed to see.
|
|
|
|
|
my $suppjoins = "";
|
|
|
|
|
my $suppwhere = "";
|
2010-10-05 20:47:44 +04:00
|
|
|
|
if (!Bugzilla->user->is_insider)
|
2008-12-15 15:53:33 +03:00
|
|
|
|
{
|
2010-10-05 20:47:44 +04:00
|
|
|
|
$suppjoins = "LEFT JOIN attachments
|
2008-12-15 15:53:33 +03:00
|
|
|
|
ON attachments.attach_id = bugs_activity.attach_id";
|
|
|
|
|
$suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0";
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
|
|
|
|
|
", bugs_activity.removed, bugs_activity.added, profiles.login_name
|
|
|
|
|
FROM bugs_activity
|
|
|
|
|
$suppjoins
|
|
|
|
|
LEFT JOIN fielddefs
|
|
|
|
|
ON bugs_activity.fieldid = fielddefs.id
|
|
|
|
|
INNER JOIN profiles
|
|
|
|
|
ON profiles.userid = bugs_activity.who
|
|
|
|
|
WHERE bugs_activity.bug_id = ?
|
|
|
|
|
$datepart
|
|
|
|
|
$attachpart
|
|
|
|
|
$suppwhere
|
|
|
|
|
ORDER BY bugs_activity.bug_when";
|
|
|
|
|
|
|
|
|
|
my $list = $dbh->selectall_arrayref($query, undef, @args);
|
|
|
|
|
|
|
|
|
|
my @operations;
|
|
|
|
|
my $operation = {};
|
|
|
|
|
my $changes = [];
|
|
|
|
|
my $incomplete_data = 0;
|
|
|
|
|
|
|
|
|
|
foreach my $entry (@$list) {
|
2010-05-15 00:02:34 +04:00
|
|
|
|
my ($fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my %change;
|
|
|
|
|
my $activity_visible = 1;
|
|
|
|
|
|
|
|
|
|
# check if the user should see this field's activity
|
|
|
|
|
if ($fieldname eq 'remaining_time'
|
|
|
|
|
|| $fieldname eq 'estimated_time'
|
|
|
|
|
|| $fieldname eq 'work_time'
|
|
|
|
|
|| $fieldname eq 'deadline')
|
|
|
|
|
{
|
2010-05-15 00:02:34 +04:00
|
|
|
|
$activity_visible = Bugzilla->user->is_timetracker;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
} else {
|
|
|
|
|
$activity_visible = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($activity_visible) {
|
|
|
|
|
# Check for the results of an old Bugzilla data corruption bug
|
2010-06-25 20:04:36 +04:00
|
|
|
|
if (($added eq '?' && $removed eq '?')
|
|
|
|
|
|| ($added =~ /^\? / || $removed =~ /^\? /)) {
|
|
|
|
|
$incomplete_data = 1;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# An operation, done by 'who' at time 'when', has a number of
|
|
|
|
|
# 'changes' associated with it.
|
|
|
|
|
# If this is the start of a new operation, store the data from the
|
|
|
|
|
# previous one, and set up the new one.
|
|
|
|
|
if ($operation->{'who'}
|
|
|
|
|
&& ($who ne $operation->{'who'}
|
|
|
|
|
|| $when ne $operation->{'when'}))
|
|
|
|
|
{
|
|
|
|
|
$operation->{'changes'} = $changes;
|
|
|
|
|
push (@operations, $operation);
|
|
|
|
|
|
|
|
|
|
# Create new empty anonymous data structures.
|
|
|
|
|
$operation = {};
|
|
|
|
|
$changes = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$operation->{'who'} = $who;
|
|
|
|
|
$operation->{'when'} = $when;
|
|
|
|
|
|
|
|
|
|
$change{'fieldname'} = $fieldname;
|
|
|
|
|
$change{'attachid'} = $attachid;
|
|
|
|
|
$change{'removed'} = $removed;
|
|
|
|
|
$change{'added'} = $added;
|
|
|
|
|
push (@$changes, \%change);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($operation->{'who'}) {
|
|
|
|
|
$operation->{'changes'} = $changes;
|
|
|
|
|
push (@operations, $operation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return(\@operations, $incomplete_data);
|
|
|
|
|
}
|
|
|
|
|
|
2009-12-17 15:20:04 +03:00
|
|
|
|
# Write into additional log file for silent comments
|
|
|
|
|
sub SilentLog
|
|
|
|
|
{
|
|
|
|
|
my ($bugid, $comment) = @_;
|
|
|
|
|
my $datadir = bz_locations()->{datadir};
|
|
|
|
|
my $fd;
|
|
|
|
|
if (-w "$datadir/silentlog")
|
|
|
|
|
{
|
|
|
|
|
my $mesg = "";
|
|
|
|
|
$comment =~ s/\r*\n+/|/gso;
|
|
|
|
|
$mesg .= "Silent comment> " . time2str("%D %H:%M:%S ", time());
|
|
|
|
|
$mesg .= " Bug $bugid User: " . Bugzilla->user->login;
|
|
|
|
|
$mesg .= " ($ENV{REMOTE_ADDR}) " if $ENV{REMOTE_ADDR};
|
|
|
|
|
$mesg .= " // $comment ";
|
|
|
|
|
if (open $fd, ">>$datadir/silentlog")
|
|
|
|
|
{
|
|
|
|
|
print $fd "$mesg\n";
|
|
|
|
|
close $fd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Update the bugs_activity table to reflect changes made in bugs.
|
2010-12-10 21:02:52 +03:00
|
|
|
|
sub LogActivityEntry
|
|
|
|
|
{
|
|
|
|
|
my ($bug_id, $col, $removed, $added, $whoid, $timestamp, $attach_id) = @_;
|
|
|
|
|
my $f = Bugzilla->get_field($col);
|
|
|
|
|
if (!$f->{has_activity})
|
|
|
|
|
{
|
|
|
|
|
$f->{has_activity} = 1;
|
|
|
|
|
$f->update;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
# in the case of CCs, deps, and keywords, there's a possibility that someone
|
|
|
|
|
# might try to add or remove a lot of them at once, which might take more
|
|
|
|
|
# space than the activity table allows. We'll solve this by splitting it
|
|
|
|
|
# into multiple entries if it's too long.
|
2010-12-10 21:02:52 +03:00
|
|
|
|
while ($removed || $added)
|
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my ($removestr, $addstr) = ($removed, $added);
|
|
|
|
|
if (length($removestr) > MAX_LINE_LENGTH) {
|
|
|
|
|
my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH);
|
|
|
|
|
$removestr = substr($removed, 0, $commaposition);
|
|
|
|
|
$removed = substr($removed, $commaposition);
|
|
|
|
|
$removed =~ s/^[,\s]+//; # remove any comma or space
|
|
|
|
|
} else {
|
|
|
|
|
$removed = ""; # no more entries
|
|
|
|
|
}
|
|
|
|
|
if (length($addstr) > MAX_LINE_LENGTH) {
|
|
|
|
|
my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH);
|
|
|
|
|
$addstr = substr($added, 0, $commaposition);
|
|
|
|
|
$added = substr($added, $commaposition);
|
|
|
|
|
$added =~ s/^[,\s]+//; # remove any comma or space
|
|
|
|
|
} else {
|
|
|
|
|
$added = ""; # no more entries
|
|
|
|
|
}
|
|
|
|
|
trick_taint($addstr);
|
|
|
|
|
trick_taint($removestr);
|
2010-12-10 21:02:52 +03:00
|
|
|
|
$dbh->do(
|
|
|
|
|
"INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added, attach_id)".
|
2010-12-10 22:14:39 +03:00
|
|
|
|
" VALUES (?, ?, ".($timestamp ? "?" : "NOW()").", ?, ?, ?, ?)", undef,
|
2010-12-10 21:02:52 +03:00
|
|
|
|
$bug_id, $whoid, ($timestamp ? ($timestamp) : ()), $f->id, $removestr, $addstr, $attach_id
|
|
|
|
|
);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# Convert WebService API and email_in.pl field names to internal DB field
|
|
|
|
|
# names.
|
|
|
|
|
sub map_fields {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
my ($params) = @_;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
|
|
|
|
|
my %field_values;
|
|
|
|
|
foreach my $field (keys %$params) {
|
|
|
|
|
my $field_name = FIELD_MAP->{$field} || $field;
|
|
|
|
|
$field_values{$field_name} = $params->{$field};
|
|
|
|
|
}
|
|
|
|
|
return \%field_values;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# CountOpenDependencies counts the number of open dependent bugs for a
|
|
|
|
|
# list of bugs and returns a list of bug_id's and their dependency count
|
|
|
|
|
# It takes one parameter:
|
|
|
|
|
# - A list of bug numbers whose dependencies are to be checked
|
|
|
|
|
sub CountOpenDependencies {
|
|
|
|
|
my (@bug_list) = @_;
|
|
|
|
|
my @dependencies;
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
|
|
my $sth = $dbh->prepare(
|
|
|
|
|
"SELECT blocked, COUNT(bug_status) " .
|
|
|
|
|
"FROM bugs, dependencies " .
|
|
|
|
|
"WHERE " . $dbh->sql_in('blocked', \@bug_list) .
|
|
|
|
|
"AND bug_id = dependson " .
|
|
|
|
|
"AND bug_status IN (" . join(', ', map {$dbh->quote($_)} BUG_STATE_OPEN) . ") " .
|
|
|
|
|
$dbh->sql_group_by('blocked'));
|
|
|
|
|
$sth->execute();
|
|
|
|
|
|
|
|
|
|
while (my ($bug_id, $dependencies) = $sth->fetchrow_array()) {
|
|
|
|
|
push(@dependencies, { bug_id => $bug_id,
|
|
|
|
|
dependencies => $dependencies });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return @dependencies;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If a bug is moved to a product which allows less votes per bug
|
|
|
|
|
# compared to the previous product, extra votes need to be removed.
|
|
|
|
|
sub RemoveVotes {
|
|
|
|
|
my ($id, $who, $reason) = (@_);
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
|
|
my $whopart = ($who) ? " AND votes.who = $who" : "";
|
|
|
|
|
|
|
|
|
|
my $sth = $dbh->prepare("SELECT profiles.login_name, " .
|
|
|
|
|
"profiles.userid, votes.vote_count, " .
|
|
|
|
|
"products.votesperuser, products.maxvotesperbug " .
|
2010-10-05 20:47:44 +04:00
|
|
|
|
"FROM profiles " .
|
2008-12-15 15:53:33 +03:00
|
|
|
|
"LEFT JOIN votes ON profiles.userid = votes.who " .
|
|
|
|
|
"LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
|
|
|
|
|
"LEFT JOIN products ON products.id = bugs.product_id " .
|
|
|
|
|
"WHERE votes.bug_id = ? " . $whopart);
|
|
|
|
|
$sth->execute($id);
|
|
|
|
|
my @list;
|
|
|
|
|
while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
|
|
|
|
|
push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# @messages stores all emails which have to be sent, if any.
|
|
|
|
|
# This array is passed to the caller which will send these emails itself.
|
|
|
|
|
my @messages = ();
|
|
|
|
|
|
|
|
|
|
if (scalar(@list)) {
|
|
|
|
|
foreach my $ref (@list) {
|
|
|
|
|
my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
|
|
|
|
|
|
|
|
|
|
$maxvotesperbug = min($votesperuser, $maxvotesperbug);
|
|
|
|
|
|
|
|
|
|
# If this product allows voting and the user's votes are in
|
|
|
|
|
# the acceptable range, then don't do anything.
|
|
|
|
|
next if $votesperuser && $oldvotes <= $maxvotesperbug;
|
|
|
|
|
|
|
|
|
|
# If the user has more votes on this bug than this product
|
|
|
|
|
# allows, then reduce the number of votes so it fits
|
|
|
|
|
my $newvotes = $maxvotesperbug;
|
|
|
|
|
|
|
|
|
|
my $removedvotes = $oldvotes - $newvotes;
|
|
|
|
|
|
|
|
|
|
if ($newvotes) {
|
|
|
|
|
$dbh->do("UPDATE votes SET vote_count = ? " .
|
|
|
|
|
"WHERE bug_id = ? AND who = ?",
|
|
|
|
|
undef, ($newvotes, $id, $userid));
|
|
|
|
|
} else {
|
|
|
|
|
$dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
|
|
|
|
|
undef, ($id, $userid));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Notice that we did not make sure that the user fit within the $votesperuser
|
|
|
|
|
# range. This is considered to be an acceptable alternative to losing votes
|
|
|
|
|
# during product moves. Then next time the user attempts to change their votes,
|
|
|
|
|
# they will be forced to fit within the $votesperuser limit.
|
|
|
|
|
|
|
|
|
|
# Now lets send the e-mail to alert the user to the fact that their votes have
|
|
|
|
|
# been reduced or removed.
|
|
|
|
|
my $vars = {
|
|
|
|
|
'to' => $name . Bugzilla->params->{'emailsuffix'},
|
|
|
|
|
'bugid' => $id,
|
|
|
|
|
'reason' => $reason,
|
|
|
|
|
|
|
|
|
|
'votesremoved' => $removedvotes,
|
|
|
|
|
'votesold' => $oldvotes,
|
|
|
|
|
'votesnew' => $newvotes,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
my $voter = new Bugzilla::User($userid);
|
|
|
|
|
my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'});
|
|
|
|
|
|
|
|
|
|
my $msg;
|
|
|
|
|
$template->process("email/votes-removed.txt.tmpl", $vars, \$msg);
|
|
|
|
|
push(@messages, $msg);
|
|
|
|
|
}
|
|
|
|
|
Bugzilla->template_inner("");
|
|
|
|
|
|
|
|
|
|
my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
|
|
|
|
|
"FROM votes WHERE bug_id = ?",
|
|
|
|
|
undef, $id) || 0;
|
|
|
|
|
$dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
|
|
|
|
|
undef, ($votes, $id));
|
|
|
|
|
}
|
|
|
|
|
# Now return the array containing emails to be sent.
|
2009-07-29 15:21:49 +04:00
|
|
|
|
return @messages;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If a user votes for a bug, or the number of votes required to
|
|
|
|
|
# confirm a bug has been reduced, check if the bug is now confirmed.
|
|
|
|
|
sub CheckIfVotedConfirmed {
|
2009-07-13 17:54:45 +04:00
|
|
|
|
my $id = shift;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $bug = new Bugzilla::Bug($id);
|
|
|
|
|
|
|
|
|
|
my $ret = 0;
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if (!$bug->everconfirmed
|
|
|
|
|
and $bug->product_obj->votes_to_confirm
|
2010-10-05 20:47:44 +04:00
|
|
|
|
and $bug->votes >= $bug->product_obj->votes_to_confirm)
|
2010-05-15 00:02:34 +04:00
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$bug->add_comment('', { type => CMT_POPULAR_VOTES });
|
|
|
|
|
|
2009-07-13 17:54:45 +04:00
|
|
|
|
if ($bug->bug_status eq 'UNCONFIRMED') {
|
|
|
|
|
# Get a valid open state.
|
|
|
|
|
my $new_status;
|
|
|
|
|
foreach my $state (@{$bug->status->can_change_to}) {
|
|
|
|
|
if ($state->is_open && $state->name ne 'UNCONFIRMED') {
|
|
|
|
|
$new_status = $state->name;
|
|
|
|
|
last;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ThrowCodeError('no_open_bug_status') unless $new_status;
|
|
|
|
|
|
|
|
|
|
# We cannot call $bug->set_status() here, because a user without
|
|
|
|
|
# canconfirm privs should still be able to confirm a bug by
|
|
|
|
|
# popular vote. We already know the new status is valid, so it's safe.
|
|
|
|
|
$bug->{bug_status} = $new_status;
|
|
|
|
|
$bug->{everconfirmed} = 1;
|
|
|
|
|
delete $bug->{'status'}; # Contains the status object.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
else {
|
2009-07-13 17:54:45 +04:00
|
|
|
|
# If the bug is in a closed state, only set everconfirmed to 1.
|
|
|
|
|
# Do not call $bug->_set_everconfirmed(), for the same reason as above.
|
|
|
|
|
$bug->{everconfirmed} = 1;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2009-07-13 17:54:45 +04:00
|
|
|
|
$bug->update();
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
$ret = 1;
|
|
|
|
|
}
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
################################################################################
|
|
|
|
|
# check_can_change_field() defines what users are allowed to change. You
|
|
|
|
|
# can add code here for site-specific policy changes, according to the
|
|
|
|
|
# instructions given in the Bugzilla Guide and below. Note that you may also
|
|
|
|
|
# have to update the Bugzilla::Bug::user() function to give people access to the
|
|
|
|
|
# options that they are permitted to change.
|
|
|
|
|
#
|
|
|
|
|
# check_can_change_field() returns true if the user is allowed to change this
|
|
|
|
|
# field, and false if they are not.
|
|
|
|
|
#
|
|
|
|
|
# The parameters to this method are as follows:
|
|
|
|
|
# $field - name of the field in the bugs table the user is trying to change
|
|
|
|
|
# $oldvalue - what they are changing it from
|
|
|
|
|
# $newvalue - what they are changing it to
|
|
|
|
|
# $PrivilegesRequired - return the reason of the failure, if any
|
|
|
|
|
################################################################################
|
|
|
|
|
sub check_can_change_field {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_);
|
|
|
|
|
my $user = Bugzilla->user;
|
|
|
|
|
|
|
|
|
|
$oldvalue = defined($oldvalue) ? $oldvalue : '';
|
|
|
|
|
$newvalue = defined($newvalue) ? $newvalue : '';
|
|
|
|
|
|
|
|
|
|
# Return true if they haven't changed this field at all.
|
|
|
|
|
if ($oldvalue eq $newvalue) {
|
|
|
|
|
return 1;
|
|
|
|
|
} elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') {
|
|
|
|
|
my ($removed, $added) = diff_arrays($oldvalue, $newvalue);
|
|
|
|
|
return 1 if !scalar(@$removed) && !scalar(@$added);
|
|
|
|
|
} elsif (trim($oldvalue) eq trim($newvalue)) {
|
|
|
|
|
return 1;
|
|
|
|
|
# numeric fields need to be compared using ==
|
|
|
|
|
} elsif (($field eq 'estimated_time' || $field eq 'remaining_time')
|
|
|
|
|
&& $oldvalue == $newvalue)
|
|
|
|
|
{
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Allow anyone to change comments.
|
|
|
|
|
if ($field =~ /^longdesc/) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If the user isn't allowed to change a field, we must tell him who can.
|
|
|
|
|
# We store the required permission set into the $PrivilegesRequired
|
|
|
|
|
# variable which gets passed to the error template.
|
|
|
|
|
#
|
|
|
|
|
# $PrivilegesRequired = 0 : no privileges required;
|
|
|
|
|
# $PrivilegesRequired = 1 : the reporter, assignee or an empowered user;
|
|
|
|
|
# $PrivilegesRequired = 2 : the assignee or an empowered user;
|
|
|
|
|
# $PrivilegesRequired = 3 : an empowered user.
|
2010-10-05 20:47:44 +04:00
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
# Only users in the time-tracking group can change time-tracking fields.
|
|
|
|
|
if ( grep($_ eq $field, qw(deadline estimated_time remaining_time)) ) {
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if (!$user->is_timetracker) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$$PrivilegesRequired = 3;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Allow anyone with (product-specific) "editbugs" privs to change anything.
|
|
|
|
|
if ($user->in_group('editbugs', $self->{'product_id'})) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# *Only* users with (product-specific) "canconfirm" privs can confirm bugs.
|
2010-05-15 00:02:34 +04:00
|
|
|
|
if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) {
|
2008-12-15 15:53:33 +03:00
|
|
|
|
$$PrivilegesRequired = 3;
|
|
|
|
|
return $user->in_group('canconfirm', $self->{'product_id'});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Make sure that a valid bug ID has been given.
|
|
|
|
|
if (!$self->{'error'}) {
|
|
|
|
|
# Allow the assignee to change anything else.
|
|
|
|
|
if ($self->{'assigned_to'} == $user->id
|
|
|
|
|
|| $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id)
|
|
|
|
|
{
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Allow the QA contact to change anything else.
|
|
|
|
|
if (Bugzilla->params->{'useqacontact'}
|
|
|
|
|
&& (($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id)
|
|
|
|
|
|| ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id)))
|
|
|
|
|
{
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# At this point, the user is either the reporter or an
|
|
|
|
|
# unprivileged user. We first check for fields the reporter
|
|
|
|
|
# is not allowed to change.
|
|
|
|
|
|
|
|
|
|
# The reporter may not:
|
|
|
|
|
# - reassign bugs, unless the bugs are assigned to him;
|
|
|
|
|
# in that case we will have already returned 1 above
|
|
|
|
|
# when checking for the assignee of the bug.
|
|
|
|
|
if ($field eq 'assigned_to') {
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
# - change the QA contact
|
|
|
|
|
if ($field eq 'qa_contact') {
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
# - change the target milestone
|
|
|
|
|
if ($field eq 'target_milestone') {
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
# - change the priority (unless he could have set it originally)
|
|
|
|
|
if ($field eq 'priority'
|
|
|
|
|
&& !Bugzilla->params->{'letsubmitterchoosepriority'})
|
|
|
|
|
{
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2009-07-13 17:54:45 +04:00
|
|
|
|
# - unconfirm bugs (confirming them is handled above)
|
|
|
|
|
if ($field eq 'everconfirmed') {
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
# - change the status from one open state to another
|
|
|
|
|
if ($field eq 'bug_status'
|
2010-10-05 20:47:44 +04:00
|
|
|
|
&& is_open_state($oldvalue) && is_open_state($newvalue))
|
2009-07-13 17:54:45 +04:00
|
|
|
|
{
|
|
|
|
|
$$PrivilegesRequired = 2;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
# The reporter is allowed to change anything else.
|
|
|
|
|
if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If we haven't returned by this point, then the user doesn't
|
|
|
|
|
# have the necessary permissions to change this field.
|
|
|
|
|
$$PrivilegesRequired = 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# A helper for check_can_change_field
|
|
|
|
|
sub _changes_everconfirmed {
|
|
|
|
|
my ($self, $field, $old, $new) = @_;
|
|
|
|
|
return 1 if $field eq 'everconfirmed';
|
|
|
|
|
if ($field eq 'bug_status') {
|
|
|
|
|
if ($self->everconfirmed) {
|
|
|
|
|
# Moving a confirmed bug to UNCONFIRMED will change everconfirmed.
|
|
|
|
|
return 1 if $new eq 'UNCONFIRMED';
|
|
|
|
|
}
|
|
|
|
|
else {
|
2010-10-05 20:47:44 +04:00
|
|
|
|
# Moving an unconfirmed bug to an open state that isn't
|
2010-05-15 00:02:34 +04:00
|
|
|
|
# UNCONFIRMED will confirm the bug.
|
|
|
|
|
return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
|
#
|
|
|
|
|
# Field Validation
|
|
|
|
|
#
|
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# 1) Check bug dependencies for loops
|
|
|
|
|
# 2) Save recursively loaded dependencies in $invocant->dependent_validators
|
|
|
|
|
sub ValidateDependencies
|
|
|
|
|
{
|
|
|
|
|
my ($invocant, $dependson, $blocked) = @_;
|
|
|
|
|
my $id = ref($invocant) ? $invocant->id : 0;
|
|
|
|
|
return unless defined $dependson || defined $blocked;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# These can be arrayrefs or they can be strings.
|
|
|
|
|
my $fields = { dependson => $dependson, blocked => $blocked };
|
|
|
|
|
for (qw(blocked dependson))
|
2008-12-15 15:53:33 +03:00
|
|
|
|
{
|
2011-09-05 17:46:27 +04:00
|
|
|
|
$fields->{$_} = [split /[\s,]+/, $fields->{$_}] if !ref $fields->{$_};
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# Load dependencies from DB
|
2008-12-15 15:53:33 +03:00
|
|
|
|
my $dbh = Bugzilla->dbh;
|
2011-09-05 17:46:27 +04:00
|
|
|
|
my $closure = {
|
|
|
|
|
blocked => { map { $_ => 1 } @{$fields->{blocked}} },
|
|
|
|
|
dependson => { map { $_ => 1 } @{$fields->{dependson}} },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($closure->{blocked}->{$id} || $closure->{dependson}->{$id})
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError('dependency_loop_single');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# We want to know on which bugs 'dependson' depends, so it's 'blocked' and vice versa
|
|
|
|
|
my $stack = {
|
|
|
|
|
blocked => { map { $_ => 1 } @{$fields->{dependson}} },
|
|
|
|
|
dependson => { map { $_ => 1 } @{$fields->{blocked}} },
|
|
|
|
|
};
|
|
|
|
|
my ($rows, $old);
|
|
|
|
|
while (%{$stack->{blocked}} || %{$stack->{dependson}})
|
|
|
|
|
{
|
|
|
|
|
# Ignore any current dependencies involving this bug,
|
|
|
|
|
# as they will be overwritten with data from the form
|
|
|
|
|
my $query = join(' OR ', map {
|
|
|
|
|
"$_ IN (".join(',', map { int($_) } keys %{$stack->{$_}}).")"
|
|
|
|
|
} grep { %{$stack->{$_}} } keys %$stack);
|
|
|
|
|
$rows = $dbh->selectall_arrayref(
|
|
|
|
|
"SELECT blocked, dependson FROM dependencies".
|
|
|
|
|
" WHERE blocked != $id AND dependson != $id AND ($query)"
|
|
|
|
|
);
|
|
|
|
|
$old = $stack;
|
|
|
|
|
$stack = { blocked => {}, dependson => {} };
|
|
|
|
|
for (@$rows)
|
|
|
|
|
{
|
|
|
|
|
if ($old->{blocked}->{$_->[0]} && !$closure->{dependson}->{$_->[1]})
|
|
|
|
|
{
|
|
|
|
|
$stack->{blocked}->{$_->[1]} = 1;
|
|
|
|
|
$closure->{dependson}->{$_->[1]} = 1;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
2011-09-05 17:46:27 +04:00
|
|
|
|
if ($old->{dependson}->{$_->[1]} && !$closure->{blocked}->{$_->[0]})
|
|
|
|
|
{
|
|
|
|
|
$stack->{dependson}->{$_->[0]} = 1;
|
|
|
|
|
$closure->{blocked}->{$_->[0]} = 1;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
my @intersect = grep { $closure->{blocked}->{$_} } keys %{$closure->{dependson}};
|
|
|
|
|
if (@intersect)
|
|
|
|
|
{
|
|
|
|
|
ThrowUserError("dependency_loop_multi", { deps => \@intersect });
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
2011-09-05 17:46:27 +04:00
|
|
|
|
# Remember closure, will be used to check BUG_ID add_to_deps custom fields
|
|
|
|
|
$invocant->dependent_validators->{dependencies} = $closure;
|
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
|
|
#####################################################################
|
|
|
|
|
# Autoloaded Accessors
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
|
|
|
|
# Determines whether an attribute access trapped by the AUTOLOAD function
|
|
|
|
|
# is for a valid bug attribute. Bug attributes are properties and methods
|
|
|
|
|
# predefined by this module as well as bug fields for which an accessor
|
|
|
|
|
# can be defined by AUTOLOAD at runtime when the accessor is first accessed.
|
|
|
|
|
#
|
|
|
|
|
# XXX Strangely, some predefined attributes are on the list, but others aren't,
|
|
|
|
|
# and the original code didn't specify why that is. Presumably the only
|
|
|
|
|
# attributes that need to be on this list are those that aren't predefined;
|
|
|
|
|
# we should verify that and update the list accordingly.
|
|
|
|
|
#
|
|
|
|
|
sub _validate_attribute {
|
|
|
|
|
my ($attribute) = @_;
|
|
|
|
|
|
|
|
|
|
my @valid_attributes = (
|
|
|
|
|
# Miscellaneous properties and methods.
|
|
|
|
|
qw(error groups product_id component_id
|
2010-05-15 00:02:34 +04:00
|
|
|
|
comments milestoneurl attachments isopened
|
2008-12-15 15:53:33 +03:00
|
|
|
|
flag_types num_attachment_flag_types
|
|
|
|
|
show_attachment_flags any_flags_requesteeble),
|
|
|
|
|
|
|
|
|
|
# Bug fields.
|
|
|
|
|
Bugzilla::Bug->fields
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return grep($attribute eq $_, @valid_attributes) ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub AUTOLOAD {
|
2010-11-10 21:21:19 +03:00
|
|
|
|
use vars qw($AUTOLOAD);
|
|
|
|
|
my $attr = $AUTOLOAD;
|
|
|
|
|
|
|
|
|
|
$attr =~ s/.*:://;
|
|
|
|
|
return unless $attr=~ /[^A-Z]/;
|
|
|
|
|
if (!_validate_attribute($attr)) {
|
|
|
|
|
require Carp;
|
|
|
|
|
Carp::confess("invalid bug attribute $attr");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
no strict 'refs';
|
|
|
|
|
*$AUTOLOAD = sub {
|
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
|
|
return $self->{$attr} if defined $self->{$attr};
|
|
|
|
|
|
|
|
|
|
$self->{_multi_selects} ||= [Bugzilla->get_fields(
|
|
|
|
|
{custom => 1, type => FIELD_TYPE_MULTI_SELECT })];
|
|
|
|
|
if ( grep($_->name eq $attr, @{$self->{_multi_selects}}) ) {
|
|
|
|
|
# There is a bug in Perl 5.10.0, which is fixed in 5.10.1,
|
|
|
|
|
# which taints $attr at this point. trick_taint() can go
|
|
|
|
|
# away once we require 5.10.1 or newer.
|
|
|
|
|
trick_taint($attr);
|
|
|
|
|
|
|
|
|
|
$self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
|
|
|
|
|
"SELECT value FROM bug_$attr WHERE bug_id = ? ORDER BY value",
|
|
|
|
|
undef, $self->id);
|
|
|
|
|
return $self->{$attr};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
goto &$AUTOLOAD;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
1;
|