summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Bug.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Bug.pm')
-rw-r--r--Bugzilla/Bug.pm438
1 files changed, 327 insertions, 111 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 7b86ab2a1..1c3b0267c 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -80,7 +80,8 @@ use constant AUDIT_UPDATES => 0;
# This is a sub because it needs to call other subroutines.
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
- my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
+ my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT
+ && $_->type != FIELD_TYPE_EXTENSION}
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
@@ -93,6 +94,7 @@ sub DB_COLUMNS {
bug_status
cclist_accessible
component_id
+ creation_ts
delta_ts
estimated_time
everconfirmed
@@ -111,12 +113,11 @@ sub DB_COLUMNS {
version
),
'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',
@custom_names);
-
+
Bugzilla::Hook::process("bug_columns", { columns => \@columns });
-
+
return @columns;
}
@@ -166,6 +167,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;
}
@@ -211,7 +215,8 @@ sub VALIDATOR_DEPENDENCIES {
};
sub UPDATE_COLUMNS {
- my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
+ my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT
+ && $_->type != FIELD_TYPE_EXTENSION}
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
my @columns = qw(
@@ -248,7 +253,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;
}
@@ -280,10 +286,6 @@ use constant FIELD_MAP => {
summary => 'short_desc',
url => 'bug_file_loc',
whiteboard => 'status_whiteboard',
-
- # These are special values for the WebService Bug.search method.
- limit => 'LIMIT',
- offset => 'OFFSET',
};
use constant REQUIRED_FIELD_MAP => {
@@ -311,21 +313,11 @@ use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_cont
#####################################################################
-# This and "new" catch every single way of creating a bug, so that we
-# can call _create_cf_accessors.
-sub _do_list_select {
- my $invocant = shift;
- $invocant->_create_cf_accessors();
- return $invocant->SUPER::_do_list_select(@_);
-}
-
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $param = shift;
- $class->_create_cf_accessors();
-
# Remove leading "#" mark if we've just been passed an id.
if (!ref $param && $param =~ /^#(\d+)$/) {
$param = $1;
@@ -333,10 +325,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 +366,35 @@ sub new {
return $self;
}
-sub check {
+sub initialize {
+ $_[0]->_create_cf_accessors();
+}
+
+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 });
}
@@ -493,6 +504,13 @@ sub preload {
# If we don't do this, can_see_bug will do one call per bug in
# the dependency lists, during get_bug_link in Bugzilla::Template.
$user->visible_bugs(\@all_dep_ids);
+
+ # We preload comments here in order to allow us to compare the time it
+ # takes to load comments from the database with the template rendering
+ # time.
+ foreach my $bug (@$bugs) {
+ $bug->comments();
+ }
}
sub possible_duplicates {
@@ -676,21 +694,20 @@ sub create {
# 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);
+ _update_delta_ts($depends_on_id, $timestamp);
}
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);
+ _update_delta_ts($blocked_id, $timestamp);
}
# Insert the values into the multiselect value tables
@@ -721,7 +738,10 @@ 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 );
+
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp });
return $bug;
}
@@ -775,8 +795,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 +806,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,10 +888,9 @@ 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);
+ _update_delta_ts($removed_id, $delta_ts);
}
foreach my $added_id (@$added) {
$dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
@@ -874,10 +898,9 @@ 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);
+ _update_delta_ts($added_id, $delta_ts);
}
if (scalar(@$removed) || scalar(@$added)) {
@@ -922,7 +945,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 +956,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
@@ -971,6 +994,10 @@ sub update {
$_->update foreach @{ $self->{_update_ref_bugs} || [] };
delete $self->{_update_ref_bugs};
+ # BMO - allow extensions to alter what is logged into bugs_activity
+ Bugzilla::Hook::process('bug_update_before_logging',
+ { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
+
# 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.
@@ -978,8 +1005,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 +1020,7 @@ sub update {
$update_dup->update();
}
}
-
+
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
@@ -1005,20 +1032,39 @@ sub update {
if (scalar(keys %$changes) || $self->{added_comments}
|| $self->{comment_isprivate})
{
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, ($delta_ts, $self->id));
+ _update_delta_ts($self->id, $delta_ts);
$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 +1072,11 @@ 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};
+
+ # BMO - some work should happen outside of the transaction block
+ Bugzilla::Hook::process('bug_after_update',
+ { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug });
return $changes;
}
@@ -1052,27 +1102,56 @@ 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);
}
+# Update a bug's delta_ts without requiring the full object to be loaded.
+sub _update_delta_ts {
+ my ($bug_id, $timestamp) = @_;
+ Bugzilla->dbh->do(
+ "UPDATE bugs SET delta_ts = ? WHERE bug_id = ?",
+ undef,
+ $timestamp, $bug_id
+ );
+ Bugzilla::Hook::process('bug_end_of_update_delta_ts',
+ { bug_id => $bug_id, timestamp => $timestamp });
+}
# This is the correct way to delete bugs from the DB.
# No bug should be deleted from anywhere else except from here.
@@ -1161,15 +1240,15 @@ sub send_changes {
changer => $user,
);
- _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced },
- $vars);
+ my $recipient_count = _send_bugmail(
+ { id => $self->id, type => 'bug', forced => \%forced }, $vars);
# If the bug was marked as a duplicate, we need to notify users on the
# other bug of any changes to that bug.
my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef;
if ($new_dup_id) {
- _send_bugmail({ forced => { changer => $user }, type => "dupe",
- id => $new_dup_id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, type => "dupe", id => $new_dup_id }, $vars);
}
# If there were changes in dependencies, we need to notify those
@@ -1188,7 +1267,7 @@ sub send_changes {
foreach my $id (@{ $self->blocked }) {
$params->{id} = $id;
- _send_bugmail($params, $vars);
+ $recipient_count += _send_bugmail($params, $vars);
}
}
}
@@ -1206,15 +1285,17 @@ sub send_changes {
delete $changed_deps{''};
foreach my $id (sort { $a <=> $b } (keys %changed_deps)) {
- _send_bugmail({ forced => { changer => $user }, type => "dep",
- id => $id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, type => "dep", id => $id }, $vars);
}
# Sending emails for the referenced bugs.
foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) {
- _send_bugmail({ forced => { changer => $user },
- id => $ref_bug_id }, $vars);
+ $recipient_count += _send_bugmail(
+ { forced => { changer => $user }, id => $ref_bug_id }, $vars);
}
+
+ return $recipient_count;
}
sub _send_bugmail {
@@ -1222,7 +1303,7 @@ sub _send_bugmail {
require Bugzilla::BugMail;
- my $results =
+ my $results =
Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params);
if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
@@ -1233,6 +1314,8 @@ sub _send_bugmail {
|| ThrowTemplateError($template->error());
$vars->{'header_done'} = 1;
}
+
+ return scalar @{ $results->{sent} };
}
#####################################################################
@@ -1655,7 +1738,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 +1749,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 +2071,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 +2090,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' });
@@ -2251,7 +2344,8 @@ sub set_all {
$self->_add_remove($params, 'see_also');
# And set custom fields.
- my @custom_fields = Bugzilla->active_custom_fields;
+ my @custom_fields = grep { $_->type != FIELD_TYPE_EXTENSION }
+ Bugzilla->active_custom_fields;
foreach my $field (@custom_fields) {
my $fname = $field->name;
if (exists $params->{$fname}) {
@@ -2266,7 +2360,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 +2390,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) = @_;
@@ -2794,19 +2889,25 @@ sub add_group {
return if $self->in_group($group);
- # Make sure that bugs in this product can actually be restricted
- # to this group by the current user.
- $self->product_obj->group_is_settable($group)
- || ThrowUserError('group_restriction_not_allowed', $args);
+ # BMO : allow bugs to be always placed into some groups by the bug's
+ # reporter
+ if ($self->{reporter_id} != Bugzilla->user->id
+ || !$self->product_obj->group_always_settable($group))
+ {
+ # Make sure that bugs in this product can actually be restricted
+ # to this group by the current user.
+ $self->product_obj->group_is_settable($group)
+ || ThrowUserError('group_restriction_not_allowed', $args);
- # 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_restriction_not_allowed', $args);
+ # 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_restriction_not_allowed', $args);
+ }
}
}
@@ -3161,8 +3262,8 @@ 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'};
+ return $self->{'assigned_to_obj'}
+ = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 });
}
sub blocked {
@@ -3239,7 +3340,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 +3380,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 +3408,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'};
@@ -3337,6 +3460,8 @@ sub comments {
}
Bugzilla::Comment->preload($self->{'comments'});
}
+ return unless defined wantarray;
+
my @comments = @{ $self->{'comments'} };
my $order = $params->{order}
@@ -3387,7 +3512,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};
}
@@ -3397,7 +3523,8 @@ sub qa_contact {
return undef if $self->{'error'};
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
- $self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
+ $self->{'qa_contact_obj'}
+ = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 });
} else {
# XXX - This is somewhat inconsistent with the assignee/reporter
# methods, which will return an empty User if they get a 0.
@@ -3411,8 +3538,8 @@ 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'};
+ return $self->{'reporter'}
+ = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 });
}
sub see_also {
@@ -3553,6 +3680,49 @@ sub groups {
}
}
+ # BMO: if required, hack in groups exposed by -visible membership
+ # (eg mozilla-corporation-confidential-visible), so reporters can add the
+ # bug to a group on show_bug.
+ # if the bug is already in the group, the user will not be able to remove
+ # it unless they are a true group member.
+ my $user = Bugzilla->user;
+ if ($self->{'reporter_id'} == $user->id) {
+ foreach my $group (@{ $user->groups }) {
+ # map from -visible group to the real one
+ my $group_name = $group->name;
+ next unless $group_name =~ s/-visible$//;
+ next if $user->in_group($group_name);
+ $group = Bugzilla::Group->new({ name => $group_name, cache => 1 });
+
+ # only show the group if it's visible to normal members
+ my ($member_control) = $dbh->selectrow_array(
+ "SELECT membercontrol
+ FROM groups
+ LEFT JOIN group_control_map
+ ON group_control_map.group_id = groups.id
+ AND group_control_map.product_id = ?
+ WHERE groups.id = ?",
+ undef,
+ $self->{product_id}, $group->id
+ );
+
+ if (
+ $member_control
+ && $member_control == CONTROLMAPSHOWN
+ && !grep { $_->{bit} == $group->id } @groups)
+ {
+ push(@groups, {
+ bit => $group->id,
+ name => $group->name,
+ ison => 0,
+ ingroup => 1,
+ mandatory => 0,
+ description => $group->description,
+ });
+ }
+ }
+ }
+
$self->{'groups'} = \@groups;
return $self->{'groups'};
@@ -3671,6 +3841,9 @@ sub editable_bug_fields {
# Ensure field exists before attempting to remove it.
splice(@fields, $location, 1) if ($location > -1);
}
+
+ Bugzilla::Hook::process('bug_editable_bug_fields', { fields => \@fields });
+
# Sorted because the old @::log_columns variable, which this replaces,
# was sorted.
return sort(@fields);
@@ -3750,7 +3923,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,16 +3978,29 @@ 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'}
+ && ($comment_id || 0) == ($operation->{'comment_id'} || 0)
+ && ($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);
+ $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id);
}
push (@$changes, \%change);
@@ -3829,6 +4015,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 +4058,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 +4065,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 +4151,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;
}
@@ -4145,6 +4358,7 @@ sub _create_cf_accessors {
my $fields = Bugzilla->fields({ custom => 1 });
foreach my $field (@$fields) {
+ next if $field->type == FIELD_TYPE_EXTENSION;
my $accessor = $class->_accessor_for($field);
my $name = "${class}::" . $field->name;
{
@@ -4154,6 +4368,8 @@ sub _create_cf_accessors {
}
}
+ Bugzilla::Hook::process('bug_create_cf_accessors');
+
Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1;
}