# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. package Bugzilla::WebService::Bug; use 5.10.1; use strict; use base qw(Bugzilla::WebService); use Bugzilla::Comment; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(filter filter_wants validate); use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trick_taint trim diff_arrays); use Bugzilla::Version; use Bugzilla::Milestone; use Bugzilla::Status; use Bugzilla::Token qw(issue_hash_token); ############# # Constants # ############# use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); use constant DATE_FIELDS => { comments => ['new_since'], search => ['last_change_time', 'creation_time'], }; use constant BASE64_FIELDS => { add_attachment => ['data'], }; use constant READ_ONLY => qw( attachments comments fields get history legal_values search ); ###################################################### # Add aliases here for old method name compatibility # ###################################################### BEGIN { # In 3.0, get was called get_bugs *get_bugs = \&get; # Before 3.4rc1, "history" was get_history. *get_history = \&history; } ########### # Methods # ########### sub fields { my ($self, $params) = validate(@_, 'ids', 'names'); Bugzilla->switch_to_shadow_db(); my @fields; if (defined $params->{ids}) { my $ids = $params->{ids}; foreach my $id (@$ids) { my $loop_field = Bugzilla::Field->check({ id => $id }); push(@fields, $loop_field); } } if (defined $params->{names}) { my $names = $params->{names}; foreach my $field_name (@$names) { my $loop_field = Bugzilla::Field->check($field_name); # Don't push in duplicate fields if we also asked for this field # in "ids". if (!grep($_->id == $loop_field->id, @fields)) { push(@fields, $loop_field); } } } if (!defined $params->{ids} and !defined $params->{names}) { @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; } my @fields_out; foreach my $field (@fields) { my $visibility_field = $field->visibility_field ? $field->visibility_field->name : undef; my $vis_values = $field->visibility_values; my $value_field = $field->value_field ? $field->value_field->name : undef; my (@values, $has_values); if ( ($field->is_select and $field->name ne 'product') or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) or $field->name eq 'keywords') { $has_values = 1; @values = @{ $self->_legal_field_values({ field => $field }) }; } if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { $value_field = 'product'; } my %field_data = ( id => $self->type('int', $field->id), type => $self->type('int', $field->type), is_custom => $self->type('boolean', $field->custom), name => $self->type('string', $field->name), display_name => $self->type('string', $field->description), is_mandatory => $self->type('boolean', $field->is_mandatory), is_on_bug_entry => $self->type('boolean', $field->enter_bug), visibility_field => $self->type('string', $visibility_field), visibility_values => [ map { $self->type('string', $_->name) } @$vis_values ], ); if ($has_values) { $field_data{value_field} = $self->type('string', $value_field); $field_data{values} = \@values; }; push(@fields_out, filter $params, \%field_data); } return { fields => \@fields_out }; } sub _legal_field_values { my ($self, $params) = @_; my $field = $params->{field}; my $field_name = $field->name; my $user = Bugzilla->user; my @result; if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { my @list; if ($field_name eq 'version') { @list = Bugzilla::Version->get_all; } elsif ($field_name eq 'component') { @list = Bugzilla::Component->get_all; } else { @list = Bugzilla::Milestone->get_all; } foreach my $value (@list) { my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0; # XXX This is very slow for large numbers of values. my $product_name = $value->product->name; if ($user->can_see_product($product_name)) { push(@result, { name => $self->type('string', $value->name), sort_key => $self->type('int', $sortkey), sortkey => $self->type('int', $sortkey), # deprecated visibility_values => [$self->type('string', $product_name)], is_active => $self->type('boolean', $value->is_active), }); } } } elsif ($field_name eq 'bug_status') { my @status_all = Bugzilla::Status->get_all; my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0, can_change_to => Bugzilla::Status->can_change_to }, 'Bugzilla::Status'); unshift(@status_all, $initial_status); foreach my $status (@status_all) { my @can_change_to; foreach my $change_to (@{ $status->can_change_to }) { # There's no need to note that a status can transition # to itself. next if $change_to->id == $status->id; my %change_to_hash = ( name => $self->type('string', $change_to->name), comment_required => $self->type('boolean', $change_to->comment_required_on_change_from($status)), ); push(@can_change_to, \%change_to_hash); } push (@result, { name => $self->type('string', $status->name), is_open => $self->type('boolean', $status->is_open), sort_key => $self->type('int', $status->sortkey), sortkey => $self->type('int', $status->sortkey), # deprecated can_change_to => \@can_change_to, visibility_values => [], }); } } elsif ($field_name eq 'keywords') { my @legal_keywords = Bugzilla::Keyword->get_all; foreach my $value (@legal_keywords) { push (@result, { name => $self->type('string', $value->name), description => $self->type('string', $value->description), }); } } else { my @values = Bugzilla::Field::Choice->type($field)->get_all(); foreach my $value (@values) { my $vis_val = $value->visibility_value; push(@result, { name => $self->type('string', $value->name), sort_key => $self->type('int' , $value->sortkey), sortkey => $self->type('int' , $value->sortkey), # deprecated visibility_values => [ defined $vis_val ? $self->type('string', $vis_val->name) : () ], }); } } return \@result; } sub comments { my ($self, $params) = validate(@_, 'ids', 'comment_ids'); if (!(defined $params->{ids} || defined $params->{comment_ids})) { ThrowCodeError('params_required', { function => 'Bug.comments', params => ['ids', 'comment_ids'] }); } my $bug_ids = $params->{ids} || []; my $comment_ids = $params->{comment_ids} || []; my $dbh = Bugzilla->switch_to_shadow_db(); my $user = Bugzilla->user; my %bugs; foreach my $bug_id (@$bug_ids) { my $bug = Bugzilla::Bug->check($bug_id); # We want the API to always return comments in the same order. my $comments = $bug->comments({ order => 'oldest_to_newest', after => $params->{new_since} }); my @result; foreach my $comment (@$comments) { next if $comment->is_private && !$user->is_insider; push(@result, $self->_translate_comment($comment, $params)); } $bugs{$bug->id}{'comments'} = \@result; } my %comments; if (scalar @$comment_ids) { my @ids = map { trim($_) } @$comment_ids; my $comment_data = Bugzilla::Comment->new_from_list(\@ids); # See if we were passed any invalid comment ids. my %got_ids = map { $_->id => 1 } @$comment_data; foreach my $comment_id (@ids) { if (!$got_ids{$comment_id}) { ThrowUserError('comment_id_invalid', { id => $comment_id }); } } # Now make sure that we can see all the associated bugs. my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); foreach my $comment (@$comment_data) { if ($comment->is_private && !$user->is_insider) { ThrowUserError('comment_is_private', { id => $comment->id }); } $comments{$comment->id} = $self->_translate_comment($comment, $params); } } return { bugs => \%bugs, comments => \%comments }; } # Helper for Bug.comments sub _translate_comment { my ($self, $comment, $filters) = @_; my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; return filter $filters, { id => $self->type('int', $comment->id), bug_id => $self->type('int', $comment->bug_id), creator => $self->type('string', $comment->author->login), author => $self->type('string', $comment->author->login), time => $self->type('dateTime', $comment->creation_ts), creation_time => $self->type('dateTime', $comment->creation_ts), is_private => $self->type('boolean', $comment->is_private), text => $self->type('string', $comment->body_full), attachment_id => $self->type('int', $attach_id), count => $self->type('int', $comment->count), }; } sub get { my ($self, $params) = validate(@_, 'ids'); Bugzilla->switch_to_shadow_db(); my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); my @bugs; my @faults; foreach my $bug_id (@$ids) { my $bug; if ($params->{permissive}) { eval { $bug = Bugzilla::Bug->check($bug_id); }; if ($@) { push(@faults, {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode, } ); undef $@; next; } } else { $bug = Bugzilla::Bug->check($bug_id); } push(@bugs, $self->_bug_to_hash($bug, $params)); } return { bugs => \@bugs, faults => \@faults }; } # this is a function that gets bug activity for list of bug ids # it can be called as the following: # $call = $rpc->call( 'Bug.history', { ids => [1,2] }); sub history { my ($self, $params) = validate(@_, 'ids'); Bugzilla->switch_to_shadow_db(); my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; $api_name{'bug_group'} = 'groups'; my @return; foreach my $bug_id (@$ids) { my %item; my $bug = Bugzilla::Bug->check($bug_id); $bug_id = $bug->id; $item{id} = $self->type('int', $bug_id); my ($activity) = $bug->get_activity; my @history; foreach my $changeset (@$activity) { my %bug_history; $bug_history{when} = $self->type('dateTime', $changeset->{when}); $bug_history{who} = $self->type('string', $changeset->{who}); $bug_history{changes} = []; foreach my $change (@{ $changeset->{changes} }) { my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; my $attach_id = delete $change->{attachid}; if ($attach_id) { $change->{attachment_id} = $self->type('int', $attach_id); } $change->{removed} = $self->type('string', $change->{removed}); $change->{added} = $self->type('string', $change->{added}); $change->{field_name} = $self->type('string', $api_field); delete $change->{fieldname}; push (@{$bug_history{changes}}, $change); } push (@history, \%bug_history); } $item{history} = \@history; # alias is returned in case users passes a mixture of ids and aliases # then they get to know which bug activity relates to which value # they passed $item{alias} = $self->type('string', $bug->alias); push(@return, \%item); } return { bugs => \@return }; } sub search { my ($self, $params) = @_; Bugzilla->switch_to_shadow_db(); if ( defined($params->{offset}) and !defined($params->{limit}) ) { ThrowCodeError('param_required', { param => 'limit', function => 'Bug.search()' }); } $params = Bugzilla::Bug::map_fields($params); delete $params->{WHERE}; unless (Bugzilla->user->is_timetracker) { delete $params->{$_} foreach qw(estimated_time remaining_time deadline); } # Do special search types for certain fields. if ( my $bug_when = delete $params->{delta_ts} ) { $params->{WHERE}->{'delta_ts >= ?'} = $bug_when; } if (my $when = delete $params->{creation_ts}) { $params->{WHERE}->{'creation_ts >= ?'} = $when; } if (my $summary = delete $params->{short_desc}) { my @strings = ref $summary ? @$summary : ($summary); my @likes = ("short_desc LIKE ?") x @strings; my $clause = join(' OR ', @likes); $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings]; } if (my $whiteboard = delete $params->{status_whiteboard}) { my @strings = ref $whiteboard ? @$whiteboard : ($whiteboard); my @likes = ("status_whiteboard LIKE ?") x @strings; my $clause = join(' OR ', @likes); $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings]; } # We want include_fields and exclude_fields to be passed to # _bug_to_hash but not to Bugzilla::Bug->match so we copy the # params and delete those before passing to Bugzilla::Bug->match. my %match_params = %{ $params }; delete $match_params{'include_fields'}; delete $match_params{'exclude_fields'}; my $bugs = Bugzilla::Bug->match(\%match_params); my $visible = Bugzilla->user->visible_bugs($bugs); my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible; return { bugs => \@hashes }; } sub possible_duplicates { my ($self, $params) = validate(@_, 'product'); my $user = Bugzilla->user; Bugzilla->switch_to_shadow_db(); # Undo the array-ification that validate() does, for "summary". $params->{summary} || ThrowCodeError('param_required', { function => 'Bug.possible_duplicates', param => 'summary' }); my @products; foreach my $name (@{ $params->{'product'} || [] }) { my $object = $user->can_enter_product($name, THROW_ERROR); push(@products, $object); } my $possible_dupes = Bugzilla::Bug->possible_duplicates( { summary => $params->{summary}, products => \@products, limit => $params->{limit} }); my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; return { bugs => \@hashes }; } sub update { my ($self, $params) = validate(@_, 'ids'); my $user = Bugzilla->login(LOGIN_REQUIRED); my $dbh = Bugzilla->dbh; # We skip certain fields because their set_ methods actually use # the external names instead of the internal names. $params = Bugzilla::Bug::map_fields($params, { summary => 1, platform => 1, severity => 1, url => 1 }); my $ids = delete $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; my %values = %$params; $values{other_bugs} = \@bugs; if (exists $values{comment} and exists $values{comment}{comment}) { $values{comment}{body} = delete $values{comment}{comment}; } # Prevent bugs that could be triggered by specifying fields that # have valid "set_" functions in Bugzilla::Bug, but shouldn't be # called using those field names. delete $values{dependencies}; delete $values{flags}; foreach my $bug (@bugs) { $bug->set_all(\%values); } my %all_changes; $dbh->bz_start_transaction(); foreach my $bug (@bugs) { $all_changes{$bug->id} = $bug->update(); } $dbh->bz_commit_transaction(); foreach my $bug (@bugs) { $bug->send_changes($all_changes{$bug->id}); } my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; # This doesn't normally belong in FIELD_MAP, but we do want to translate # "bug_group" back into "groups". $api_name{'bug_group'} = 'groups'; my @result; foreach my $bug (@bugs) { my %hash = ( id => $self->type('int', $bug->id), last_change_time => $self->type('dateTime', $bug->delta_ts), changes => {}, ); # alias is returned in case users pass a mixture of ids and aliases, # so that they can know which set of changes relates to which value # they passed. $hash{alias} = $self->type('string', $bug->alias); my %changes = %{ $all_changes{$bug->id} }; foreach my $field (keys %changes) { my $change = $changes{$field}; my $api_field = $api_name{$field} || $field; # We normalize undef to an empty string, so that the API # stays consistent for things like Deadline that can become # empty. $change->[0] = '' if !defined $change->[0]; $change->[1] = '' if !defined $change->[1]; $hash{changes}->{$api_field} = { removed => $self->type('string', $change->[0]), added => $self->type('string', $change->[1]) }; } push(@result, \%hash); } return { bugs => \@result }; } sub create { my ($self, $params) = @_; Bugzilla->login(LOGIN_REQUIRED); $params = Bugzilla::Bug::map_fields($params); my $bug = Bugzilla::Bug->create($params); Bugzilla::BugMail::Send($bug->bug_id, { changer => $bug->reporter }); return { id => $self->type('int', $bug->bug_id) }; } sub legal_values { my ($self, $params) = @_; Bugzilla->switch_to_shadow_db(); defined $params->{field} or ThrowCodeError('param_required', { param => 'field' }); my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; my @global_selects = @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; my $values; if (grep($_->name eq $field, @global_selects)) { # The field is a valid one. trick_taint($field); $values = get_legal_field_values($field); } elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { my $id = $params->{product_id}; defined $id || ThrowCodeError('param_required', { function => 'Bug.legal_values', param => 'product_id' }); grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) || ThrowUserError('product_access_denied', { id => $id }); my $product = new Bugzilla::Product($id); my @objects; if ($field eq 'version') { @objects = @{$product->versions}; } elsif ($field eq 'target_milestone') { @objects = @{$product->milestones}; } elsif ($field eq 'component') { @objects = @{$product->components}; } $values = [map { $_->name } @objects]; } else { ThrowCodeError('invalid_field_name', { field => $params->{field} }); } my @result; foreach my $val (@$values) { push(@result, $self->type('string', $val)); } return { values => \@result }; } sub add_attachment { my ($self, $params) = validate(@_, 'ids'); my $dbh = Bugzilla->dbh; Bugzilla->login(LOGIN_REQUIRED); defined $params->{ids} || ThrowCodeError('param_required', { param => 'ids' }); defined $params->{data} || ThrowCodeError('param_required', { param => 'data' }); my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} }; my @created; $dbh->bz_start_transaction(); my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); foreach my $bug (@bugs) { my $attachment = Bugzilla::Attachment->create({ bug => $bug, creation_ts => $timestamp, data => $params->{data}, description => $params->{summary}, filename => $params->{file_name}, mimetype => $params->{content_type}, ispatch => $params->{is_patch}, isprivate => $params->{is_private}, }); my $comment = $params->{comment} || ''; $attachment->bug->add_comment($comment, { isprivate => $attachment->isprivate, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id }); push(@created, $attachment); } $_->bug->update($timestamp) foreach @created; $dbh->bz_commit_transaction(); $_->send_changes() foreach @bugs; my %attachments = map { $_->id => $self->_attachment_to_hash($_, $params) } @created; return { attachments => \%attachments }; } sub add_comment { my ($self, $params) = @_; # The user must login in order add a comment my $user = Bugzilla->login(LOGIN_REQUIRED); # Check parameters defined $params->{id} || ThrowCodeError('param_required', { param => 'id' }); my $comment = $params->{comment}; (defined $comment && trim($comment) ne '') || ThrowCodeError('param_required', { param => 'comment' }); my $bug = Bugzilla::Bug->check_for_edit($params->{id}); # Backwards-compatibility for versions before 3.6 if (defined $params->{private}) { $params->{is_private} = delete $params->{private}; } # Append comment $bug->add_comment($comment, { isprivate => $params->{is_private}, work_time => $params->{work_time} }); # Capture the call to bug->update (which creates the new comment) in # a transaction so we're sure to get the correct comment_id. my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); $bug->update(); my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id'); $dbh->bz_commit_transaction(); # Send mail. Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }); return { id => $self->type('int', $new_comment_id) }; } sub update_see_also { my ($self, $params) = @_; my $user = Bugzilla->login(LOGIN_REQUIRED); # Check parameters $params->{ids} || ThrowCodeError('param_required', { param => 'id' }); my ($add, $remove) = @$params{qw(add remove)}; ($add || $remove) or ThrowCodeError('params_required', { params => ['add', 'remove'] }); my @bugs; foreach my $id (@{ $params->{ids} }) { my $bug = Bugzilla::Bug->check_for_edit($id); push(@bugs, $bug); if ($remove) { $bug->remove_see_also($_) foreach @$remove; } if ($add) { $bug->add_see_also($_) foreach @$add; } } my %changes; foreach my $bug (@bugs) { my $change = $bug->update(); if (my $see_also = $change->{see_also}) { $changes{$bug->id}->{see_also} = { removed => [split(', ', $see_also->[0])], added => [split(', ', $see_also->[1])], }; } else { # We still want a changes entry, for API consistency. $changes{$bug->id}->{see_also} = { added => [], removed => [] }; } Bugzilla::BugMail::Send($bug->id, { changer => $user }); } return { changes => \%changes }; } sub attachments { my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); Bugzilla->switch_to_shadow_db(); if (!(defined $params->{ids} or defined $params->{attachment_ids})) { ThrowCodeError('param_required', { function => 'Bug.attachments', params => ['ids', 'attachment_ids'] }); } my $ids = $params->{ids} || []; my $attach_ids = $params->{attachment_ids} || []; my %bugs; foreach my $bug_id (@$ids) { my $bug = Bugzilla::Bug->check($bug_id); $bugs{$bug->id} = []; foreach my $attach (@{$bug->attachments}) { push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params); } } my %attachments; foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { Bugzilla::Bug->check($attach->bug_id); if ($attach->isprivate && !Bugzilla->user->is_insider) { ThrowUserError('auth_failure', {action => 'access', object => 'attachment', attach_id => $attach->id}); } $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params); } return { bugs => \%bugs, attachments => \%attachments }; } sub update_tags { my ($self, $params) = @_; Bugzilla->login(LOGIN_REQUIRED); my $ids = $params->{ids}; my $tags = $params->{tags}; ThrowCodeError('param_required', { function => 'Bug.update_tags', param => 'ids' }) if !defined $ids; ThrowCodeError('param_required', { function => 'Bug.update_tags', param => 'tags' }) if !defined $tags; my %changes; foreach my $bug_id (@$ids) { my $bug = Bugzilla::Bug->check($bug_id); my @old_tags = @{ $bug->tags }; $bug->remove_tag($_) foreach @{ $tags->{remove} || [] }; $bug->add_tag($_) foreach @{ $tags->{add} || [] }; my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags); my @removed = map { $self->type('string', $_) } @$removed; my @added = map { $self->type('string', $_) } @$added; $changes{$bug->id}->{tags} = { removed => \@removed, added => \@added }; } return { changes => \%changes }; } ############################## # Private Helper Subroutines # ############################## # A helper for get() and search(). This is done in this fashion in order # to produce a stable API and to explicitly type return values. # The internals of Bugzilla::Bug are not stable enough to just # return them directly. sub _bug_to_hash { my ($self, $bug, $params) = @_; # All the basic bug attributes are here, in alphabetical order. # A bug attribute is "basic" if it doesn't require an additional # database call to get the info. my %item = ( alias => $self->type('string', $bug->alias), classification => $self->type('string', $bug->classification), component => $self->type('string', $bug->component), creation_time => $self->type('dateTime', $bug->creation_ts), id => $self->type('int', $bug->bug_id), is_confirmed => $self->type('boolean', $bug->everconfirmed), last_change_time => $self->type('dateTime', $bug->delta_ts), op_sys => $self->type('string', $bug->op_sys), platform => $self->type('string', $bug->rep_platform), priority => $self->type('string', $bug->priority), product => $self->type('string', $bug->product), resolution => $self->type('string', $bug->resolution), severity => $self->type('string', $bug->bug_severity), status => $self->type('string', $bug->bug_status), summary => $self->type('string', $bug->short_desc), target_milestone => $self->type('string', $bug->target_milestone), url => $self->type('string', $bug->bug_file_loc), version => $self->type('string', $bug->version), whiteboard => $self->type('string', $bug->status_whiteboard), ); # First we handle any fields that require extra SQL calls. # We don't do the SQL calls at all if the filter would just # eliminate them anyway. if (filter_wants $params, 'assigned_to') { $item{'assigned_to'} = $self->type('string', $bug->assigned_to->login); } if (filter_wants $params, 'blocks') { my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; $item{'blocks'} = \@blocks; } if (filter_wants $params, 'cc') { my @cc = map { $self->type('string', $_) } @{ $bug->cc || [] }; $item{'cc'} = \@cc; } if (filter_wants $params, 'creator') { $item{'creator'} = $self->type('string', $bug->reporter->login); } if (filter_wants $params, 'depends_on') { my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; $item{'depends_on'} = \@depends_on; } if (filter_wants $params, 'dupe_of') { $item{'dupe_of'} = $self->type('int', $bug->dup_id); } if (filter_wants $params, 'groups') { my @groups = map { $self->type('string', $_->name) } @{ $bug->groups_in }; $item{'groups'} = \@groups; } if (filter_wants $params, 'is_open') { $item{'is_open'} = $self->type('boolean', $bug->status->is_open); } if (filter_wants $params, 'keywords') { my @keywords = map { $self->type('string', $_->name) } @{ $bug->keyword_objects }; $item{'keywords'} = \@keywords; } if (filter_wants $params, 'qa_contact') { my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; $item{'qa_contact'} = $self->type('string', $qa_login); } if (filter_wants $params, 'see_also') { my @see_also = map { $self->type('string', $_->name) } @{ $bug->see_also }; $item{'see_also'} = \@see_also; } if (filter_wants $params, 'flags') { $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; } # And now custom fields my @custom_fields = Bugzilla->active_custom_fields; foreach my $field (@custom_fields) { my $name = $field->name; next if !filter_wants $params, $name; if ($field->type == FIELD_TYPE_BUG_ID) { $item{$name} = $self->type('int', $bug->$name); } elsif ($field->type == FIELD_TYPE_DATETIME) { $item{$name} = $self->type('dateTime', $bug->$name); } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { my @values = map { $self->type('string', $_) } @{ $bug->$name }; $item{$name} = \@values; } else { $item{$name} = $self->type('string', $bug->$name); } } # Timetracking fields are only sent if the user can see them. if (Bugzilla->user->is_timetracker) { $item{'estimated_time'} = $self->type('double', $bug->estimated_time); $item{'remaining_time'} = $self->type('double', $bug->remaining_time); # No need to format $bug->deadline specially, because Bugzilla::Bug # already does it for us. $item{'deadline'} = $self->type('string', $bug->deadline); $item{'actual_time'} = $self->type('double', $bug->actual_time); } if (Bugzilla->user->id) { my $token = issue_hash_token([$bug->id, $bug->delta_ts]); $item{'update_token'} = $self->type('string', $token); } # The "accessible" bits go here because they have long names and it # makes the code look nicer to separate them out. $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); return filter $params, \%item; } sub _attachment_to_hash { my ($self, $attach, $filters) = @_; my $item = filter $filters, { creation_time => $self->type('dateTime', $attach->attached), last_change_time => $self->type('dateTime', $attach->modification_time), id => $self->type('int', $attach->id), bug_id => $self->type('int', $attach->bug_id), file_name => $self->type('string', $attach->filename), summary => $self->type('string', $attach->description), description => $self->type('string', $attach->description), content_type => $self->type('string', $attach->contenttype), is_private => $self->type('int', $attach->isprivate), is_obsolete => $self->type('int', $attach->isobsolete), is_patch => $self->type('int', $attach->ispatch), }; # creator/attacher require an extra lookup, so we only send them if # the filter wants them. foreach my $field (qw(creator attacher)) { if (filter_wants $filters, $field) { $item->{$field} = $self->type('string', $attach->attacher->login); } } if (filter_wants $filters, 'data') { $item->{'data'} = $self->type('base64', $attach->data); } if (filter_wants $filters, 'size') { $item->{'size'} = $self->type('int', $attach->datasize); } if (filter_wants $filters, 'flags') { $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; } return $item; } sub _flag_to_hash { my ($self, $flag) = @_; my $item = { id => $self->type('int', $flag->id), name => $self->type('string', $flag->name), type_id => $self->type('int', $flag->type_id), creation_date => $self->type('dateTime', $flag->creation_date), modification_date => $self->type('dateTime', $flag->modification_date), status => $self->type('string', $flag->status) }; foreach my $field (qw(setter requestee)) { my $field_id = $field . "_id"; $item->{$field} = $self->type('string', $flag->$field->login) if $flag->$field_id; } return $item; } 1; __END__ =head1 NAME Bugzilla::Webservice::Bug - The API for creating, changing, and getting the details of bugs. =head1 DESCRIPTION This part of the Bugzilla API allows you to file a new bug in Bugzilla, or get information about bugs that have already been filed. =head1 METHODS See L for a description of how parameters are passed, and what B, B, and B mean. =head1 Utility Functions =head2 fields B =over =item B Get information about valid bug fields, including the lists of legal values for each field. =item B You can pass either field ids or field names. B: If neither C nor C is specified, then all non-obsolete fields will be returned. In addition to the parameters below, this method also accepts the standard L and L arguments. =over =item C (array) - An array of integer field ids. =item C (array) - An array of strings representing field names. =back =item B A hash containing a single element, C. This is an array of hashes, containing the following keys: =over =item C C An integer id uniquely identifying this field in this installation only. =item C C The number of the fieldtype. The following values are defined: =over =item C<0> Unknown =item C<1> Free Text =item C<2> Drop Down =item C<3> Multiple-Selection Box =item C<4> Large Text Box =item C<5> Date/Time =item C<6> Bug Id =item C<7> Bug URLs ("See Also") =back =item C C True when this is a custom field, false otherwise. =item C C The internal name of this field. This is a unique identifier for this field. If this is not a custom field, then this name will be the same across all Bugzilla installations. =item C C The name of the field, as it is shown in the user interface. =item C C True if the field must have a value when filing new bugs. Also, mandatory fields cannot have their value cleared when updating bugs. =item C C For custom fields, this is true if the field is shown when you enter a new bug. For standard fields, this is currently always false, even if the field shows up when entering a bug. (To know whether or not a standard field is valid on bug entry, see L.) =item C C The name of a field that controls the visibility of this field in the user interface. This field only appears in the user interface when the named field is equal to one of the values in C. Can be null. =item C C of Cs This field is only shown when C matches one of these values. When C is null, then this is an empty array. =item C C The name of the field that controls whether or not particular values of the field are shown in the user interface. Can be null. =item C This is an array of hashes, representing the legal values for select-type (drop-down and multiple-selection) fields. This is also populated for the C, C, C, and C fields, but not for the C field (you must use L for that. For fields that aren't select-type fields, this will simply be an empty array. Each hash has the following keys: =over =item C C The actual value--this is what you would specify for this field in L, etc. =item C C Values, when displayed in a list, are sorted first by this integer and then secondly by their name. =item C B - Use C instead. =item C If C is defined for this field, then this value is only shown if the C is set to one of the values listed in this array. Note that for per-product fields, C is set to C<'product'> and C will reflect which product(s) this value appears in. =item C C This value is defined only for certain product specific fields such as version, target_milestone or component. When true, the value is active, otherwise the value is not active. =item C C The description of the value. This item is only included for the C field. =item C C For C values, determines whether this status specifies that the bug is "open" (true) or "closed" (false). This item is only included for the C field. =item C For C values, this is an array of hashes that determines which statuses you can transition to from this status. (This item is only included for the C field.) Each hash contains the following items: =over =item C the name of the new status =item C this C True if a comment is required when you change a bug into this status using this transition. =back =back =back =item B =over =item 51 (Invalid Field Name or Id) You specified an invalid field name or id. =back =item B =over =item Added in Bugzilla B<3.6>. =item The C return value was added in Bugzilla B<4.0>. =item C was renamed to C in Bugzilla B<4.2>. =item C return key for C was added in Bugzilla B<4.4>. =back =back =head2 legal_values B - Use L instead. =over =item B Tells you what values are allowed for a particular field. =item B =over =item C - The name of the field you want information about. This should be the same as the name you would use in L, below. =item C - If you're picking a product-specific field, you have to specify the id of the product you want the values for. =back =item B C - An array of strings: the legal values for this field. The values will be sorted as they normally would be in Bugzilla. =item B =over =item 106 (Invalid Product) You were required to specify a product, and either you didn't, or you specified an invalid product (or a product that you can't access). =item 108 (Invalid Field Name) You specified a field that doesn't exist or isn't a drop-down field. =back =back =head1 Bug Information =head2 attachments B =over =item B It allows you to get data about attachments, given a list of bugs and/or attachment ids. B: Private attachments will only be returned if you are in the insidergroup or if you are the submitter of the attachment. =item B B: At least one of C or C is required. =over =item C See the description of the C parameter in the L method. =item C C An array of integer attachment ids. =back Also accepts the L, and L arguments. =item B A hash containing two elements: C and C. The return value looks like this: { bugs => { 1345 => [ { (attachment) }, { (attachment) } ], 9874 => [ { (attachment) }, { (attachment) } ], }, attachments => { 234 => { (attachment) }, 123 => { (attachment) }, } } The attachments of any bugs that you specified in the C argument in input are returned in C on output. C is a hash that has integer bug IDs for keys and the values are arrayrefs that contain hashes as attachments. (Fields for attachments are described below.) For any attachments that you specified directly in C, they are returned in C on output. This is a hash where the attachment ids point directly to hashes describing the individual attachment. The fields for each attachment (where it says C<(attachment)> in the diagram above) are: =over =item C C The raw data of the attachment, encoded as Base64. =item C C The length (in bytes) of the attachment. =item C C The time the attachment was created. =item C C The last time the attachment was modified. =item C C The numeric id of the attachment. =item C C The numeric id of the bug that the attachment is attached to. =item C C The file name of the attachment. =item C C A short string describing the attachment. Also returned as C, for backwards-compatibility with older Bugzillas. (However, this backwards-compatibility will go away in Bugzilla 5.0.) =item C C The MIME type of the attachment. =item C C True if the attachment is private (only visible to a certain group called the "insidergroup"), False otherwise. =item C C True if the attachment is obsolete, False otherwise. =item C C True if the attachment is a patch, False otherwise. =item C C The login name of the user that created the attachment. Also returned as C, for backwards-compatibility with older Bugzillas. (However, this backwards-compatibility will go away in Bugzilla 5.0.) =item C An array of hashes containing the information about flags currently set for each attachment. Each flag hash contains the following items: =over =item C C The id of the flag. =item C C The name of the flag. =item C C The type id of the flag. =item C C The timestamp when this flag was originally created. =item C C The timestamp when the flag was last modified. =item C C The current status of the flag. =item C C The login name of the user who created or last modified the flag. =item C C The login name of the user this flag has been requested to be granted or denied. Note, this field is only returned if a requestee is set. =back =back =item B This method can throw all the same errors as L. In addition, it can also throw the following error: =over =item 304 (Auth Failure, Attachment is Private) You specified the id of a private attachment in the C argument, and you are not in the "insider group" that can see private attachments. =back =item B =over =item Added in Bugzilla B<3.6>. =item In Bugzilla B<4.0>, the C return value was renamed to C. =item In Bugzilla B<4.0>, the C return value was renamed to C. =item The C return value was added in Bugzilla B<4.0>. =item In Bugzilla B<4.2>, the C return value was removed (this attribute no longer exists for attachments). =item The C return value was added in Bugzilla B<4.4>. =item The C array was added in Bugzilla B<4.4>. =back =back =head2 comments B =over =item B This allows you to get data about comments, given a list of bugs and/or comment ids. =item B B: At least one of C or C is required. In addition to the parameters below, this method also accepts the standard L and L arguments. =over =item C C An array that can contain both bug IDs and bug aliases. All of the comments (that are visible to you) will be returned for the specified bugs. =item C C An array of integer comment_ids. These comments will be returned individually, separate from any other comments in their respective bugs. =item C C If specified, the method will only return comments I than this time. This only affects comments returned from the C argument. You will always be returned all comments you request in the C argument, even if they are older than this date. =back =item B Two items are returned: =over =item C This is used for bugs specified in C. This is a hash, where the keys are the numeric ids of the bugs, and the value is a hash with a single key, C, which is an array of comments. (The format of comments is described below.) Note that any individual bug will only be returned once, so if you specify an id multiple times in C, it will still only be returned once. =item C Each individual comment requested in C is returned here, in a hash where the numeric comment id is the key, and the value is the comment. (The format of comments is described below.) =back A "comment" as described above is a hash that contains the following keys: =over =item id C The globally unique ID for the comment. =item bug_id C The ID of the bug that this comment is on. =item attachment_id C If the comment was made on an attachment, this will be the ID of that attachment. Otherwise it will be null. =item count C The number of the comment local to the bug. The Description is 0, comments start with 1. =item text C The actual text of the comment. =item creator C The login name of the comment's author. Also returned as C, for backwards-compatibility with older Bugzillas. (However, this backwards-compatibility will go away in Bugzilla 5.0.) =item time C The time (in Bugzilla's timezone) that the comment was added. =item creation_time C This is exactly same as the C