From 9bd3f08dce23acc052819d97d1f082666c354b20 Mon Sep 17 00:00:00 2001 From: Simon Green Date: Thu, 22 May 2014 09:17:46 +1000 Subject: Bug 1008764: Add a web service to create and update Flag types r=glob, a=justdave --- Bugzilla/FlagType.pm | 16 +- Bugzilla/WebService.pm | 2 + Bugzilla/WebService/Constants.pm | 13 +- Bugzilla/WebService/FlagType.pm | 647 +++++++++++++++++++++ .../WebService/Server/REST/Resources/FlagType.pm | 56 ++ buglist.cgi | 47 +- template/en/default/list/edit-multiple.html.tmpl | 7 +- 7 files changed, 777 insertions(+), 11 deletions(-) create mode 100644 Bugzilla/WebService/FlagType.pm create mode 100644 Bugzilla/WebService/Server/REST/Resources/FlagType.pm diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index 773996b2e..34973684a 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -644,9 +644,19 @@ sub sqlify_criteria { my @criteria = ("1=1"); if ($criteria->{name}) { - my $name = $dbh->quote($criteria->{name}); - trick_taint($name); # Detaint data as we have quoted it. - push(@criteria, "flagtypes.name = $name"); + if (ref($criteria->{name}) eq 'ARRAY') { + my @names = map { $dbh->quote($_) } @{$criteria->{name}}; + # Detaint data as we have quoted it. + foreach my $name (@names) { + trick_taint($name); + } + push @criteria, $dbh->sql_in('flagtypes.name', \@names); + } + else { + my $name = $dbh->quote($criteria->{name}); + trick_taint($name); # Detaint data as we have quoted it. + push(@criteria, "flagtypes.name = $name"); + } } if ($criteria->{target_type}) { # The target type is stored in the database as a one-character string diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index ebad7930a..1dc04c1f6 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -365,6 +365,8 @@ objects. =item L +=item L + =item L =item L diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index e18e2b8ec..5164ec0c1 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -81,8 +81,9 @@ use constant WS_ERROR_CODE => { illegal_field => 104, freetext_too_long => 104, # Component errors - require_component => 105, - component_name_too_long => 105, + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, # Invalid Product no_products => 106, entry_access_denied => 106, @@ -191,6 +192,13 @@ use constant WS_ERROR_CODE => { # Search errors are 1000-1100 buglist_parameters_required => 1000, + # Flag type errors are 1100-1200 + flag_type_name_invalid => 1101, + flag_type_description_invalid => 1102, + flag_type_cc_list_invalid => 1103, + flag_type_sortkey_invalid => 1104, + flag_type_not_editable => 1105, + # Errors thrown by the WebService itself. The ones that are negative # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php xmlrpc_invalid_value => -32600, @@ -269,6 +277,7 @@ sub WS_DISPATCH { 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', 'Bug' => 'Bugzilla::WebService::Bug', 'Classification' => 'Bugzilla::WebService::Classification', + 'FlagType' => 'Bugzilla::WebService::FlagType', 'Group' => 'Bugzilla::WebService::Group', 'Product' => 'Bugzilla::WebService::Product', 'User' => 'Bugzilla::WebService::User', diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm new file mode 100644 index 000000000..d755b8885 --- /dev/null +++ b/Bugzilla/WebService/FlagType.pm @@ -0,0 +1,647 @@ +# 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::FlagType; + +use 5.10.1; +use strict; + +use parent qw(Bugzilla::WebService); +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::FlagType; +use Bugzilla::Product; +use Bugzilla::Util qw(trim); + +use List::MoreUtils qw(uniq); + +sub create { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + Bugzilla->user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "add", + object => "flagtypes" }); + + $params->{name} || ThrowCodeError('param_required', { param => 'name' }); + $params->{description} || ThrowCodeError('param_required', { param => 'description' }); + + my %args = ( + sortkey => 1, + name => undef, + inclusions => ['0:0'], # Default to __ALL__:__ALL__ + cc_list => '', + description => undef, + is_requestable => 'on', + exclusions => [], + is_multiplicable => 'on', + request_group => '', + is_active => 'on', + is_specifically_requestable => 'on', + target_type => 'bug', + grant_group => '', + ); + + foreach my $key (keys %args) { + $args{$key} = $params->{$key} if defined($params->{$key}); + } + + $args{name} = trim($params->{name}); + $args{description} = trim($params->{description}); + + # Is specifically requestable is actually is_requesteeable + if (exists $args{is_specifically_requestable}) { + $args{is_requesteeble} = delete $args{is_specifically_requestable}; + } + + # Default is on for the tickbox flags. + # If the user has set them to 'off' then undefine them so the flags are not ticked + foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) { + if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { + $args{$arg_name} = undef; + } + } + + # Process group inclusions and exclusions + $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions}; + $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions}; + + my $flagtype = Bugzilla::FlagType->create(\%args); + + return { id => $self->type('int', $flagtype->id) }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + Bugzilla->login(LOGIN_REQUIRED); + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "edit", + object => "flagtypes" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'FlagType.update', params => ['ids', 'names'] }); + + # Get the list of unique flag type ids we are updating + my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); + if (defined $params->{names}) { + push @flag_type_ids, map { $_->id } + @{ Bugzilla::FlagType::match({ name => $params->{names} }) }; + } + @flag_type_ids = uniq @flag_type_ids; + + # We delete names and ids to keep only new values to set. + delete $params->{names}; + delete $params->{ids}; + + # Process group inclusions and exclusions + # We removed them from $params because these are handled differently + my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions}; + my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions}; + + $dbh->bz_start_transaction(); + my %changes = (); + + foreach my $flag_type_id (@flag_type_ids) { + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id); + + if ($can_fully_edit) { + $flagtype->set_all($params); + } + elsif (scalar keys %$params) { + ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); + } + + # Process the clusions + foreach my $type ('inclusions', 'exclusions') { + my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; + next if not defined $clusions; + + my @extra_clusions = (); + if (!$user->in_group('editcomponents')) { + my $products = $user->get_products_by_permission('editcomponents'); + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->$type}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; + } + } + + $flagtype->set_clusions({ + $type => [@$clusions, @extra_clusions], + }); + } + + my $returned_changes = $flagtype->update(); + $changes{$flagtype->id} = { + name => $flagtype->name, + changes => $returned_changes, + }; + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $flag_type_id (keys %changes) { + my %hash = ( + id => $self->type('int', $flag_type_id), + name => $self->type('string', $changes{$flag_type_id}{name}), + changes => {}, + ); + + foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) { + my $change = $changes{$flag_type_id}{changes}{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + + push(@result, \%hash); + } + + return { flagtypes => \@result }; +} + +sub _process_lists { + my $list = shift; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + my @component_list; + + foreach my $item (@$list) { + # A hash with products as the key and component names as the values + if(ref($item) eq 'HASH') { + while (my ($product_name, $component_names) = each %$item) { + my $product = Bugzilla::Product->check({name => $product_name}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', { name => $product_name }); + } + my @component_ids; + + foreach my $comp_name (@$component_names) { + my $component = Bugzilla::Component->check({product => $product, name => $comp_name}); + ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component; + push @component_list, $product->id . ':' . $component->id; + } + } + } + elsif(!ref($item)) { + # These are whole products + my $product = Bugzilla::Product->check({name => $item}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', { name => $item }); + } + push @component_list, $product->id . ':0'; + } + else { + # The user has passed something invalid + ThrowCodeError('param_invalid', { param => $item }); + } + } + + return \@component_list; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::WebService::FlagType - API for creating flags. + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to create new flags + +=head1 METHODS + +See L for a description of what B, B, +and B mean, and for more description about error codes. + +=head2 Create Flag + +=over + +=item C B + +=item B + +Creates a new FlagType + +=item B + +POST /rest/flagtype + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B + +At a minimum the following two arguments must be supplied: + +=over + +=item C (string) - The name of the new Flag Type. + +=item C (string) - A description for the Flag Type object. + +=back + +=item B + +C flag_id + +The ID of the new FlagType object is returned. + +=item B + +=over + +=item name B + +C A short name identifying this type. + +=item description B + +C A comprehensive description of this type. + +=item inclusions B + +An array of strings or a hash containing product names, and optionally +component names. If you provide a string, the flag type will be shown on +all bugs in that product. If you provide a hash, the key represents the +product name, and the value is the components of the product to be included. + +For example: + + [ 'FooProduct', + { + BarProduct => [ 'C1', 'C3' ], + BazProduct => [ 'C7' ] + } + ] + +This flag will be added to B components of I, +components C1 and C3 of I, and C7 of I. + +=item exclusions B + +An array of strings or hashes containing product names. This uses the same +fromat as inclusions. + +This will exclude the flag from all products and components specified. + +=item sortkey B + +C A number between 1 and 32767 by which this type will be sorted when +displayed to users in a list; ignore if you don't care what order the types +appear in or if you want them to appear in alphabetical order. + +=item is_active B + +C Flag of this type appear in the UI and can be set. Default is B. + +=item is_requestable B + +C Users can ask for flags of this type to be set. Default is B. + +=item cc_list B + +C An array of strings. If the flag type is requestable, who should +receive e-mail notification of requests. This is an array of e-mail addresses +which do not need to be Bugzilla logins. + +=item is_specifically_requestable B + +C Users can ask specific other users to set flags of this type as +opposed to just asking the wind. Default is B. + +=item is_multiplicable B + +C Multiple flags of this type can be set on the same bug. Default is B. + +=item grant_group B + +C The group allowed to grant/deny flags of this type (to allow all +users to grant/deny these flags, select no group). Default is B. + +=item request_group B + +C If flags of this type are requestable, the group allowed to request +them (to allow all users to request these flags, select no group). Note that +the request group alone has no effect if the grant group is not defined! +Default is B. + +=back + +=item B + +=over + +=item 51 (Group Does Not Exist) + +The group name you entered does not exist, or you do not have access to it. + +=item 105 (Unknown component) + +The component does not exist for this product. + +=item 106 (Product Access Denied) + +Either the product does not exist or you don't have editcomponents privileges +to it. + +=item 501 (Illegal Email Address) + +One of the e-mail address in the CC list is invalid. An e-mail in the CC +list does NOT need to be a valid Bugzilla user. + +=item 1101 (Flag Type Name invalid) + +You must specify a non-blank name for this flag type. It must +no contain spaces or commas, and must be 50 characters or less. + +=item 1102 (Flag type must have description) + +You must specify a description for this flag type. + +=item 1103 (Flag type CC list is invalid + +The CC list must be 200 characters or less. + +=item 1104 (Flag Type Sort Key Not Valid) + +The sort key is not a valid number. + +=item 1105 (Flag Type Not Editable) + +This flag type is not available for the products you can administer. Therefore +you can not edit attributes of the flag type, other than the inclusion and +exclusion list. + +=back + +=item B + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +B + +=over + +=item B + +This allows you to update a flag type in Bugzilla. + +=item B + +PUT /rest/flagtype/ + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C and C params will be overridden as +it is pulled from the URL path. + +=item B + +B The following parameters specify which products you are updating. +You must set one or both of these parameters. + +=over + +=item C + +C of Cs. Numeric ids of the flag types that you wish to update. + +=item C + +C of Cs. Names of the flag types that you wish to update. If +many flag types have the same name, this will change ALL of them. + +=back + +B The following parameters specify the new values you want to set for +the products you are updating. + +=over + +=item name + +C A short name identifying this type. + +=item description + +C A comprehensive description of this type. + +=item inclusions B + +An array of strings or a hash containing product names, and optionally +component names. If you provide a string, the flag type will be shown on +all bugs in that product. If you provide a hash, the key represents the +product name, and the value is the components of the product to be included. + +for example + + [ 'FooProduct', + { + BarProduct => [ 'C1', 'C3' ], + BazProduct => [ 'C7' ] + } + ] + +This flag will be added to B components of I, +components C1 and C3 of I, and C7 of I. + +=item exclusions B + +An array of strings or hashes containing product names. +This uses the same fromat as inclusions. + +This will exclude the flag from all products and components specified. + +=item sortkey + +C A number between 1 and 32767 by which this type will be sorted when +displayed to users in a list; ignore if you don't care what order the types +appear in or if you want them to appear in alphabetical order. + +=item is_active + +C Flag of this type appear in the UI and can be set. + +=item is_requestable + +C Users can ask for flags of this type to be set. + +=item cc_list + +C An array of strings. If the flag type is requestable, who should +receive e-mail notification of requests. This is an array of e-mail addresses +which do not need to be Bugzilla logins. + +=item is_specifically_requestable + +C Users can ask specific other users to set flags of this type as +opposed to just asking the wind. + +=item is_multiplicable + +C Multiple flags of this type can be set on the same bug. + +=item grant_group + +C The group allowed to grant/deny flags of this type (to allow all +users to grant/deny these flags, select no group). + +=item request_group + +C If flags of this type are requestable, the group allowed to request +them (to allow all users to request these flags, select no group). Note that +the request group alone has no effect if the grant group is not defined! + +=back + +=item B + +A C with a single field "flagtypes". This points to an array of hashes +with the following fields: + +=over + +=item C + +C The id of the product that was updated. + +=item C + +C The name of the product that was updated. + +=item C + +C The changes that were actually done on this product. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C + +C The value that this field was changed to. + +=item C + +C The value that was previously set in this field. + +=back + +Note that booleans will be represented with the strings '1' and '0'. + +Here's an example of what a return value might look like: + + { + products => [ + { + id => 123, + changes => { + name => { + removed => 'FooFlagType', + added => 'BarFlagType' + }, + is_requestable => { + removed => '1', + added => '0', + } + } + } + ] + } + +=back + +=item B + +=over + +=item 51 (Group Does Not Exist) + +The group name you entered does not exist, or you do not have access to it. + +=item 105 (Unknown component) + +The component does not exist for this product. + +=item 106 (Product Access Denied) + +Either the product does not exist or you don't have editcomponents privileges +to it. + +=item 501 (Illegal Email Address) + +One of the e-mail address in the CC list is invalid. An e-mail in the CC +list does NOT need to be a valid Bugzilla user. + +=item 1101 (Flag Type Name invalid) + +You must specify a non-blank name for this flag type. It must +no contain spaces or commas, and must be 50 characters or less. + +=item 1102 (Flag type must have description) + +You must specify a description for this flag type. + +=item 1103 (Flag type CC list is invalid + +The CC list must be 200 characters or less. + +=item 1104 (Flag Type Sort Key Not Valid) + +The sort key is not a valid number. + +=item 1105 (Flag Type Not Editable) + +This flag type is not available for the products you can administer. Therefore +you can not edit attributes of the flag type, other than the inclusion and +exclusion list. + +=back + +=item B + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm new file mode 100644 index 000000000..745785838 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm @@ -0,0 +1,56 @@ +# 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::Server::REST::Resources::FlagType; + +use 5.10.1; +use strict; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::FlagType; + +use Bugzilla::Error; + +BEGIN { + *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/flagtype$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/flagtype/([^/]+)$}, { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + }, + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::FlagType - The Flag Type REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to create and update Flag types. + +See L for more details on how to use this +part of the REST API. diff --git a/buglist.cgi b/buglist.cgi index e0a4a6aaa..d88939171 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -259,7 +259,7 @@ sub GetGroups { my %legal_groups; foreach my $product_name (@$product_names) { - my $product = new Bugzilla::Product({name => $product_name}); + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); foreach my $gid (keys %{$product->group_controls}) { # The user can only edit groups they belong to. @@ -874,7 +874,7 @@ $vars->{'time_info'} = $time_info; if (!$user->in_group('editbugs')) { foreach my $product (keys %$bugproducts) { - my $prod = new Bugzilla::Product({name => $product}); + my $prod = Bugzilla::Product->new({name => $product, cache => 1}); if (!$user->in_group('editbugs', $prod->id)) { $vars->{'caneditbugs'} = 0; last; @@ -905,12 +905,12 @@ $vars->{'currenttime'} = localtime(time()); my @products = keys %$bugproducts; my $one_product; if (scalar(@products) == 1) { - $one_product = new Bugzilla::Product({ name => $products[0] }); + $one_product = Bugzilla::Product->new({ name => $products[0], cache => 1 }); } # This is used in the "Zarroo Boogs" case. elsif (my @product_input = $cgi->param('product')) { if (scalar(@product_input) == 1 and $product_input[0] ne '') { - $one_product = new Bugzilla::Product({ name => $cgi->param('product') }); + $one_product = Bugzilla::Product->new({ name => $cgi->param('product'), cache => 1 }); } } # We only want the template to use it if the user can actually @@ -993,10 +993,47 @@ if ($dotweak && scalar @bugs) { $vars->{'versions'} = [map($_->name, grep($_->is_active, @{ $one_product->versions }))]; $vars->{'components'} = [map($_->name, grep($_->is_active, @{ $one_product->components }))]; if (Bugzilla->params->{'usetargetmilestone'}) { - $vars->{'targetmilestones'} = [map($_->name, grep($_->is_active, + $vars->{'milestones'} = [map($_->name, grep($_->is_active, @{ $one_product->milestones }))]; } } + else { + # We will only show the values at are active in all products. + my %values = (); + my @fields = ('components', 'versions'); + if (Bugzilla->params->{'usetargetmilestone'}) { + push @fields, 'milestones'; + } + + # Go through each product and count the number of times each field + # is used + foreach my $product_name (@products) { + my $product = Bugzilla::Product->new({name => $product_name, cache => 1}); + foreach my $field (@fields) { + my $list = $product->$field; + foreach my $item (@$list) { + ++$values{$field}{$item->name} if $item->is_active; + } + } + } + + # Now we get the list of each field and see which values have + # $product_count (i.e. appears in every product) + my $product_count = scalar(@products); + foreach my $field (@fields) { + my @values = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}}; + if (scalar @values) { + @{$vars->{$field}} = $field eq 'version' + ? sort { vers_cmp(lc($a), lc($b)) } @values + : sort { lc($a) cmp lc($b) } @values + } + + # Do we need to show a warning about limited visiblity? + if (@values != scalar keys %{$values{$field}}) { + $vars->{excluded_values} = 1; + } + } + } } # If we're editing a stored query, use the existing query name as default for diff --git a/template/en/default/list/edit-multiple.html.tmpl b/template/en/default/list/edit-multiple.html.tmpl index 5adc47a59..144ae10c9 100644 --- a/template/en/default/list/edit-multiple.html.tmpl +++ b/template/en/default/list/edit-multiple.html.tmpl @@ -44,6 +44,11 @@ +[% IF excluded_values %] +

Only values that are available for all products of the above + [%+ terms.bugs %] are shown.

+[% END %] + @@ -124,7 +129,7 @@ [% END %] -- cgit v1.2.3-24-g4f1b
[% PROCESS selectmenu menuname = "target_milestone" - menuitems = targetmilestones %] + menuitems = milestones %]