summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Bug.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Bug.pm')
-rw-r--r--Bugzilla/Bug.pm243
1 files changed, 189 insertions, 54 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 7b86ab2a1..613aefdc9 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -166,6 +166,9 @@ sub VALIDATORS {
elsif ($field->type == FIELD_TYPE_DATETIME) {
$validator = \&_check_datetime_field;
}
+ elsif ($field->type == FIELD_TYPE_DATE) {
+ $validator = \&_check_date_field;
+ }
elsif ($field->type == FIELD_TYPE_FREETEXT) {
$validator = \&_check_freetext_field;
}
@@ -248,7 +251,8 @@ use constant NUMERIC_COLUMNS => qw(
);
sub DATE_COLUMNS {
- my @fields = @{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) };
+ my @fields = (@{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) },
+ @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) });
return map { $_->name } @fields;
}
@@ -333,10 +337,14 @@ sub new {
# If we get something that looks like a word (not a number),
# make it the "name" param.
- if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
+ if (!defined $param
+ || (!ref($param) && (!$param || $param =~ /\D/))
+ || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/)))
+ {
# But only if aliases are enabled.
if (Bugzilla->params->{'usebugaliases'} && $param) {
- $param = { name => $param };
+ $param = { name => ref($param) ? $param->{id} : $param,
+ cache => ref($param) ? $param->{cache} : 0 };
}
else {
# Aliases are off, and we got something that's not a number.
@@ -370,20 +378,31 @@ sub new {
return $self;
}
-sub check {
+sub cache_key {
my $class = shift;
- my ($id, $field) = @_;
+ my $key = $class->SUPER::cache_key(@_)
+ || return;
+ return $key . ',' . Bugzilla->user->id;
+}
- ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+sub check {
+ my $class = shift;
+ my ($param, $field) = @_;
# 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;
+ my $id = ref($param)
+ ? ($param->{id} = trim($param->{id}))
+ : ($param = trim($param));
+ ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+
+ my $self = $class->new($param);
if ($self->{error}) {
+ # For error messages, use the id that was returned by new(), because
+ # it's cleaned up.
+ $id = $self->id;
+
if ($self->{error} eq 'NotFound') {
ThrowUserError("bug_id_does_not_exist", { bug_id => $id });
}
@@ -721,7 +740,7 @@ sub create {
# 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');
+ $bug->_sync_fulltext( new_bug => 1 );
return $bug;
}
@@ -775,8 +794,9 @@ sub run_create_validators {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
# XXX This is just a temporary hack until all updating happens
# inside this function.
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
@@ -785,6 +805,10 @@ sub update {
my ($changes, $old_bug) = $self->SUPER::update(@_);
+ Bugzilla::Hook::process('bug_start_of_update',
+ { timestamp => $delta_ts, bug => $self,
+ old_bug => $old_bug, changes => $changes });
+
# Certain items in $changes have to be fixed so that they hold
# a name instead of an ID.
foreach my $field (qw(product_id component_id)) {
@@ -863,7 +887,7 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($removed_id, $other, $self->id, '',
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# 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);
@@ -874,7 +898,7 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($added_id, $other, '', $self->id,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# 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);
@@ -922,7 +946,7 @@ sub update {
$comment = Bugzilla::Comment->insert_create_data($comment);
if ($comment->work_time) {
LogActivityEntry($self->id, "work_time", "", $comment->work_time,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
}
}
@@ -933,7 +957,7 @@ sub update {
my ($from, $to)
= $comment->is_private ? (0, 1) : (1, 0);
LogActivityEntry($self->id, "longdescs.isprivate", $from, $to,
- Bugzilla->user->id, $delta_ts, $comment->id);
+ $user->id, $delta_ts, $comment->id);
}
# Insert the values into the multiselect value tables
@@ -978,8 +1002,8 @@ sub update {
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);
+ LogActivityEntry($self->id, $field, $from, $to,
+ $user->id, $delta_ts);
}
# Check if we have to update the duplicates table and the other bug.
@@ -993,7 +1017,7 @@ sub update {
$update_dup->update();
}
}
-
+
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
@@ -1010,15 +1034,35 @@ sub update {
$self->{delta_ts} = $delta_ts;
}
+ # Update bug ignore data if user wants to ignore mail for this bug
+ if (exists $self->{'bug_ignored'}) {
+ my $bug_ignored_changed;
+ if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) {
+ $dbh->do('INSERT INTO email_bug_ignore
+ (user_id, bug_id) VALUES (?, ?)',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+
+ }
+ elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) {
+ $dbh->do('DELETE FROM email_bug_ignore
+ WHERE user_id = ? AND bug_id = ?',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+ }
+ delete $user->{bugs_ignored} if $bug_ignored_changed;
+ }
+
$dbh->bz_commit_transaction();
# 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()
- if $self->{added_comments} || $changes->{short_desc}
- || $self->{comment_isprivate};
+ $self->_sync_fulltext(
+ update_short_desc => $changes->{short_desc},
+ update_comments => $self->{added_comments} || $self->{comment_isprivate}
+ );
# Remove obsolete internal variables.
delete $self->{'_old_assigned_to'};
@@ -1026,7 +1070,7 @@ sub update {
# 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};
+ delete $user->{_visible_bugs_cache}->{$self->id};
return $changes;
}
@@ -1052,25 +1096,43 @@ sub _extract_multi_selects {
# Should be called any time you update short_desc or change a comment.
sub _sync_fulltext {
- my ($self, $new_bug) = @_;
+ my ($self, %options) = @_;
my $dbh = Bugzilla->dbh;
- if ($new_bug) {
- $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc)
- SELECT bug_id, short_desc FROM bugs WHERE bug_id = ?',
- undef, $self->id);
+
+ my($all_comments, $public_comments);
+ if ($options{new_bug} || $options{update_comments}) {
+ my $comments = $dbh->selectall_arrayref(
+ 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
+ undef, $self->id);
+ $all_comments = join("\n", map { $_->[0] } @$comments);
+ my @no_private = grep { !$_->[1] } @$comments;
+ $public_comments = join("\n", map { $_->[0] } @no_private);
}
- else {
- $dbh->do('UPDATE bugs_fulltext SET short_desc = ? WHERE bug_id = ?',
- undef, $self->short_desc, $self->id);
+
+ if ($options{new_bug}) {
+ $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments,
+ comments_noprivate)
+ VALUES (?, ?, ?, ?)',
+ undef,
+ $self->id, $self->short_desc, $all_comments, $public_comments);
+ } else {
+ my(@names, @values);
+ if ($options{update_short_desc}) {
+ push @names, 'short_desc';
+ push @values, $self->short_desc;
+ }
+ if ($options{update_comments}) {
+ push @names, ('comments', 'comments_noprivate');
+ push @values, ($all_comments, $public_comments);
+ }
+ if (@names) {
+ $dbh->do('UPDATE bugs_fulltext SET ' .
+ join(', ', map { "$_ = ?" } @names) .
+ ' WHERE bug_id = ?',
+ undef,
+ @values, $self->id);
+ }
}
- my $comments = $dbh->selectall_arrayref(
- 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
- undef, $self->id);
- my $all = join("\n", map { $_->[0] } @$comments);
- my @no_private = grep { !$_->[1] } @$comments;
- my $nopriv_string = join("\n", map { $_->[0] } @no_private);
- $dbh->do('UPDATE bugs_fulltext SET comments = ?, comments_noprivate = ?
- WHERE bug_id = ?', undef, $all, $nopriv_string, $self->id);
}
@@ -1655,7 +1717,9 @@ sub _check_groups {
foreach my $name (@$group_names) {
my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name });
- if (!$product->group_is_settable($group)) {
+ # BMO : allow bugs to be always placed into some groups
+ if (!$product->group_always_settable($group)
+ && !$product->group_is_settable($group)) {
ThrowUserError('group_restriction_not_allowed', { %args, name => $name });
}
$add_groups{$group->id} = $group;
@@ -1664,7 +1728,7 @@ sub _check_groups {
# Now enforce mandatory groups.
$add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory };
-
+
my @add_groups = values %add_groups;
return \@add_groups;
}
@@ -1986,8 +2050,12 @@ sub _check_field_is_mandatory {
}
}
+sub _check_date_field {
+ my ($invocant, $date) = @_;
+ return $invocant->_check_datetime_field($date, undef, {date_only => 1});
+}
sub _check_datetime_field {
- my ($invocant, $date_time) = @_;
+ my ($invocant, $date_time, $field, $params) = @_;
# Empty datetimes are empty strings or strings only containing
# 0's, whitespace, and punctuation.
@@ -2001,6 +2069,10 @@ sub _check_datetime_field {
ThrowUserError('illegal_date', { date => $date,
format => 'YYYY-MM-DD' });
}
+ if ($time && $params->{date_only}) {
+ ThrowUserError('illegal_date', { date => $date_time,
+ format => 'YYYY-MM-DD' });
+ }
if ($time && !validate_time($time)) {
ThrowUserError('illegal_time', { 'time' => $time,
format => 'HH:MM:SS' });
@@ -2266,7 +2338,7 @@ sub set_all {
# we have to check that the current assignee, qa, and CCs are still
# valid if we've switched products, under strict_isolation. We can only
# do that here, because if they *did* change the assignee, qa, or CC,
- # then we don't want to check the original ones, only the new ones.
+ # then we don't want to check the original ones, only the new ones.
$self->_check_strict_isolation() if $product_changed;
}
@@ -2296,6 +2368,7 @@ sub reset_assigned_to {
my $comp = $self->component_obj;
$self->set_assigned_to($comp->default_assignee);
}
+sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); }
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
sub set_comment_is_private {
my ($self, $comment_id, $isprivate) = @_;
@@ -3239,7 +3312,8 @@ 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});
+ $self->{component_obj} =
+ new Bugzilla::Component({ id => $self->{component_id}, cache => 1 });
return $self->{component_obj};
}
@@ -3278,6 +3352,26 @@ sub depends_on_obj {
return $self->{depends_on_obj};
}
+sub duplicates {
+ my $self = shift;
+ return $self->{duplicates} if exists $self->{duplicates};
+ return [] if $self->{error};
+ $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids);
+ return $self->{duplicates};
+}
+
+sub duplicate_ids {
+ my $self = shift;
+ return $self->{duplicate_ids} if exists $self->{duplicate_ids};
+ return [] if $self->{error};
+
+ my $dbh = Bugzilla->dbh;
+ $self->{duplicate_ids} =
+ $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?',
+ undef, $self->id);
+ return $self->{duplicate_ids};
+}
+
sub flag_types {
my ($self) = @_;
return $self->{'flag_types'} if exists $self->{'flag_types'};
@@ -3286,7 +3380,8 @@ sub flag_types {
my $vars = { target_type => 'bug',
product_id => $self->{product_id},
component_id => $self->{component_id},
- bug_id => $self->bug_id };
+ bug_id => $self->bug_id,
+ active_or_has_flags => $self->bug_id };
$self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars);
return $self->{'flag_types'};
@@ -3387,7 +3482,8 @@ sub product {
sub product_obj {
my $self = shift;
return {} if $self->{error};
- $self->{product_obj} ||= new Bugzilla::Product($self->{product_id});
+ $self->{product_obj} ||=
+ new Bugzilla::Product({ id => $self->{product_id}, cache => 1 });
return $self->{product_obj};
}
@@ -3750,7 +3846,7 @@ sub GetBugActivity {
$datepart
$attachpart
$suppwhere
- ORDER BY bugs_activity.bug_when";
+ ORDER BY bugs_activity.bug_when, bugs_activity.id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
@@ -3805,14 +3901,26 @@ sub GetBugActivity {
$changes = [];
}
+ # If this is the same field as the previoius item, then concatenate
+ # the data into the same change.
+ if ($operation->{'who'} && $who eq $operation->{'who'}
+ && $when eq $operation->{'when'}
+ && $fieldname eq $operation->{'fieldname'}
+ && ($attachid || 0) == ($operation->{'attachid'} || 0))
+ {
+ my $old_change = pop @$changes;
+ $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
+ $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added);
+ }
+
$operation->{'who'} = $who;
$operation->{'when'} = $when;
+ $operation->{'fieldname'} = $change{'fieldname'} = $fieldname;
+ $operation->{'attachid'} = $change{'attachid'} = $attachid;
- $change{'fieldname'} = $fieldname;
- $change{'attachid'} = $attachid;
$change{'removed'} = $removed;
$change{'added'} = $added;
-
+
if ($comment_id) {
$change{'comment'} = Bugzilla::Comment->new($comment_id);
}
@@ -3829,6 +3937,35 @@ sub GetBugActivity {
return(\@operations, $incomplete_data);
}
+sub _join_activity_entries {
+ my ($field, $current_change, $new_change) = @_;
+ # We need to insert characters as these were removed by old
+ # LogActivityEntry code.
+
+ return $new_change if $current_change eq '';
+
+ # Buglists and see_also need the comma restored
+ if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ', ' . $new_change;
+ }
+ }
+
+ # Assume bug_file_loc contain a single url, don't insert a delimiter
+ if ($field eq 'bug_file_loc') {
+ return $current_change . $new_change;
+ }
+
+ # All other fields get a space
+ if (substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ' ' . $new_change;
+ }
+}
+
# Update the bugs_activity table to reflect changes made in bugs.
sub LogActivityEntry {
my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_;
@@ -3843,7 +3980,6 @@ sub LogActivityEntry {
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
}
@@ -3851,7 +3987,6 @@ sub LogActivityEntry {
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
}
@@ -3938,8 +4073,8 @@ sub check_can_change_field {
return 1;
}
- # Allow anyone to change comments.
- if ($field =~ /^longdesc/) {
+ # Allow anyone to change comments, or set flags
+ if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') {
return 1;
}