diff options
Diffstat (limited to 'extensions/BzAPI/lib/Resources')
-rw-r--r-- | extensions/BzAPI/lib/Resources/Bug.pm | 860 | ||||
-rw-r--r-- | extensions/BzAPI/lib/Resources/Bugzilla.pm | 118 | ||||
-rw-r--r-- | extensions/BzAPI/lib/Resources/User.pm | 79 |
3 files changed, 1057 insertions, 0 deletions
diff --git a/extensions/BzAPI/lib/Resources/Bug.pm b/extensions/BzAPI/lib/Resources/Bug.pm new file mode 100644 index 000000000..eed439a00 --- /dev/null +++ b/extensions/BzAPI/lib/Resources/Bug.pm @@ -0,0 +1,860 @@ +# 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::Extension::BzAPI::Resources::Bug; + +use 5.10.1; +use strict; + +use Bugzilla::Bug; +use Bugzilla::Error; +use Bugzilla::Token qw(issue_hash_token); +use Bugzilla::Util qw(trick_taint diff_arrays); +use Bugzilla::WebService::Constants; + +use Bugzilla::Extension::BzAPI::Util; +use Bugzilla::Extension::BzAPI::Constants; + +use List::MoreUtils qw(uniq); +use List::Util qw(max); + +################# +# REST Handlers # +################# + +BEGIN { + require Bugzilla::WebService::Bug; + *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource; +} + +sub rest_handlers { + my $rest_handlers = [ + qr{^/bug$}, { + GET => { + request => \&search_bugs_request, + response => \&search_bugs_response + }, + POST => { + request => \&create_bug_request, + response => \&create_bug_response + } + }, + qr{^/bug/([^/]+)$}, { + GET => { + response => \&get_bug_response + }, + PUT => { + request => \&update_bug_request, + response => \&update_bug_response + } + }, + qr{^/bug/([^/]+)/comment$}, { + GET => { + response => \&get_comments_response + }, + POST => { + request => \&add_comment_request, + response => \&add_comment_response + } + }, + qr{^/bug/([^/]+)/history$}, { + GET => { + response => \&get_history_response + } + }, + qr{^/bug/([^/]+)/attachment$}, { + GET => { + response => \&get_attachments_response + }, + POST => { + request => \&add_attachment_request, + response => \&add_attachment_response + } + }, + qr{^/bug/attachment/([^/]+)$}, { + GET => { + response => \&get_attachment_response + }, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/attachment/([^/]+)$}, { + GET => { + response => \&get_attachment_response + }, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/bug/([^/]+)/flag$}, { + GET => { + resource => { + method => 'get', + params => sub { + return { ids => [ $_[0] ], + include_fields => ['flags'] }; + } + }, + response => \&get_bug_flags_response, + } + }, + qr{^/count$}, { + GET => { + resource => { + method => 'get_bug_count' + } + } + }, + qr{^/attachment/([^/]+)$}, { + GET => { + resource => { + method => 'attachments', + params => sub { + return { attachment_ids => [ $_[0] ] }; + } + } + }, + PUT => { + resource => { + method => 'update_attachment', + params => sub { + return { ids => [ $_[0] ] }; + } + } + } + } + ]; + return $rest_handlers; +} + +######################### +# REST Resource Methods # +######################### + +# Return bug counts based on row/col/table fields +# FIXME Borrowed a lot of code from report.cgi, eventually +# this should be broken into it's own module so that report.cgi +# and here can share the same code. +sub get_bug_count_resource { + my ($self, $params) = @_; + + Bugzilla->switch_to_shadow_db(); + + my $col_field = $params->{x_axis_field} || ''; + my $row_field = $params->{y_axis_field} || ''; + my $tbl_field = $params->{z_axis_field} || ''; + + my $dimensions = $col_field ? + $row_field ? + $tbl_field ? 3 : 2 : 1 : 0; + + if ($dimensions == 0) { + $col_field = "bug_status"; + $params->{x_axis_field} = "bug_status"; + } + + # Valid bug fields that can be reported on. + my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; + + # Convert external names to internal if necessary + $params = Bugzilla::Bug::map_fields($params); + $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field; + $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field; + $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field; + + # Validate the values in the axis fields or throw an error. + !$row_field + || ($valid_columns->{$row_field} && trick_taint($row_field)) + || ThrowCodeError("report_axis_invalid", { fld => "x", val => $row_field }); + !$col_field + || ($valid_columns->{$col_field} && trick_taint($col_field)) + || ThrowCodeError("report_axis_invalid", { fld => "y", val => $col_field }); + !$tbl_field + || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) + || ThrowCodeError("report_axis_invalid", { fld => "z", val => $tbl_field }); + + my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field); + + my $search = new Bugzilla::Search( + fields => \@axis_fields, + params => $params, + allow_unlimited => 1, + ); + + my ($results, $extra_data) = $search->data; + + # We have a hash of hashes for the data itself, and a hash to hold the + # row/col/table names. + my %data; + my %names; + + # Read the bug data and count the bugs for each possible value of row, column + # and table. + # + # We detect a numerical field, and sort appropriately, if all the values are + # numeric. + my $col_isnumeric = 1; + my $row_isnumeric = 1; + my $tbl_isnumeric = 1; + + foreach my $result (@$results) { + # handle empty dimension member names + my $row = check_value($row_field, $result); + my $col = check_value($col_field, $result); + my $tbl = check_value($tbl_field, $result); + + $data{$tbl}{$col}{$row}++; + $names{"col"}{$col}++; + $names{"row"}{$row}++; + $names{"tbl"}{$tbl}++; + + $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); + $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); + $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); + } + + my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); + my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); + my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); + + push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); + + my @data; + foreach my $tbl (@tbl_names) { + my @tbl_data; + foreach my $row (@row_names) { + my @col_data; + foreach my $col (@col_names) { + $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; + push(@col_data, $data{$tbl}{$col}{$row}); + if ($tbl ne "-total-") { + # This is a bit sneaky. We spend every loop except the last + # building up the -total- data, and then last time round, + # we process it as another tbl, and push() the total values + # into the image_data array. + $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; + } + } + push(@tbl_data, \@col_data); + } + push(@data, \@tbl_data); + } + + my $result = {}; + if ($dimensions == 0) { + my $sum = 0; + + # If the search returns no results, we just get an 0-byte file back + # and so there is no data at all. + if (@data) { + foreach my $value (@{ $data[0][0] }) { + $sum += $value; + } + } + + $result = { + 'data' => $sum + }; + } + elsif ($dimensions == 1) { + $result = { + 'x_labels' => \@col_names, + 'data' => $data[0][0] || [] + }; + } + elsif ($dimensions == 2) { + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'data' => $data[0] || [[]] + }; + } + elsif ($dimensions == 3) { + if (@data > 1 && $tbl_names[-1] eq "-total-") { + # Last table is a total, which we discard + pop(@data); + pop(@tbl_names); + } + + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'z_labels' => \@tbl_names, + 'data' => @data ? \@data : [[[]]] + }; + } + + return $result; +} + +sub get_names { + my ($names, $isnumeric, $field_name) = @_; + my ($field, @sorted); + # XXX - This is a hack to handle the actual_time/work_time field, + # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. + $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; + + # _realname fields aren't real Bugzilla::Field objects, but they are a + # valid axis, so we don't vailidate them as Bugzilla::Field objects. + $field = Bugzilla::Field->check($field_name) + if ($field_name && $field_name !~ /_realname$/); + + if ($field && $field->is_select) { + foreach my $value (@{$field->legal_values}) { + push(@sorted, $value->name) if $names->{$value->name}; + } + unshift(@sorted, '---') if $field_name eq 'resolution'; + @sorted = uniq @sorted; + } + elsif ($isnumeric) { + # It's not a field we are preserving the order of, so sort it + # numerically... + @sorted = sort { $a <=> $b } keys %$names; + } + else { + # ...or alphabetically, as appropriate. + @sorted = sort keys %$names; + } + + return @sorted; +} + +sub check_value { + my ($field, $result) = @_; + + my $value; + if (!defined $field) { + $value = ''; + } + elsif ($field eq '') { + $value = ' '; + } + else { + $value = shift @$result; + $value = ' ' if (!defined $value || $value eq ''); + $value = '---' if ($field eq 'resolution' && $value eq ' '); + } + return $value; +} + +######################## +# REST Request Methods # +######################## + +sub search_bugs_request { + my ($params) = @_; + + if (defined $params->{changed_field} + && $params->{changed_field} eq "creation_time") + { + $params->{changed_field} = "[Bug creation]"; + } + + my $FIELD_NEW_TO_OLD = { reverse %{ BUG_FIELD_MAP() } }; + + # Update values of various forms. + foreach my $key (keys %$params) { + # First, search types. These are found in the value of any field ending + # _type, and the value of any field matching type\d-\d-\d. + if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) { + $params->{$key} + = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key}; + } + + # Field names hiding in values instead of keys: changed_field, boolean + # charts and axis names. + if ($key =~ /^(field\d+-\d+-\d+| + changed_field| + (x|y|z)_axis_field)$ + /x) { + $params->{$key} + = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key}; + } + } + + # Update field names + foreach my $field (keys %$FIELD_NEW_TO_OLD) { + if (defined $params->{$field}) { + $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field}; + } + } + + if (exists $params->{bug_id_type}) { + $params->{bug_id_type} + = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type}; + } + + # Time field names are screwy, and got reused. We can't put this mapping + # in NEW2OLD as everything will go haywire. actual_time has to be queried + # as work_time even though work_time is the submit-only field for _adding_ + # to actual_time, which can't be arbitrarily manipulated. + if (defined $params->{work_time}) { + $params->{actual_time} = delete $params->{work_time}; + } + + # Other convenience search ariables used by BzAPI + my @field_ids = grep(/^f(\d+)$/, keys %$params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + foreach my $field (qw(setters.login_name requestees.login_name)) { + if (my $value = delete $params->{$field}) { + $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field; + $params->{"o${last_field_id}"} = 'equals'; + $params->{"v${last_field_id}"} = $value; + $last_field_id++; + } + } +} + +sub create_bug_request { + my ($params) = @_; + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to set to. + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + $params->{$field} = $params->{$field}->{name}; + } + } + + # CC should just be a list of bugzilla logins + if (exists $params->{cc}) { + $params->{cc} = [ map { $_->{name} } @{ $params->{cc} } ]; + } + + # Comment + if (exists $params->{comments}) { + $params->{comment_is_private} = $params->{comments}->[0]->{is_private}; + $params->{description} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } + + # Some fields are not supported by Bugzilla::Bug->create but are supported + # by Bugzilla::Bug->update :( + my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {}; + foreach my $field (qw(remaining_time)) { + next if !exists $params->{$field}; + $cache->{$field} = delete $params->{$field}; + } + + # remove username/password + delete $params->{username}; + delete $params->{password}; +} + +sub update_bug_request { + my ($params) = @_; + + my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids}; + my $bug = Bugzilla::Bug->check($bug_id); + + # Convert groups to proper add/remove lists + if (exists $params->{groups}) { + my @new_groups = map { $_->{name} } @{ $params->{groups} }; + my @old_groups = map { $_->name } @{ $bug->groups_in }; + my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups); + if (@$added || @$removed) { + my $groups_data = {}; + $groups_data->{add} = $added if @$added; + $groups_data->{remove} = $removed if @$removed; + $params->{groups} = $groups_data; + } + else { + delete $params->{groups}; + } + } + + # Other fields such as keywords, blocks depends_on + # support 'set' which will make the list exactly what + # the user passes in. + foreach my $field (qw(blocks depends_on dependson keywords)) { + if (exists $params->{$field}) { + $params->{$field} = { set => $params->{$field} }; + } + } + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to change to. Also if defined + # but set to NULL then we reset them to default + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + if (!$params->{$field}) { + $params->{"reset_$field"} = 1; + delete $params->{$field}; + } + else { + $params->{$field} = $params->{$field}->{name}; + } + } + } + + # CC is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{cc}) { + my $new_cc = [ map { $_->{name} } @{ $params->{cc} } ]; + my ($removed, $added) = diff_arrays($bug->cc, $new_cc); + if (@$added || @$removed) { + my $cc_data = {}; + $cc_data->{add} = $added if @$added; + $cc_data->{remove} = $removed if @$removed; + $params->{cc} = $cc_data; + } + else { + delete $params->{cc}; + } + } + + # see_also is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{see_also}) { + my $old_see_also = [ map { $_->name } @{ $bug->see_also } ]; + my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also}); + if (@$added || @$removed) { + my $data = {}; + $data->{add} = $added if @$added; + $data->{remove} = $removed if @$removed; + $params->{see_also} = $data; + } + else { + delete $params->{see_also}; + } + } + + # BzAPI allows for adding comments by appending to the list of current + # comments and passing the whole list back. + # 1. If a comment id is specified, the user can update the comment privacy + # 2. If no id is specified it is considered a new comment but only the last + # one will be accepted. + my %comment_is_private; + foreach my $comment (@{ $params->{'comments'} }) { + if (my $id = $comment->{'id'}) { + # Existing comment; tweak privacy flags if necessary + $comment_is_private{$id} + = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0; + } + else { + # New comment to be added + # If multiple new comments are specified, only the last one will be + # added. + $params->{comment} = { + body => $comment->{text}, + is_private => ($comment->{'is_private'} && + $comment->{'is_private'} eq "true") ? 1 : 0 + }; + } + } + $params->{comment_is_private} = \%comment_is_private if %comment_is_private; + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{ $params->{flags} }) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + # If no flag id provided, assume it is new + if (!exists $flag->{id}) { + $flag->{new} = 1; + } + } + } +} + +sub add_comment_request { + my ($params) = @_; + $params->{comment} = delete $params->{text} if $params->{text}; +} + +sub add_attachment_request { + my ($params) = @_; + + # Bug.add_attachment uses 'summary' for description. + if ($params->{description}) { + $params->{summary} = $params->{description}; + delete $params->{description}; + } + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{ $params->{flags} }) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } +} + +sub update_attachment_request { + my ($params) = @_; + + # Stash away for midair checking later + if ($params->{last_change_time}) { + my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; + $stash->{last_change_time} = delete $params->{last_change_time}; + } + + # Immutable values + foreach my $key (qw(attacher bug_id bug_ref creation_time + encoding id ref size update_token)) { + delete $params->{$key}; + } + + # Convert setter and requestee to standard values + if (exists $params->{flags}) { + foreach my $flag (@{ $params->{flags} }) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } + # Add comment if one is provided + if ($params->{comments}) { + $params->{comment} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } +} + +######################### +# REST Response Methods # +######################### + +sub search_bugs_response { + my ($result, $response) = @_; + my $cache = Bugzilla->request_cache; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + + my $bug_objs = $cache->{bzapi_search_bugs}; + + my @fixed_bugs; + foreach my $bug_data (@{$$result->{bugs}}) { + my $bug_obj = shift @$bug_objs; + my $fixed = fix_bug($bug_data, $bug_obj); + + # CC count and Dupe count + if (filter_wants_nocache($params, 'cc_count')) { + $fixed->{cc_count} = scalar @{ $bug_obj->cc } + if $bug_obj->cc; + } + if (filter_wants_nocache($params, 'dupe_count')) { + $fixed->{dupe_count} = scalar @{ $bug_obj->duplicate_ids } + if $bug_obj->duplicate_ids; + } + + push(@fixed_bugs, $fixed); + } + + $$result->{bugs} = \@fixed_bugs; +} + +sub create_bug_response { + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + return if !exists $$result->{id}; + my $bug_id = $$result->{id}; + + $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id") }; + $response->code(STATUS_CREATED); +} + +sub get_bug_response { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + return if !exists $$result->{bugs}; + my $bug_data = $$result->{bugs}->[0]; + + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug_obj = Bugzilla::Bug->check($bug_id); + my $fixed = fix_bug($bug_data, $bug_obj); + + $$result = $fixed; +} + +sub update_bug_response { + my ($result) = @_; + return if !exists $$result->{bugs} + || !scalar @{$$result->{bugs}}; + $$result = { ok => 1 }; +} + +# Get all comments for a bug +sub get_comments_response { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); + + my $comment_objs = $bug->comments({ order => 'oldest_to_newest', + after => $params->{new_since} }); + my @filtered_comment_objs; + foreach my $comment (@$comment_objs) { + next if $comment->is_private && !Bugzilla->user->is_insider; + push(@filtered_comment_objs, $comment); + } + + my $comments_data = $$result->{bugs}->{$bug_id}->{comments}; + + my @fixed_comments; + foreach my $comment_data (@$comments_data) { + my $comment_obj = shift @filtered_comment_objs; + my $fixed = fix_comment($comment_data, $comment_obj); + + if (exists $fixed->{creator}) { + # /bug/<ID>/comment returns full login for creator but not for /bug/<ID>?include_fields=comments :( + $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login); + # /bug/<ID>/comment does not return real_name for creator but returns ref + $fixed->{creator}->{'ref'} = $rpc->type('string', ref_urlbase() . "/user/" . $comment_obj->author->login); + delete $fixed->{creator}->{real_name}; + } + + push(@fixed_comments, filter($params, $fixed)); + } + + $$result = { comments => \@fixed_comments }; +} + +# Format the return response on successful comment creation +sub add_comment_response { + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + return if !exists $$result->{id}; + my $bug_id = $rpc->bz_rest_params->{id}; + + $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment") }; + $response->code(STATUS_CREATED); +} + +# Get the history for a bug +sub get_history_response { + my ($result) = @_; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + my $history = $$result->{bugs}->[0]->{history}; + + my @new_history; + foreach my $changeset (@$history) { + $changeset = fix_changeset($changeset); + push(@new_history, filter($params, $changeset)); + } + + $$result = { history => \@new_history }; +} + +# Get all attachments for a bug +sub get_attachments_response { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); + my $attachment_objs = $bug->attachments; + + my $attachments_data = $$result->{bugs}->{$bug_id}; + + my @fixed_attachments; + foreach my $attachment (@$attachments_data) { + my $attachment_obj = shift @$attachment_objs; + my $fixed = fix_attachment($attachment, $attachment_obj); + + if ((filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata})) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; + } + + push(@fixed_attachments, filter($params, $fixed)); + } + + $$result = { attachments => \@fixed_attachments }; +} + +# Format the return response on successful attachment creation +sub add_attachment_response { + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + my ($attach_id) = keys %{ $$result->{attachments} }; + + $$result = { ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), id => $attach_id }; + $response->code(STATUS_CREATED); +} + +# Update an attachment's metadata +sub update_attachment_response { + my ($result) = @_; + $$result = { ok => 1 }; +} + +# Get a single attachment by attachment_id +sub get_attachment_response { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{attachments}; + my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0]; + my $attachment_data = $$result->{attachments}->{$attach_id}; + my $attachment_obj = Bugzilla::Attachment->new($attach_id); + my $fixed = fix_attachment($attachment_data, $attachment_obj); + + if ((filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata})) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; + } + + $fixed = filter($params, $fixed); + + $$result = $fixed; +} + +# Get a list of flags for a bug +sub get_bug_flags_response { + my ($result) = @_; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + my $flags = $$result->{bugs}->[0]->{flags}; + + my @new_flags; + foreach my $flag (@$flags) { + push(@new_flags, fix_flag($flag)); + } + + $$result = { flags => \@new_flags }; +} + +1; diff --git a/extensions/BzAPI/lib/Resources/Bugzilla.pm b/extensions/BzAPI/lib/Resources/Bugzilla.pm new file mode 100644 index 000000000..6c436e861 --- /dev/null +++ b/extensions/BzAPI/lib/Resources/Bugzilla.pm @@ -0,0 +1,118 @@ +# 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::Extension::BzAPI::Resources::Bugzilla; + +use 5.10.1; +use strict; + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Keyword; +use Bugzilla::Product; +use Bugzilla::Status; +use Bugzilla::Field; + +use Digest::MD5 qw(md5_base64); + +######################### +# REST Resource Methods # +######################### + +BEGIN { + require Bugzilla::WebService::Bugzilla; + *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration; +} + +sub rest_handlers { + my $rest_handlers = [ + qr{^/configuration$}, { + GET => { + resource => { + method => 'get_configuration' + } + } + } + ]; + return $rest_handlers; +} + +sub get_configuration { + my ($self) = @_; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + + # Get data from the shadow DB as they don't change very often. + Bugzilla->switch_to_shadow_db; + + # Pass a bunch of Bugzilla configuration to the templates. + my $vars = {}; + $vars->{'priority'} = get_legal_field_values('priority'); + $vars->{'severity'} = get_legal_field_values('bug_severity'); + $vars->{'platform'} = get_legal_field_values('rep_platform'); + $vars->{'op_sys'} = get_legal_field_values('op_sys'); + $vars->{'keyword'} = [ map($_->name, Bugzilla::Keyword->get_all) ]; + $vars->{'resolution'} = get_legal_field_values('resolution'); + $vars->{'status'} = get_legal_field_values('bug_status'); + $vars->{'custom_fields'} = + [ grep {$_->is_select} Bugzilla->active_custom_fields ]; + + # Include a list of product objects. + if ($params->{'product'}) { + my @products = $params->{'product'}; + foreach my $product_name (@products) { + my $product = new Bugzilla::Product({ name => $product_name }); + if ($product && $user->can_see_product($product->name)) { + push (@{$vars->{'products'}}, $product); + } + } + } else { + $vars->{'products'} = $user->get_selectable_products; + } + + # We set the 2nd argument to 1 to also preload flag types. + Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 }); + + # Allow consumers to specify whether or not they want flag data. + if (defined $params->{'flags'}) { + $vars->{'show_flags'} = $params->{'flags'}; + } + else { + # We default to sending flag data. + $vars->{'show_flags'} = 1; + } + + # Create separate lists of open versus resolved statuses. This should really + # be made part of the configuration. + my @open_status; + my @closed_status; + foreach my $status (@{$vars->{'status'}}) { + is_open_state($status) ? push(@open_status, $status) + : push(@closed_status, $status); + } + $vars->{'open_status'} = \@open_status; + $vars->{'closed_status'} = \@closed_status; + + # Generate a list of fields that can be queried. + my @fields = @{Bugzilla::Field->match({obsolete => 0})}; + # Exclude fields the user cannot query. + if (!Bugzilla->user->is_timetracker) { + @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields; + } + $vars->{'field'} = \@fields; + + my $json; + Bugzilla->template->process('config.json.tmpl', $vars, \$json); + my $result = {}; + if ($json) { + $result = $self->json->decode($json); + } + return $result; +} + +1; diff --git a/extensions/BzAPI/lib/Resources/User.pm b/extensions/BzAPI/lib/Resources/User.pm new file mode 100644 index 000000000..7fbcdb871 --- /dev/null +++ b/extensions/BzAPI/lib/Resources/User.pm @@ -0,0 +1,79 @@ +# 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::Extension::BzAPI::Resources::User; + +use 5.10.1; +use strict; + +use Bugzilla::Extension::BzAPI::Util; + +sub rest_handlers { + my $rest_handlers = [ + qr{/user$}, { + GET => { + response => \&get_users, + }, + }, + qr{/user/([^/]+)$}, { + GET => { + response => \&get_user, + }, + } + ]; + return $rest_handlers; +} + +sub get_users { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{users}; + + my @users; + foreach my $user (@{$$result->{users}}) { + my $object = Bugzilla::User->new( + { id => $user->{id}, cache => 1 }); + + $user = fix_user($user, $object); + + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); + + push(@users, filter($params, $user)); + } + + $$result->{users} = \@users; +} + +sub get_user { + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{users}; + my $user = $$result->{users}->[0] || return; + my $object = Bugzilla::User->new({ id => $user->{id}, cache => 1 }); + + $user = fix_user($user, $object); + + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); + + $user = filter($params, $user); + + $$result = $user; +} + +1; |