Commit c93887f2 authored by Max Kanat-Alexander's avatar Max Kanat-Alexander

Bug 617477: Fix numerous consistency and behavior issues surroudning Bug.update

and Bugzilla::Bug. See https://bugzilla.mozilla.org/show_bug.cgi?id=617477#c2 for details. r=LpSolit, a=LpSolit
parent 9fe88ea6
...@@ -130,7 +130,7 @@ sub VALIDATORS { ...@@ -130,7 +130,7 @@ sub VALIDATORS {
creation_ts => \&_check_creation_ts, creation_ts => \&_check_creation_ts,
deadline => \&_check_deadline, deadline => \&_check_deadline,
dup_id => \&_check_dup_id, dup_id => \&_check_dup_id,
estimated_time => \&Bugzilla::Object::check_time, estimated_time => \&_check_time_field,
everconfirmed => \&Bugzilla::Object::check_boolean, everconfirmed => \&Bugzilla::Object::check_boolean,
groups => \&_check_groups, groups => \&_check_groups,
keywords => \&_check_keywords, keywords => \&_check_keywords,
...@@ -138,7 +138,7 @@ sub VALIDATORS { ...@@ -138,7 +138,7 @@ sub VALIDATORS {
priority => \&_check_priority, priority => \&_check_priority,
product => \&_check_product, product => \&_check_product,
qa_contact => \&_check_qa_contact, qa_contact => \&_check_qa_contact,
remaining_time => \&Bugzilla::Object::check_time, remaining_time => \&_check_time_field,
rep_platform => \&_check_select_field, rep_platform => \&_check_select_field,
resolution => \&_check_resolution, resolution => \&_check_resolution,
short_desc => \&_check_short_desc, short_desc => \&_check_short_desc,
...@@ -1456,12 +1456,14 @@ sub _check_creation_ts { ...@@ -1456,12 +1456,14 @@ sub _check_creation_ts {
sub _check_deadline { sub _check_deadline {
my ($invocant, $date) = @_; my ($invocant, $date) = @_;
# Check time-tracking permissions. # When filing bugs, we're forgiving and just return undef if
# deadline() returns '' instead of undef if no deadline is set. # the user isn't a timetracker. When updating bugs, check_can_change_field
my $current = ref $invocant ? ($invocant->deadline || undef) : undef; # controls permissions, so we don't want to check them here.
return $current unless Bugzilla->user->is_timetracker; if (!ref $invocant and !Bugzilla->user->is_timetracker) {
return undef;
}
# Validate entered deadline # Validate entered deadline
$date = trim($date); $date = trim($date);
return undef if !$date; return undef if !$date;
...@@ -1787,6 +1789,10 @@ sub _check_short_desc { ...@@ -1787,6 +1789,10 @@ sub _check_short_desc {
if (!defined $short_desc || $short_desc eq '') { if (!defined $short_desc || $short_desc eq '') {
ThrowUserError("require_summary"); ThrowUserError("require_summary");
} }
if (length($short_desc) > MAX_FREETEXT_LENGTH) {
ThrowUserError('freetext_too_long',
{ field => 'short_desc', text => $short_desc });
}
return $short_desc; return $short_desc;
} }
...@@ -1876,6 +1882,20 @@ sub _check_target_milestone { ...@@ -1876,6 +1882,20 @@ sub _check_target_milestone {
return $object->name; return $object->name;
} }
sub _check_time_field {
my ($invocant, $value, $field, $params) = @_;
# When filing bugs, we're forgiving and just return 0 if
# the user isn't a timetracker. When updating bugs, check_can_change_field
# controls permissions, so we don't want to check them here.
if (!ref $invocant and !Bugzilla->user->is_timetracker) {
return 0;
}
# check_time is in Bugzilla::Object.
return $invocant->check_time($value, $field, $params);
}
sub _check_version { sub _check_version {
my ($invocant, $version, undef, $params) = @_; my ($invocant, $version, undef, $params) = @_;
$version = trim($version); $version = trim($version);
...@@ -1940,11 +1960,12 @@ sub _check_datetime_field { ...@@ -1940,11 +1960,12 @@ sub _check_datetime_field {
sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; } sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; }
sub _check_freetext_field { sub _check_freetext_field {
my ($invocant, $text) = @_; my ($invocant, $text, $field) = @_;
$text = (defined $text) ? trim($text) : ''; $text = (defined $text) ? trim($text) : '';
if (length($text) > MAX_FREETEXT_LENGTH) { if (length($text) > MAX_FREETEXT_LENGTH) {
ThrowUserError('freetext_too_long', { text => $text }); ThrowUserError('freetext_too_long',
{ field => $field, text => $text });
} }
return $text; return $text;
} }
...@@ -2221,7 +2242,6 @@ sub reset_assigned_to { ...@@ -2221,7 +2242,6 @@ sub reset_assigned_to {
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); } sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
sub set_comment_is_private { sub set_comment_is_private {
my ($self, $comment_id, $isprivate) = @_; my ($self, $comment_id, $isprivate) = @_;
return unless Bugzilla->user->is_insider;
# We also allow people to pass in a hash of comment ids to update. # We also allow people to pass in a hash of comment ids to update.
if (ref $comment_id) { if (ref $comment_id) {
...@@ -2237,6 +2257,7 @@ sub set_comment_is_private { ...@@ -2237,6 +2257,7 @@ sub set_comment_is_private {
$isprivate = $isprivate ? 1 : 0; $isprivate = $isprivate ? 1 : 0;
if ($isprivate != $comment->is_private) { if ($isprivate != $comment->is_private) {
ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider;
$self->{comment_isprivate} ||= []; $self->{comment_isprivate} ||= [];
$comment->set_is_private($isprivate); $comment->set_is_private($isprivate);
push @{$self->{comment_isprivate}}, $comment; push @{$self->{comment_isprivate}}, $comment;
...@@ -2561,7 +2582,13 @@ sub set_bug_status { ...@@ -2561,7 +2582,13 @@ sub set_bug_status {
else { else {
# We do this here so that we can make sure closed statuses have # We do this here so that we can make sure closed statuses have
# resolutions. # resolutions.
my $resolution = delete $params->{resolution} || $self->resolution; my $resolution = $self->resolution;
# We need to check "defined" to prevent people from passing
# a blank resolution in the WebService, which would otherwise fail
# silently.
if (defined $params->{resolution}) {
$resolution = delete $params->{resolution};
}
$self->set_resolution($resolution, $params); $self->set_resolution($resolution, $params);
# Changing between closed statuses zeros the remaining time. # Changing between closed statuses zeros the remaining time.
...@@ -3810,7 +3837,8 @@ sub check_can_change_field { ...@@ -3810,7 +3837,8 @@ sub check_can_change_field {
} elsif (trim($oldvalue) eq trim($newvalue)) { } elsif (trim($oldvalue) eq trim($newvalue)) {
return 1; return 1;
# numeric fields need to be compared using == # numeric fields need to be compared using ==
} elsif (($field eq 'estimated_time' || $field eq 'remaining_time') } elsif (($field eq 'estimated_time' || $field eq 'remaining_time'
|| $field eq 'work_time')
&& $oldvalue == $newvalue) && $oldvalue == $newvalue)
{ {
return 1; return 1;
...@@ -3845,7 +3873,7 @@ sub check_can_change_field { ...@@ -3845,7 +3873,7 @@ sub check_can_change_field {
# $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. # $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user.
# Only users in the time-tracking group can change time-tracking fields. # Only users in the time-tracking group can change time-tracking fields.
if ( grep($_ eq $field, qw(deadline estimated_time remaining_time)) ) { if ( grep($_ eq $field, TIMETRACKING_FIELDS) ) {
if (!$user->is_timetracker) { if (!$user->is_timetracker) {
$$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED;
return 0; return 0;
......
...@@ -77,7 +77,7 @@ use constant VALIDATORS => { ...@@ -77,7 +77,7 @@ use constant VALIDATORS => {
use constant VALIDATOR_DEPENDENCIES => { use constant VALIDATOR_DEPENDENCIES => {
extra_data => ['type'], extra_data => ['type'],
bug_id => ['who'], bug_id => ['who'],
work_time => ['who'], work_time => ['who', 'bug_id'],
isprivate => ['who'], isprivate => ['who'],
}; };
...@@ -180,6 +180,17 @@ sub set_extra_data { $_[0]->set('extra_data', $_[1]); } ...@@ -180,6 +180,17 @@ sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
# Validators # # Validators #
############## ##############
sub run_create_validators {
my $self = shift;
my $params = $self->SUPER::run_create_validators(@_);
# Sometimes this run_create_validators is called with parameters that
# skip bug_id validation, so it might not exist in the resulting hash.
if (defined $params->{bug_id}) {
$params->{bug_id} = $params->{bug_id}->id;
}
return $params;
}
sub _check_extra_data { sub _check_extra_data {
my ($invocant, $extra_data, undef, $params) = @_; my ($invocant, $extra_data, undef, $params) = @_;
my $type = blessed($invocant) ? $invocant->type : $params->{type}; my $type = blessed($invocant) ? $invocant->type : $params->{type};
...@@ -246,7 +257,7 @@ sub _check_bug_id { ...@@ -246,7 +257,7 @@ sub _check_bug_id {
$bug->check_can_change_field('longdesc', 0, 1, \$privs) $bug->check_can_change_field('longdesc', 0, 1, \$privs)
|| ThrowUserError('illegal_change', || ThrowUserError('illegal_change',
{ field => 'longdesc', privs => $privs }); { field => 'longdesc', privs => $privs });
return $bug->id; return $bug;
} }
sub _check_who { sub _check_who {
...@@ -276,7 +287,12 @@ sub _check_work_time { ...@@ -276,7 +287,12 @@ sub _check_work_time {
# Call down to Bugzilla::Object, letting it know negative # Call down to Bugzilla::Object, letting it know negative
# values are ok # values are ok
return $invocant->check_time( $value_in, $field, $params, 1); my $time = $invocant->check_time($value_in, $field, $params, 1);
my $privs;
$params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
|| ThrowUserError('illegal_change',
{ field => 'work_time', privs => $privs });
return $time;
} }
sub _check_thetext { sub _check_thetext {
......
...@@ -544,9 +544,6 @@ sub check_time { ...@@ -544,9 +544,6 @@ sub check_time {
: 0; : 0;
$current ||= 0; $current ||= 0;
# Don't let the user set the value if they aren't a timetracker
return $current unless Bugzilla->user->is_timetracker;
# Get the new value or zero if it isn't defined # Get the new value or zero if it isn't defined
$value = trim($value) || 0; $value = trim($value) || 0;
......
...@@ -48,7 +48,6 @@ use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); ...@@ -48,7 +48,6 @@ use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
use constant DATE_FIELDS => { use constant DATE_FIELDS => {
comments => ['new_since'], comments => ['new_since'],
search => ['last_change_time', 'creation_time'], search => ['last_change_time', 'creation_time'],
update => ['deadline'],
}; };
use constant BASE64_FIELDS => { use constant BASE64_FIELDS => {
...@@ -470,7 +469,10 @@ sub update { ...@@ -470,7 +469,10 @@ sub update {
my $user = Bugzilla->login(LOGIN_REQUIRED); my $user = Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh; my $dbh = Bugzilla->dbh;
$params = Bugzilla::Bug::map_fields($params, { summary => 1 }); # We skip certain fields because their set_ methods actually use
# the external names instead of the internal names.
$params = Bugzilla::Bug::map_fields($params,
{ summary => 1, platform => 1, severity => 1, url => 1 });
my $ids = delete $params->{ids}; my $ids = delete $params->{ids};
defined $ids || ThrowCodeError('param_required', { param => 'ids' }); defined $ids || ThrowCodeError('param_required', { param => 'ids' });
...@@ -539,6 +541,11 @@ sub update { ...@@ -539,6 +541,11 @@ sub update {
foreach my $field (keys %changes) { foreach my $field (keys %changes) {
my $change = $changes{$field}; my $change = $changes{$field};
my $api_field = $api_name{$field} || $field; my $api_field = $api_name{$field} || $field;
# We normalize undef to an empty string, so that the API
# stays consistent for things like Deadline that can become
# empty.
$change->[0] = '' if !defined $change->[0];
$change->[1] = '' if !defined $change->[1];
$hash{changes}->{$api_field} = { $hash{changes}->{$api_field} = {
removed => $self->type('string', $change->[0]), removed => $self->type('string', $change->[0]),
added => $self->type('string', $change->[1]) added => $self->type('string', $change->[1])
...@@ -891,7 +898,9 @@ sub _bug_to_hash { ...@@ -891,7 +898,9 @@ sub _bug_to_hash {
if (Bugzilla->user->is_timetracker) { if (Bugzilla->user->is_timetracker) {
$item{'estimated_time'} = $self->type('double', $bug->estimated_time); $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
$item{'remaining_time'} = $self->type('double', $bug->remaining_time); $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
$item{'deadline'} = $self->type('dateTime', $bug->deadline); # No need to format $bug->deadline specially, because Bugzilla::Bug
# already does it for us.
$item{'deadline'} = $self->type('string', $bug->deadline);
} }
if (Bugzilla->user->id) { if (Bugzilla->user->id) {
...@@ -1616,7 +1625,8 @@ C<string> The login name of the person who filed this bug (the reporter). ...@@ -1616,7 +1625,8 @@ C<string> The login name of the person who filed this bug (the reporter).
=item C<deadline> =item C<deadline>
C<dateTime> The day that this bug is due to be completed. C<string> The day that this bug is due to be completed, in the format
C<YYYY-MM-DD>.
If you are not in the time-tracking group, this field will not be included If you are not in the time-tracking group, this field will not be included
in the return value. in the return value.
...@@ -2284,7 +2294,8 @@ A hash with one element, C<id>. This is the id of the newly-filed bug. ...@@ -2284,7 +2294,8 @@ A hash with one element, C<id>. This is the id of the newly-filed bug.
=item 51 (Invalid Object) =item 51 (Invalid Object)
The component you specified is not valid for this Product. You specified a field value that is invalid. The error message will have
more details.
=item 103 (Invalid Alias) =item 103 (Invalid Alias)
...@@ -2309,6 +2320,11 @@ you don't have permission to enter bugs in this product. ...@@ -2309,6 +2320,11 @@ you don't have permission to enter bugs in this product.
You didn't specify a summary for the bug. You didn't specify a summary for the bug.
=item 116 (Dependency Loop)
You specified values in the C<blocks> or C<depends_on> fields
that would cause a circular dependency between bugs.
=item 504 (Invalid User) =item 504 (Invalid User)
Either the QA Contact, Assignee, or CC lists have some invalid user Either the QA Contact, Assignee, or CC lists have some invalid user
...@@ -2331,6 +2347,9 @@ method. ...@@ -2331,6 +2347,9 @@ method.
Before Bugzilla 4.0, you had to use the undocumented C<commentprivacy> Before Bugzilla 4.0, you had to use the undocumented C<commentprivacy>
argument. argument.
=item Error 116 was added in Bugzilla B<4.0>. Before that, dependency
loop errors had a generic code of C<32000>.
=back =back
=back =back
...@@ -2495,7 +2514,7 @@ doesn't support aliases or (b) there is no bug with that alias. ...@@ -2495,7 +2514,7 @@ doesn't support aliases or (b) there is no bug with that alias.
The id you specified doesn't exist in the database. The id you specified doesn't exist in the database.
=item 108 (Bug Edit Denied) =item 109 (Bug Edit Denied)
You did not have the necessary rights to edit the bug. You did not have the necessary rights to edit the bug.
...@@ -2647,9 +2666,8 @@ C<string> The Component the bug is in. ...@@ -2647,9 +2666,8 @@ C<string> The Component the bug is in.
=item C<deadline> =item C<deadline>
C<dateTime> The Deadline field--a date specifying when the bug must C<string> The Deadline field--a date specifying when the bug must
be completed by. The time specified is ignored--only the date is be completed by, in the format C<YYYY-MM-DD>.
significant.
=item C<dupe_of> =item C<dupe_of>
...@@ -2898,21 +2916,91 @@ Here's an example of what a return value might look like: ...@@ -2898,21 +2916,91 @@ Here's an example of what a return value might look like:
] ]
} }
Currently, some fields are not tracked in changes: C<comment>,
C<comment_is_private>, and C<work_time>. This means that they will not
show up in the return value even if they were successfully updated.
This may change in a future version of Bugzilla.
=item B<Errors> =item B<Errors>
This function can throw all of the errors that L</get> can throw, plus: This function can throw all of the errors that L</get>, L</create>,
and L</add_comment> can throw, plus:
=over =over
=item 103 (Invalid Alias) =item 50 (Empty Field)
Either you tried to set an alias when changing multiple bugs at once, You tried to set some field to be empty, but that field cannot be empty.
or the alias you specified is invalid for some reason. The error message will have more details.
=back =item 52 (Input Not A Number)
You tried to set a numeric field to a value that wasn't numeric.
=item 54 (Number Too Large)
You tried to set a numeric field to a value larger than that field can
accept.
=item 55 (Number Too Small)
You tried to set a negative value in a numeric field that does not accept
negative values.
=item 56 (Bad Date/Time)
You specified an invalid date or time in a date/time field (such as
the C<deadline> field or a custom date/time field).
=item 112 (See Also Invalid)
You attempted to add an invalid value to the C<see_also> field.
=item 115 (Permission Denied)
You don't have permission to change a particular field to a particular value.
The error message will have more detail.
FIXME: Plus a whole load of other errors that we haven't documented yet, =item 116 (Dependency Loop)
which we won't even know about until after we do QA for 4.0.
You specified a value in the C<blocks> or C<depends_on> fields that causes
a dependency loop.
=item 117 (Invalid Comment ID)
You specified a comment id in C<comment_is_private> that isn't on this bug.
=item 118 (Duplicate Loop)
You specified a value for C<dupe_of> that causes an infinite loop of
duplicates.
=item 119 (dupe_of Required)
You changed the resolution to C<DUPLICATE> but did not specify a value
for the C<dupe_of> field.
=item 120 (Group Add/Remove Denied)
You tried to add or remove a group that you don't have permission to modify
for this bug, or you tried to add a group that isn't valid in this product.
=item 121 (Resolution Required)
You tried to set the C<status> field to a closed status, but you didn't
specify a resolution.
=item 122 (Resolution On Open Status)
This bug has an open status, but you specified a value for the C<resolution>
field.
=item 123 (Invalid Status Transition)
You tried to change from one status to another, but the status workflow
rules don't allow that change.
=back
=item B<History> =item B<History>
...@@ -3010,7 +3098,7 @@ This method can throw all of the errors that L</get> throws, plus: ...@@ -3010,7 +3098,7 @@ This method can throw all of the errors that L</get> throws, plus:
=over =over
=item 108 (Bug Edit Denied) =item 109 (Bug Edit Denied)
You did not have the necessary rights to edit the bug. You did not have the necessary rights to edit the bug.
......
...@@ -49,13 +49,17 @@ our @EXPORT = qw( ...@@ -49,13 +49,17 @@ our @EXPORT = qw(
use constant WS_ERROR_CODE => { use constant WS_ERROR_CODE => {
# Generic errors (Bugzilla::Object and others) are 50-99. # Generic errors (Bugzilla::Object and others) are 50-99.
object_not_specified => 50, object_not_specified => 50,
reassign_to_empty => 50,
param_required => 50, param_required => 50,
params_required => 50, params_required => 50,
undefined_field => 50,
object_does_not_exist => 51, object_does_not_exist => 51,
param_must_be_numeric => 52, param_must_be_numeric => 52,
number_not_numeric => 52, number_not_numeric => 52,
param_invalid => 53, param_invalid => 53,
number_too_large => 54, number_too_large => 54,
number_too_small => 55,
illegal_date => 56,
# Bug errors usually occupy the 100-200 range. # Bug errors usually occupy the 100-200 range.
improper_bug_id_field_value => 100, improper_bug_id_field_value => 100,
bug_id_does_not_exist => 101, bug_id_does_not_exist => 101,
...@@ -88,6 +92,7 @@ use constant WS_ERROR_CODE => { ...@@ -88,6 +92,7 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110, comment_is_private => 110,
comment_id_invalid => 111, comment_id_invalid => 111,
comment_too_long => 114, comment_too_long => 114,
comment_invalid_isprivate => 117,
# See Also errors # See Also errors
bug_url_invalid => 112, bug_url_invalid => 112,
bug_url_too_long => 112, bug_url_too_long => 112,
...@@ -96,6 +101,20 @@ use constant WS_ERROR_CODE => { ...@@ -96,6 +101,20 @@ use constant WS_ERROR_CODE => {
# Note: 114 is above in the Comment-related section. # Note: 114 is above in the Comment-related section.
# Bug update errors # Bug update errors
illegal_change => 115, illegal_change => 115,
# Dependency errors
dependency_loop_single => 116,
dependency_loop_multi => 116,
# Note: 117 is above in the Comment-related section.
# Dup errors
dupe_loop_detected => 118,
dupe_id_required => 119,
# Group errors
group_change_denied => 120,
group_invalid_restriction => 120,
# Status/Resolution errors
missing_resolution => 121,
resolution_not_allowed => 122,
illegal_bug_status_transition => 123,
# Authentication errors are usually 300-400. # Authentication errors are usually 300-400.
invalid_username_or_password => 300, invalid_username_or_password => 300,
......
...@@ -36,6 +36,9 @@ sub datetime_format_inbound { ...@@ -36,6 +36,9 @@ sub datetime_format_inbound {
my ($self, $time) = @_; my ($self, $time) = @_;
my $converted = datetime_from($time, Bugzilla->local_timezone); my $converted = datetime_from($time, Bugzilla->local_timezone);
if (!defined $converted) {
ThrowUserError('illegal_date', { date => $time });
}
$time = $converted->ymd() . ' ' . $converted->hms(); $time = $converted->ymd() . ' ' . $converted->hms();
return $time return $time
} }
......
...@@ -707,10 +707,10 @@ ...@@ -707,10 +707,10 @@
[% ELSIF error == "freetext_too_long" %] [% ELSIF error == "freetext_too_long" %]
[% title = "Text Too Long" %] [% title = "Text Too Long" %]
The text you entered is too long ([% text.length FILTER html %] characters, The text you entered in the [% field_descs.$field FILTER html %]
above the maximum length allowed of [% constants.MAX_FREETEXT_LENGTH FILTER none %] field is too long ([% text.length FILTER html %] characters,
characters): above the maximum length allowed of
<p><em>[% text FILTER html %]</em></p> [%+ constants.MAX_FREETEXT_LENGTH FILTER none %] characters).
[% ELSIF error == "group_cannot_delete" %] [% ELSIF error == "group_cannot_delete" %]
[% title = "Cannot Delete Group" %] [% title = "Cannot Delete Group" %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment