diff options
Diffstat (limited to 'Bugzilla/Bug.pm')
-rw-r--r-- | Bugzilla/Bug.pm | 243 |
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; } |