diff --git a/Bugzilla/API/V1/BugField.pm b/Bugzilla/API/V1/BugField.pm new file mode 100644 index 0000000000..22d1233f07 --- /dev/null +++ b/Bugzilla/API/V1/BugField.pm @@ -0,0 +1,384 @@ +# 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::API::V1::BugField; + +use 5.10.1; +use Mojo::Base qw( Mojolicious::Controller ); + +use List::MoreUtils qw(uniq); +use Mojo::JSON qw(true false); + +use Bugzilla::Bug; +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Field; +use Bugzilla::FlagType; +use Bugzilla::Keyword; +use Bugzilla::Milestone; +use Bugzilla::Product; +use Bugzilla::Status; +use Bugzilla::Version; +use Bugzilla::WebService::Util qw(filter); + +use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); + +sub setup_routes { + my ($class, $r) = @_; + my $routes + = $r->under('/' => sub { Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); }); + + my @routes = ( + ['/field/bug' => 'fields'], + ['/field/bug/#id_or_name' => 'fields'], + ['/field/bug/#field/values' => 'legal_values'], + ['/field/bug/#field/#product_id/values' => 'legal_values'], + ['/flag_types/#product' => 'flag_types'], + ['/flag_types/#product/#component' => 'flag_types'], + ); + foreach my $route (@routes) { + my ($path, $action) = @$route; + $routes->get($path)->to("V1::BugField#$action"); + $routes->options($path)->to('V1::BugField#options'); + } +} + +sub options { + my ($self) = @_; + + $self->res->headers->header('Allow' => 'GET'); + $self->res->headers->header('Access-Control-Allow-Methods' => 'GET'); + + return $self->rendered(200); +} + +# GET /field/bug and /field/bug/ +sub fields { + my ($self) = @_; + + $self->bugzilla->login; + + my $params = {}; + for my $name (qw(ids names include_fields exclude_fields)) { + my $values = $self->every_param($name); + $params->{$name} = $values if @$values; + } + + for my $name (qw(include_fields exclude_fields)) { + $params->{$name} = [map { split(/[\s,]+/) } @{$params->{$name}}] + if exists $params->{$name}; + } + + # As on the legacy REST server, an id or name in the path is added to the + # ones from the query string. + if (defined(my $id_or_name = $self->stash('id_or_name'))) { + my $key = $id_or_name =~ /^\d+$/ ? 'ids' : 'names'; + $params->{$key} = [uniq($id_or_name, @{$params->{$key} || []})]; + } + + Bugzilla->switch_to_shadow_db(); + + my @fields; + if (defined $params->{ids}) { + foreach my $id (@{$params->{ids}}) { + push(@fields, Bugzilla::Field->check({id => $id})); + } + } + + if (defined $params->{names}) { + foreach my $field_name (@{$params->{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 = @{_legal_field_values($field)}; + } + + if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { + $value_field = 'product'; + } + + my %field_data = ( + id => 0 + $field->id, + type => 0 + $field->type, + is_custom => $field->custom ? true : false, + name => $field->name, + display_name => $field->description, + is_mandatory => $field->is_mandatory ? true : false, + is_on_bug_entry => $field->enter_bug ? true : false, + visibility_field => $visibility_field, + visibility_values => [map { $_->name } @$vis_values], + ); + if ($has_values) { + $field_data{value_field} = $value_field; + $field_data{values} = \@values; + } + push(@fields_out, filter $params, \%field_data); + } + + return $self->render(json => {fields => \@fields_out}); +} + +sub _legal_field_values { + my ($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' ? 0 + $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 => $value->name, + sort_key => $sortkey, + sortkey => $sortkey, # deprecated + visibility_values => [$product_name], + } + ); + } + } + } + + elsif ($field_name eq 'bug_status') { + my @status_all = Bugzilla::Status->get_all; + 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; + push( + @can_change_to, + { + name => $change_to->name, + comment_required => $change_to->comment_required_on_change_from($status) + ? true + : false, + } + ); + } + + push( + @result, + { + name => $status->name, + is_open => $status->is_open ? true : false, + sort_key => 0 + $status->sortkey, + sortkey => 0 + $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) { + next unless $value->is_active; + push(@result, {name => $value->name, description => $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 => $value->name, + sort_key => 0 + $value->sortkey, + sortkey => 0 + $value->sortkey, # deprecated + visibility_values => [defined $vis_val ? $vis_val->name : ()], + } + ); + } + } + + return \@result; +} + +# GET /field/bug//values and /field/bug///values +sub legal_values { + my ($self) = @_; + + my $user = $self->bugzilla->login; + + Bugzilla->switch_to_shadow_db(); + + my $field_param = $self->stash('field'); + my $field = Bugzilla::Bug::FIELD_MAP->{$field_param} || $field_param; + + 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. + $values = get_legal_field_values($field); + } + elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { + + # The path value wins over a product_id query parameter, as on the legacy + # REST server. + my $id = $self->param('product_id'); + defined $id + || return $self->code_error('param_required', + {function => 'Bug.legal_values', param => 'product_id'}); + grep($_->id eq $id, @{$user->get_accessible_products}) + || return $self->user_error('product_access_denied', {id => $id}); + + my $product = Bugzilla::Product->new($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 { + return $self->code_error('invalid_field_name', {field => $field_param}); + } + + return $self->render(json => {values => [map {"$_"} @$values]}); +} + +# GET /flag_types/ and /flag_types// +sub flag_types { + my ($self) = @_; + + $self->bugzilla->login; + + Bugzilla->switch_to_shadow_db(); + + # A component query parameter is used when the path has none, as on the + # legacy REST server. + my $product = $self->stash('product'); + my $component = $self->param('component'); + + $product = Bugzilla::Product->check({name => $product, cache => 1}); + $component + = Bugzilla::Component->check( + {name => $component, product => $product, cache => 1}) + if $component; + + my $flag_params = {product_id => $product->id}; + $flag_params->{component_id} = $component->id if $component; + my $matched_flag_types = Bugzilla::FlagType::match($flag_params); + + my $flag_types = {bug => [], attachment => []}; + foreach my $flag_type (@$matched_flag_types) { + push( + @{$flag_types->{$flag_type->target_type}}, + _flagtype_to_hash($flag_type, $product) + ) if exists $flag_types->{$flag_type->target_type}; + } + + return $self->render(json => $flag_types); +} + +sub _flagtype_to_hash { + my ($flagtype, $product) = @_; + my $user = Bugzilla->user; + + my @values = ('X'); + push(@values, '?') + if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); + push(@values, '+', '-') if $user->can_set_flag($flagtype); + + my $item = { + id => 0 + $flagtype->id, + name => $flagtype->name, + description => $flagtype->description, + type => $flagtype->target_type, + values => \@values, + is_active => $flagtype->is_active ? true : false, + is_requesteeble => $flagtype->is_requesteeble ? true : false, + is_multiplicable => $flagtype->is_multiplicable ? true : false, + }; + + my $inclusions + = _flagtype_clusions_to_hash($flagtype->inclusions, $product->id); + my $exclusions + = _flagtype_clusions_to_hash($flagtype->exclusions, $product->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + + return $item; +} + +sub _flagtype_clusions_to_hash { + my ($clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } + } + } + return $result; +} + +1; diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 264d2bfed3..a094dde9df 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -17,7 +17,6 @@ use Bugzilla::Comment; use Bugzilla::Comment::TagWeights; use Bugzilla::Constants; use Bugzilla::Error; -use Bugzilla::Field; use Bugzilla::Logging; use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util @@ -25,13 +24,9 @@ use Bugzilla::WebService::Util use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trim detaint_natural remote_ip); -use Bugzilla::Version; -use Bugzilla::Milestone; -use Bugzilla::Status; use Bugzilla::Token qw(issue_hash_token); use Bugzilla::Search; use Bugzilla::Product; -use Bugzilla::FlagType; use Bugzilla::Search::Quicksearch; use List::Util qw(max); @@ -45,8 +40,6 @@ use Type::Utils; # Constants # ############# -use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); - sub DATE_FIELDS { my $fields = { comments => ['new_since'], @@ -73,10 +66,8 @@ use constant BASE64_FIELDS => {add_attachment => ['data'],}; use constant READ_ONLY => qw( attachments comments - fields get history - legal_values search ); @@ -86,10 +77,8 @@ use constant PUBLIC_METHODS => qw( attachments comments create - fields get history - legal_values possible_duplicates render_comment search @@ -140,180 +129,6 @@ BEGIN { # 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)], - } - ); - } - } - } - - elsif ($field_name eq 'bug_status') { - my @status_all = Bugzilla::Status->get_all; - 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) { - next unless $value->is_active; - 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'); @@ -1010,58 +825,6 @@ sub create { 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. - $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; @@ -1444,41 +1207,6 @@ sub attachments { return {bugs => \%bugs, attachments => \%attachments}; } -sub flag_types { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - defined $params->{product} - || ThrowCodeError('param_required', - {function => 'Bug.flag_types', param => 'product'}); - - my $product = delete $params->{product}; - my $component = delete $params->{component}; - - $product = Bugzilla::Product->check({name => $product, cache => 1}); - $component - = Bugzilla::Component->check( - {name => $component, product => $product, cache => 1}) - if $component; - - my $flag_params = {product_id => $product->id}; - $flag_params->{component_id} = $component->id if $component; - my $matched_flag_types = Bugzilla::FlagType::match($flag_params); - - my $flag_types = {bug => [], attachment => []}; - foreach my $flag_type (@$matched_flag_types) { - push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'bug'; - push( - @{$flag_types->{attachment}}, - $self->_flagtype_to_hash($flag_type, $product) - ) if $flag_type->target_type eq 'attachment'; - } - - return $flag_types; -} - sub _get_comment_reactions_with_users { my ($self, $comment) = @_; my $reactions_with_users = $comment->reactions_with_users(); @@ -2034,61 +1762,6 @@ sub _flag_to_hash { return $item; } -sub _flagtype_to_hash { - my ($self, $flagtype, $product) = @_; - my $user = Bugzilla->user; - - my @values = ('X'); - push(@values, '?') - if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); - push(@values, '+', '-') if $user->can_set_flag($flagtype); - - my $item = { - id => $self->type('int', $flagtype->id), - name => $self->type('string', $flagtype->name), - description => $self->type('string', $flagtype->description), - type => $self->type('string', $flagtype->target_type), - values => \@values, - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) - }; - - if ($product) { - my $inclusions - = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); - my $exclusions - = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); - - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - - # no need to return anything if there's just "any component" - $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $item; -} - -sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); - push @$result, $component->name; - } - else { - return ['']; - } - } - } - return $result; -} - sub _add_update_tokens { my ($self, $params, $bugs, $hashes) = @_; @@ -2124,393 +1797,6 @@ Although the data input and output is the same for JSON-RPC and REST, the directions for how to access the data via REST is noted in each method where applicable. -=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 have several options for retreiving information about fields. The first -part is the request method and the rest is the related path needed. - -To get information about all fields: - -GET /field/bug - -To get information related to a single field: - -GET /field/bug/ - -The returned data format is the same as below. - -=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 -C 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 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>. - -=back - -=item REST API call added in Bugzilla B<5.0> - -=back - -=head2 flag_types - -B - -=over - -=item B - -Get information about valid flag types that can be set for bugs and attachments. - -=item B - -You have several options for retreiving information about flag types. The first -part is the request method and the rest is the related path needed. - -To get information about all flag types for a product: - -GET /flag_types/ - -To get information about flag_types for a product and component: - -GET /flag_types// - -The returned data format is the same as below. - -=item B - -You must pass a product name and an optional component name. - -=over - -=item C (string) - The name of a valid product. - -=item C (string) - An optional valid component name associated with the product. - -=back - -=item B - -A hash containing two keys, C and C. Each key value is an array of hashes, -containing the following keys: - -=over - -=item C - -C An integer id uniquely identifying this flag type. - -=item C - -C The name for the flag type. - -=item C - -C The target of the flag type which is either C or C. - -=item C - -C The description of the flag type. - -=item C - -C An array of string values that the user can set on the flag type. - -=item C - -C Users can ask specific other users to set flags of this type. - -=item C - -C Multiple flags of this type can be set for the same bug or attachment. - -=back - -=item B - -=over - -=item 106 (Product Access Denied) - -Either the product does not exist or you don't have access to it. - -=item 51 (Invalid Component) - -The component provided does not exist in the product. - -=back - -=item B - -=over - -=item Added in Bugzilla B<5.0>. - -=back - -=back - -=head2 legal_values - -B - Use L instead. - -=over - -=item B - -Tells you what values are allowed for a particular field. - -=item B - -To get information on the values for a field based on field name: - -GET /field/bug//values - -To get information based on field name and a specific product: - -GET /field/bug///values - -The returned data format is the same as below. - -=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 - -=item B - -=over - -=item REST API call added in Bugzilla B<5.0>. - -=back - -=back - =head1 Bug Information =head2 attachments diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm index 77a9ccafd0..3e622050ad 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -146,56 +146,6 @@ sub _rest_resources { return {ids => [$_[0]]}; } } - }, - qr{^/field/bug$}, - {GET => {method => 'fields',}}, - qr{^/field/bug/([^/]+)$}, - { - GET => { - method => 'fields', - params => sub { - my $value = $_[0]; - my $param = 'names'; - $param = 'ids' if $value =~ /^\d+$/; - return {$param => [$_[0]]}; - } - } - }, - qr{^/field/bug/([^/]+)/values$}, - { - GET => { - method => 'legal_values', - params => sub { - return {field => $_[0]}; - } - } - }, - qr{^/field/bug/([^/]+)/([^/]+)/values$}, - { - GET => { - method => 'legal_values', - params => sub { - return {field => $_[0], product_id => $_[1]}; - } - } - }, - qr{^/flag_types/([^/]+)/([^/]+)$}, - { - GET => { - method => 'flag_types', - params => sub { - return {product => $_[0], component => $_[1]}; - } - } - }, - qr{^/flag_types/([^/]+)$}, - { - GET => { - method => 'flag_types', - params => sub { - return {product => $_[0]}; - } - } } ]; return $rest_resources; diff --git a/qa/t/rest_bug_legal_values.t b/qa/t/rest_bug_legal_values.t index 7f663f177b..1dced2b1e6 100644 --- a/qa/t/rest_bug_legal_values.t +++ b/qa/t/rest_bug_legal_values.t @@ -118,4 +118,9 @@ foreach my $test (@all_tests) { } } +$t->get_ok($url . "rest/field/bug/version/values?product_id=$public_product" => + api_headers(undef))->status_is(200); +cmp_ok(scalar @{$t->tx->res->json->{values}}, + '>', 0, 'product_id can be passed as a query parameter'); + done_testing(); diff --git a/qa/t/rest_field.t b/qa/t/rest_field.t index ce95f4b8c6..e02b75cd58 100644 --- a/qa/t/rest_field.t +++ b/qa/t/rest_field.t @@ -191,4 +191,23 @@ foreach my $test (@all_tests) { } } +# A field in the path is combined with the ones from the query string. +$t->get_ok( + $url . 'rest/field/bug/bug_status?names=priority' => api_headers(undef)) + ->status_is(200); +is_deeply( + [sort map { $_->{name} } @{$t->tx->res->json->{fields}}], + ['bug_status', 'priority'], + 'Field names from the path and the query string are combined' +); + +$t->get_ok( + $url . 'rest/field/bug/priority?include_fields=id,name' => api_headers(undef)) + ->status_is(200); +is_deeply( + [sort keys %{$t->tx->res->json->{fields}[0]}], + ['id', 'name'], + 'include_fields filters the returned keys' +); + done_testing(); diff --git a/qa/t/rest_flag_types.t b/qa/t/rest_flag_types.t new file mode 100644 index 0000000000..3627e4838b --- /dev/null +++ b/qa/t/rest_flag_types.t @@ -0,0 +1,83 @@ +#!/usr/bin/env perl +# 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. + +##################################################### +# Test for REST call to Bug.flag_types() # +# GET /rest/flag_types/ # +# GET /rest/flag_types// # +##################################################### + +use 5.10.1; +use strict; +use warnings; +use lib qw(lib ../../lib ../../local/lib/perl5); + +use Bugzilla; +use List::Util qw(first); +use QA::Util qw(get_config); +use QA::REST::Util qw(api_headers); + +use Test::Mojo; +use Test::More; + +# spec_multi_flag is created by generate_test_data.pl for 'Another Product:c1' +# only, with editbugs as grant group and no request group. +use constant FLAG_NAME => 'spec_multi_flag'; + +my $config = get_config(); +my $admin_api_key = $config->{admin_user_api_key}; +my $url = Bugzilla->localconfig->urlbase; + +my $t = Test::Mojo->new(); +$t->ua->max_redirects(1); + +sub get_flag { + my ($path, $api_key) = @_; + $t->get_ok($url . "rest/flag_types/$path" => api_headers($api_key)) + ->status_is(200) + ->json_has('/bug') + ->json_has('/attachment'); + return first { $_->{name} eq FLAG_NAME } @{$t->tx->res->json->{bug}}; +} + +my $flag = get_flag('Another Product'); +ok($flag, FLAG_NAME . ' is returned for its product'); +like($flag->{id}, qr/^\d+$/, 'id is numeric'); +is($flag->{type}, 'bug', 'type is bug'); +ok($flag->{is_requesteeble}, 'is_requesteeble is true'); +ok($flag->{is_multiplicable}, 'is_multiplicable is true'); +is_deeply($flag->{inclusions}, ['c1'], 'inclusions lists the component'); +is_deeply($flag->{values}, ['X', '?'], 'Logged-out user can only request'); + +$flag = get_flag('Another Product', $admin_api_key); +is_deeply($flag->{values}, ['X', '?', '+', '-'], 'editbugs user can also set'); + +ok(get_flag('Another Product/c1'), FLAG_NAME . ' is returned for c1'); +ok(!get_flag('Another Product/c2'), FLAG_NAME . ' is not returned for c2'); +ok( + get_flag('Another Product?component=c1'), + 'component can be passed as a query parameter' +); +ok( + !get_flag('Another Product/c2?component=c1'), + 'component in the path wins over the query parameter' +); + +$t->get_ok($url . 'rest/flag_types/No Such Product' => api_headers(undef)) + ->status_isnt(200) + ->json_like('/message' => qr/does not exist/); +$t->get_ok($url + . 'rest/flag_types/Another Product/No Such Component' => api_headers(undef)) + ->status_isnt(200) + ->json_like('/message' => qr/No Such Component/); + +$t->options_ok($url . 'rest/flag_types/Another Product/c1') + ->status_is(200) + ->header_is('Allow' => 'GET'); + +done_testing();