diff options
Diffstat (limited to 'Bugzilla/Bug.pm')
-rw-r--r-- | Bugzilla/Bug.pm | 7665 |
1 files changed, 3932 insertions, 3733 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index ee48ed7a2..5673ab6e3 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -41,9 +41,9 @@ use Role::Tiny::With; use base qw(Bugzilla::Object Exporter); @Bugzilla::Bug::EXPORT = qw( - bug_alias_to_id - LogActivityEntry - editable_bug_fields + bug_alias_to_id + LogActivityEntry + editable_bug_fields ); my %CLEANUP; @@ -56,192 +56,196 @@ use constant DB_TABLE => 'bugs'; use constant ID_FIELD => 'bug_id'; use constant NAME_FIELD => 'alias'; use constant LIST_ORDER => ID_FIELD; + # Bugs have their own auditing table, bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; + # This will be enabled later use constant USE_MEMCACHED => 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 } - Bugzilla->active_custom_fields({skip_extensions => 1}); - my @custom_names = map {$_->name} @custom; - - my @columns = (qw( - alias - assigned_to - bug_file_loc - bug_id - bug_severity - bug_status - cclist_accessible - component_id - creation_ts - delta_ts - estimated_time - everconfirmed - lastdiffed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ), - 'reporter AS reporter_id', - $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', - @custom_names); - - Bugzilla::Hook::process("bug_columns", { columns => \@columns }); - - return @columns; + my $dbh = Bugzilla->dbh; + my @custom = grep { $_->type != FIELD_TYPE_MULTI_SELECT } + Bugzilla->active_custom_fields({skip_extensions => 1}); + my @custom_names = map { $_->name } @custom; + + my @columns = ( + qw( + alias + assigned_to + bug_file_loc + bug_id + bug_severity + bug_status + cclist_accessible + component_id + creation_ts + delta_ts + estimated_time + everconfirmed + lastdiffed + op_sys + priority + product_id + qa_contact + remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ), 'reporter AS reporter_id', + $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', @custom_names + ); + + Bugzilla::Hook::process("bug_columns", {columns => \@columns}); + + return @columns; } sub VALIDATORS { - my $validators = { - alias => \&_check_alias, - assigned_to => \&_check_assigned_to, - bug_file_loc => \&_check_bug_file_loc, - bug_severity => \&_check_select_field, - bug_status => \&_check_bug_status, - cc => \&_check_cc, - comment => \&_check_comment, - component => \&_check_component, - creation_ts => \&_check_creation_ts, - deadline => \&_check_deadline, - dup_id => \&_check_dup_id, - estimated_time => \&_check_time_field, - everconfirmed => \&Bugzilla::Object::check_boolean, - groups => \&_check_groups, - keywords => \&_check_keywords, - op_sys => \&_check_select_field, - priority => \&_check_priority, - product => \&_check_product, - qa_contact => \&_check_qa_contact, - remaining_time => \&_check_time_field, - rep_platform => \&_check_select_field, - resolution => \&_check_resolution, - short_desc => \&_check_short_desc, - status_whiteboard => \&_check_status_whiteboard, - target_milestone => \&_check_target_milestone, - version => \&_check_version, - - cclist_accessible => \&Bugzilla::Object::check_boolean, - reporter_accessible => \&Bugzilla::Object::check_boolean, - }; - - # Set up validators for custom fields. - foreach my $field (Bugzilla->active_custom_fields) { - my $validator; - if ($field->type == FIELD_TYPE_SINGLE_SELECT) { - $validator = \&_check_select_field; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $validator = \&_check_multi_select_field; - } - 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; - } - elsif ($field->type == FIELD_TYPE_BUG_ID) { - $validator = \&_check_bugid_field; - } - elsif ($field->type == FIELD_TYPE_INTEGER) { - $validator = \&_check_integer_field; - } - else { - $validator = \&_check_default_field; - } - $validators->{$field->name} = $validator; + my $validators = { + alias => \&_check_alias, + assigned_to => \&_check_assigned_to, + bug_file_loc => \&_check_bug_file_loc, + bug_severity => \&_check_select_field, + bug_status => \&_check_bug_status, + cc => \&_check_cc, + comment => \&_check_comment, + component => \&_check_component, + creation_ts => \&_check_creation_ts, + deadline => \&_check_deadline, + dup_id => \&_check_dup_id, + estimated_time => \&_check_time_field, + everconfirmed => \&Bugzilla::Object::check_boolean, + groups => \&_check_groups, + keywords => \&_check_keywords, + op_sys => \&_check_select_field, + priority => \&_check_priority, + product => \&_check_product, + qa_contact => \&_check_qa_contact, + remaining_time => \&_check_time_field, + rep_platform => \&_check_select_field, + resolution => \&_check_resolution, + short_desc => \&_check_short_desc, + status_whiteboard => \&_check_status_whiteboard, + target_milestone => \&_check_target_milestone, + version => \&_check_version, + + cclist_accessible => \&Bugzilla::Object::check_boolean, + reporter_accessible => \&Bugzilla::Object::check_boolean, + }; + + # Set up validators for custom fields. + foreach my $field (Bugzilla->active_custom_fields) { + my $validator; + if ($field->type == FIELD_TYPE_SINGLE_SELECT) { + $validator = \&_check_select_field; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $validator = \&_check_multi_select_field; + } + 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; + } + elsif ($field->type == FIELD_TYPE_BUG_ID) { + $validator = \&_check_bugid_field; + } + elsif ($field->type == FIELD_TYPE_INTEGER) { + $validator = \&_check_integer_field; } + else { + $validator = \&_check_default_field; + } + $validators->{$field->name} = $validator; + } - return $validators; -}; + return $validators; +} sub VALIDATOR_DEPENDENCIES { - my $cache = Bugzilla->request_cache; - return $cache->{bug_validator_dependencies} - if $cache->{bug_validator_dependencies}; - - my %deps = ( - assigned_to => ['component'], - bug_status => ['product', 'comment', 'target_milestone'], - cc => ['component'], - comment => ['creation_ts'], - component => ['product'], - dup_id => ['bug_status', 'resolution'], - groups => ['product'], - keywords => ['product'], - resolution => ['bug_status'], - qa_contact => ['component'], - target_milestone => ['product'], - version => ['product'], - ); - - foreach my $field (@{ Bugzilla->fields }) { - $deps{$field->name} = [ $field->visibility_field->name ] - if $field->{visibility_field_id}; - } - - $cache->{bug_validator_dependencies} = \%deps; - return \%deps; -}; + my $cache = Bugzilla->request_cache; + return $cache->{bug_validator_dependencies} + if $cache->{bug_validator_dependencies}; + + my %deps = ( + assigned_to => ['component'], + bug_status => ['product', 'comment', 'target_milestone'], + cc => ['component'], + comment => ['creation_ts'], + component => ['product'], + dup_id => ['bug_status', 'resolution'], + groups => ['product'], + keywords => ['product'], + resolution => ['bug_status'], + qa_contact => ['component'], + target_milestone => ['product'], + version => ['product'], + ); + + foreach my $field (@{Bugzilla->fields}) { + $deps{$field->name} = [$field->visibility_field->name] + if $field->{visibility_field_id}; + } + + $cache->{bug_validator_dependencies} = \%deps; + return \%deps; +} sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT } - Bugzilla->active_custom_fields({skip_extensions => 1}); - 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 - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ); - push(@columns, @custom_names); - return @columns; -}; - -use constant NUMERIC_COLUMNS => qw( + my @custom = grep { $_->type != FIELD_TYPE_MULTI_SELECT } + Bugzilla->active_custom_fields({skip_extensions => 1}); + 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 + op_sys + priority + product_id + qa_contact remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ); + push(@columns, @custom_names); + return @columns; +} + +use constant NUMERIC_COLUMNS => qw( + estimated_time + remaining_time ); sub DATE_COLUMNS { - my @fields = (@{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) }, - @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) }); - return map { $_->name } @fields; + my @fields = ( + @{Bugzilla->fields({type => FIELD_TYPE_DATETIME})}, + @{Bugzilla->fields({type => FIELD_TYPE_DATE})} + ); + return map { $_->name } @fields; } # Used in LogActivityEntry(). Gives the max length of lines in the @@ -253,32 +257,30 @@ use constant MAX_LINE_LENGTH => 254; # of Bugzilla. (These are the field names that the WebService and email_in.pl # use.) use constant FIELD_MAP => { - blocks => 'blocked', - cc_accessible => 'cclist_accessible', - commentprivacy => 'comment_is_private', - creation_time => 'creation_ts', - creator => 'reporter', - description => 'comment', - depends_on => 'dependson', - dupe_of => 'dup_id', - id => 'bug_id', - is_confirmed => 'everconfirmed', - is_cc_accessible => 'cclist_accessible', - is_creator_accessible => 'reporter_accessible', - last_change_time => 'delta_ts', - comment_count => 'longdescs.count', - platform => 'rep_platform', - severity => 'bug_severity', - status => 'bug_status', - summary => 'short_desc', - url => 'bug_file_loc', - whiteboard => 'status_whiteboard', + blocks => 'blocked', + cc_accessible => 'cclist_accessible', + commentprivacy => 'comment_is_private', + creation_time => 'creation_ts', + creator => 'reporter', + description => 'comment', + depends_on => 'dependson', + dupe_of => 'dup_id', + id => 'bug_id', + is_confirmed => 'everconfirmed', + is_cc_accessible => 'cclist_accessible', + is_creator_accessible => 'reporter_accessible', + last_change_time => 'delta_ts', + comment_count => 'longdescs.count', + platform => 'rep_platform', + severity => 'bug_severity', + status => 'bug_status', + summary => 'short_desc', + url => 'bug_file_loc', + whiteboard => 'status_whiteboard', }; -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', - component_id => 'component', -}; +use constant REQUIRED_FIELD_MAP => + {product_id => 'product', component_id => 'component',}; # Creation timestamp is here because it needs to be validated # but it can be NULL in the database (see comments in create above) @@ -296,7 +298,8 @@ use constant REQUIRED_FIELD_MAP => { # # Groups are in a separate table, but must always be validated so that # mandatory groups get set on bugs. -use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_contact groups); +use constant EXTRA_REQUIRED_FIELDS => + qw(creation_ts target_milestone cc qa_contact groups); with 'Bugzilla::Elastic::Role::Object'; @@ -305,118 +308,97 @@ sub ES_TYPE {'bug'} sub ES_INDEX { Bugzilla->params->{elasticsearch_index} } sub ES_SETTINGS { - return { - number_of_shards => 2, - analysis => { - filter => { - asciifolding_original => { - type => "asciifolding", - preserve_original => \1, - }, - }, - analyzer => { - autocomplete => { - type => 'custom', - tokenizer => 'keyword', - filter => [ 'lowercase', 'asciifolding_original' ], - }, - folding => { - tokenizer => 'standard', - filter => [ 'standard', 'lowercase', 'asciifolding_original' ], - }, - bz_text_analyzer => { - type => 'standard', - filter => [ 'lowercase', 'stop' ], - max_token_length => '20' - }, - bz_equals_analyzer => { - type => 'custom', - filter => ['lowercase'], - tokenizer => 'keyword', - }, - whiteboard_words => { - type => 'custom', - tokenizer => 'whiteboard_words_pattern', - filter => ['stop'] - }, - whiteboard_shingle_words => { - type => 'custom', - tokenizer => 'whiteboard_words_pattern', - filter => [ 'stop', 'shingle', 'lowercase' ] - }, - whiteboard_tokens => { - type => 'custom', - tokenizer => 'whiteboard_tokens_pattern', - filter => [ 'stop', 'lowercase' ] - }, - whiteboard_shingle_tokens => { - type => 'custom', - tokenizer => 'whiteboard_tokens_pattern', - filter => [ 'stop', 'shingle', 'lowercase' ] - } - }, - tokenizer => { - whiteboard_tokens_pattern => { - type => 'pattern', - pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*' - }, - whiteboard_words_pattern => { - type => 'pattern', - pattern => '[\\[\\];,\\s]+' - }, - }, + return { + number_of_shards => 2, + analysis => { + filter => { + asciifolding_original => {type => "asciifolding", preserve_original => \1,}, + }, + analyzer => { + autocomplete => { + type => 'custom', + tokenizer => 'keyword', + filter => ['lowercase', 'asciifolding_original'], }, - }; + folding => { + tokenizer => 'standard', + filter => ['standard', 'lowercase', 'asciifolding_original'], + }, + bz_text_analyzer => { + type => 'standard', + filter => ['lowercase', 'stop'], + max_token_length => '20' + }, + bz_equals_analyzer => + {type => 'custom', filter => ['lowercase'], tokenizer => 'keyword',}, + whiteboard_words => { + type => 'custom', + tokenizer => 'whiteboard_words_pattern', + filter => ['stop'] + }, + whiteboard_shingle_words => { + type => 'custom', + tokenizer => 'whiteboard_words_pattern', + filter => ['stop', 'shingle', 'lowercase'] + }, + whiteboard_tokens => { + type => 'custom', + tokenizer => 'whiteboard_tokens_pattern', + filter => ['stop', 'lowercase'] + }, + whiteboard_shingle_tokens => { + type => 'custom', + tokenizer => 'whiteboard_tokens_pattern', + filter => ['stop', 'shingle', 'lowercase'] + } + }, + tokenizer => { + whiteboard_tokens_pattern => + {type => 'pattern', pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*'}, + whiteboard_words_pattern => {type => 'pattern', pattern => '[\\[\\];,\\s]+'}, + }, + }, + }; } sub _bz_field { - my ($field, @fields) = @_; - - return ( - $field => { - type => 'string', - analyzer => 'bz_text_analyzer', - fields => { - eq => { - type => 'string', - analyzer => 'bz_equals_analyzer', - }, - @fields, - }, - }, - ); + my ($field, @fields) = @_; + + return ( + $field => { + type => 'string', + analyzer => 'bz_text_analyzer', + fields => + {eq => {type => 'string', analyzer => 'bz_equals_analyzer',}, @fields,}, + }, + ); } sub ES_PROPERTIES { - return { - _bz_field('priority'), - _bz_field('bug_severity'), - _bz_field('bug_status'), - _bz_field('resolution'), - status_whiteboard => { type => 'string', analyzer => 'whiteboard_shingle_tokens' }, - delta_ts => { type => 'string', index => 'not_analyzed' }, - _bz_field('product'), - _bz_field('component'), - _bz_field('classification'), - _bz_field('short_desc'), - _bz_field('assigned_to'), - _bz_field('reporter'), - }; + return { + _bz_field('priority'), _bz_field('bug_severity'), _bz_field('bug_status'), + _bz_field('resolution'), + status_whiteboard => + {type => 'string', analyzer => 'whiteboard_shingle_tokens'}, + delta_ts => {type => 'string', index => 'not_analyzed'}, + _bz_field('product'), _bz_field('component'), _bz_field('classification'), + _bz_field('short_desc'), _bz_field('assigned_to'), _bz_field('reporter'), + }; } -sub ES_OBJECTS_AT_ONCE { 4000 } +sub ES_OBJECTS_AT_ONCE {4000} sub ES_SELECT_UPDATED_SQL { - my ($class, $mtime) = @_; + my ($class, $mtime) = @_; - my @fields = ( - 'keywords', 'short_desc', 'product', 'component', - 'cf_crash_signature', 'alias', 'status_whiteboard', - 'bug_status', 'resolution', 'priority', 'assigned_to' - ); - my $fields = join(', ', ("?") x @fields); + my @fields = ( + 'keywords', 'short_desc', 'product', 'component', + 'cf_crash_signature', 'alias', 'status_whiteboard', 'bug_status', + 'resolution', 'priority', 'assigned_to' + ); + my $fields = join(', ', ("?") x @fields); - my $sql = qq{ + my $sql = qq{ SELECT DISTINCT bug_id FROM @@ -468,368 +450,378 @@ sub ES_SELECT_UPDATED_SQL { AND field = 'name' AND at_time > FROM_UNIXTIME(?) }; - return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]); + return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]); } sub es_document { - my ($self) = @_; - return { - bug_id => $self->id, - product => $self->product_obj->name, - alias => $self->alias, - keywords => [ map { $_->name } @{$self->keyword_objects} ], - priority => $self->priority, - bug_status => $self->bug_status, - resolution => $self->resolution, - component => $self->component_obj->name, - classification => $self->product_obj->classification->name, - status_whiteboard => $self->status_whiteboard, - short_desc => $self->short_desc, - assigned_to => $self->assigned_to->login, - reporter => $self->reporter->login, - delta_ts => $self->delta_ts, - bug_severity => $self->bug_severity, - }; + my ($self) = @_; + return { + bug_id => $self->id, + product => $self->product_obj->name, + alias => $self->alias, + keywords => [map { $_->name } @{$self->keyword_objects}], + priority => $self->priority, + bug_status => $self->bug_status, + resolution => $self->resolution, + component => $self->component_obj->name, + classification => $self->product_obj->classification->name, + status_whiteboard => $self->status_whiteboard, + short_desc => $self->short_desc, + assigned_to => $self->assigned_to->login, + reporter => $self->reporter->login, + delta_ts => $self->delta_ts, + bug_severity => $self->bug_severity, + }; } ##################################################################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - # Remove leading "#" mark if we've just been passed an id. - if (!ref $param && $param =~ /^#(\d+)$/) { - $param = $1; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + # Remove leading "#" mark if we've just been passed an id. + if (!ref $param && $param =~ /^#(\d+)$/) { + $param = $1; + } + + # If we get something that looks like a word (not a number), + # make it the "name" param. + 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 => ref($param) ? $param->{id} : $param, + cache => ref($param) ? $param->{cache} : 0 + }; } - - # If we get something that looks like a word (not a number), - # make it the "name" param. - 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 => ref($param) ? $param->{id} : $param, - cache => ref($param) ? $param->{cache} : 0 }; - } - 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; - } + 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 = {}; + if (ref $param) { + $error_self->{bug_id} = $param->{name}; + $error_self->{error} = 'InvalidBugId'; } - - 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 = {}; - if (ref $param) { - $error_self->{bug_id} = $param->{name}; - $error_self->{error} = 'InvalidBugId'; - } - else { - $error_self->{bug_id} = $param; - $error_self->{error} = 'NotFound'; - } - bless $error_self, $class; - return $error_self; + else { + $error_self->{bug_id} = $param; + $error_self->{error} = 'NotFound'; } + bless $error_self, $class; + return $error_self; + } - $CLEANUP{$self->id} = $self; - weaken($CLEANUP{$self->id}); + $CLEANUP{$self->id} = $self; + weaken($CLEANUP{$self->id}); - return $self; + return $self; } sub initialize { - $_[0]->_create_cf_accessors(); + $_[0]->_create_cf_accessors(); } sub object_cache_key { - my $class = shift; - my $key = $class->SUPER::object_cache_key(@_) - || return; - return $key . ',' . Bugzilla->user->id; + my $class = shift; + my $key = $class->SUPER::object_cache_key(@_) || return; + return $key . ',' . Bugzilla->user->id; } sub CLEANUP { - foreach my $bug (values %CLEANUP) { - next unless $bug; - delete $bug->{depends_on_obj}; - delete $bug->{blocks_obj}; - } - %CLEANUP = (); + foreach my $bug (values %CLEANUP) { + next unless $bug; + delete $bug->{depends_on_obj}; + delete $bug->{blocks_obj}; + } + %CLEANUP = (); } sub check { - my $class = shift; - my ($param, $field) = @_; + 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 $id = ref($param) - ? ($param->{id} = trim($param->{id})) - : ($param = trim($param)); - 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 $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); + 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}) { - 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 }); - } - } + # For error messages, use the id that was returned by new(), because + # it's cleaned up. + $id = $self->id; - unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { - $self->check_is_visible; + 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}); } - return $self; + } + + 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). - if ($user->id) { - ThrowUserError("bug_access_denied", { bug_id => $self->id }); - } else { - ThrowUserError("bug_access_query", { bug_id => $self->id }); - } - } -} + my $self = shift; + my $user = Bugzilla->user; -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; - } - } + if (!$user->can_see_bug($self->id)) { - # 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"}; - $params->{$field} = $translated{$field} - if exists $translated{$field}; + # 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). + if ($user->id) { + ThrowUserError("bug_access_denied", {bug_id => $self->id}); } - foreach my $field (qw(product component)) { - delete $params->{$field}; - $params->{"${field}_id"} = $translated{$field} - if exists $translated{$field}; + else { + ThrowUserError("bug_access_query", {bug_id => $self->id}); } + } +} - return $class->SUPER::match(@_); +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"}; + $params->{$field} = $translated{$field} if exists $translated{$field}; + } + foreach my $field (qw(product component)) { + delete $params->{$field}; + $params->{"${field}_id"} = $translated{$field} if exists $translated{$field}; + } + + return $class->SUPER::match(@_); } # Helps load up information for bugs for show_bug.cgi and other situations # that will need to access info on lots of bugs. sub preload { - my ($class, $bugs) = @_; - my $user = Bugzilla->user; - - # It would be faster but MUCH more complicated to select all the - # deps for the entire list in one SQL statement. If we ever have - # a profile that proves that that's necessary, we can switch over - # to the more complex method. - my @all_dep_ids; - foreach my $bug (@$bugs) { - push(@all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson }); - } - @all_dep_ids = uniq @all_dep_ids; - # 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(); - } - - foreach my $bug (@$bugs) { - $bug->_preload_referenced_bugs(); - } + my ($class, $bugs) = @_; + my $user = Bugzilla->user; + + # It would be faster but MUCH more complicated to select all the + # deps for the entire list in one SQL statement. If we ever have + # a profile that proves that that's necessary, we can switch over + # to the more complex method. + my @all_dep_ids; + foreach my $bug (@$bugs) { + push(@all_dep_ids, @{$bug->blocked}, @{$bug->dependson}); + } + @all_dep_ids = uniq @all_dep_ids; + + # 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(); + } + + foreach my $bug (@$bugs) { + $bug->_preload_referenced_bugs(); + } } # Helps load up bugs referenced in comments by retrieving them with a single # query from the database and injecting bug objects into the object-cache. sub _preload_referenced_bugs { - my $self = shift; - my @referenced_bug_ids; - - # inject current duplicates into the object-cache first - foreach my $bug (@{ $self->duplicates }) { - $bug->object_cache_set() - unless Bugzilla::Bug->object_cache_get($bug->id); - } - - # preload bugs from comments - require Bugzilla::Template; - foreach my $comment (@{ $self->comments }) { - if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { - # duplicate bugs that aren't currently in $self->duplicates - push @referenced_bug_ids, $comment->extra_data - unless Bugzilla::Bug->object_cache_get($comment->extra_data); - } - else { - # bugs referenced in comments - Bugzilla::Template::quoteUrls($comment->body, undef, undef, undef, - sub { - my $bug_id = $_[0]; - push @referenced_bug_ids, $bug_id - unless Bugzilla::Bug->object_cache_get($bug_id); - }); + my $self = shift; + my @referenced_bug_ids; + + # inject current duplicates into the object-cache first + foreach my $bug (@{$self->duplicates}) { + $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); + } + + # preload bugs from comments + require Bugzilla::Template; + foreach my $comment (@{$self->comments}) { + if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { + + # duplicate bugs that aren't currently in $self->duplicates + push @referenced_bug_ids, $comment->extra_data + unless Bugzilla::Bug->object_cache_get($comment->extra_data); + } + else { + # bugs referenced in comments + Bugzilla::Template::quoteUrls( + $comment->body, + undef, undef, undef, + sub { + my $bug_id = $_[0]; + push @referenced_bug_ids, $bug_id + unless Bugzilla::Bug->object_cache_get($bug_id); } + ); } + } - # inject into object-cache - my $referenced_bugs = Bugzilla::Bug->new_from_list( - [ uniq @referenced_bug_ids ]); - foreach my $bug (@$referenced_bugs) { - $bug->object_cache_set(); - } + # inject into object-cache + my $referenced_bugs = Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]); + foreach my $bug (@$referenced_bugs) { + $bug->object_cache_set(); + } - # preload bug visibility - Bugzilla->user->visible_bugs(\@referenced_bug_ids); + # preload bug visibility + Bugzilla->user->visible_bugs(\@referenced_bug_ids); } sub possible_duplicates { - my ($class, $params) = @_; - my $short_desc = $params->{summary}; - my $products = $params->{products} || []; - my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; - $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; - $products = [$products] if !ref($products) eq 'ARRAY'; - - my $orig_limit = $limit; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'possible_duplicates', - param => $orig_limit }); - - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my @words = split(/[\b\s]+/, $short_desc || ''); - # Remove leading/trailing punctuation from words + my ($class, $params) = @_; + my $short_desc = $params->{summary}; + my $products = $params->{products} || []; + my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; + $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; + $products = [$products] if !ref($products) eq 'ARRAY'; + + my $orig_limit = $limit; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'possible_duplicates', param => $orig_limit}); + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my @words = split(/[\b\s]+/, $short_desc || ''); + + # Remove leading/trailing punctuation from words + foreach my $word (@words) { + $word =~ s/(?:^\W+|\W+$)//g; + } + + # And make sure that each word is longer than 2 characters. + @words = grep { defined $_ and length($_) > 2 } @words; + + return [] if !@words; + + my ($where_sql, $relevance_sql); + if ($dbh->FULLTEXT_OR) { + my $joined_terms = join($dbh->FULLTEXT_OR, @words); + ($where_sql, $relevance_sql) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); + $relevance_sql ||= $where_sql; + } + else { + my (@where, @relevance); foreach my $word (@words) { - $word =~ s/(?:^\W+|\W+$)//g; + my ($term, $rel_term) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word); + push(@where, $term); + push(@relevance, $rel_term || $term); } - # And make sure that each word is longer than 2 characters. - @words = grep { defined $_ and length($_) > 2 } @words; - - return [] if !@words; - my ($where_sql, $relevance_sql); - if ($dbh->FULLTEXT_OR) { - my $joined_terms = join($dbh->FULLTEXT_OR, @words); - ($where_sql, $relevance_sql) = - $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); - $relevance_sql ||= $where_sql; - } - else { - my (@where, @relevance); - foreach my $word (@words) { - my ($term, $rel_term) = $dbh->sql_fulltext_search( - 'bugs_fulltext.short_desc', $word); - push(@where, $term); - push(@relevance, $rel_term || $term); - } - - $where_sql = join(' OR ', @where); - $relevance_sql = join(' + ', @relevance); - } + $where_sql = join(' OR ', @where); + $relevance_sql = join(' + ', @relevance); + } - my $product_ids = join(',', map { $_->id } @$products); - my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; + my $product_ids = join(',', map { $_->id } @$products); + my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; - # Because we collapse duplicates, we want to get slightly more bugs - # than were actually asked for. - my $sql_limit = $limit + 5; + # Because we collapse duplicates, we want to get slightly more bugs + # than were actually asked for. + my $sql_limit = $limit + 5; - my $possible_dupes = $dbh->selectall_arrayref( - "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, + my $possible_dupes = $dbh->selectall_arrayref( + "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, ($relevance_sql) AS relevance FROM bugs INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id WHERE ($where_sql) $product_sql - ORDER BY relevance DESC, bug_id DESC " . - $dbh->sql_limit($sql_limit), {Slice=>{}}); - - my @actual_dupe_ids; - # Resolve duplicates into their ultimate target duplicates. - foreach my $bug (@$possible_dupes) { - my $push_id = $bug->{bug_id}; - if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { - $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); - } - push(@actual_dupe_ids, $push_id); - } - @actual_dupe_ids = uniq @actual_dupe_ids; - if (scalar @actual_dupe_ids > $limit) { - @actual_dupe_ids = @actual_dupe_ids[0..($limit-1)]; + ORDER BY relevance DESC, bug_id DESC " . $dbh->sql_limit($sql_limit), + {Slice => {}} + ); + + my @actual_dupe_ids; + + # Resolve duplicates into their ultimate target duplicates. + foreach my $bug (@$possible_dupes) { + my $push_id = $bug->{bug_id}; + if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { + $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); } + push(@actual_dupe_ids, $push_id); + } + @actual_dupe_ids = uniq @actual_dupe_ids; + if (scalar @actual_dupe_ids > $limit) { + @actual_dupe_ids = @actual_dupe_ids[0 .. ($limit - 1)]; + } - my $visible = $user->visible_bugs(\@actual_dupe_ids); - return $class->new_from_list($visible); + my $visible = $user->visible_bugs(\@actual_dupe_ids); + return $class->new_from_list($visible); } # Docs for create() (there's no POD in this file yet, but we very @@ -871,654 +863,699 @@ sub possible_duplicates { # 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; - - # BMO - allow parameter alteration before creation. also add support for - # fields which are not bug columns (eg bug_mentors). extensions should move - # fields from $params to $stash, then use the bug_end_of_create hook to - # update the database - my $stash = {}; - Bugzilla::Hook::process('bug_before_create', { params => $params, - stash => $stash }); - - $dbh->bz_start_transaction(); - - # These fields have default values which we can use if they are undefined. - $params->{bug_severity} = Bugzilla->params->{defaultseverity} - unless defined $params->{bug_severity}; - $params->{priority} = Bugzilla->params->{defaultpriority} - unless defined $params->{priority}; - - # BMO - per-product hw/os defaults - if (!defined $params->{rep_platform} || !defined $params->{op_sys}) { - if (my $product = Bugzilla::Product->new({ name => $params->{product}, cache => 1 })) { - $params->{rep_platform} //= $product->default_platform; - $params->{op_sys} //= $product->default_op_sys; - } - } - - # 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}; - my $keywords = delete $params->{keywords}; - my $creation_comment = delete $params->{comment}; - my $see_also = delete $params->{see_also}; - my $comment_tags = delete $params->{comment_tags}; - - # We don't want the bug to appear in the system until it's correctly - # protected by groups. - my $timestamp = delete $params->{creation_ts}; - - my $ms_values = $class->_extract_multi_selects($params); - my $bug = $class->insert_create_data($params); - - # Add the group restrictions - my $sth_group = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $group (@$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 (?,?)'); - foreach my $user_id (@$cc_ids) { - $sth_cc->execute($bug->bug_id, $user_id); - } - - # 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 (?, ?)'); - - 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); - _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); - _update_delta_ts($blocked_id, $timestamp); - } - - # 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); - } - } - - # Insert any see_also values - if ($see_also) { - my $see_also_array = $see_also; - if (!ref $see_also_array) { - $see_also = trim($see_also); - $see_also_array = [ split(/[\s,]+/, $see_also) ]; - } - foreach my $value (@$see_also_array) { - $bug->add_see_also($value); - } - foreach my $see_also (@{ $bug->see_also }) { - $see_also->insert_create_data($see_also); - } - foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) { - $ref_bug->update(); - } - delete $bug->{_update_ref_bugs}; - } - - # Comment #0 handling... - - # We now have a bug id so we can fill this out - $creation_comment->{'bug_id'} = $bug->id; - - # Insert the comment. We always insert a comment on bug creation, - # but sometimes it's blank. - my $comment = Bugzilla::Comment->insert_create_data($creation_comment); - - # Add comment tags - if (defined $comment_tags && Bugzilla->user->can_tag_comments) { - $comment_tags = ref $comment_tags ? $comment_tags : [ $comment_tags ]; - foreach my $tag (@{$comment_tags}) { - $comment->add_tag($tag) if defined $tag; - } - $comment->update(); - } - - # BMO - add the stash param from bug_start_of_create - Bugzilla::Hook::process('bug_end_of_create', { bug => $bug, - timestamp => $timestamp, - stash => $stash, - }); - - - $bug->_sync_fulltext( new_bug => 1 ); - - $dbh->bz_commit_transaction(); - - # BMO - some work should happen outside of the transaction block - Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp }); - - return $bug; -} - -sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - - # Add classification for checking mandatory fields which depend on it - $params->{classification} = $params->{product}->classification->name; - - my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1, - enter_bug => 1, - obsolete => 0 }) }; - foreach my $field (@mandatory_fields) { - $class->_check_field_is_mandatory($params->{$field->name}, $field, - $params); - } + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; + + # BMO - allow parameter alteration before creation. also add support for + # fields which are not bug columns (eg bug_mentors). extensions should move + # fields from $params to $stash, then use the bug_end_of_create hook to + # update the database + my $stash = {}; + Bugzilla::Hook::process('bug_before_create', + {params => $params, stash => $stash}); + + $dbh->bz_start_transaction(); + + # These fields have default values which we can use if they are undefined. + $params->{bug_severity} = Bugzilla->params->{defaultseverity} + unless defined $params->{bug_severity}; + $params->{priority} = Bugzilla->params->{defaultpriority} + unless defined $params->{priority}; + + # BMO - per-product hw/os defaults + if (!defined $params->{rep_platform} || !defined $params->{op_sys}) { + if (my $product + = Bugzilla::Product->new({name => $params->{product}, cache => 1})) + { + $params->{rep_platform} //= $product->default_platform; + $params->{op_sys} //= $product->default_op_sys; + } + } + + # 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}; + my $keywords = delete $params->{keywords}; + my $creation_comment = delete $params->{comment}; + my $see_also = delete $params->{see_also}; + my $comment_tags = delete $params->{comment_tags}; + + # We don't want the bug to appear in the system until it's correctly + # protected by groups. + my $timestamp = delete $params->{creation_ts}; + + my $ms_values = $class->_extract_multi_selects($params); + my $bug = $class->insert_create_data($params); + + # Add the group restrictions + my $sth_group + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $group (@$groups) { + $sth_group->execute($bug->bug_id, $group->id); + } + + $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', + undef, $timestamp, $bug->bug_id); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - my $component = delete $params->{component}; - $params->{component_id} = $component->id; + # Update the bug instance as well + $bug->{creation_ts} = $timestamp; - # Callers cannot set reporter, creation_ts, or delta_ts. - $params->{reporter} = $class->_check_reporter(); - $params->{delta_ts} = $params->{creation_ts}; + # Add the CCs + my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); + foreach my $user_id (@$cc_ids) { + $sth_cc->execute($bug->bug_id, $user_id); + } - if ($params->{estimated_time}) { - $params->{remaining_time} = $params->{estimated_time}; - } + # 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); + } - $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, - $params->{qa_contact}, $product); + # Set up dependencies (blocked/dependson) + my $sth_deps = $dbh->prepare( + 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - ($params->{dependson}, $params->{blocked}) = - $class->_check_dependencies($params->{dependson}, $params->{blocked}, - $product); + foreach my $depends_on_id (@$depends_on) { + $sth_deps->execute($bug->bug_id, $depends_on_id); - # You can't set these fields on bug creation (or sometimes ever). - delete $params->{resolution}; - delete $params->{lastdiffed}; - delete $params->{bug_id}; - delete $params->{classification}; + # Log the reverse action on the other bug. + LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + _update_delta_ts($depends_on_id, $timestamp); + } + foreach my $blocked_id (@$blocked) { + $sth_deps->execute($blocked_id, $bug->bug_id); - Bugzilla::Hook::process('bug_end_of_create_validators', - { params => $params }); + # Log the reverse action on the other bug. + LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + _update_delta_ts($blocked_id, $timestamp); + } - return $params; -} - -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # XXX This is just a temporary hack until all updating happens - # inside this function. - my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - $dbh->bz_start_transaction(); - - 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)) { - my $change = delete $changes->{$field}; - if ($change) { - my $new_field = $field; - $new_field =~ s/_id$//; - $changes->{$new_field} = - [$self->{"_old_${new_field}_name"}, $self->$new_field]; - } - } - foreach my $field (qw(qa_contact assigned_to)) { - if ($changes->{$field}) { - my ($from, $to) = @{ $changes->{$field} }; - $from = $old_bug->$field->login if $from; - $to = $self->$field->login if $to; - $changes->{$field} = [$from, $to]; - } + # 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); } + } - # CC - my @old_cc = map {$_->id} @{$old_bug->cc_users}; - my @new_cc = map {$_->id} @{$self->cc_users}; - my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); - - if (scalar @$removed_cc) { - $dbh->do('DELETE FROM cc WHERE bug_id = ? AND ' - . $dbh->sql_in('who', $removed_cc), undef, $self->id); - } - foreach my $user_id (@$added_cc) { - $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]; + # Insert any see_also values + if ($see_also) { + my $see_also_array = $see_also; + if (!ref $see_also_array) { + $see_also = trim($see_also); + $see_also_array = [split(/[\s,]+/, $see_also)]; } - - # 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) { - $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND ' - . $dbh->sql_in('keywordid', $removed_kw), undef, $self->id); + foreach my $value (@$see_also_array) { + $bug->add_see_also($value); } - foreach my $keyword_id (@$added_kw) { - $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', - undef, $self->id, $keyword_id); + foreach my $see_also (@{$bug->see_also}) { + $see_also->insert_create_data($see_also); } - # 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]; + foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) { + $ref_bug->update(); } + delete $bug->{_update_ref_bugs}; + } - # Dependencies - foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { - my ($type, $other) = @$pair; - my $old = $old_bug->$type; - my $new = $self->$type; + # Comment #0 handling... - 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); + # We now have a bug id so we can fill this out + $creation_comment->{'bug_id'} = $bug->id; - # Add an activity entry for the other bug. - LogActivityEntry($removed_id, $other, $self->id, '', - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - _update_delta_ts($removed_id, $delta_ts); - } - foreach my $added_id (@$added) { - $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", - undef, $added_id, $self->id); - - # Add an activity entry for the other bug. - LogActivityEntry($added_id, $other, '', $self->id, - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - _update_delta_ts($added_id, $delta_ts); - } - - if (scalar(@$removed) || scalar(@$added)) { - $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; - } - } + # Insert the comment. We always insert a comment on bug creation, + # but sometimes it's blank. + my $comment = Bugzilla::Comment->insert_create_data($creation_comment); - # 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)]; - - # we only audit when bugs protected with a secure-mail enabled group - # are made public - if (!scalar @{ $self->groups_in } && any { $old_groups{$_}->secure_mail } @$removed_gr) { - Bugzilla->audit(sprintf('%s made Bug %s public (%s)', $user->login, $self->id, $self->short_desc)); - } + # Add comment tags + if (defined $comment_tags && Bugzilla->user->can_tag_comments) { + $comment_tags = ref $comment_tags ? $comment_tags : [$comment_tags]; + foreach my $tag (@{$comment_tags}) { + $comment->add_tag($tag) if defined $tag; } + $comment->update(); + } - # Comments and comment tags - foreach my $comment (@{$self->{added_comments} || []}) { - # Override the Comment's timestamp to be identical to the update - # timestamp. - $comment->{bug_when} = $delta_ts; - $comment = Bugzilla::Comment->insert_create_data($comment); - if ($comment->work_time) { - LogActivityEntry($self->id, "work_time", "", $comment->work_time, - $user->id, $delta_ts); - } - foreach my $tag (@{$self->{added_comment_tags} || []}) { - $comment->add_tag($tag) if defined $tag; - } - $comment->update() if @{$self->{added_comment_tags} || []}; - } + # BMO - add the stash param from bug_start_of_create + Bugzilla::Hook::process('bug_end_of_create', + {bug => $bug, timestamp => $timestamp, stash => $stash,}); - # Comment Privacy - foreach my $comment (@{$self->{comment_isprivate} || []}) { - $comment->update(); - my ($from, $to) - = $comment->is_private ? (0, 1) : (1, 0); - LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - $user->id, $delta_ts, $comment->id); - } + $bug->_sync_fulltext(new_bug => 1); - # Clear the cache of comments - delete $self->{comments}; + $dbh->bz_commit_transaction(); - # 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)]; + # BMO - some work should happen outside of the transaction block + Bugzilla::Hook::process('bug_after_create', + {bug => $bug, timestamp => $timestamp}); - $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); - } - } - } + return $bug; +} - # See Also +sub run_create_validators { + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); - my ($removed_see, $added_see) = - diff_arrays($old_bug->see_also, $self->see_also, 'name'); + # Add classification for checking mandatory fields which depend on it + $params->{classification} = $params->{product}->classification->name; - $_->remove_from_db foreach @$removed_see; - $_->insert_create_data($_) foreach @$added_see; + my @mandatory_fields + = @{Bugzilla->fields({is_mandatory => 1, enter_bug => 1, obsolete => 0})}; + foreach my $field (@mandatory_fields) { + $class->_check_field_is_mandatory($params->{$field->name}, $field, $params); + } - # If any changes were found, record it in the activity log - if (scalar @$removed_see || scalar @$added_see) { - $changes->{see_also} = [join(', ', map { $_->name } @$removed_see), - join(', ', map { $_->name } @$added_see)]; - } + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + my $component = delete $params->{component}; + $params->{component_id} = $component->id; - $_->update foreach @{ $self->{_update_ref_bugs} || [] }; - delete $self->{_update_ref_bugs}; + # Callers cannot set reporter, creation_ts, or delta_ts. + $params->{reporter} = $class->_check_reporter(); + $params->{delta_ts} = $params->{creation_ts}; - # Flags - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + if ($params->{estimated_time}) { + $params->{remaining_time} = $params->{estimated_time}; + } - # 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 }); + $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, + $params->{qa_contact}, $product); - # 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, - $user->id, $delta_ts); - } + ($params->{dependson}, $params->{blocked}) + = $class->_check_dependencies($params->{dependson}, $params->{blocked}, + $product); - # 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(); - } - } + # You can't set these fields on bug creation (or sometimes ever). + delete $params->{resolution}; + delete $params->{lastdiffed}; + delete $params->{bug_id}; + delete $params->{classification}; - $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; - } + Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params}); - Bugzilla::Hook::process('bug_end_of_update', - { bug => $self, timestamp => $delta_ts, changes => $changes, - old_bug => $old_bug }); + return $params; +} - # If any change occurred, refresh the timestamp of the bug. - if (scalar(keys %$changes) || $self->{added_comments} - || $self->{comment_isprivate}) - { - _update_delta_ts($self->id, $delta_ts); - $self->{delta_ts} = $delta_ts; - } +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # Update last-visited - if ($user->is_involved_in_bug($self)) { - $self->update_user_last_visit($user, $delta_ts); - } + # XXX This is just a temporary hack until all updating happens + # inside this function. + my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # If a user is no longer involved, remove their last visit entry - my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id}); - foreach my $lv (@$last_visits) { - $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); - } + $dbh->bz_start_transaction(); - # 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; + my ($changes, $old_bug) = $self->SUPER::update(@_); - } - 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; - } - - $self->_sync_fulltext( - update_short_desc => $changes->{short_desc}, - update_comments => $self->{added_comments} || $self->{comment_isprivate} + 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)) { + my $change = delete $changes->{$field}; + if ($change) { + my $new_field = $field; + $new_field =~ s/_id$//; + $changes->{$new_field} = [$self->{"_old_${new_field}_name"}, $self->$new_field]; + } + } + foreach my $field (qw(qa_contact assigned_to)) { + if ($changes->{$field}) { + my ($from, $to) = @{$changes->{$field}}; + $from = $old_bug->$field->login if $from; + $to = $self->$field->login if $to; + $changes->{$field} = [$from, $to]; + } + } + + # CC + my @old_cc = map { $_->id } @{$old_bug->cc_users}; + my @new_cc = map { $_->id } @{$self->cc_users}; + my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); + + if (scalar @$removed_cc) { + $dbh->do( + 'DELETE FROM cc WHERE bug_id = ? AND ' . $dbh->sql_in('who', $removed_cc), + undef, $self->id); + } + foreach my $user_id (@$added_cc) { + $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]; + } + + # 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) { + $dbh->do( + 'DELETE FROM keywords WHERE bug_id = ? AND ' + . $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); + } + + # 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; + + 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); + + # Add an activity entry for the other bug. + LogActivityEntry($removed_id, $other, $self->id, '', $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + _update_delta_ts($removed_id, $delta_ts); + } + foreach my $added_id (@$added) { + $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", + undef, $added_id, $self->id); + + # Add an activity entry for the other bug. + LogActivityEntry($added_id, $other, '', $self->id, $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + _update_delta_ts($added_id, $delta_ts); + } + + 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)]; + + # we only audit when bugs protected with a secure-mail enabled group + # are made public + if ( + !scalar @{$self->groups_in} && any { $old_groups{$_}->secure_mail } + @$removed_gr + ) + { + Bugzilla->audit(sprintf( + '%s made Bug %s public (%s)', + $user->login, $self->id, $self->short_desc + )); + } + } + + # Comments and comment tags + foreach my $comment (@{$self->{added_comments} || []}) { + + # Override the Comment's timestamp to be identical to the update + # timestamp. + $comment->{bug_when} = $delta_ts; + $comment = Bugzilla::Comment->insert_create_data($comment); + if ($comment->work_time) { + LogActivityEntry($self->id, "work_time", "", $comment->work_time, $user->id, + $delta_ts); + } + foreach my $tag (@{$self->{added_comment_tags} || []}) { + $comment->add_tag($tag) if defined $tag; + } + $comment->update() if @{$self->{added_comment_tags} || []}; + } + + # Comment Privacy + foreach my $comment (@{$self->{comment_isprivate} || []}) { + $comment->update(); + + my ($from, $to) = $comment->is_private ? (0, 1) : (1, 0); + LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, $user->id, + $delta_ts, $comment->id); + } + + # Clear the cache of comments + delete $self->{comments}; + + # 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); + } + } + } + + # See Also + + my ($removed_see, $added_see) + = diff_arrays($old_bug->see_also, $self->see_also, 'name'); + + $_->remove_from_db foreach @$removed_see; + $_->insert_create_data($_) foreach @$added_see; + + # If any changes were found, record it in the activity log + if (scalar @$removed_see || scalar @$added_see) { + $changes->{see_also} = [ + join(', ', map { $_->name } @$removed_see), + join(', ', map { $_->name } @$added_see) + ]; + } + + $_->update foreach @{$self->{_update_ref_bugs} || []}; + delete $self->{_update_ref_bugs}; + + # Flags + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + # 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. + 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, $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(); + } + } + + $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; + } + + Bugzilla::Hook::process( + 'bug_end_of_update', + { + bug => $self, + timestamp => $delta_ts, + changes => $changes, + old_bug => $old_bug + } + ); + + # If any change occurred, refresh the timestamp of the bug. + if ( scalar(keys %$changes) + || $self->{added_comments} + || $self->{comment_isprivate}) + { + _update_delta_ts($self->id, $delta_ts); + $self->{delta_ts} = $delta_ts; + } + + # Update last-visited + if ($user->is_involved_in_bug($self)) { + $self->update_user_last_visit($user, $delta_ts); + } + + # If a user is no longer involved, remove their last visit entry + my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id}); + foreach my $lv (@$last_visits) { + $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); + } + + # 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; + } + + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} || $self->{comment_isprivate} + ); + + $dbh->bz_commit_transaction(); + + # Remove obsolete internal variables. + delete $self->{'_old_assigned_to'}; + delete $self->{'_old_qa_contact'}; + + # Also flush the visible_bugs cache for this bug as the user's + # relationship with this bug may have changed. + 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 + } + ); - $dbh->bz_commit_transaction(); - - # Remove obsolete internal variables. - delete $self->{'_old_assigned_to'}; - delete $self->{'_old_qa_contact'}; - - # Also flush the visible_bugs cache for this bug as the user's - # relationship with this bug may have changed. - 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; + 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; - } + 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; + } + return \%ms_values; } # Should be called any time you update short_desc or change a comment. sub _sync_fulltext { - my ($self, %options) = @_; - my $dbh = Bugzilla->dbh; - - 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); - } - - if ($options{new_bug}) { - $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments, + my ($self, %options) = @_; + my $dbh = Bugzilla->dbh; + + 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); + } + + 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); - } + 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 + ); + } + } } # 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 }); + 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. # 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 - - # 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); - - # 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)) { - $dbh->do("DELETE FROM attach_data WHERE " - . $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); - - undef $self; + 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 + + # 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); + + # 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)) { + $dbh->do("DELETE FROM attach_data WHERE " . $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); + + undef $self; } ##################################################################### @@ -1526,91 +1563,91 @@ sub remove_from_db { ##################################################################### sub send_changes { - my ($self, $changes) = @_; - my @results; - my $user = Bugzilla->user; - - my $old_qa = $changes->{'qa_contact'} - ? $changes->{'qa_contact'}->[0] : ''; - my $old_own = $changes->{'assigned_to'} - ? $changes->{'assigned_to'}->[0] : ''; - my $old_cc = $changes->{cc} - ? $changes->{cc}->[0] : ''; - - my %forced = ( - cc => [split(/[,;]+/, $old_cc)], - owner => $old_own, - qacontact => $old_qa, - changer => $user, - ); - - push @results, _send_bugmail( - { id => $self->id, type => 'bug', forced => \%forced }); - - # 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) { - push @results, _send_bugmail( - { forced => { changer => $user }, type => "dupe", id => $new_dup_id }); - } - - # If there were changes in dependencies, we need to notify those - # dependencies. - if ($changes->{'bug_status'}) { - my ($old_status, $new_status) = @{ $changes->{'bug_status'} }; - - # If this bug has changed from opened to closed or vice-versa, - # then all of the bugs we block need to be notified. - if (is_open_state($old_status) ne is_open_state($new_status)) { - my $params = { forced => { changer => $user }, - type => 'dep', - dep_only => 1, - blocker => $self, - changes => $changes }; - - foreach my $id (@{ $self->blocked }) { - $params->{id} = $id; - push @results, _send_bugmail($params); - } - } - } - - # To get a list of all changed dependencies, convert the "changes" arrays - # into a long string, then collapse that string into unique numbers in - # a hash. - my $all_changed_deps = join(', ', @{ $changes->{'dependson'} || [] }); - $all_changed_deps = join(', ', @{ $changes->{'blocked'} || [] }, - $all_changed_deps); - my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); - # When clearning one field (say, blocks) and filling in the other - # (say, dependson), an empty string can get into the hash and cause - # an error later. - delete $changed_deps{''}; - - foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { - push @results, _send_bugmail( - { forced => { changer => $user }, type => "dep", id => $id }); - } - - # Sending emails for the referenced bugs. - foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - push @results, _send_bugmail( - { forced => { changer => $user }, id => $ref_bug_id }); - } - - return \@results; + my ($self, $changes) = @_; + my @results; + my $user = Bugzilla->user; + + my $old_qa = $changes->{'qa_contact'} ? $changes->{'qa_contact'}->[0] : ''; + my $old_own = $changes->{'assigned_to'} ? $changes->{'assigned_to'}->[0] : ''; + my $old_cc = $changes->{cc} ? $changes->{cc}->[0] : ''; + + my %forced = ( + cc => [split(/[,;]+/, $old_cc)], + owner => $old_own, + qacontact => $old_qa, + changer => $user, + ); + + push @results, + _send_bugmail({id => $self->id, type => 'bug', forced => \%forced}); + + # 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) { + push @results, + _send_bugmail( + {forced => {changer => $user}, type => "dupe", id => $new_dup_id}); + } + + # If there were changes in dependencies, we need to notify those + # dependencies. + if ($changes->{'bug_status'}) { + my ($old_status, $new_status) = @{$changes->{'bug_status'}}; + + # If this bug has changed from opened to closed or vice-versa, + # then all of the bugs we block need to be notified. + if (is_open_state($old_status) ne is_open_state($new_status)) { + my $params = { + forced => {changer => $user}, + type => 'dep', + dep_only => 1, + blocker => $self, + changes => $changes + }; + + foreach my $id (@{$self->blocked}) { + $params->{id} = $id; + push @results, _send_bugmail($params); + } + } + } + + # To get a list of all changed dependencies, convert the "changes" arrays + # into a long string, then collapse that string into unique numbers in + # a hash. + my $all_changed_deps = join(', ', @{$changes->{'dependson'} || []}); + $all_changed_deps + = join(', ', @{$changes->{'blocked'} || []}, $all_changed_deps); + my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); + + # When clearning one field (say, blocks) and filling in the other + # (say, dependson), an empty string can get into the hash and cause + # an error later. + delete $changed_deps{''}; + + foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { + push @results, + _send_bugmail({forced => {changer => $user}, type => "dep", id => $id}); + } + + # Sending emails for the referenced bugs. + foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) { + push @results, _send_bugmail({forced => {changer => $user}, id => $ref_bug_id}); + } + + return \@results; } sub _send_bugmail { - my ($params) = @_; + my ($params) = @_; - require Bugzilla::BugMail; + require Bugzilla::BugMail; - my $sent_bugmail = - Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); + my $sent_bugmail + = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); - return { params => $params, sent_bugmail => $sent_bugmail }; + return {params => $params, sent_bugmail => $sent_bugmail}; } ##################################################################### @@ -1618,902 +1655,925 @@ sub _send_bugmail { ##################################################################### 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) > 40) { - 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 }); - } + my ($invocant, $alias) = @_; + $alias = trim($alias); + return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias); - return $alias; + # Make sure the alias isn't too long. + if (length($alias) > 40) { + 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, undef, $params) = @_; - my $user = Bugzilla->user; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - - # 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; - } - return $id; + my ($invocant, $assignee, undef, $params) = @_; + my $user = Bugzilla->user; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + + # 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; + } + return $id; } sub _check_bug_file_loc { - my ($invocant, $url) = @_; - $url = '' if !defined($url); - # On bug entry, if bug_file_loc is "http://", the default, use an - # 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; + my ($invocant, $url) = @_; + $url = '' if !defined($url); + + # On bug entry, if bug_file_loc is "http://", the default, use an + # 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 { - my ($invocant, $new_status, undef, $params) = @_; - my $user = Bugzilla->user; - my @valid_statuses; - my $old_status; # Note that this is undef for new bugs. - - my ($product, $comment); - if (ref $invocant) { - @valid_statuses = @{$invocant->statuses_available}; - $product = $invocant->product_obj; - $old_status = $invocant->status; - my $comments = $invocant->{added_comments} || []; - $comment = $comments->[-1]; - } - else { - $product = $params->{product}; - $comment = $params->{comment}; - @valid_statuses = @{Bugzilla::Status->can_change_to()}; - if (!$product->allows_unconfirmed) { - @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses; - } - } - - # 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]; - } - } + my ($invocant, $new_status, undef, $params) = @_; + my $user = Bugzilla->user; + my @valid_statuses; + my $old_status; # Note that this is undef for new bugs. + + my ($product, $comment); + if (ref $invocant) { + @valid_statuses = @{$invocant->statuses_available}; + $product = $invocant->product_obj; + $old_status = $invocant->status; + my $comments = $invocant->{added_comments} || []; + $comment = $comments->[-1]; + } + else { + $product = $params->{product}; + $comment = $params->{comment}; + @valid_statuses = @{Bugzilla::Status->can_change_to()}; + if (!$product->allows_unconfirmed) { + @valid_statuses = grep { $_->name ne 'UNCONFIRMED' } @valid_statuses; + } + } + + # 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 { - # 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]; - } + $new_status + = ($valid_statuses[0]->name ne 'UNCONFIRMED') + ? $valid_statuses[0] + : $valid_statuses[1]; } + } } - - # Time to validate the bug status. - $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); - # We skip this check if we are changing from a status to itself. - if ( (!$old_status || $old_status->id != $new_status->id) - && !grep {$_->name eq $new_status->name} @valid_statuses) - { - 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->{'thetext'}) - { - ThrowUserError('comment_required', { old => $old_status, - new => $new_status }); - - } - - if (ref $invocant - && ($new_status->name eq 'IN_PROGRESS' - # Backwards-compat for the old default workflow. - or $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 }); - } - - if (!blessed $invocant) { - $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; - } - - return $new_status->name; + 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]; + } + } + } + + # Time to validate the bug status. + $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); + + # We skip this check if we are changing from a status to itself. + if ((!$old_status || $old_status->id != $new_status->id) + && !grep { $_->name eq $new_status->name } @valid_statuses) + { + 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->{'thetext'}) + { + ThrowUserError('comment_required', {old => $old_status, new => $new_status}); + + } + + if ( + ref $invocant && ( + $new_status->name eq 'IN_PROGRESS' + + # Backwards-compat for the old default workflow. + or $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}); + } + + if (!blessed $invocant) { + $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; + } + + return $new_status->name; } sub _check_cc { - my ($invocant, $ccs, undef, $params) = @_; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - return [map {$_->id} @{$component->initial_cc}] unless $ccs; + my ($invocant, $ccs, undef, $params) = @_; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + return [map { $_->id } @{$component->initial_cc}] unless $ccs; - # Allow comma-separated input as well as arrayrefs. - $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; + # Allow comma-separated input as well as arrayrefs. + $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; - my %cc_ids; - foreach my $person (@$ccs) { - $person = trim($person); - next unless $person; - my $id = login_to_id($person, THROW_ERROR); - $cc_ids{$id} = 1; - } + my %cc_ids; + foreach my $person (@$ccs) { + $person = trim($person); + next unless $person; + my $id = login_to_id($person, THROW_ERROR); + $cc_ids{$id} = 1; + } - # Enforce Default CC - $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + # Enforce Default CC + $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); - return [keys %cc_ids]; + return [keys %cc_ids]; } sub _check_comment { - my ($invocant, $comment_txt, undef, $params) = @_; + my ($invocant, $comment_txt, undef, $params) = @_; - # Comment can be empty. We should force it to be empty if the text is undef - if (!defined $comment_txt) { - $comment_txt = ''; - } + # Comment can be empty. We should force it to be empty if the text is undef + if (!defined $comment_txt) { + $comment_txt = ''; + } - # Load up some data - my $isprivate = delete $params->{comment_is_private}; - my $timestamp = $params->{creation_ts}; + # Load up some data + my $isprivate = delete $params->{comment_is_private}; + my $timestamp = $params->{creation_ts}; - # Create the new comment so we can check it - my $comment = { - thetext => $comment_txt, - bug_when => $timestamp, - }; + # Create the new comment so we can check it + my $comment = {thetext => $comment_txt, bug_when => $timestamp,}; - # We don't include the "isprivate" column unless it was specified. - # This allows it to fall back to its database default. - if (defined $isprivate) { - $comment->{isprivate} = $isprivate; - } + # We don't include the "isprivate" column unless it was specified. + # This allows it to fall back to its database default. + if (defined $isprivate) { + $comment->{isprivate} = $isprivate; + } - # Validate comment. We have to do this special as a comment normally - # requires a bug to be already created. For a new bug, the first comment - # obviously can't get the bug if the bug is created after this - # (see bug 590334) - Bugzilla::Comment->check_required_create_fields($comment); - $comment = Bugzilla::Comment->run_create_validators($comment, - { skip => ['bug_id'] } - ); + # Validate comment. We have to do this special as a comment normally + # requires a bug to be already created. For a new bug, the first comment + # obviously can't get the bug if the bug is created after this + # (see bug 590334) + Bugzilla::Comment->check_required_create_fields($comment); + $comment + = Bugzilla::Comment->run_create_validators($comment, {skip => ['bug_id']}); - return $comment; + return $comment; } sub _check_component { - my ($invocant, $name, undef, $params) = @_; - $name = trim($name); - $name || ThrowUserError("require_component"); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_comp = blessed($invocant) ? $invocant->component : ''; - my $object = Bugzilla::Component->check({ product => $product, name => $name }); - if ($object->name ne $old_comp && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $name }); - } - return $object; + my ($invocant, $name, undef, $params) = @_; + $name = trim($name); + $name || ThrowUserError("require_component"); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_comp = blessed($invocant) ? $invocant->component : ''; + my $object = Bugzilla::Component->check({product => $product, name => $name}); + if ($object->name ne $old_comp && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $name}); + } + return $object; } sub _check_creation_ts { - return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); } sub _check_deadline { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - # When filing bugs, we're forgiving and just return undef 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 undef; - } + # When filing bugs, we're forgiving and just return undef 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 undef; + } - # Validate entered deadline - $date = trim($date); - return undef if !$date; - validate_date($date) - || ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - return $date; + # 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 || ''); - - foreach my $type (qw(dependson blocked)) { - my @bug_ids = ref($deps_in{$type}) - ? @{$deps_in{$type}} - : split(/[\s,]+/, $deps_in{$type}); - # Eliminate nulls. - @bug_ids = grep {$_} @bug_ids; - # We do this up here to make sure all aliases are converted to IDs. - @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids; - - my $user = Bugzilla->user; - my @check_access = @bug_ids; - # When we're updating a bug, only added or removed bug_ids are - # 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); - - # If a user has editbugs they are allowed to add dependencies on - # bugs that they cannot see -- only check access for bugs that are - # removed. - if ($user->in_group('editbugs')) { - @check_access = @$removed; - } - else { - @check_access = (@$added, @$removed); - } - - # Check field permissions if we've changed anything. - if (@$added || @$removed) { - my $privs; - if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { - ThrowUserError('illegal_change', { field => $type, - privs => $privs }); - } - } - } + my ($invocant, $depends_on, $blocks, $product) = @_; - foreach my $modified_id (@check_access) { - # Check that the user has access to the other bug. - my $delta_bug = $invocant->check($modified_id); - # 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"}) { - if (!$user->can_edit_product($delta_bug->{'product_id'})) { - ThrowUserError("illegal_change_deps", {field => $type}); - } - } - } + if (!ref $invocant) { - $deps_in{$type} = \@bug_ids; - } + # Only editbugs users can set dependencies on bug entry. + return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id); + } - # And finally, check for dependency loops. - my $bug_id = ref($invocant) ? $invocant->id : 0; - my %deps = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id); + my %deps_in = (dependson => $depends_on || '', blocked => $blocks || ''); - return ($deps{'dependson'}, $deps{'blocked'}); -} + foreach my $type (qw(dependson blocked)) { + my @bug_ids + = ref($deps_in{$type}) + ? @{$deps_in{$type}} + : split(/[\s,]+/, $deps_in{$type}); -sub _check_dup_id { - my ($self, $dupe_of) = @_; - my $dbh = Bugzilla->dbh; + # Eliminate nulls. + @bug_ids = grep {$_} @bug_ids; - $dupe_of = trim($dupe_of); - $dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' }); - # 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 - # if a string is passed. Group restrictions are checked below. - my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); - $dupe_of = $dupe_of_bug->id; + # We do this up here to make sure all aliases are converted to IDs. + @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids; - # If the dupe is unchanged, we have nothing more to check. - return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); + my $user = Bugzilla->user; + my @check_access = @bug_ids; - # 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). - $dupe_of_bug->check_is_visible; + # When we're updating a bug, only added or removed bug_ids are + # 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); + + # If a user has editbugs they are allowed to add dependencies on + # bugs that they cannot see -- only check access for bugs that are + # removed. + if ($user->in_group('editbugs')) { + @check_access = @$removed; + } + else { + @check_access = (@$added, @$removed); + } + + # Check field permissions if we've changed anything. + if (@$added || @$removed) { + my $privs; + if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { + ThrowUserError('illegal_change', {field => $type, privs => $privs}); + } + } + } - # Make sure a loop isn't created when marking this bug - # as duplicate. - _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + foreach my $modified_id (@check_access) { - 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; - } - # 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 - # 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. - $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; - $vars->{'original_bug_id'} = $dupe_of; - $vars->{'duplicate_bug_id'} = $self->id; - print $cgi->header(); - $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + # Check that the user has access to the other bug. + my $delta_bug = $invocant->check($modified_id); + + # 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"}) { + if (!$user->can_edit_product($delta_bug->{'product_id'})) { + ThrowUserError("illegal_change_deps", {field => $type}); } + } } - return $dupe_of; + $deps_in{$type} = \@bug_ids; + } + + # And finally, check for dependency loops. + my $bug_id = ref($invocant) ? $invocant->id : 0; + my %deps + = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id); + + return ($deps{'dependson'}, $deps{'blocked'}); } -sub _check_groups { - my ($invocant, $group_names, undef, $params) = @_; - - my $bug_id = blessed($invocant) ? $invocant->id : undef; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my %add_groups; - - # In email or WebServices, when the "groups" item actually - # isn't specified, then just add the default groups. - if (!defined $group_names) { - my $available = $product->groups_available; - foreach my $group (@$available) { - $add_groups{$group->id} = $group if $group->{is_default}; - } +sub _check_dup_id { + my ($self, $dupe_of) = @_; + my $dbh = Bugzilla->dbh; + + $dupe_of = trim($dupe_of); + $dupe_of || ThrowCodeError('undefined_field', {field => 'dup_id'}); + + # 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 + # if a string is passed. Group restrictions are checked below. + my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); + $dupe_of = $dupe_of_bug->id; + + # 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). + $dupe_of_bug->check_is_visible; + + # Make sure a loop isn't created when marking this bug + # as duplicate. + _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + + 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; + } + + # 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 + # 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 { - # Allow a comma-separated list, for email_in.pl. - $group_names = [map { trim($_) } split(',', $group_names)] - if !ref $group_names; - - # First check all the groups they chose to set. - my %args = ( product => $product->name, bug_id => $bug_id, action => 'add' ); - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); - - # 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; - } - } + # 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; - # Now enforce mandatory groups. - $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; + # Ask the user what they want to do about the reporter. + $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; + $vars->{'original_bug_id'} = $dupe_of; + $vars->{'duplicate_bug_id'} = $self->id; + print $cgi->header(); + $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + } - my @add_groups = values %add_groups; - return \@add_groups; + return $dupe_of; } -sub _check_keywords { - my ($invocant, $keywords_in, undef, $params) = @_; +sub _check_groups { + my ($invocant, $group_names, undef, $params) = @_; - return [] if !defined $keywords_in; + my $bug_id = blessed($invocant) ? $invocant->id : undef; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my %add_groups; - my $keyword_array = $keywords_in; - if (!ref $keyword_array) { - $keywords_in = trim($keywords_in); - $keyword_array = [split(/[\s,]+/, $keywords_in)]; + # In email or WebServices, when the "groups" item actually + # isn't specified, then just add the default groups. + if (!defined $group_names) { + my $available = $product->groups_available; + foreach my $group (@$available) { + $add_groups{$group->id} = $group if $group->{is_default}; } + } + else { + # Allow a comma-separated list, for email_in.pl. + $group_names = [map { trim($_) } split(',', $group_names)] if !ref $group_names; - my %keywords; - foreach my $keyword (@$keyword_array) { - next unless $keyword; - my $obj = Bugzilla::Keyword->check($keyword); - $keywords{$obj->id} = $obj; - } + # First check all the groups they chose to set. + my %args = (product => $product->name, bug_id => $bug_id, action => 'add'); + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check_no_disclose({%args, name => $name}); - my %old_kw_id; - if (blessed $invocant) { - my @old_keywords = @{$invocant->keyword_objects}; - %old_kw_id = map { $_->id => 1 } @old_keywords; + # 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; } + } - foreach my $keyword (values %keywords) { - next if $keyword->is_active || exists $old_kw_id{$keyword->id}; - ThrowUserError('value_inactive', - { value => $keyword->name, class => ref $keyword }); - } + # Now enforce mandatory groups. + $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory}; + + my @add_groups = values %add_groups; + return \@add_groups; +} - return [values %keywords]; +sub _check_keywords { + my ($invocant, $keywords_in, undef, $params) = @_; + + return [] if !defined $keywords_in; + + my $keyword_array = $keywords_in; + if (!ref $keyword_array) { + $keywords_in = trim($keywords_in); + $keyword_array = [split(/[\s,]+/, $keywords_in)]; + } + + my %keywords; + foreach my $keyword (@$keyword_array) { + next unless $keyword; + my $obj = Bugzilla::Keyword->check($keyword); + $keywords{$obj->id} = $obj; + } + + my %old_kw_id; + if (blessed $invocant) { + my @old_keywords = @{$invocant->keyword_objects}; + %old_kw_id = map { $_->id => 1 } @old_keywords; + } + + foreach my $keyword (values %keywords) { + next if $keyword->is_active || exists $old_kw_id{$keyword->id}; + ThrowUserError('value_inactive', + {value => $keyword->name, class => ref $keyword}); + } + + 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. - my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); - return $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. + my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); + return $product; } sub _check_priority { - my ($invocant, $priority) = @_; - if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { - $priority = Bugzilla->params->{'defaultpriority'}; - } - return $invocant->_check_select_field($priority, 'priority'); + my ($invocant, $priority) = @_; + if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $priority = Bugzilla->params->{'defaultpriority'}; + } + return $invocant->_check_select_field($priority, 'priority'); } sub _check_qa_contact { - my ($invocant, $qa_contact, undef, $params) = @_; - $qa_contact = trim($qa_contact) if !ref $qa_contact; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - 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; - } + my ($invocant, $qa_contact, undef, $params) = @_; + $qa_contact = trim($qa_contact) if !ref $qa_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + 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; } + } - # 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); - } + # 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); + } - # "0" always means "undef", for QA Contact. - return $id || undef; + # "0" always means "undef", for QA Contact. + return $id || undef; } 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!). - Bugzilla->login(LOGIN_REQUIRED); - $reporter = Bugzilla->user->id; - } - return $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!). + Bugzilla->login(LOGIN_REQUIRED); + $reporter = Bugzilla->user->id; + } + return $reporter; } sub _check_resolution { - my ($self, $resolution) = @_; - $resolution = trim($resolution); - - # 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; - - # Make sure this is a valid resolution. - $resolution = $self->_check_select_field($resolution, 'resolution'); - - # Don't allow open bugs to have resolutions. - ThrowUserError('resolution_not_allowed') if $self->status->is_open; - - # Check noresolveonopenblockers. - if (Bugzilla->params->{"noresolveonopenblockers"} - && $resolution eq 'FIXED' - && (!$self->resolution || $resolution ne $self->resolution) - && scalar @{$self->dependson}) - { - my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson); - my $count_open = grep { $_->isopened } @$dep_bugs; - if ($count_open) { - ThrowUserError("still_unresolved_bugs", - { bug_id => $self->id, dep_count => $count_open }); - } - } - - # Check if they're changing the resolution and need to comment. - if (Bugzilla->params->{'commentonchange_resolution'} - && $self->resolution && $resolution ne $self->resolution - && !$self->{added_comments}) - { - ThrowUserError('comment_required'); - } - - return $resolution; + my ($self, $resolution) = @_; + $resolution = trim($resolution); + + # 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; + + # Make sure this is a valid resolution. + $resolution = $self->_check_select_field($resolution, 'resolution'); + + # Don't allow open bugs to have resolutions. + ThrowUserError('resolution_not_allowed') if $self->status->is_open; + + # Check noresolveonopenblockers. + if ( Bugzilla->params->{"noresolveonopenblockers"} + && $resolution eq 'FIXED' + && (!$self->resolution || $resolution ne $self->resolution) + && scalar @{$self->dependson}) + { + my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson); + my $count_open = grep { $_->isopened } @$dep_bugs; + if ($count_open) { + ThrowUserError("still_unresolved_bugs", + {bug_id => $self->id, dep_count => $count_open}); + } + } + + # Check if they're changing the resolution and need to comment. + if ( Bugzilla->params->{'commentonchange_resolution'} + && $self->resolution + && $resolution ne $self->resolution + && !$self->{added_comments}) + { + ThrowUserError('comment_required'); + } + + 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; + my ($invocant, $short_desc) = @_; - if (!defined $short_desc || $short_desc eq '') { - ThrowUserError("require_summary"); - } - if (length($short_desc) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => 'short_desc', text => $short_desc }); - } - return $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"); + } + if (length($short_desc) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', + {field => 'short_desc', text => $short_desc}); + } + 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 ($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; + my @related_users = @$ccs; + push(@related_users, $assignee) if $assignee; - if (Bugzilla->params->{'useqacontact'} && $qa_contact) { - push(@related_users, $qa_contact); - } + if (Bugzilla->params->{'useqacontact'} && $qa_contact) { + push(@related_users, $qa_contact); + } - @related_users = @{Bugzilla::User->new_from_list(\@related_users)} - if !ref $invocant; + @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); - } + # 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); + } + 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"}; - if (!$user->can_edit_product($self->{product_id})) { - ThrowUserError('invalid_user_group', - { users => $user->login, - product => $self->product, - bug_id => $self->id }); - } + my ($self, $user) = @_; + return unless Bugzilla->params->{"strict_isolation"}; + if (!$user->can_edit_product($self->{product_id})) { + ThrowUserError('invalid_user_group', + {users => $user->login, product => $self->product, bug_id => $self->id}); + } } sub _check_tag_name { - my ($invocant, $tag) = @_; + my ($invocant, $tag) = @_; + + $tag = clean_text($tag); + $tag || ThrowUserError('no_tag_to_edit'); + ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; + trick_taint($tag); - $tag = clean_text($tag); - $tag || ThrowUserError('no_tag_to_edit'); - ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; - trick_taint($tag); - # Tags are all lowercase. - return lc($tag); + # Tags are all lowercase. + return lc($tag); } sub _check_target_milestone { - my ($invocant, $target, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; - $target = trim($target); - $target = $product->default_milestone if !defined $target; - my $object = Bugzilla::Milestone->check( - { product => $product, name => $target }); - if ($old_target && $object->name ne $old_target && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $target }); - } - return $object->name; + my ($invocant, $target, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; + $target = trim($target); + $target = $product->default_milestone if !defined $target; + my $object = Bugzilla::Milestone->check({product => $product, name => $target}); + if ($old_target && $object->name ne $old_target && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $target}); + } + return $object->name; } sub _check_time_field { - my ($invocant, $value, $field, $params) = @_; + 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; - } + # 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); + # check_time is in Bugzilla::Object. + return $invocant->check_time($value, $field, $params); } sub _check_version { - my ($invocant, $version, undef, $params) = @_; - $version = trim($version); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_vers = blessed($invocant) ? $invocant->version : ''; - my $object = Bugzilla::Version->check({ product => $product, name => $version }); - if ($object->name ne $old_vers && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $version }); - } - return $object->name; + my ($invocant, $version, undef, $params) = @_; + $version = trim($version); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_vers = blessed($invocant) ? $invocant->version : ''; + my $object = Bugzilla::Version->check({product => $product, name => $version}); + if ($object->name ne $old_vers && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $version}); + } + return $object->name; } # Custom Field Validators sub _check_field_is_mandatory { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - if (!blessed($field)) { - $field = Bugzilla::Field->new({ name => $field }); - return if !$field; - } + if (!blessed($field)) { + $field = Bugzilla::Field->new({name => $field}); + return if !$field; + } - return if !$field->is_mandatory; + return if !$field->is_mandatory; - return if !$field->is_visible_on_bug($params || $invocant); + return if !$field->is_visible_on_bug($params || $invocant); - return if ($field->type == FIELD_TYPE_SINGLE_SELECT - && scalar @{ get_legal_field_values($field->name) } == 1); + return + if ($field->type == FIELD_TYPE_SINGLE_SELECT + && scalar @{get_legal_field_values($field->name)} == 1); - return if ($field->type == FIELD_TYPE_MULTI_SELECT - && !scalar @{ get_legal_field_values($field->name) }); + return + if ($field->type == FIELD_TYPE_MULTI_SELECT + && !scalar @{get_legal_field_values($field->name)}); - if (ref($value) eq 'ARRAY') { - $value = join('', @$value); - } + if (ref($value) eq 'ARRAY') { + $value = join('', @$value); + } - $value = trim($value); - if (!defined($value) - or $value eq "" - or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) - or ($value =~ EMPTY_DATETIME_REGEX - and $field->type == FIELD_TYPE_DATETIME)) - { - ThrowUserError('required_field', { field => $field }); - } + $value = trim($value); + if ( !defined($value) + or $value eq "" + or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) + or ($value =~ EMPTY_DATETIME_REGEX and $field->type == FIELD_TYPE_DATETIME)) + { + ThrowUserError('required_field', {field => $field}); + } } sub _check_date_field { - my ($invocant, $date) = @_; - return $invocant->_check_datetime_field($date, undef, {date_only => 1}); + my ($invocant, $date) = @_; + return $invocant->_check_datetime_field($date, undef, {date_only => 1}); } -sub _check_datetime_field { - my ($invocant, $date_time, $field, $params) = @_; - - # 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 && $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' }); - } - return $date_time +sub _check_datetime_field { + my ($invocant, $date_time, $field, $params) = @_; + + # 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 && $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'}); + } + return $date_time; } sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; } sub _check_freetext_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; - if (length($text) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => $field, text => $text }); - } - return $text; + $text = (defined $text) ? trim($text) : ''; + if (length($text) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', {field => $field, text => $text}); + } + return $text; } sub _check_multi_select_field { - my ($invocant, $values, $field) = @_; + my ($invocant, $values, $field) = @_; - # 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)]; - } + # Allow users (mostly email_in.pl) to specify multi-selects as + # comma-separated values. + if (defined $values and !ref $values) { - return [] if !$values; - my @checked_values; - foreach my $value (@$values) { - push(@checked_values, $invocant->_check_select_field($value, $field)); - } - return \@checked_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)]; + } + + return [] if !$values; + my @checked_values; + foreach my $value (@$values) { + push(@checked_values, $invocant->_check_select_field($value, $field)); + } + return \@checked_values; } sub _check_select_field { - my ($invocant, $value, $field) = @_; - my $object = Bugzilla::Field::Choice->type($field)->check($value); - return $object->name; + my ($invocant, $value, $field) = @_; + my $object = Bugzilla::Field::Choice->type($field)->check($value); + return $object->name; } sub _check_bugid_field { - my ($invocant, $value, $field) = @_; - return undef if !$value; + my ($invocant, $value, $field) = @_; + return undef if !$value; - # check that the value is a valid, visible bug id - my $checked_id = $invocant->check($value, $field)->id; + # check that the value is a valid, visible bug id + my $checked_id = $invocant->check($value, $field)->id; - # check for loop (can't have a loop if this is a new bug) - if (ref $invocant) { - _check_relationship_loop($field, $invocant->bug_id, $checked_id); - } + # check for loop (can't have a loop if this is a new bug) + if (ref $invocant) { + _check_relationship_loop($field, $invocant->bug_id, $checked_id); + } - return $checked_id; + return $checked_id; } sub _check_integer_field { - my ($invocant, $value, $field) = @_; - $value = defined($value) ? trim($value) : ''; + my ($invocant, $value, $field) = @_; + $value = defined($value) ? trim($value) : ''; - # BMO - allow empty values - if ($value eq '') { - return undef; - } + # BMO - allow empty values + if ($value eq '') { + return undef; + } - my $orig_value = $value; - if (!detaint_signed($value)) { - ThrowUserError("number_not_integer", - {field => $field, num => $orig_value}); - } - elsif ($value > MAX_INT_32) { - ThrowUserError("number_too_large", - {field => $field, num => $orig_value, max_num => MAX_INT_32}); - } + my $orig_value = $value; + if (!detaint_signed($value)) { + ThrowUserError("number_not_integer", {field => $field, num => $orig_value}); + } + elsif ($value > MAX_INT_32) { + ThrowUserError("number_too_large", + {field => $field, num => $orig_value, max_num => MAX_INT_32}); + } - return $value; + return $value; } sub _check_relationship_loop { - # Generates a dependency tree for a given bug. Calls itself recursively - # to generate sub-trees for the bug's dependencies. - my ($field, $bug_id, $dep_id, $ids) = @_; - # Don't do anything if this bug doesn't have any dependencies. - return unless defined($dep_id); + # Generates a dependency tree for a given bug. Calls itself recursively + # to generate sub-trees for the bug's dependencies. + my ($field, $bug_id, $dep_id, $ids) = @_; - # Check whether we have seen this bug yet - $ids = {} unless defined $ids; - $ids->{$bug_id} = 1; - if ($ids->{$dep_id}) { - ThrowUserError("relationship_loop_single", { - 'bug_id' => $bug_id, - 'dep_id' => $dep_id, - 'field_name' => $field}); - } + # Don't do anything if this bug doesn't have any dependencies. + return unless defined($dep_id); - # Get this dependency's record from the database - my $dbh = Bugzilla->dbh; - my $next_dep_id = $dbh->selectrow_array( - "SELECT $field FROM bugs WHERE bug_id = ?", undef, $dep_id); + # Check whether we have seen this bug yet + $ids = {} unless defined $ids; + $ids->{$bug_id} = 1; + if ($ids->{$dep_id}) { + ThrowUserError("relationship_loop_single", + {'bug_id' => $bug_id, 'dep_id' => $dep_id, 'field_name' => $field}); + } + + # Get this dependency's record from the database + my $dbh = Bugzilla->dbh; + my $next_dep_id + = $dbh->selectrow_array("SELECT $field FROM bugs WHERE bug_id = ?", + undef, $dep_id); - _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); + _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); } ##################################################################### @@ -2521,31 +2581,32 @@ sub _check_relationship_loop { ##################################################################### sub fields { - my $class = shift; - - my @fields = - ( - # 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 - product component version rep_platform op_sys - bug_status resolution dup_id see_also - bug_file_loc status_whiteboard keywords - priority bug_severity target_milestone - dependson blocked everconfirmed - reporter assigned_to cc estimated_time - remaining_time actual_time deadline), - - # Conditional Fields - Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), - # Custom Fields - map { $_->name } Bugzilla->active_custom_fields - ); - Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} ); + my $class = shift; + + my @fields = ( + + # 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 + product component version rep_platform op_sys + bug_status resolution dup_id see_also + bug_file_loc status_whiteboard keywords + priority bug_severity target_milestone + dependson blocked everconfirmed + reporter assigned_to cc estimated_time + remaining_time actual_time deadline), - return @fields; + # Conditional Fields + Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), + + # Custom Fields + map { $_->name } Bugzilla->active_custom_fields + ); + Bugzilla::Hook::process('bug_fields', {'fields' => \@fields}); + + return @fields; } ##################################################################### @@ -2554,30 +2615,29 @@ sub fields { # 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 }); - } - $self->_check_field_is_mandatory($value, $field); + 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}); + } + $self->_check_field_is_mandatory($value, $field); } ################# @@ -2587,523 +2647,568 @@ sub _set_global_validator { # Note that if you are changing multiple bugs at once, you must pass # other_bugs to set_all in order for it to behave properly. sub set_all { - my $self = shift; - my ($input_params) = @_; - - # Clone the data as we are going to alter it, and this would affect - # subsequent bugs when calling set_all() again, as some fields would - # be modified or no longer defined. - my $params = {}; - %$params = %$input_params; - - # BMO - allow extensions to morph params - Bugzilla::Hook::process('bug_start_of_set_all', { bug => $self, params => $params }); - - # You cannot mark bugs as duplicate when changing several bugs at once - # (because currently there is no way to check for duplicate loops in that - # situation). You also cannot set the alias of several bugs at once. - if ($params->{other_bugs} and scalar @{ $params->{other_bugs} } > 1) { - ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; - ThrowUserError('multiple_alias_not_allowed') - if defined $params->{alias}; - } - - # For security purposes, and because lots of other checks depend on it, - # we set the product first before anything else. - my $product_changed; # Used only for strict_isolation checks. - if (exists $params->{'product'}) { - $product_changed = $self->_set_product($params->{'product'}, $params); - } - - # strict_isolation checks mean that we should set the groups - # immediately after changing the product. - $self->_add_remove($params, 'groups'); - - if (exists $params->{'dependson'} or exists $params->{'blocked'}) { - my %set_deps; - foreach my $name (qw(dependson blocked)) { - my @dep_ids = @{ $self->$name }; - # If only one of the two fields was passed in, then we need to - # retain the current value for the other one. - if (!exists $params->{$name}) { - $set_deps{$name} = \@dep_ids; - next; - } - - # Explicitly setting them to a particular value overrides - # add/remove. - if (exists $params->{$name}->{set}) { - $set_deps{$name} = $params->{$name}->{set}; - next; - } - - foreach my $add (@{ $params->{$name}->{add} || [] }) { - push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); - } - foreach my $remove (@{ $params->{$name}->{remove} || [] }) { - @dep_ids = grep($_ != $remove, @dep_ids); - } - $set_deps{$name} = \@dep_ids; - } - - $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); - } + my $self = shift; + my ($input_params) = @_; + + # Clone the data as we are going to alter it, and this would affect + # subsequent bugs when calling set_all() again, as some fields would + # be modified or no longer defined. + my $params = {}; + %$params = %$input_params; + + # BMO - allow extensions to morph params + Bugzilla::Hook::process('bug_start_of_set_all', + {bug => $self, params => $params}); + + # You cannot mark bugs as duplicate when changing several bugs at once + # (because currently there is no way to check for duplicate loops in that + # situation). You also cannot set the alias of several bugs at once. + if ($params->{other_bugs} and scalar @{$params->{other_bugs}} > 1) { + ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; + ThrowUserError('multiple_alias_not_allowed') if defined $params->{alias}; + } + + # For security purposes, and because lots of other checks depend on it, + # we set the product first before anything else. + my $product_changed; # Used only for strict_isolation checks. + if (exists $params->{'product'}) { + $product_changed = $self->_set_product($params->{'product'}, $params); + } + + # strict_isolation checks mean that we should set the groups + # immediately after changing the product. + $self->_add_remove($params, 'groups'); + + if (exists $params->{'dependson'} or exists $params->{'blocked'}) { + my %set_deps; + foreach my $name (qw(dependson blocked)) { + my @dep_ids = @{$self->$name}; + + # If only one of the two fields was passed in, then we need to + # retain the current value for the other one. + if (!exists $params->{$name}) { + $set_deps{$name} = \@dep_ids; + next; + } + + # Explicitly setting them to a particular value overrides + # add/remove. + if (exists $params->{$name}->{set}) { + $set_deps{$name} = $params->{$name}->{set}; + next; + } + + foreach my $add (@{$params->{$name}->{add} || []}) { + push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); + } + foreach my $remove (@{$params->{$name}->{remove} || []}) { + @dep_ids = grep($_ != $remove, @dep_ids); + } + $set_deps{$name} = \@dep_ids; + } + + $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); + } + + if (exists $params->{'keywords'}) { + + # Sorting makes the order "add, remove, set", just like for other + # fields. + foreach my $action (sort keys %{$params->{'keywords'}}) { + $self->modify_keywords($params->{'keywords'}->{$action}, $action); + } + } + + if (exists $params->{'comment'} or exists $params->{'work_time'}) { + + # Add a comment as needed to each bug. This is done early because + # there are lots of things that want to check if we added a comment. + $self->add_comment( + $params->{'comment'}->{'body'}, + { + isprivate => $params->{'comment'}->{'is_private'}, + work_time => $params->{'work_time'} + } + ); + } - if (exists $params->{'keywords'}) { - # Sorting makes the order "add, remove, set", just like for other - # fields. - foreach my $action (sort keys %{ $params->{'keywords'} }) { - $self->modify_keywords($params->{'keywords'}->{$action}, $action); - } - } + if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) { + $self->{added_comment_tags} + = ref $params->{comment_tags} + ? $params->{comment_tags} + : [$params->{comment_tags}]; + } - if (exists $params->{'comment'} or exists $params->{'work_time'}) { - # Add a comment as needed to each bug. This is done early because - # there are lots of things that want to check if we added a comment. - $self->add_comment($params->{'comment'}->{'body'}, - { isprivate => $params->{'comment'}->{'is_private'}, - work_time => $params->{'work_time'} }); - } + my %normal_set_all; + foreach my $name (keys %$params) { - if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) { - $self->{added_comment_tags} = ref $params->{comment_tags} - ? $params->{comment_tags} - : [ $params->{comment_tags} ]; + # These are handled separately below. + if ($self->can("set_$name")) { + $normal_set_all{$name} = $params->{$name}; } + } + $self->SUPER::set_all(\%normal_set_all); - my %normal_set_all; - foreach my $name (keys %$params) { - # These are handled separately below. - if ($self->can("set_$name")) { - $normal_set_all{$name} = $params->{$name}; - } - } - $self->SUPER::set_all(\%normal_set_all); - - $self->reset_assigned_to if $params->{'reset_assigned_to'}; - $self->reset_qa_contact if $params->{'reset_qa_contact'}; + $self->reset_assigned_to if $params->{'reset_assigned_to'}; + $self->reset_qa_contact if $params->{'reset_qa_contact'}; - $self->_add_remove($params, 'see_also'); + $self->_add_remove($params, 'see_also'); - # And set 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}) { - $self->set_custom_field($field, $params->{$fname}); - } + # And set 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}) { + $self->set_custom_field($field, $params->{$fname}); } + } - $self->_add_remove($params, 'cc'); + $self->_add_remove($params, 'cc'); - # Theoretically you could move a product without ever specifying - # a new assignee or qa_contact, or adding/removing any CCs. So, - # 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. - $self->_check_strict_isolation() if $product_changed; + # Theoretically you could move a product without ever specifying + # a new assignee or qa_contact, or adding/removing any CCs. So, + # 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. + $self->_check_strict_isolation() if $product_changed; } # Helper for set_all that helps with fields that have an "add/remove" # pattern instead of a "set_" pattern. sub _add_remove { - my ($self, $params, $name) = @_; - my @add = @{ $params->{$name}->{add} || [] }; - my @remove = @{ $params->{$name}->{remove} || [] }; - $name =~ s/s$//; - my $add_method = "add_$name"; - my $remove_method = "remove_$name"; - $self->$add_method($_) foreach @add; - $self->$remove_method($_) foreach @remove; + my ($self, $params, $name) = @_; + my @add = @{$params->{$name}->{add} || []}; + my @remove = @{$params->{$name}->{remove} || []}; + $name =~ s/s$//; + my $add_method = "add_$name"; + my $remove_method = "remove_$name"; + $self->$add_method($_) foreach @add; + $self->$remove_method($_) foreach @remove; } 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'}; + 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); + my $self = shift; + 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) = @_; + my ($self, $comment_id, $isprivate) = @_; - # We also allow people to pass in a hash of comment ids to update. - if (ref $comment_id) { - while (my ($id, $is) = each %$comment_id) { - $self->set_comment_is_private($id, $is); - } - return; - } - - my ($comment) = grep($comment_id == $_->id, @{ $self->comments }); - ThrowUserError('comment_invalid_isprivate', { id => $comment_id }) - if !$comment; - - $isprivate = $isprivate ? 1 : 0; - if ($isprivate != $comment->is_private) { - ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider; - $self->{comment_isprivate} ||= []; - $comment->set_is_private($isprivate); - push @{$self->{comment_isprivate}}, $comment; - } -} -sub set_component { - my ($self, $name) = @_; - my $old_comp = $self->component_obj; - my $component = $self->_check_component($name); - if ($old_comp->id != $component->id) { - $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); - } + # We also allow people to pass in a hash of comment ids to update. + if (ref $comment_id) { + while (my ($id, $is) = each %$comment_id) { + $self->set_comment_is_private($id, $is); } + return; + } + + my ($comment) = grep($comment_id == $_->id, @{$self->comments}); + ThrowUserError('comment_invalid_isprivate', {id => $comment_id}) if !$comment; + + $isprivate = $isprivate ? 1 : 0; + if ($isprivate != $comment->is_private) { + ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider; + $self->{comment_isprivate} ||= []; + $comment->set_is_private($isprivate); + push @{$self->{comment_isprivate}}, $comment; + } } -sub set_custom_field { - my ($self, $field, $value) = @_; - if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { - $value = $value->[0]; +sub set_component { + my ($self, $name) = @_; + my $old_comp = $self->component_obj; + my $component = $self->_check_component($name); + if ($old_comp->id != $component->id) { + $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); } - ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom; - $self->set($field->name, $value); + } +} + +sub set_custom_field { + my ($self, $field, $value) = @_; + + 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; - delete $self->{depends_on_obj}; - delete $self->{blocks_obj}; + 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; + delete $self->{depends_on_obj}; + delete $self->{blocks_obj}; } 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; - - # Make sure that we have the DUPLICATE resolution. This is needed - # if somebody calls set_dup_id without calling set_bug_status or - # set_resolution. - if ($self->resolution ne 'DUPLICATE') { - # Even if the current status is VERIFIED, we change it back to - # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, - # because that's the same thing the UI does when you click on the - # "Mark as Duplicate" link. If people really want to retain their - # current status, they can use set_bug_status and set the DUPLICATE - # resolution before getting here. - $self->set_bug_status( - Bugzilla->params->{'duplicate_or_move_bug_status'}, - { resolution => 'DUPLICATE' }); - } - - # 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; - - # 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.) - if ($self->{added_comments}) { - 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 }); - } + 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; + + # Make sure that we have the DUPLICATE resolution. This is needed + # if somebody calls set_dup_id without calling set_bug_status or + # set_resolution. + if ($self->resolution ne 'DUPLICATE') { + + # Even if the current status is VERIFIED, we change it back to + # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, + # because that's the same thing the UI does when you click on the + # "Mark as Duplicate" link. If people really want to retain their + # current status, they can use set_bug_status and set the DUPLICATE + # resolution before getting here. + $self->set_bug_status(Bugzilla->params->{'duplicate_or_move_bug_status'}, + {resolution => 'DUPLICATE'}); + } + + # 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; + + # 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.) + if ($self->{added_comments}) { + 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]); } +sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } + sub set_flags { - my ($self, $flags, $new_flags) = @_; - Bugzilla::Hook::process('bug_set_flags', { bug => $self, flags => $flags, new_flags => $new_flags }); - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + my ($self, $flags, $new_flags) = @_; + Bugzilla::Hook::process('bug_set_flags', + {bug => $self, flags => $flags, new_flags => $new_flags}); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } -sub set_op_sys { $_[0]->set('op_sys', $_[1]); } -sub set_platform { $_[0]->set('rep_platform', $_[1]); } -sub set_priority { $_[0]->set('priority', $_[1]); } +sub set_op_sys { $_[0]->set('op_sys', $_[1]); } +sub set_platform { $_[0]->set('rep_platform', $_[1]); } +sub set_priority { $_[0]->set('priority', $_[1]); } + # For security reasons, you have to use set_all to change the product. # See the strict_isolation check in set_all for an explanation. sub _set_product { - my ($self, $name, $params) = @_; - my $old_product = $self->product_obj; - my $product = $self->_check_product($name); - - my $product_changed = 0; - if ($old_product->id != $product->id) { - $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}; - $product_changed = 1; - } - - $params ||= {}; - # We delete these so that they're not set again later in set_all. - my $comp_name = delete $params->{component} || $self->component; - my $vers_name = delete $params->{version} || $self->version; - my $tm_name = delete $params->{target_milestone}; - # This way, if usetargetmilestone is off and we've changed products, - # set_target_milestone will reset our target_milestone to - # $product->default_milestone. But if we haven't changed products, - # we don't reset anything. - if (!defined $tm_name - && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) - { - $tm_name = $self->target_milestone; - } - - if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Try to set each value with the new product. - # Have to set error_mode because Throw*Error calls exit() otherwise. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $component_ok = eval { $self->set_component($comp_name); 1; }; - my $version_ok = eval { $self->set_version($vers_name); 1; }; - my $milestone_ok = 1; - # Reporters can move bugs between products but not set the TM. - if ($self->check_can_change_field('target_milestone', 0, 1)) { - $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; - } - else { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - # If there were any errors thrown, make sure we don't mess up any - # other part of Bugzilla that checks $@. - undef $@; - Bugzilla->error_mode($old_error_mode); - - my $invalid_groups; - my @idlist = ($self->id); - push(@idlist, map { $_->id } @{ $params->{other_bugs} }) - if $params->{other_bugs}; - @idlist = uniq @idlist; - - my $verified = $params->{product_change_confirmed}; - - # BMO - if everything is ok then we can skip the verfication page when using bug_modal - if (Bugzilla->input_params->{format} // '' eq 'modal' - && !$verified - && $component_ok - && $version_ok - && $milestone_ok - ) { - $invalid_groups = $self->get_invalid_groups({ bug_ids => \@idlist, product => $product }); - my $has_invalid_group = 0; - foreach my $group (@$invalid_groups) { - if (any { $_ eq $group->name } @{ $params->{groups}->{add} }) { - $has_invalid_group = 1; - last; - } - } - $verified = - # always check for invalid groups - !$has_invalid_group - # never skip verification when changing multiple bugs - && scalar(@idlist) == 1 - # ensure the user has seen the group ui for private bugs - && (!@{ $self->groups_in } || Bugzilla->input_params->{group_verified}); - } + my ($self, $name, $params) = @_; + my $old_product = $self->product_obj; + my $product = $self->_check_product($name); - my %vars; - if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { - $vars{defaults} = { - # Note that because of the eval { set } above, these are - # already set correctly if they're valid, otherwise they're - # set to some invalid value which the template will ignore. - component => $self->component, - version => $self->version, - milestone => $milestone_ok ? $self->target_milestone - : $product->default_milestone - }; - $vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})]; - $vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})]; - $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; - } + my $product_changed = 0; + if ($old_product->id != $product->id) { + $self->{product_id} = $product->id; + $self->{product} = $product->name; + $self->{product_obj} = $product; - if (!$verified) { - $vars{verify_bug_groups} = 1; - $vars{old_groups} = $invalid_groups || $self->get_invalid_groups({ bug_ids => \@idlist, product => $product }); - } + # For update() + $self->{_old_product_name} = $old_product->name; - if (%vars) { - $vars{product} = $product; - $vars{bug} = $self; - require Bugzilla::Error::Template; - die Bugzilla::Error::Template->new( - file => "bug/process/verify-new-product.html.tmpl", - vars => \%vars - ); - } + # Delete fields that depend upon the old Product value. + delete $self->{choices}; + $product_changed = 1; + } + + $params ||= {}; + + # We delete these so that they're not set again later in set_all. + my $comp_name = delete $params->{component} || $self->component; + my $vers_name = delete $params->{version} || $self->version; + my $tm_name = delete $params->{target_milestone}; + + # This way, if usetargetmilestone is off and we've changed products, + # set_target_milestone will reset our target_milestone to + # $product->default_milestone. But if we haven't changed products, + # we don't reset anything. + if (!defined $tm_name + && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) + { + $tm_name = $self->target_milestone; + } + + if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Try to set each value with the new product. + # Have to set error_mode because Throw*Error calls exit() otherwise. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $component_ok = eval { $self->set_component($comp_name); 1; }; + my $version_ok = eval { $self->set_version($vers_name); 1; }; + my $milestone_ok = 1; + + # Reporters can move bugs between products but not set the TM. + if ($self->check_can_change_field('target_milestone', 0, 1)) { + $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; } else { - # When we're not in the browser (or we didn't change the product), we - # just die if any of these are invalid. - $self->set_component($comp_name); - $self->set_version($vers_name); - if ($product_changed - and !$self->check_can_change_field('target_milestone', 0, 1)) - { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - else { - $self->set_target_milestone($tm_name); - } + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; } - if ($product_changed) { - # Remove groups that can't be set in the new product. - # We copy this array because the original array is modified while we're - # working, and that confuses "foreach". - my @current_groups = @{$self->groups_in}; - foreach my $group (@current_groups) { - if (!$product->group_is_valid($group)) { - $self->remove_group($group); - } - } + # If there were any errors thrown, make sure we don't mess up any + # other part of Bugzilla that checks $@. + undef $@; + Bugzilla->error_mode($old_error_mode); + + my $invalid_groups; + my @idlist = ($self->id); + push(@idlist, map { $_->id } @{$params->{other_bugs}}) if $params->{other_bugs}; + @idlist = uniq @idlist; + + my $verified = $params->{product_change_confirmed}; - # Make sure the bug is in all the mandatory groups for the new product. - foreach my $group (@{$product->groups_mandatory}) { - $self->add_group($group); +# BMO - if everything is ok then we can skip the verfication page when using bug_modal + if (Bugzilla->input_params->{format} + // '' eq 'modal' && !$verified && $component_ok && $version_ok && $milestone_ok) + { + $invalid_groups + = $self->get_invalid_groups({bug_ids => \@idlist, product => $product}); + my $has_invalid_group = 0; + foreach my $group (@$invalid_groups) { + if (any { $_ eq $group->name } @{$params->{groups}->{add}}) { + $has_invalid_group = 1; + last; } + } + $verified = + + # always check for invalid groups + !$has_invalid_group + + # never skip verification when changing multiple bugs + && scalar(@idlist) == 1 + + # ensure the user has seen the group ui for private bugs + && (!@{$self->groups_in} || Bugzilla->input_params->{group_verified}); + } + + my %vars; + if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { + $vars{defaults} = { + + # Note that because of the eval { set } above, these are + # already set correctly if they're valid, otherwise they're + # set to some invalid value which the template will ignore. + component => $self->component, + version => $self->version, + milestone => $milestone_ok + ? $self->target_milestone + : $product->default_milestone + }; + $vars{components} + = [map { $_->name } grep($_->is_active, @{$product->components})]; + $vars{milestones} + = [map { $_->name } grep($_->is_active, @{$product->milestones})]; + $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; + } + + if (!$verified) { + $vars{verify_bug_groups} = 1; + $vars{old_groups} = $invalid_groups + || $self->get_invalid_groups({bug_ids => \@idlist, product => $product}); + } + + if (%vars) { + $vars{product} = $product; + $vars{bug} = $self; + require Bugzilla::Error::Template; + die Bugzilla::Error::Template->new( + file => "bug/process/verify-new-product.html.tmpl", + vars => \%vars + ); + } + } + else { + # When we're not in the browser (or we didn't change the product), we + # just die if any of these are invalid. + $self->set_component($comp_name); + $self->set_version($vers_name); + if ($product_changed + and !$self->check_can_change_field('target_milestone', 0, 1)) + { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; + } + else { + $self->set_target_milestone($tm_name); + } + } + + if ($product_changed) { + + # Remove groups that can't be set in the new product. + # We copy this array because the original array is modified while we're + # working, and that confuses "foreach". + my @current_groups = @{$self->groups_in}; + foreach my $group (@current_groups) { + if (!$product->group_is_valid($group)) { + $self->remove_group($group); + } + } + + # Make sure the bug is in all the mandatory groups for the new product. + foreach my $group (@{$product->groups_mandatory}) { + $self->add_group($group); } + } - return $product_changed; + return $product_changed; } 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'}; + 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); + 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. sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; } sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); } + sub set_resolution { - my ($self, $value, $params) = @_; + my ($self, $value, $params) = @_; - my $old_res = $self->resolution; - $self->set('resolution', $value); - delete $self->{choices}; - my $new_res = $self->resolution; + my $old_res = $self->resolution; + $self->set('resolution', $value); + delete $self->{choices}; + my $new_res = $self->resolution; - if ($new_res ne $old_res) { - # 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(); - } + if ($new_res ne $old_res) { + + # Clear the dup_id if we're leaving the dup resolution. + if ($old_res eq 'DUPLICATE') { + $self->_clear_dup_id(); } - # 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 (my $dup_id = $params->{dup_id}) { - $self->set_dup_id($dup_id); - } - elsif (!$self->dup_id) { - ThrowUserError('dupe_id_required'); - } + # Duplicates should have no remaining time left. + elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } - # This method has handled dup_id, so set_all doesn't have to worry - # about it now. - delete $params->{dup_id}; + # 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 (my $dup_id = $params->{dup_id}) { + $self->set_dup_id($dup_id); + } + elsif (!$self->dup_id) { + ThrowUserError('dupe_id_required'); + } + } + + # This method has handled dup_id, so set_all doesn't have to worry + # about it now. + delete $params->{dup_id}; } + sub clear_resolution { - my $self = shift; - if (!$self->status->is_open) { - ThrowUserError('resolution_cant_clear', { bug_id => $self->id }); - } - $self->{'resolution'} = ''; - $self->_clear_dup_id; + my $self = shift; + if (!$self->status->is_open) { + ThrowUserError('resolution_cant_clear', {bug_id => $self->id}); + } + $self->{'resolution'} = ''; + $self->_clear_dup_id; } -sub set_severity { $_[0]->set('bug_severity', $_[1]); } +sub set_severity { $_[0]->set('bug_severity', $_[1]); } + sub set_bug_status { - my ($self, $status, $params) = @_; - my $old_status = $self->status; - $self->set('bug_status', $status); - delete $self->{'status'}; - delete $self->{'statuses_available'}; - delete $self->{'choices'}; - my $new_status = $self->status; - - if ($new_status->is_open) { - # Check for the everconfirmed transition - $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); - $self->clear_resolution(); - # Calling clear_resolution handled the "resolution" and "dup_id" - # setting, so set_all doesn't have to worry about them. - delete $params->{resolution}; - delete $params->{dup_id}; + my ($self, $status, $params) = @_; + my $old_status = $self->status; + $self->set('bug_status', $status); + delete $self->{'status'}; + delete $self->{'statuses_available'}; + delete $self->{'choices'}; + my $new_status = $self->status; + + if ($new_status->is_open) { + + # Check for the everconfirmed transition + $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); + $self->clear_resolution(); + + # Calling clear_resolution handled the "resolution" and "dup_id" + # setting, so set_all doesn't have to worry about them. + delete $params->{resolution}; + delete $params->{dup_id}; + } + else { + # We do this here so that we can make sure closed statuses have + # resolutions. + 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}; } - else { - # We do this here so that we can make sure closed statuses have - # resolutions. - 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. - if ($new_status->id != $old_status->id && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + # 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]); } @@ -3120,355 +3225,374 @@ sub set_version { $_[0]->set('version', $_[1]); } # 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); + 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 $currentUser = Bugzilla->user; - if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { - ThrowUserError('cc_remove_denied'); - } - my $cc_users = $self->cc_users; - @$cc_users = grep { $_->id != $user->id } @$cc_users; + my ($self, $user_or_name) = @_; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + my $currentUser = Bugzilla->user; + if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { + ThrowUserError('cc_remove_denied'); + } + 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) = @_; + my ($self, $comment, $params) = @_; - $params ||= {}; + $params ||= {}; - # Fill out info that doesn't change and callers may not pass in - $params->{'bug_id'} = $self; - $params->{'thetext'} = defined($comment) ? $comment : ''; + # Fill out info that doesn't change and callers may not pass in + $params->{'bug_id'} = $self; + $params->{'thetext'} = defined($comment) ? $comment : ''; - # Validate all the entered data - Bugzilla::Comment->check_required_create_fields($params); - $params = Bugzilla::Comment->run_create_validators($params); + # Validate all the entered data + Bugzilla::Comment->check_required_create_fields($params); + $params = Bugzilla::Comment->run_create_validators($params); - # This makes it so we won't create new comments when there is nothing - # to add - if ($params->{'thetext'} eq '' - && !($params->{type} || abs($params->{work_time} || 0))) - { - return; - } + # This makes it so we won't create new comments when there is nothing + # to add + if ($params->{'thetext'} eq '' + && !($params->{type} || abs($params->{work_time} || 0))) + { + return; + } - # If the user has explicitly set remaining_time, this will be overridden - # later in set_all. But if they haven't, this keeps remaining_time - # up-to-date. - if ($params->{work_time}) { - $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); - } + # If the user has explicitly set remaining_time, this will be overridden + # later in set_all. But if they haven't, this keeps remaining_time + # up-to-date. + if ($params->{work_time}) { + $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); + } - $self->{added_comments} ||= []; + $self->{added_comments} ||= []; - push(@{$self->{added_comments}}, $params); + push(@{$self->{added_comments}}, $params); } # 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) = @_; - - $action ||= 'set'; - if (!grep($action eq $_, qw(add remove set))) { - $action = 'set'; - } - - $keywords = $self->_check_keywords($keywords); - - my (@result, $any_changes); - if ($action eq 'set') { - @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; + my ($self, $keywords, $action) = @_; + + $action ||= 'set'; + if (!grep($action eq $_, qw(add remove set))) { + $action = 'set'; + } + + $keywords = $self->_check_keywords($keywords); + + my (@result, $any_changes); + if ($action eq 'set') { + @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 { - # 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; + delete $keys{$_->id} foreach @$keywords; } - # Make sure we retain the sort order. - @result = sort {lc($a->name) cmp lc($b->name)} @result; + @result = values %keys; + $any_changes = scalar @$keywords; + } - 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 }); - } + # Make sure we retain the sort order. + @result = sort { lc($a->name) cmp lc($b->name) } @result; - $self->{'keyword_objects'} = \@result; + 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; } sub add_group { - my ($self, $group) = @_; - - # If the user enters "FoO" but the DB has "Foo", $group->name would - # return "Foo" and thus revealing the existence of the group name. - # So we have to store and pass the name as entered by the user to - # the error message, if we have it. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'add' }; - - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - - # If the bug is already in this group, then there is nothing to do. - return if $self->in_group($group); - - # BMO : allow bugs to be always placed into some groups by the bug's - # reporter, or by users with editbugs - my $user = Bugzilla->user; - if (!$self->product_obj->group_always_settable($group) - || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs'))) - { - # 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 (!$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); - } - } - } - - my $current_groups = $self->groups_in; - push(@$current_groups, $group); + my ($self, $group) = @_; + + # If the user enters "FoO" but the DB has "Foo", $group->name would + # return "Foo" and thus revealing the existence of the group name. + # So we have to store and pass the name as entered by the user to + # the error message, if we have it. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'add' + }; + + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + + # If the bug is already in this group, then there is nothing to do. + return if $self->in_group($group); + + # BMO : allow bugs to be always placed into some groups by the bug's + # reporter, or by users with editbugs + my $user = Bugzilla->user; + if (!$self->product_obj->group_always_settable($group) + || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs'))) + { + # 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 (!$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); + } + } + } + + my $current_groups = $self->groups_in; + push(@$current_groups, $group); } sub remove_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # See add_group() for the reason why we store the user input. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'remove' }; + # See add_group() for the reason why we store the user input. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'remove' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug isn't in this group, then either the name is misspelled, - # or the group really doesn't exist. Let the user know about this problem. - $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); + # If the bug isn't in this group, then either the name is misspelled, + # or the group really doesn't exist. Let the user know about this problem. + $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); - # Check if this is a valid group for this product. You can *always* - # remove a group that is not valid for this product (set_product does this). - # This particularly happens when we're moving a bug to a new product. - # You still have to be a member of an inactive group to remove it. - if ($self->product_obj->group_is_valid($group)) { - my $controls = $self->product_obj->group_controls->{$group->id}; + # Check if this is a valid group for this product. You can *always* + # remove a group that is not valid for this product (set_product does this). + # This particularly happens when we're moving a bug to a new product. + # You still have to be a member of an inactive group to remove it. + if ($self->product_obj->group_is_valid($group)) { + my $controls = $self->product_obj->group_controls->{$group->id}; - # Nobody can ever remove a Mandatory group, unless it became inactive. - if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { - ThrowUserError('group_invalid_removal', $args); - } + # Nobody can ever remove a Mandatory group, unless it became inactive. + if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { + ThrowUserError('group_invalid_removal', $args); + } - # 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_invalid_removal', $args); - } - } + # 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_invalid_removal', $args); + } } + } - my $current_groups = $self->groups_in; - @$current_groups = grep { $_->id != $group->id } @$current_groups; + my $current_groups = $self->groups_in; + @$current_groups = grep { $_->id != $group->id } @$current_groups; } sub add_see_also { - my ($self, $input, $skip_recursion) = @_; - - # This is needed by xt/search.t. - $input = $input->name if blessed($input); - - $input = trim($input); - return if !$input; - - my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - - my $params = { value => $uri, bug_id => $self, class => $class }; - $class->check_required_create_fields($params); + my ($self, $input, $skip_recursion) = @_; - my $field_values = $class->run_create_validators($params); - my $value = $field_values->{value}->as_string; - trick_taint($value); - $field_values->{value} = $value; + # This is needed by xt/search.t. + $input = $input->name if blessed($input); - # 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($_->name) eq lc($value) } @{ $self->see_also }) { - my $privs; - my $can = $self->check_can_change_field('see_also', '', $value, \$privs); - if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - newvalue => $value, - privs => $privs }); - } - # If this is a link to a local bug then save the - # ref bug id for sending changes email. - my $ref_bug = delete $field_values->{ref_bug}; - if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local') - and !$skip_recursion - and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) - { - $ref_bug->add_see_also($self->id, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } - push @{ $self->{see_also} }, bless ($field_values, $class); - } -} + $input = trim($input); + return if !$input; -sub remove_see_also { - my ($self, $url, $skip_recursion) = @_; - my $see_also = $self->see_also; + my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - # This is needed by xt/search.t. - $url = $url->name if blessed($url); + my $params = {value => $uri, bug_id => $self, class => $class}; + $class->check_required_create_fields($params); - my ($removed_bug_url, $new_see_also) = - part { lc($_->name) ne lc($url) } @$see_also; + my $field_values = $class->run_create_validators($params); + my $value = $field_values->{value}->as_string; + trick_taint($value); + $field_values->{value} = $value; + # 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($_->name) eq lc($value) } @{$self->see_also}) { my $privs; - my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, \$privs); + my $can = $self->check_can_change_field('see_also', '', $value, \$privs); if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - oldvalue => $url, - privs => $privs }); + ThrowUserError('illegal_change', + {field => 'see_also', newvalue => $value, privs => $privs}); } - # Since we remove also the url from the referenced bug, - # we need to notify changes for that bug too. - $removed_bug_url = $removed_bug_url->[0]; - if (!$skip_recursion and $removed_bug_url - and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') - and $removed_bug_url->ref_bug_url) + # If this is a link to a local bug then save the + # ref bug id for sending changes email. + my $ref_bug = delete $field_values->{ref_bug}; + if ( $class->isa('Bugzilla::BugUrl::Bugzilla::Local') + and !$skip_recursion + and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) { - my $ref_bug - = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + $ref_bug->add_see_also($self->id, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; + } + push @{$self->{see_also}}, bless($field_values, $class); + } +} - if (Bugzilla->user->can_edit_product($ref_bug->product_id) - and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) - { - my $self_url = $removed_bug_url->local_uri($self->id); - $ref_bug->remove_see_also($self_url, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } +sub remove_see_also { + my ($self, $url, $skip_recursion) = @_; + my $see_also = $self->see_also; + + # This is needed by xt/search.t. + $url = $url->name if blessed($url); + + my ($removed_bug_url, $new_see_also) + = part { lc($_->name) 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}); + } + + # Since we remove also the url from the referenced bug, + # we need to notify changes for that bug too. + $removed_bug_url = $removed_bug_url->[0]; + if ( !$skip_recursion + and $removed_bug_url + and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') + and $removed_bug_url->ref_bug_url) + { + my $ref_bug = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + + if (Bugzilla->user->can_edit_product($ref_bug->product_id) + and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) + { + my $self_url = $removed_bug_url->local_uri($self->id); + $ref_bug->remove_see_also($self_url, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; } + } - $self->{see_also} = $new_see_also || []; + $self->{see_also} = $new_see_also || []; } sub add_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); + + my $tag_id = $user->tags->{$tag}->{id}; + + # If this tag doesn't exist for this user yet, create it. + if (!$tag_id) { + $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', + undef, ($user->id, $tag)); + + $tag_id = $dbh->selectrow_array( + 'SELECT id FROM tag + WHERE name = ? AND user_id = ?', undef, + ($tag, $user->id) + ); - my $tag_id = $user->tags->{$tag}->{id}; - # If this tag doesn't exist for this user yet, create it. - if (!$tag_id) { - $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', - undef, ($user->id, $tag)); + # The list has changed. + delete $user->{tags}; + } - $tag_id = $dbh->selectrow_array('SELECT id FROM tag - WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); - # The list has changed. - delete $user->{tags}; - } - # Do nothing if this tag is already set for this bug. - return if grep { $_ eq $tag } @{$self->tags}; + # Do nothing if this tag is already set for this bug. + return if grep { $_ eq $tag } @{$self->tags}; - # Increment the counter. Do it before the SQL call below, - # to not count the tag twice. - $user->tags->{$tag}->{bug_count}++; + # Increment the counter. Do it before the SQL call below, + # to not count the tag twice. + $user->tags->{$tag}->{bug_count}++; - $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', - undef, ($self->id, $tag_id)); + $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', + undef, ($self->id, $tag_id)); - push(@{$self->{tags}}, $tag); + push(@{$self->{tags}}, $tag); } sub remove_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); - my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - # Do nothing if the user doesn't use this tag, or didn't set it for this bug. - return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); + my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', - undef, ($self->id, $tag_id)); + # Do nothing if the user doesn't use this tag, or didn't set it for this bug. + return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); - $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; + $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', + undef, ($self->id, $tag_id)); - # Decrement the counter, and delete the tag if no bugs are using it anymore. - if (!--$user->tags->{$tag}->{bug_count}) { - $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); + $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; - # The list has changed. - delete $user->{tags}; - } + # Decrement the counter, and delete the tag if no bugs are using it anymore. + if (!--$user->tags->{$tag}->{bug_count}) { + $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', + undef, ($tag, $user->id)); + + # The list has changed. + delete $user->{tags}; + } } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # This method doesn't support several users using the same bug object. - if (!exists $self->{tags}) { - $self->{tags} = $dbh->selectcol_arrayref( - 'SELECT name FROM bug_tag + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # This method doesn't support several users using the same bug object. + if (!exists $self->{tags}) { + $self->{tags} = $dbh->selectcol_arrayref( + 'SELECT name FROM bug_tag INNER JOIN tag ON tag.id = bug_tag.tag_id - WHERE bug_id = ? AND user_id = ?', - undef, ($self->id, $user->id)); - } - return $self->{tags}; + WHERE bug_id = ? AND user_id = ?', undef, ($self->id, $user->id) + ); + } + return $self->{tags}; } ##################################################################### @@ -3478,31 +3602,31 @@ sub tags { # These are accessors that don't need to access the database. # Keep them in alphabetical order. -sub alias { return $_[0]->{alias} } -sub bug_file_loc { return $_[0]->{bug_file_loc} } -sub bug_id { return $_[0]->{bug_id} } -sub bug_severity { return $_[0]->{bug_severity} } -sub bug_status { return $_[0]->{bug_status} } -sub cclist_accessible { return $_[0]->{cclist_accessible} } -sub component_id { return $_[0]->{component_id} } -sub creation_ts { return $_[0]->{creation_ts} } -sub estimated_time { return $_[0]->{estimated_time} } -sub deadline { return $_[0]->{deadline} } -sub delta_ts { return $_[0]->{delta_ts} } -sub error { return $_[0]->{error} } -sub everconfirmed { return $_[0]->{everconfirmed} } -sub lastdiffed { return $_[0]->{lastdiffed} } -sub op_sys { return $_[0]->{op_sys} } -sub priority { return $_[0]->{priority} } -sub product_id { return $_[0]->{product_id} } -sub remaining_time { return $_[0]->{remaining_time} } +sub alias { return $_[0]->{alias} } +sub bug_file_loc { return $_[0]->{bug_file_loc} } +sub bug_id { return $_[0]->{bug_id} } +sub bug_severity { return $_[0]->{bug_severity} } +sub bug_status { return $_[0]->{bug_status} } +sub cclist_accessible { return $_[0]->{cclist_accessible} } +sub component_id { return $_[0]->{component_id} } +sub creation_ts { return $_[0]->{creation_ts} } +sub estimated_time { return $_[0]->{estimated_time} } +sub deadline { return $_[0]->{deadline} } +sub delta_ts { return $_[0]->{delta_ts} } +sub error { return $_[0]->{error} } +sub everconfirmed { return $_[0]->{everconfirmed} } +sub lastdiffed { return $_[0]->{lastdiffed} } +sub op_sys { return $_[0]->{op_sys} } +sub priority { return $_[0]->{priority} } +sub product_id { return $_[0]->{product_id} } +sub remaining_time { return $_[0]->{remaining_time} } sub reporter_accessible { return $_[0]->{reporter_accessible} } -sub rep_platform { return $_[0]->{rep_platform} } -sub resolution { return $_[0]->{resolution} } -sub short_desc { return $_[0]->{short_desc} } -sub status_whiteboard { return $_[0]->{status_whiteboard} } -sub target_milestone { return $_[0]->{target_milestone} } -sub version { return $_[0]->{version} } +sub rep_platform { return $_[0]->{rep_platform} } +sub resolution { return $_[0]->{resolution} } +sub short_desc { return $_[0]->{short_desc} } +sub status_whiteboard { return $_[0]->{status_whiteboard} } +sub target_milestone { return $_[0]->{target_milestone} } +sub version { return $_[0]->{version} } ##################################################################### # Complex Accessors @@ -3521,668 +3645,691 @@ sub version { return $_[0]->{version} } # security holes. sub dup_id { - my ($self) = @_; - return $self->{'dup_id'} if exists $self->{'dup_id'}; + my ($self) = @_; + return $self->{'dup_id'} if exists $self->{'dup_id'}; - $self->{'dup_id'} = undef; - return if $self->{'error'}; + $self->{'dup_id'} = undef; + return if $self->{'error'}; - if ($self->{'resolution'} eq 'DUPLICATE') { - my $dbh = Bugzilla->dbh; - $self->{'dup_id'} = - $dbh->selectrow_array(q{SELECT dupe_of + if ($self->{'resolution'} eq 'DUPLICATE') { + my $dbh = Bugzilla->dbh; + $self->{'dup_id'} = $dbh->selectrow_array( + q{SELECT dupe_of FROM duplicates - WHERE dupe = ?}, - undef, - $self->{'bug_id'}); - } - return $self->{'dup_id'}; + WHERE dupe = ?}, undef, $self->{'bug_id'} + ); + } + return $self->{'dup_id'}; } sub _resolve_ultimate_dup_id { - my ($bug_id, $dupe_of, $loops_are_an_error) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); - - my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); - my $last_dup = $bug_id; - - my %dupes; - while ($this_dup) { - if ($this_dup == $bug_id) { - if ($loops_are_an_error) { - ThrowUserError('dupe_loop_detected', { bug_id => $bug_id, - dupe_of => $dupe_of }); - } - else { - return $last_dup; - } - } - # 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. - return $last_dup if exists $dupes{$this_dup}; - $dupes{$this_dup} = 1; - $last_dup = $this_dup; - $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + my ($bug_id, $dupe_of, $loops_are_an_error) = @_; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); + + my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); + my $last_dup = $bug_id; + + my %dupes; + while ($this_dup) { + if ($this_dup == $bug_id) { + if ($loops_are_an_error) { + ThrowUserError('dupe_loop_detected', {bug_id => $bug_id, dupe_of => $dupe_of}); + } + else { + return $last_dup; + } } - return $last_dup; + # 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. + return $last_dup if exists $dupes{$this_dup}; + $dupes{$this_dup} = 1; + $last_dup = $this_dup; + $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + } + + return $last_dup; } sub actual_time { - my ($self) = @_; - return $self->{'actual_time'} if exists $self->{'actual_time'}; + my ($self) = @_; + return $self->{'actual_time'} if exists $self->{'actual_time'}; - if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) { - $self->{'actual_time'} = undef; - return $self->{'actual_time'}; - } + if ($self->{'error'} || !Bugzilla->user->is_timetracker) { + $self->{'actual_time'} = undef; + return $self->{'actual_time'}; + } - my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time) + my $sth = Bugzilla->dbh->prepare( + "SELECT SUM(work_time) FROM longdescs - WHERE longdescs.bug_id=?"); - $sth->execute($self->{bug_id}); - $self->{'actual_time'} = $sth->fetchrow_array(); - return $self->{'actual_time'}; + 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) = @_; - return $self->{'any_flags_requesteeble'} - if exists $self->{'any_flags_requesteeble'}; - return 0 if $self->{'error'}; + my ($self) = @_; + return $self->{'any_flags_requesteeble'} + if exists $self->{'any_flags_requesteeble'}; + return 0 if $self->{'error'}; + + my $any_flags_requesteeble + = grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - 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; + # 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; - return $self->{'any_flags_requesteeble'}; + return $self->{'any_flags_requesteeble'}; } sub attachments { - my ($self) = @_; - return $self->{'attachments'} if exists $self->{'attachments'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'attachments'} if exists $self->{'attachments'}; + return [] if $self->{'error'}; - $self->{'attachments'} = - Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); - return $self->{'attachments'}; + $self->{'attachments'} + = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); + 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'}; - return $self->{'assigned_to_obj'} - = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); + my ($self) = @_; + return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; + $self->{'assigned_to'} = 0 if $self->{'error'}; + return $self->{'assigned_to_obj'} + = new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1}); } 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'}; + my ($self) = @_; + return $self->{'blocked'} if exists $self->{'blocked'}; + return [] if $self->{'error'}; + $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); + return $self->{'blocked'}; } sub blocks_obj { - my ($self) = @_; - $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); - return $self->{blocks_obj}; + my ($self) = @_; + $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); + return $self->{blocks_obj}; } sub bug_group { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->groups_in})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->groups_in})); } sub related_bugs { - my ($self, $relationship) = @_; - return [] if $self->{'error'}; + my ($self, $relationship) = @_; + return [] if $self->{'error'}; - my $field_name = $relationship->name; - $self->{'related_bugs'}->{$field_name} ||= $self->match({$field_name => $self->id}); - return $self->{'related_bugs'}->{$field_name}; + my $field_name = $relationship->name; + $self->{'related_bugs'}->{$field_name} + ||= $self->match({$field_name => $self->id}); + return $self->{'related_bugs'}->{$field_name}; } sub cc { - my ($self) = @_; - return $self->{'cc'} if exists $self->{'cc'}; - return [] if $self->{'error'}; + 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 + 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); + ORDER BY profiles.login_name}, undef, $self->bug_id + ); - $self->{'cc'} = undef if !scalar(@{$self->{'cc'}}); + $self->{'cc'} = undef if !scalar(@{$self->{'cc'}}); - return $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'}; + my $self = shift; + return $self->{'cc_users'} if exists $self->{'cc_users'}; + return [] if $self->{'error'}; - 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'}; + 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 '' if $self->{error}; - ($self->{component}) //= $self->component_obj->name; - return $self->{component}; + my ($self) = @_; + return '' if $self->{error}; + ($self->{component}) //= $self->component_obj->name; + 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({ id => $self->{component_id}, cache => 1 }); - return $self->{component_obj}; + my ($self) = @_; + return $self->{component_obj} if defined $self->{component_obj}; + return {} if $self->{error}; + $self->{component_obj} + = new Bugzilla::Component({id => $self->{component_id}, cache => 1}); + 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}; + 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}; + 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'}; - $self->{'dependson'} = - EmitDependList("blocked", "dependson", $self->bug_id); - return $self->{'dependson'}; + my ($self) = @_; + return $self->{'dependson'} if exists $self->{'dependson'}; + return [] if $self->{'error'}; + $self->{'dependson'} = EmitDependList("blocked", "dependson", $self->bug_id); + return $self->{'dependson'}; } sub depends_on_obj { - my ($self) = @_; - $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); - return $self->{depends_on_obj}; + my ($self) = @_; + $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); + 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}; + 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 $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}; + 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'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'flag_types'} if exists $self->{'flag_types'}; + return [] if $self->{'error'}; - my $vars = { target_type => 'bug', - product_id => $self->{product_id}, - component_id => $self->{component_id}, - bug_id => $self->bug_id, - active_or_has_flags => $self->bug_id }; + my $vars = { + target_type => 'bug', + product_id => $self->{product_id}, + component_id => $self->{component_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'}; + $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); + return $self->{'flag_types'}; } sub flags { - my $self = shift; + my $self = shift; - # Don't cache it as it must be in sync with ->flag_types. - $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; - return $self->{flags}; + # Don't cache it as it must be in sync with ->flag_types. + $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; + return $self->{flags}; } sub isopened { - my $self = shift; - return is_open_state($self->{bug_status}) ? 1 : 0; + my $self = shift; + return is_open_state($self->{bug_status}) ? 1 : 0; } sub keywords { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->keyword_objects})); + 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 $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'}; + 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'}; } sub has_keyword { - my ($self, $keyword) = @_; - $keyword = lc($keyword); - return any { lc($_->name) eq $keyword } @{ $self->keyword_objects }; + my ($self, $keyword) = @_; + $keyword = lc($keyword); + return any { lc($_->name) eq $keyword } @{$self->keyword_objects}; } sub comments { - my ($self, $params) = @_; - return [] if $self->{'error'}; - $params ||= {}; - - if (!defined $self->{'comments'}) { - $self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id }); - my $count = 0; - foreach my $comment (@{ $self->{'comments'} }) { - $comment->{count} = $count++; - $comment->{bug} = $self; - weaken($comment->{bug}); - } - # Some bugs may have no comments when upgrading old installations. - Bugzilla::Comment->preload($self->{'comments'}) if @{ $self->{'comments'} }; - # BMO - for comment deletion support - Bugzilla::Hook::process('bug_comments', - { bug => $self, comments => $self->{'comments'} }); - } - return unless defined wantarray; - - my @comments = @{ $self->{'comments'} }; - - my $order = $params->{order} - || Bugzilla->user->setting('comment_sort_order'); - if ($order ne 'oldest_to_newest') { - @comments = reverse @comments; - if ($order eq 'newest_to_oldest_desc_first') { - unshift(@comments, pop @comments); - } - } + my ($self, $params) = @_; + return [] if $self->{'error'}; + $params ||= {}; - if ($params->{after}) { - my $from = datetime_from($params->{after}); - @comments = grep { datetime_from($_->creation_ts) > $from } @comments; + if (!defined $self->{'comments'}) { + $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id}); + my $count = 0; + foreach my $comment (@{$self->{'comments'}}) { + $comment->{count} = $count++; + $comment->{bug} = $self; + weaken($comment->{bug}); } - if ($params->{to}) { - my $to = datetime_from($params->{to}); - @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; + + # Some bugs may have no comments when upgrading old installations. + Bugzilla::Comment->preload($self->{'comments'}) if @{$self->{'comments'}}; + + # BMO - for comment deletion support + Bugzilla::Hook::process('bug_comments', + {bug => $self, comments => $self->{'comments'}}); + } + return unless defined wantarray; + + my @comments = @{$self->{'comments'}}; + + my $order = $params->{order} || Bugzilla->user->setting('comment_sort_order'); + if ($order ne 'oldest_to_newest') { + @comments = reverse @comments; + if ($order eq 'newest_to_oldest_desc_first') { + unshift(@comments, pop @comments); } - return \@comments; + } + + if ($params->{after}) { + my $from = datetime_from($params->{after}); + @comments = grep { datetime_from($_->creation_ts) > $from } @comments; + } + if ($params->{to}) { + my $to = datetime_from($params->{to}); + @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; + } + return \@comments; } sub comment_count { - my ($self) = @_; - return $self->{comment_count} if $self->{comment_count}; - my $dbh = Bugzilla->dbh; - return $self->{comment_count} = - $dbh->selectrow_array('SELECT COUNT(longdescs.comment_id) + my ($self) = @_; + return $self->{comment_count} if $self->{comment_count}; + my $dbh = Bugzilla->dbh; + return $self->{comment_count} = $dbh->selectrow_array( + 'SELECT COUNT(longdescs.comment_id) FROM longdescs - WHERE longdescs.bug_id = ?', - undef, $self->id); + WHERE longdescs.bug_id = ?', undef, $self->id + ); } # This is needed by xt/search.t. sub percentage_complete { - my $self = shift; - return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; - my $remaining = $self->remaining_time; - my $actual = $self->actual_time; - my $total = $remaining + $actual; - return undef if $total == 0; - # Search.pm truncates this value to an integer, so we want to as well, - # since this is mostly used in a test where its value needs to be - # identical to what the database will return. - return int(100 * ($actual / $total)); + my $self = shift; + return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; + my $remaining = $self->remaining_time; + my $actual = $self->actual_time; + my $total = $remaining + $actual; + return undef if $total == 0; + + # Search.pm truncates this value to an integer, so we want to as well, + # since this is mostly used in a test where its value needs to be + # identical to what the database will return. + return int(100 * ($actual / $total)); } sub product { - my ($self) = @_; - return '' if $self->{error}; - ($self->{product}) //= $self->product_obj->name; - return $self->{product}; + my ($self) = @_; + return '' if $self->{error}; + ($self->{product}) //= $self->product_obj->name; + 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({ id => $self->{product_id}, cache => 1 }); - return $self->{product_obj}; + my $self = shift; + return {} if $self->{error}; + $self->{product_obj} + ||= new Bugzilla::Product({id => $self->{product_id}, cache => 1}); + 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({ 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. - # However, we're keeping it this way now, for backwards-compatibility. - $self->{'qa_contact_obj'} = undef; - } - return $self->{'qa_contact_obj'}; + 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({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. + # 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'}; - return $self->{'reporter'} - = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); + my ($self) = @_; + return $self->{'reporter'} if exists $self->{'reporter'}; + $self->{'reporter_id'} = 0 if $self->{'error'}; + return $self->{'reporter'} + = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1}); } sub see_also { - my ($self) = @_; - return [] if $self->{'error'}; - if (!exists $self->{see_also}) { - my $ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT id FROM bug_see_also WHERE bug_id = ?', - undef, $self->id); + my ($self) = @_; + return [] if $self->{'error'}; + if (!exists $self->{see_also}) { + my $ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT id FROM bug_see_also WHERE bug_id = ?', + undef, $self->id); - my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); + my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); - $self->{see_also} = $bug_urls; - } - return $self->{see_also}; + $self->{see_also} = $bug_urls; + } + return $self->{see_also}; } sub status { - my $self = shift; - return undef if $self->{'error'}; + my $self = shift; + return undef if $self->{'error'}; - $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); - return $self->{'status'}; + $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); + return $self->{'status'}; } sub statuses_available { - my $self = shift; - return [] if $self->{'error'}; - return $self->{'statuses_available'} - if defined $self->{'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 }; + 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; - } + # 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); - } + my @available; + foreach my $status (@statuses) { - # 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); - } + # 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); + } - $self->{'statuses_available'} = \@available; - return $self->{'statuses_available'}; + # 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'}; } sub show_attachment_flags { - my ($self) = @_; - return $self->{'show_attachment_flags'} - 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'}; + my ($self) = @_; + return $self->{'show_attachment_flags'} + 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 groups { - my $self = shift; - return $self->{'groups'} if exists $self->{'groups'}; - return [] if $self->{'error'}; + 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" + . " FROM groups" + . " 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'}); + + my $rows = $sth->fetchall_arrayref(); + foreach my $row (@$rows) { + my ($groupid, $name, $description, $ison, $ingroup, $isactive, $membercontrol, + $othercontrol) + = @$row; + + $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); - 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" . - " FROM groups" . - " 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'}); - - my $rows = $sth->fetchall_arrayref(); - foreach my $row (@$rows) { - my ($groupid, $name, $description, $ison, $ingroup, $isactive, - $membercontrol, $othercontrol) = @$row; - - $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)) - )) + push( + @groups, { - my $ismandatory = $isactive - && ($membercontrol == CONTROLMAPMANDATORY); - - push (@groups, { "bit" => $groupid, - "name" => $name, - "ison" => $ison, - "ingroup" => $ingroup, - "mandatory" => $ismandatory, - "description" => $description }); + "bit" => $groupid, + "name" => $name, + "ison" => $ison, + "ingroup" => $ingroup, + "mandatory" => $ismandatory, + "description" => $description } - } - - # BMO: if required, hack in groups exposed by -visible membership - # (eg mozilla-employee-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 + ); + } + } + + # BMO: if required, hack in groups exposed by -visible membership + # (eg mozilla-employee-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, - }); - } - } + 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; + $self->{'groups'} = \@groups; - return $self->{'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'}; + 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 in_group { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; } sub user { - my $self = shift; - return $self->{'user'} if exists $self->{'user'}; - return {} if $self->{'error'}; + my $self = shift; + return $self->{'user'} if exists $self->{'user'}; + return {} if $self->{'error'}; - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my $prod_id = $self->{'product_id'}; + 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}; + 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'} = {canconfirm => $canconfirm, - canedit => $canedit, - isreporter => $isreporter}; - return $self->{'user'}; + $self->{'user'} + = {canconfirm => $canconfirm, canedit => $canedit, isreporter => $isreporter}; + return $self->{'user'}; } # 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. sub choices { - my $self = shift; - return $self->{'choices'} if exists $self->{'choices'}; - return {} if $self->{'error'}; - my $user = Bugzilla->user; - - my @products = @{ $user->get_enterable_products }; - # The current product is part of the popup, even if new bugs are no longer - # allowed for that product - if (!grep($_->name eq $self->product_obj->name, @products)) { - unshift(@products, $self->product_obj); - } - my %class_ids = map { $_->classification_id => 1 } @products; - my $classifications = - Bugzilla::Classification->new_from_list([keys %class_ids]); - - my %choices = ( - bug_status => $self->statuses_available, - classification => $classifications, - product => \@products, - component => $self->product_obj->components, - version => $self->product_obj->versions, - target_milestone => $self->product_obj->milestones, - ); - - my $resolution_field = new Bugzilla::Field({ name => 'resolution' }); - # Don't include the empty resolution in drop-downs. - my @resolutions = grep($_->name, @{ $resolution_field->legal_values }); - $choices{'resolution'} = \@resolutions; - - foreach my $key (keys %choices) { - my $name = $self->$key; - $choices{$key} = [grep { $_->is_active || $_->name eq $name } @{ $choices{$key} }]; - } - - $self->{'choices'} = \%choices; - return $self->{'choices'}; + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + my @products = @{$user->get_enterable_products}; + + # The current product is part of the popup, even if new bugs are no longer + # allowed for that product + if (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + my %class_ids = map { $_->classification_id => 1 } @products; + my $classifications + = Bugzilla::Classification->new_from_list([keys %class_ids]); + + my %choices = ( + bug_status => $self->statuses_available, + classification => $classifications, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + ); + + my $resolution_field = new Bugzilla::Field({name => 'resolution'}); + + # Don't include the empty resolution in drop-downs. + my @resolutions = grep($_->name, @{$resolution_field->legal_values}); + $choices{'resolution'} = \@resolutions; + + foreach my $key (keys %choices) { + my $name = $self->$key; + $choices{$key} + = [grep { $_->is_active || $_->name eq $name } @{$choices{$key}}]; + } + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } # Convenience Function. If you need speed, use this. If you need @@ -4191,12 +4338,12 @@ sub choices { # 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); + 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); } ##################################################################### @@ -4206,26 +4353,31 @@ sub bug_alias_to_id { # Returns a list of currently active and editable bug fields, # including multi-select fields. sub editable_bug_fields { - my @fields = Bugzilla->dbh->bz_table_columns('bugs'); - # Add multi-select fields - push(@fields, map { $_->name } @{Bugzilla->fields({obsolete => 0, - type => FIELD_TYPE_MULTI_SELECT})}); - # Obsolete custom fields are not editable. - my @obsolete_fields = @{ Bugzilla->fields({obsolete => 1, custom => 1}) }; - @obsolete_fields = map { $_->name } @obsolete_fields; - foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", - "lastdiffed", @obsolete_fields) - { - my $location = firstidx { $_ eq $remove } @fields; - # Ensure field exists before attempting to remove it. - splice(@fields, $location, 1) if ($location > -1); - } + my @fields = Bugzilla->dbh->bz_table_columns('bugs'); + + # Add multi-select fields + push(@fields, + map { $_->name } + @{Bugzilla->fields({obsolete => 0, type => FIELD_TYPE_MULTI_SELECT})}); + + # Obsolete custom fields are not editable. + my @obsolete_fields = @{Bugzilla->fields({obsolete => 1, custom => 1})}; + @obsolete_fields = map { $_->name } @obsolete_fields; + foreach + my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", + @obsolete_fields) + { + my $location = firstidx { $_ eq $remove } @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 }); + Bugzilla::Hook::process('bug_editable_bug_fields', {fields => \@fields}); - # Sorted because the old @::log_columns variable, which this replaces, - # was sorted. - return sort(@fields); + # Sorted because the old @::log_columns variable, which this replaces, + # was sorted. + return sort(@fields); } # XXX - When Bug::update() will be implemented, we should make this routine @@ -4233,84 +4385,86 @@ sub editable_bug_fields { # Join with bug_status and bugs tables to show bugs with open statuses first, # and then the others sub EmitDependList { - my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; - my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; + my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; + my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; - my $dbh = Bugzilla->dbh; - $exclude_resolved = $exclude_resolved ? 1 : 0; - my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; + my $dbh = Bugzilla->dbh; + $exclude_resolved = $exclude_resolved ? 1 : 0; + my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; - $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( - "SELECT $target_field + $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( + "SELECT $target_field FROM dependencies INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id INNER JOIN bug_status ON bugs.bug_status = bug_status.value WHERE $my_field = ? $is_open_clause - ORDER BY is_open DESC, $target_field"); + ORDER BY is_open DESC, $target_field" + ); - return $dbh->selectcol_arrayref( - $cache->{"${target_field}_sth_$exclude_resolved"}, - undef, $bug_id); + return $dbh->selectcol_arrayref( + $cache->{"${target_field}_sth_$exclude_resolved"}, + undef, $bug_id); } # Creates a lot of bug objects in the same order as the input array. sub _bugs_in_order { - my ($self, $bug_ids) = @_; - my %bug_map; - # there's no need to load bugs from the database if they are already in the - # object-cache - my @missing_ids; - foreach my $bug_id (@$bug_ids) { - if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { - $bug_map{$bug_id} = $bug; - } - else { - push @missing_ids, $bug_id; - } + my ($self, $bug_ids) = @_; + my %bug_map; + + # there's no need to load bugs from the database if they are already in the + # object-cache + my @missing_ids; + foreach my $bug_id (@$bug_ids) { + if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { + $bug_map{$bug_id} = $bug; } - my $bugs = $self->new_from_list(\@missing_ids); - foreach my $bug (@$bugs) { - $bug_map{$bug->id} = $bug; + else { + push @missing_ids, $bug_id; } - return [ map { $bug_map{$_} } @$bug_ids ]; + } + my $bugs = $self->new_from_list(\@missing_ids); + foreach my $bug (@$bugs) { + $bug_map{$bug->id} = $bug; + } + return [map { $bug_map{$_} } @$bug_ids]; } # Get the activity of a bug, starting from $starttime (if given). # This routine assumes Bugzilla::Bug->check has been previously called. sub GetBugActivity { - my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_; - 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 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 = ""; - if (!Bugzilla->user->is_insider) - { - $suppjoins = "LEFT JOIN attachments + my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_; + 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 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 = ""; + if (!Bugzilla->user->is_insider) { + $suppjoins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; - } + $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; + } - my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " . - $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') . - " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, + my $query + = "SELECT fielddefs.name, bugs_activity.attach_id, " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, bugs_activity.comment_id FROM bugs_activity $suppjoins @@ -4323,24 +4477,26 @@ sub GetBugActivity { $attachpart $suppwhere "; - if (Bugzilla->params->{'comment_taggers_group'} - && $include_comment_tags - && !$attach_id) - { - # Only includes comment tag activity for comments the user is allowed to see. - $suppjoins = ""; - $suppwhere = ""; - if (!Bugzilla->user->is_insider) { - $suppjoins = "INNER JOIN longdescs + if ( Bugzilla->params->{'comment_taggers_group'} + && $include_comment_tags + && !$attach_id) + { + # Only includes comment tag activity for comments the user is allowed to see. + $suppjoins = ""; + $suppwhere = ""; + if (!Bugzilla->user->is_insider) { + $suppjoins = "INNER JOIN longdescs ON longdescs.comment_id = longdescs_tags_activity.comment_id"; - $suppwhere = "AND longdescs.isprivate = 0"; - } + $suppwhere = "AND longdescs.isprivate = 0"; + } - $query .= " + $query .= " UNION ALL SELECT 'comment_tag' AS name, - NULL AS attach_id," . - $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when, + NULL AS attach_id," + . $dbh->sql_date_format('longdescs_tags_activity.bug_when', + '%Y.%m.%d %H:%i:%s') + . " AS bug_when, longdescs_tags_activity.removed, longdescs_tags_activity.added, profiles.login_name, @@ -4352,213 +4508,227 @@ sub GetBugActivity { $datepart $suppwhere "; - push @args, $bug_id; - push @args, $starttime if defined $starttime; + push @args, $bug_id; + push @args, $starttime if defined $starttime; + } + + $query .= "ORDER BY bug_when, comment_id"; + + my $list = $dbh->selectall_arrayref($query, undef, @args); + + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; + + foreach my $entry (@$list) { + my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) + = @$entry; + 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') + { + $activity_visible = Bugzilla->user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' + && !Bugzilla->user->is_insider + && $added) + { + $activity_visible = 0; + } + else { + $activity_visible = 1; } - $query .= "ORDER BY bug_when, comment_id"; + if ($activity_visible) { - my $list = $dbh->selectall_arrayref($query, undef, @args); + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; + } - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; + # 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); - foreach my $entry (@$list) { - my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry; - my %change; - my $activity_visible = 1; + # Create new empty anonymous data structures. + $operation = {}; + $changes = []; + } - # 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') - { - $activity_visible = Bugzilla->user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !Bugzilla->user->is_insider - && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } + # 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); + } - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # 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 = []; - } - - # 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{'removed'} = $removed; - $change{'added'} = $added; - - if ($comment_id) { - $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id); - } - - push (@$changes, \%change); - } - } + $operation->{'who'} = $who; + $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + $change{'removed'} = $removed; + $change{'added'} = $added; + + if ($comment_id) { + $operation->{comment_id} = $change{'comment'} + = Bugzilla::Comment->new($comment_id); + } + + push(@$changes, \%change); } + } + + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + } - return(\@operations, $incomplete_data); + 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; - } - } + my ($field, $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; - } + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + return $new_change if $current_change eq ''; - # All other fields get a space unless the first character of the second - # string is a comma or space + # 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; + 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 unless the first character of the second + # string is a comma or space + if (substr($new_change, 0, 1) eq ',' || 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, - $attach_id) = @_; - 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. - while ($removed || $added) { - 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); - } 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); - } else { - $added = ""; # no more entries - } - trick_taint($addstr); - trick_taint($removestr); - my $fieldid = get_field_id($col); - $dbh->do( - "INSERT INTO bugs_activity + my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id, $attach_id) + = @_; + 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. + while ($removed || $added) { + 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); + } + 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); + } + else { + $added = ""; # no more entries + } + trick_taint($addstr); + trick_taint($removestr); + my $fieldid = get_field_id($col); + $dbh->do( + "INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - undef, - ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id, - $attach_id)); - } + undef, + ( + $i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id, $attach_id + ) + ); + } } # Update bug_user_last_visit table sub update_user_last_visit { - my ($self, $user, $last_visit_ts) = @_; - my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id, - user_id => $user->id })->[0]; - - if ($lv) { - $lv->set(last_visit_ts => $last_visit_ts); - $lv->update; - } - else { - Bugzilla::BugUserLastVisit->create({ bug_id => $self->id, - user_id => $user->id, - last_visit_ts => $last_visit_ts }); - } + my ($self, $user, $last_visit_ts) = @_; + my $lv + = Bugzilla::BugUserLastVisit->match({bug_id => $self->id, user_id => $user->id + })->[0]; + + if ($lv) { + $lv->set(last_visit_ts => $last_visit_ts); + $lv->update; + } + else { + Bugzilla::BugUserLastVisit->create({ + bug_id => $self->id, user_id => $user->id, last_visit_ts => $last_visit_ts + }); + } } # Convert WebService API and email_in.pl field names to internal DB field # names. sub map_fields { - my ($params, $except) = @_; + my ($params, $except) = @_; - my %field_values; - foreach my $field (keys %$params) { - my $field_name; - if ($except->{$field}) { - $field_name = $field; - } - else { - $field_name = FIELD_MAP->{$field} || $field; - } - $field_values{$field_name} = $params->{$field}; + my %field_values; + foreach my $field (keys %$params) { + my $field_name; + if ($except->{$field}) { + $field_name = $field; + } + else { + $field_name = FIELD_MAP->{$field} || $field; } - return \%field_values; + $field_values{$field_name} = $params->{$field}; + } + return \%field_values; } # Return the groups which are no longer valid in the specified product sub get_invalid_groups { - my ($invocant, $params) = @_; - my @idlist = @{ $params->{bug_ids} }; - my $product = $params->{product}; - my $gids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT bgm.group_id + my ($invocant, $params) = @_; + my @idlist = @{$params->{bug_ids}}; + my $product = $params->{product}; + my $gids = Bugzilla->dbh->selectcol_arrayref( + 'SELECT bgm.group_id FROM bug_group_map AS bgm WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ') AND bgm.group_id NOT IN @@ -4567,10 +4737,11 @@ sub get_invalid_groups { WHERE gcm.product_id = ? AND ( (gcm.membercontrol != ? AND gcm.group_id IN (' - . Bugzilla->user->groups_as_string . ')) - OR gcm.othercontrol != ?) )', - undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA)); - return Bugzilla::Group->new_from_list($gids); + . Bugzilla->user->groups_as_string . ')) + OR gcm.othercontrol != ?) )', undef, + (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA) + ); + return Bugzilla::Group->new_from_list($gids); } ################################################################################ @@ -4590,163 +4761,186 @@ sub get_invalid_groups { # $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; + 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' - || $field eq 'work_time') - && $oldvalue == $newvalue) + } + elsif ( + ( + $field eq 'estimated_time' + || $field eq 'remaining_time' + || $field eq 'work_time' + ) + && $oldvalue == $newvalue + ) + { + return 1; + } + + my @priv_results; + Bugzilla::Hook::process( + 'bug_check_can_change_field', { - return 1; - } - - my @priv_results; - Bugzilla::Hook::process('bug_check_can_change_field', - { bug => $self, field => $field, - new_value => $newvalue, old_value => $oldvalue, - priv_results => \@priv_results }); - if (my $priv_required = first { $_ > 0 } @priv_results) { - $$PrivilegesRequired = $priv_required; - return 0; - } - my $allow_found = first { $_ == 0 } @priv_results; - if (defined $allow_found) { - return 1; - } - - # Allow anyone to change comments, or set flags - if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { - 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 = PRIVILEGES_REQUIRED_NONE : no privileges required; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. - - # Only users in the time-tracking group can change time-tracking fields. - if ( grep($_ eq $field, TIMETRACKING_FIELDS) ) { - if (!$user->is_timetracker) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return 0; - } - } - - # Allow anyone with (product-specific) "editbugs" privs to change anything. - if ($user->in_group('editbugs', $self->{'product_id'})) { - return 1; + bug => $self, + field => $field, + new_value => $newvalue, + old_value => $oldvalue, + priv_results => \@priv_results + } + ); + if (my $priv_required = first { $_ > 0 } @priv_results) { + $$PrivilegesRequired = $priv_required; + return 0; + } + my $allow_found = first { $_ == 0 } @priv_results; + if (defined $allow_found) { + return 1; + } + + # Allow anyone to change comments, or set flags + if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { + 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 = PRIVILEGES_REQUIRED_NONE : no privileges required; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. + + # Only users in the time-tracking group can change time-tracking fields. + if (grep($_ eq $field, TIMETRACKING_FIELDS)) { + if (!$user->is_timetracker) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + 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. + if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + 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; } - # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. - if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return $user->in_group('canconfirm', $self->{'product_id'}); + # 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; } + } - # 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; - } + # 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. - # 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; - } - } + # 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 = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # 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. + # - change the QA contact + if ($field eq 'qa_contact') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # 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 = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the QA contact - if ($field eq 'qa_contact') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the target milestone - if ($field eq 'target_milestone') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the priority (unless he could have set it originally) - if ($field eq 'priority' - && !Bugzilla->params->{'letsubmitterchoosepriority'}) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - unconfirm bugs (confirming them is handled above) - if ($field eq 'everconfirmed') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the status from one open state to another - if ($field eq 'bug_status' - && is_open_state($oldvalue) && is_open_state($newvalue)) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } + # - change the target milestone + if ($field eq 'target_milestone') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # The reporter is allowed to change anything else. - if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { - return 1; - } + # - change the priority (unless he could have set it originally) + if ($field eq 'priority' && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # If we haven't returned by this point, then the user doesn't - # have the necessary permissions to change this field. - $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; + # - unconfirm bugs (confirming them is handled above) + if ($field eq 'everconfirmed') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + + # - change the status from one open state to another + if ( $field eq 'bug_status' + && is_open_state($oldvalue) + && is_open_state($newvalue)) + { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; return 0; + } + + # 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 = PRIVILEGES_REQUIRED_REPORTER; + return 0; } # 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 { - # Moving an unconfirmed bug to an open state that isn't - # UNCONFIRMED will confirm the bug. - return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); - } + 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'; } - return 0; + else { + # Moving an unconfirmed bug to an open state that isn't + # UNCONFIRMED will confirm the bug. + return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); + } + } + return 0; } # @@ -4755,71 +4949,75 @@ sub _changes_everconfirmed { # Validate and return a hash of dependencies sub ValidateDependencies { - my $fields = {}; - # These can be arrayrefs or they can be strings. - $fields->{'dependson'} = shift; - $fields->{'blocked'} = shift; - my $id = shift || 0; - - unless (defined($fields->{'dependson'}) - || defined($fields->{'blocked'})) - { - return; - } - - my $dbh = Bugzilla->dbh; - my %deps; - my %deptree; - foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { - my ($me, $target) = @{$pair}; - $deptree{$target} = []; - $deps{$target} = []; - next unless $fields->{$target}; - - my %seen; - my $target_array = ref($fields->{$target}) ? $fields->{$target} - : [split(/[\s,]+/, $fields->{$target})]; - foreach my $i (@$target_array) { - if ($id == $i) { - ThrowUserError("dependency_loop_single"); - } - if (!exists $seen{$i}) { - push(@{$deptree{$target}}, $i); - $seen{$i} = 1; - } - } - # populate $deps{$target} as first-level deps only. - # and find remainder of dependency tree in $deptree{$target} - @{$deps{$target}} = @{$deptree{$target}}; - my @stack = @{$deps{$target}}; - while (@stack) { - my $i = shift @stack; - my $dep_list = - $dbh->selectcol_arrayref("SELECT $target + my $fields = {}; + + # These can be arrayrefs or they can be strings. + $fields->{'dependson'} = shift; + $fields->{'blocked'} = shift; + my $id = shift || 0; + + unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) { + return; + } + + my $dbh = Bugzilla->dbh; + my %deps; + my %deptree; + foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { + my ($me, $target) = @{$pair}; + $deptree{$target} = []; + $deps{$target} = []; + next unless $fields->{$target}; + + my %seen; + my $target_array + = ref($fields->{$target}) + ? $fields->{$target} + : [split(/[\s,]+/, $fields->{$target})]; + foreach my $i (@$target_array) { + if ($id == $i) { + ThrowUserError("dependency_loop_single"); + } + if (!exists $seen{$i}) { + push(@{$deptree{$target}}, $i); + $seen{$i} = 1; + } + } + + # populate $deps{$target} as first-level deps only. + # and find remainder of dependency tree in $deptree{$target} + @{$deps{$target}} = @{$deptree{$target}}; + my @stack = @{$deps{$target}}; + while (@stack) { + my $i = shift @stack; + my $dep_list = $dbh->selectcol_arrayref( + "SELECT $target FROM dependencies - WHERE $me = ?", undef, $i); - foreach my $t (@$dep_list) { - # ignore any _current_ dependencies involving this bug, - # as they will be overwritten with data from the form. - if ($t != $id && !exists $seen{$t}) { - push(@{$deptree{$target}}, $t); - push @stack, $t; - $seen{$t} = 1; - } - } + WHERE $me = ?", undef, $i + ); + foreach my $t (@$dep_list) { + + # ignore any _current_ dependencies involving this bug, + # as they will be overwritten with data from the form. + if ($t != $id && !exists $seen{$t}) { + push(@{$deptree{$target}}, $t); + push @stack, $t; + $seen{$t} = 1; } + } } + } - my @deps = @{$deptree{'dependson'}}; - my @blocks = @{$deptree{'blocked'}}; - my %union = (); - my %isect = (); - foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } - my @isect = keys %isect; - if (scalar(@isect) > 0) { - ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); - } - return %deps; + my @deps = @{$deptree{'dependson'}}; + my @blocks = @{$deptree{'blocked'}}; + my %union = (); + my %isect = (); + foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } + my @isect = keys %isect; + if (scalar(@isect) > 0) { + ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); + } + return %deps; } @@ -4828,60 +5026,61 @@ sub ValidateDependencies { ##################################################################### sub _create_cf_accessors { - my ($invocant) = @_; - my $class = ref($invocant) || $invocant; - return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; - - 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; - { - no strict 'refs'; - next if defined *{$name}; - *{$name} = $accessor; - } + my ($invocant) = @_; + my $class = ref($invocant) || $invocant; + return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; + + 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; + { + no strict 'refs'; + next if defined *{$name}; + *{$name} = $accessor; } + } - Bugzilla::Hook::process('bug_create_cf_accessors'); + Bugzilla::Hook::process('bug_create_cf_accessors'); - Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; + Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; } sub _accessor_for { - my ($class, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - return $class->_multi_select_accessor($field->name); - } - return $class->_cf_accessor($field->name); + my ($class, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + return $class->_multi_select_accessor($field->name); + } + return $class->_cf_accessor($field->name); } sub _cf_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + return $self->{$field}; + }; + return $accessor; } sub _multi_select_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - $self->{$field} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", - undef, $self->id); - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + $self->{$field} + ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", + undef, $self->id); + return $self->{$field}; + }; + return $accessor; } sub has_attachment_with_mimetype { - my ($self, $type) = @_; - return any { $_->contenttype eq $type } @{ $self->attachments }; + my ($self, $type) = @_; + return any { $_->contenttype eq $type } @{$self->attachments}; } 1; |