diff options
author | David Lawrence <dkl@mozilla.com> | 2015-04-08 19:48:36 +0200 |
---|---|---|
committer | David Lawrence <dkl@mozilla.com> | 2015-04-08 19:48:36 +0200 |
commit | dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a (patch) | |
tree | aa190d8cc9e2b313dd7e85bf763c1bfe5502b75d /Bugzilla | |
parent | e6d2fb75aa3c183323c534a214f3dd9be5638676 (diff) | |
download | bugzilla-dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a.tar.gz bugzilla-dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a.tar.xz |
Bug 1051056: The REST API needs to be versioned so that new changes can be made that do not break compatibility
r=dylan,a=glob
Diffstat (limited to 'Bugzilla')
26 files changed, 12373 insertions, 1395 deletions
diff --git a/Bugzilla/API/1_0/Constants.pm b/Bugzilla/API/1_0/Constants.pm new file mode 100644 index 000000000..caf670c7b --- /dev/null +++ b/Bugzilla/API/1_0/Constants.pm @@ -0,0 +1,311 @@ +# 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::1_0::Constants; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Hook; + +use parent qw(Exporter); + +our @EXPORT = qw( + WS_ERROR_CODE + + STATUS_OK + STATUS_CREATED + STATUS_ACCEPTED + STATUS_NO_CONTENT + STATUS_MULTIPLE_CHOICES + STATUS_BAD_REQUEST + STATUS_NOT_FOUND + STATUS_GONE + REST_STATUS_CODE_MAP + + ERROR_UNKNOWN_FATAL + ERROR_UNKNOWN_TRANSIENT + + REST_CONTENT_TYPE_WHITELIST + + API_AUTH_HEADERS +); + +# This maps the error names in global/*-error.html.tmpl to numbers. +# Generally, transient errors should have a number above 0, and +# fatal errors should have a number below 0. +# +# This hash should generally contain any error that could be thrown +# by the WebService interface. If it's extremely unlikely that the +# error could be thrown (like some CodeErrors), it doesn't have to +# be listed here. +# +# "Transient" means "If you resubmit that request with different data, +# it may work." +# +# "Fatal" means, "There's something wrong with Bugzilla, probably +# something an administrator would have to fix." +# +# NOTE: Numbers must never be recycled. If you remove a number, leave a +# comment that it was retired. Also, if an error changes its name, you'll +# have to fix it here. +use constant WS_ERROR_CODE => { + # Generic errors (Bugzilla::Object and others) are 50-9 + object_not_specified => 50, + reassign_to_empty => 50, + param_required => 50, + params_required => 50, + undefined_field => 50, + object_does_not_exist => 51, + param_must_be_numeric => 52, + number_not_numeric => 52, + param_invalid => 53, + number_too_large => 54, + number_too_small => 55, + illegal_date => 56, + # Bug errors usually occupy the 100-200 range. + improper_bug_id_field_value => 100, + bug_id_does_not_exist => 101, + bug_access_denied => 102, + bug_access_query => 102, + # These all mean "invalid alias" + alias_too_long => 103, + alias_in_use => 103, + alias_is_numeric => 103, + alias_has_comma_or_space => 103, + multiple_alias_not_allowed => 103, + # Misc. bug field errors + illegal_field => 104, + freetext_too_long => 104, + # Component errors + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, + # Invalid Product + no_products => 106, + entry_access_denied => 106, + product_access_denied => 106, + product_disabled => 106, + # Invalid Summary + require_summary => 107, + # Invalid field name + invalid_field_name => 108, + # Not authorized to edit the bug + product_edit_denied => 109, + # Comment-related errors + comment_is_private => 110, + comment_id_invalid => 111, + comment_too_long => 114, + comment_invalid_isprivate => 117, + markdown_disabled => 140, + # Comment tagging + comment_tag_disabled => 125, + comment_tag_invalid => 126, + comment_tag_too_long => 127, + comment_tag_too_short => 128, + # See Also errors + bug_url_invalid => 112, + bug_url_too_long => 112, + # Insidergroup Errors + user_not_insider => 113, + # Note: 114 is above in the Comment-related section. + # Bug update errors + illegal_change => 115, + # Dependency errors + dependency_loop_single => 116, + dependency_loop_multi => 116, + # Note: 117 is above in the Comment-related section. + # Dup errors + dupe_loop_detected => 118, + dupe_id_required => 119, + # Bug-related group errors + group_invalid_removal => 120, + group_restriction_not_allowed => 120, + # Status/Resolution errors + missing_resolution => 121, + resolution_not_allowed => 122, + illegal_bug_status_transition => 123, + # Flag errors + flag_status_invalid => 129, + flag_update_denied => 130, + flag_type_requestee_disabled => 131, + flag_not_unique => 132, + flag_type_not_unique => 133, + flag_type_inactive => 134, + + # Authentication errors are usually 300-400. + invalid_login_or_password => 300, + account_disabled => 301, + auth_invalid_email => 302, + extern_id_conflict => -303, + auth_failure => 304, + password_too_short => 305, + password_not_complex => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, + + # Except, historically, AUTH_NODATA, which is 410. + login_required => 410, + + # User errors are 500-600. + account_exists => 500, + illegal_email_address => 501, + auth_cant_create_account => 501, + account_creation_disabled => 501, + account_creation_restricted => 501, + password_too_short => 502, + # Error 503 password_too_long no longer exists. + invalid_username => 504, + # This is from strict_isolation, but it also basically means + # "invalid user." + invalid_user_group => 504, + user_access_by_id_denied => 505, + user_access_by_match_denied => 505, + + # Attachment errors are 600-700. + file_too_large => 600, + invalid_content_type => 601, + # Error 602 attachment_illegal_url no longer exists. + file_not_specified => 603, + missing_attachment_description => 604, + # Error 605 attachment_url_disabled no longer exists. + zero_length_file => 606, + + # Product erros are 700-800 + product_blank_name => 700, + product_name_too_long => 701, + product_name_already_in_use => 702, + product_name_diff_in_case => 702, + product_must_have_description => 703, + product_must_have_version => 704, + product_must_define_defaultmilestone => 705, + product_admin_denied => 706, + + # Group errors are 800-900 + empty_group_name => 800, + group_exists => 801, + empty_group_description => 802, + invalid_regexp => 803, + invalid_group_name => 804, + group_cannot_view => 805, + + # Classification errors are 900-1000 + auth_classification_not_enabled => 900, + + # 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, + + # Component errors are 1200-1300 + component_already_exists => 1200, + component_is_last => 1201, + component_has_bugs => 1202, + component_blank_name => 1210, + component_blank_description => 1211, + multiple_components_update_not_allowed => 1212, + component_need_initialowner => 1213, + + # 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, + unknown_method => -32601, + json_rpc_post_only => 32610, + json_rpc_invalid_callback => 32611, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, +}; + +# RESTful webservices use the http status code +# to describe whether a call was successful or +# to describe the type of error that occurred. +use constant STATUS_OK => 200; +use constant STATUS_CREATED => 201; +use constant STATUS_ACCEPTED => 202; +use constant STATUS_NO_CONTENT => 204; +use constant STATUS_MULTIPLE_CHOICES => 300; +use constant STATUS_BAD_REQUEST => 400; +use constant STATUS_NOT_AUTHORIZED => 401; +use constant STATUS_NOT_FOUND => 404; +use constant STATUS_GONE => 410; + +# The integer value is the error code above returned by +# the related webvservice call. We choose the appropriate +# http status code based on the error code or use the +# default STATUS_BAD_REQUEST. +sub REST_STATUS_CODE_MAP { + my $status_code_map = { + 51 => STATUS_NOT_FOUND, + 101 => STATUS_NOT_FOUND, + 102 => STATUS_NOT_AUTHORIZED, + 106 => STATUS_NOT_AUTHORIZED, + 109 => STATUS_NOT_AUTHORIZED, + 110 => STATUS_NOT_AUTHORIZED, + 113 => STATUS_NOT_AUTHORIZED, + 115 => STATUS_NOT_AUTHORIZED, + 120 => STATUS_NOT_AUTHORIZED, + 300 => STATUS_NOT_AUTHORIZED, + 301 => STATUS_NOT_AUTHORIZED, + 302 => STATUS_NOT_AUTHORIZED, + 303 => STATUS_NOT_AUTHORIZED, + 304 => STATUS_NOT_AUTHORIZED, + 410 => STATUS_NOT_AUTHORIZED, + 504 => STATUS_NOT_AUTHORIZED, + 505 => STATUS_NOT_AUTHORIZED, + 32614 => STATUS_NOT_FOUND, + _default => STATUS_BAD_REQUEST + }; + + Bugzilla::Hook::process('webservice_status_code_map', + { status_code_map => $status_code_map }); + + return $status_code_map; +}; + +# These are the fallback defaults for errors not in ERROR_CODE. +use constant ERROR_UNKNOWN_FATAL => -32000; +use constant ERROR_UNKNOWN_TRANSIENT => 32000; + +use constant ERROR_GENERAL => 999; + +# The first content type specified is used as the default. +use constant REST_CONTENT_TYPE_WHITELIST => qw( + application/json + application/javascript + text/javascript + text/html +); + +# Custom HTTP headers that can be used for API authentication rather than +# passing as URL parameters. This is useful if you do not want sensitive +# information to show up in webserver log files. +use constant API_AUTH_HEADERS => { + X_BUGZILLA_LOGIN => 'Bugzilla_login', + X_BUGZILLA_PASSWORD => 'Bugzilla_password', + X_BUGZILLA_API_KEY => 'Bugzilla_api_key', + X_BUGZILLA_TOKEN => 'Bugzilla_token', +}; + +1; + +=head1 B<Methods in need of POD> + +=over + +=item REST_STATUS_CODE_MAP + +=item WS_DISPATCH + +=back diff --git a/Bugzilla/API/1_0/Resource.pm b/Bugzilla/API/1_0/Resource.pm new file mode 100644 index 000000000..9881d3713 --- /dev/null +++ b/Bugzilla/API/1_0/Resource.pm @@ -0,0 +1,147 @@ +# 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. + +# This is the base class for $self in WebService API method calls. For the +# actual RPC server, see Bugzilla::API::Server and its subclasses. + +package Bugzilla::API::1_0::Resource; + +use 5.10.1; +use strict; +use warnings; + +use Moo; + +##################### +# Default Constants # +##################### + +# Used by the server to convert incoming date fields apprpriately. +use constant DATE_FIELDS => {}; + +# Used by the server to convert incoming base64 fields appropriately. +use constant BASE64_FIELDS => {}; + +# For some methods, we shouldn't call Bugzilla->login before we call them +use constant LOGIN_EXEMPT => { }; + +# Used to allow methods to be called in the JSON-RPC WebService via GET. +# Methods that can modify data MUST not be listed here. +use constant READ_ONLY => (); + +# Whitelist of methods that a client is allowed to access when making +# an API call. +use constant PUBLIC_METHODS => (); + +# Array of path mappings for method names for the API. Also describes +# how path values are mapped to method parameters values. +use constant REST_RESOURCES => []; + +################## +# Public Methods # +################## + +sub login_exempt { + my ($class, $method) = @_; + return $class->LOGIN_EXEMPT->{$method}; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource - The Web Service Resource interface to Bugzilla + +=head1 DESCRIPTION + +This is the standard API for external programs that want to interact +with Bugzilla. It provides endpoints or methods in various modules. + +You can interact with this API via L<REST|Bugzilla::API::1_0::Server>. + +=head1 CALLING METHODS + +Methods are grouped into "packages", like C<Bug> for +L<Bugzilla::API::1_0::Resource::Bug>. So, for example, +L<Bugzilla::API::1_0::Resource::Bug/get>, is called as C<Bug.get>. + +For REST, the "package" is more determined by the path used to access the +resource. See each relevant method for specific details on how to access via REST. + +=head1 USAGE + +Full documentation on how to use the Bugzilla API can be found at +L<https://bugzilla.readthedocs.org/en/latest/api/index.html>. + +=head1 ERRORS + +If a particular API call fails, it will throw an error in the appropriate format +providing at least a numeric error code and descriptive text for the error. + +The various errors that functions can throw are specified by the +documentation of those functions. + +Each error that Bugzilla can throw has a specific numeric code that will +not change between versions of Bugzilla. If your code needs to know what +error Bugzilla threw, use the numeric code. Don't try to parse the +description, because that may change from version to version of Bugzilla. + +Note that if you display the error to the user in an HTML program, make +sure that you properly escape the error, as it will not be HTML-escaped. + +=head2 Transient vs. Fatal Errors + +If the error code is a number greater than 0, the error is considered +"transient," which means that it was an error made by the user, not +some problem with Bugzilla itself. + +If the error code is a number less than 0, the error is "fatal," which +means that it's some error in Bugzilla itself that probably requires +administrative attention. + +Negative numbers and positive numbers don't overlap. That is, if there's +an error 302, there won't be an error -302. + +=head2 Unknown Errors + +Sometimes a function will throw an error that doesn't have a specific +error code. In this case, the code will be C<-32000> if it's a "fatal" +error, and C<32000> if it's a "transient" error. + +=head1 SEE ALSO + +=head2 API Resource Modules + +=over + +=item L<Bugzilla::API::1_0::Resource::Bug> + +=item L<Bugzilla::API::1_0::Resource::Bugzilla> + +=item L<Bugzilla::API::1_0::Resource::Classification> + +=item L<Bugzilla::API::1_0::Resource::FlagType> + +=item L<Bugzilla::API::1_0::Resource::Component> + +=item L<Bugzilla::API::1_0::Resource::Group> + +=item L<Bugzilla::API::1_0::Resource::Product> + +=item L<Bugzilla::API::1_0::Resource::User> + +=back + +=head1 B<Methods in need of POD> + +=over + +=item login_exempt + +=back diff --git a/Bugzilla/API/1_0/Resource/Bug.pm b/Bugzilla/API/1_0/Resource/Bug.pm new file mode 100644 index 000000000..c61b2c6c2 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Bug.pm @@ -0,0 +1,4881 @@ +# 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::1_0::Resource::Bug; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Comment; +use Bugzilla::Comment::TagWeights; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Bug; +use Bugzilla::BugMail; +use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural); +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 Moo; +use List::Util qw(max); +use List::MoreUtils qw(uniq); +use Storable qw(dclone); + +extends 'Bugzilla::API::1_0::Resource'; + +############# +# Constants # +############# + +use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); + +use constant DATE_FIELDS => { + comments => ['new_since'], + history => ['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 + search_comment_tags +); + +use constant PUBLIC_METHODS => qw( + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also + update_tags +); + +use constant ATTACHMENT_MAPPED_SETTERS => { + file_name => 'filename', + summary => 'description', +}; + +use constant ATTACHMENT_MAPPED_RETURNS => { + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/bug$}, { + GET => { + method => 'search', + }, + POST => { + method => 'create', + status_code => STATUS_CREATED + } + }, + qr{^/bug/$}, { + GET => { + method => 'get' + } + }, + qr{^/bug/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + return { ids => [ $_[0] ] }; + } + }, + PUT => { + method => 'update', + params => sub { + return { ids => [ $_[0] ] }; + } + } + }, + qr{^/bug/([^/]+)/comment$}, { + GET => { + method => 'comments', + params => sub { + return { ids => [ $_[0] ] }; + } + }, + POST => { + method => 'add_comment', + params => sub { + return { id => $_[0] }; + }, + success_code => STATUS_CREATED + } + }, + qr{^/bug/comment/([^/]+)$}, { + GET => { + method => 'comments', + params => sub { + return { comment_ids => [ $_[0] ] }; + } + } + }, + qr{^/bug/comment/tags/([^/]+)$}, { + GET => { + method => 'search_comment_tags', + params => sub { + return { query => $_[0] }; + }, + }, + }, + qr{^/bug/comment/([^/]+)/tags$}, { + PUT => { + method => 'update_comment_tags', + params => sub { + return { comment_id => $_[0] }; + }, + }, + }, + qr{^/bug/([^/]+)/history$}, { + GET => { + method => 'history', + params => sub { + return { ids => [ $_[0] ] }; + }, + } + }, + qr{^/bug/([^/]+)/attachment$}, { + GET => { + method => 'attachments', + params => sub { + return { ids => [ $_[0] ] }; + } + }, + POST => { + method => 'add_attachment', + params => sub { + return { ids => [ $_[0] ] }; + }, + success_code => STATUS_CREATED + } + }, + qr{^/bug/attachment/([^/]+)$}, { + GET => { + method => 'attachments', + params => sub { + return { attachment_ids => [ $_[0] ] }; + } + }, + PUT => { + method => 'update_attachment', + params => sub { + 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] }; + } + } + }, + ]; + return $rest_resources; +} + +###################################################### +# 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 => as_int($field->id), + type => as_int($field->type), + is_custom => as_boolean($field->custom), + name => as_string($field->name), + display_name => as_string($field->description), + is_mandatory => as_boolean($field->is_mandatory), + is_on_bug_entry => as_boolean($field->enter_bug), + visibility_field => as_string($visibility_field), + visibility_values => as_name_array($vis_values) + ); + if ($has_values) { + $field_data{value_field} = as_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 => as_string($value->name), + sort_key => as_int($sortkey), + sortkey => as_int($sortkey), # deprecated + visibility_values => [ as_string($product_name) ], + is_active => as_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 => as_string($change_to->name), + comment_required => as_boolean( + $change_to->comment_required_on_change_from($status)), + ); + push(@can_change_to, \%change_to_hash); + } + + push (@result, { + name => as_string($status->name), + is_open => as_boolean($status->is_open), + sort_key => as_int($status->sortkey), + sortkey => as_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 => as_string($value->name), + description => as_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 => as_string($value->name), + sort_key => as_int($value->sortkey), + sortkey => as_int($value->sortkey), # deprecated + visibility_values => [ + defined $vis_val ? as_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 }; +} + +sub render_comment { + my ($self, $params) = @_; + + unless (defined $params->{text}) { + ThrowCodeError('params_required', + { function => 'Bug.render_comment', + params => ['text'] }); + } + + Bugzilla->switch_to_shadow_db(); + my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; + + my $markdown = $params->{markdown} ? 1 : 0; + my $tmpl = $markdown ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]' : '[% text FILTER markdown(bug) %]'; + + my $html; + my $template = Bugzilla->template; + $template->process( + \$tmpl, + { bug => $bug, text => $params->{text}}, + \$html + ); + + return { html => $html }; +} + +# Helper for Bug.comments +sub _translate_comment { + my ($self, $comment, $filters, $types, $prefix) = @_; + my $attach_id = $comment->is_about_attachment ? $comment->extra_data + : undef; + + my $comment_hash = { + id => as_int($comment->id), + bug_id => as_int($comment->bug_id), + creator => as_email($comment->author->login), + time => as_datetime($comment->creation_ts), + creation_time => as_datetime($comment->creation_ts), + is_private => as_boolean($comment->is_private), + is_markdown => as_boolean($comment->is_markdown), + text => as_string($comment->body_full), + attachment_id => as_int($attach_id), + count => as_int($comment->count), + }; + + # Don't load comment tags unless enabled + if (Bugzilla->params->{'comment_taggers_group'}) { + $comment_hash->{tags} = as_string_array($comment->tags); + } + + return filter($filters, $comment_hash, $types, $prefix); +} + +sub get { + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + + my (@bugs, @faults, @hashes); + + # Cache permissions for bugs. This highly reduces the number of calls to the DB. + # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. + my @int = grep { $_ =~ /^\d+$/ } @$ids; + Bugzilla->user->visible_bugs(\@int); + + 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, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); + } + + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + Bugzilla->api_server->etag(\@hashes); + + $self->_add_update_tokens($params, \@bugs, \@hashes); + + return { bugs => \@hashes, 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} = as_int($bug_id); + + my ($activity) = $bug->get_activity(undef, $params->{new_since}); + + my @history; + foreach my $changeset (@$activity) { + my %bug_history; + $bug_history{when} = as_datetime($changeset->{when}); + $bug_history{who} = as_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} = as_int($attach_id); + } + $change->{removed} = as_string($change->{removed}); + $change->{added} = as_string($change->{added}); + $change->{field_name} = as_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} = as_string_array($bug->alias); + + push(@return, \%item); + } + + return { bugs => \@return }; +} + +sub search { + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + Bugzilla->switch_to_shadow_db(); + + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; + + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } + + if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { + ThrowCodeError('param_required', + { param => 'limit', function => 'Bug.search()' }); + } + + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; + } + } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = ( fields => ['bug_id'] ); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + + # Do special search types for certain fields. + if (my $change_when = delete $match_params->{'delta_ts'}) { + $match_params->{"f${last_field_id}"} = 'delta_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $change_when; + $last_field_id++; + } + if (my $creation_when = delete $match_params->{'creation_ts'}) { + $match_params->{"f${last_field_id}"} = 'creation_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; + } + + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { + $match_params->{$param . '_type'} = 'allwordssubstr'; + } + } + if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { + next if !exists $match_params->{$role}; + my $value = delete $match_params->{$role}; + $match_params->{"f${last_field_id}"} = $role; + $match_params->{"o${last_field_id}"} = "anywordssubstr"; + $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + # If no other parameters have been passed other than limit and offset + # then we throw error if system is configured to do so. + if (!grep(!/^(limit|offset)$/, keys %$match_params) + && !Bugzilla->params->{search_allow_no_criteria}) + { + ThrowUserError('buglist_parameters_required'); + } + + $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order}; + $options{params} = $match_params; + + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; + + if (!scalar @$data) { + return { bugs => [] }; + } + + # Search.pm won't return bugs that the user shouldn't see so no filtering is needed. + my @bug_ids = map { $_->[0] } @$data; + my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; + my @bugs = map { $bug_objects{$_} } @bug_ids; + @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; + + return { bugs => \@bugs }; +} + +sub possible_duplicates { + my ($self, $params) = validate(@_, 'products'); + 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->{'products'} || [] }) { + 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; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); + 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}; + + # For backwards compatibility, treat alias string or array as a set action + if (exists $values{alias}) { + if (not ref $values{alias}) { + $values{alias} = { set => [ $values{alias} ] }; + } + elsif (ref $values{alias} eq 'ARRAY') { + $values{alias} = { set => $values{alias} }; + } + } + + my $flags = delete $values{flags}; + + foreach my $bug (@bugs) { + $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); + } + } + + 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 => as_int($bug->id), + last_change_time => as_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} = as_string_array($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 => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + + push(@result, \%hash); + } + + return { bugs => \@result }; +} + +sub create { + my ($self, $api, $params) = @_; + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + + $params = Bugzilla::Bug::map_fields($params); + + my $flags = delete $params->{flags}; + + # We start a nested transaction in case flag setting fails + # we want the bug creation to roll back as well. + $dbh->bz_start_transaction(); + + my $bug = Bugzilla::Bug->create($params); + + # Set bug flags + if ($flags) { + my ($flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($flags, $new_flags); + $bug->update($bug->creation_ts); + } + + $dbh->bz_commit_transaction(); + + $bug->send_changes(); + + return { id => as_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, as_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)'); + + my $flags = delete $params->{flags}; + + 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}, + }); + + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); + $attachment->set_flags($old_flags, $new_flags); + } + + $attachment->update($timestamp); + my $comment = $params->{comment} || ''; + + my $is_markdown = 0; + if (ref $params->{comment} eq 'HASH') { + $is_markdown = $params->{comment}->{is_markdown}; + $comment = $params->{comment}->{body}; + } + + ThrowUserError('markdown_disabled') + if $is_markdown && !Bugzilla->user->use_markdown(); + + $attachment->bug->add_comment($comment, + { is_markdown => $is_markdown, + 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 @created_ids = map { $_->id } @created; + + return { ids => \@created_ids }; +} + +sub update_attachment { + my ($self, $params) = validate(@_, 'ids'); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + + # Some fields cannot be sent to set_all + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } + + $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + + # Get all the attachments, after verifying that they exist and are editable + my @attachments = (); + my %bugs = (); + foreach my $id (@$ids) { + my $attachment = Bugzilla::Attachment->new($id) + || ThrowUserError("invalid_attach_id", { attach_id => $id }); + my $bug = $attachment->bug; + $attachment->_check_bug; + $attachment->validate_can_edit + || ThrowUserError("illegal_attachment_edit", { attach_id => $id }); + + push @attachments, $attachment; + $bugs{$bug->id} = $bug; + } + + my $flags = delete $params->{flags}; + my $comment = delete $params->{comment}; + my $is_markdown = 0; + + if (ref $comment eq 'HASH') { + $is_markdown = $comment->{is_markdown}; + $comment = $comment->{body}; + } + + ThrowUserError('markdown_disabled') + if $is_markdown && !$user->use_markdown(); + + # Update the values + foreach my $attachment (@attachments) { + $attachment->set_all($params); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $attachment->bug, $attachment); + $attachment->set_flags($old_flags, $new_flags); + } + } + + $dbh->bz_start_transaction(); + + # Do the actual update and get information to return to user + my @result; + foreach my $attachment (@attachments) { + my $changes = $attachment->update(); + + if ($comment = trim($comment)) { + $attachment->bug->add_comment($comment, + { is_markdown => $is_markdown, + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id }); + } + + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + + my %hash = ( + id => as_int($attachment->id), + last_change_time => as_datetime($attachment->modification_time), + changes => {}, + ); + + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $hash{changes}->{$field} = { + removed => as_string($change->[0] // ''), + added => as_string($change->[1] // '') + }; + } + + push(@result, \%hash); + } + + $dbh->bz_commit_transaction(); + + # Email users about the change + foreach my $bug (values %bugs) { + $bug->update(); + $bug->send_changes(); + } + + # Return the information to the user + return { attachments => \@result }; +} + +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}; + } + + ThrowUserError('markdown_disabled') + if $params->{is_markdown} && !$user->use_markdown(); + + # Append comment + $bug->add_comment($comment, { isprivate => $params->{is_private}, + is_markdown => $params->{is_markdown}, + work_time => $params->{work_time} }); + $bug->update(); + + my $new_comment_id = $bug->{added_comments}[0]->id; + + # Send mail. + Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }); + + return { id => as_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() unless Bugzilla->user->id; + + 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); + + $removed = as_string_array($removed); + $added = as_string_array($added); + + $changes{$bug->id}->{tags} = { + removed => $removed, + added => $added + }; + } + + return { changes => \%changes }; +} + +sub update_comment_tags { + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + $user->can_tag_comments + || ThrowUserError("auth_failure", + { group => Bugzilla->params->{'comment_taggers_group'}, + action => "update", + object => "comment_tags" }); + + my $comment_id = $params->{comment_id} + // ThrowCodeError('param_required', + { function => 'Bug.update_comment_tags', + param => 'comment_id' }); + + my $comment = Bugzilla::Comment->new($comment_id) + || return []; + $comment->bug->check_is_visible(); + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', { id => $comment_id }); + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + foreach my $tag (@{ $params->{add} || [] }) { + $comment->add_tag($tag) if defined $tag; + } + foreach my $tag (@{ $params->{remove} || [] }) { + $comment->remove_tag($tag) if defined $tag; + } + $comment->update(); + $dbh->bz_commit_transaction(); + + return $comment->tags; +} + +sub search_comment_tags { + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + Bugzilla->user->can_tag_comments + || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'}, + action => "search", + object => "comment_tags"}); + + my $query = $params->{query}; + $query + // ThrowCodeError('param_required', { param => 'query' }); + my $limit = $params->{limit} || 7; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', { param => 'limit', + function => 'Bug.search_comment_tags' }); + + + my $tags = Bugzilla::Comment::TagWeights->match({ + WHERE => { + 'tag LIKE ?' => "\%$query\%", + }, + LIMIT => $limit, + }); + return [ map { $_->tag } @$tags ]; +} + +############################## +# 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 = %{ filter $params, { + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + deadline => as_string($bug->deadline), + id => as_int($bug->bug_id), + is_confirmed => as_boolean($bug->everconfirmed), + op_sys => as_string($bug->op_sys), + platform => as_string($bug->rep_platform), + priority => as_string($bug->priority), + resolution => as_string($bug->resolution), + severity => as_string($bug->bug_severity), + status => as_string($bug->bug_status), + summary => as_string($bug->short_desc), + target_milestone => as_string($bug->target_milestone), + url => as_string($bug->bug_file_loc), + version => as_string($bug->version), + whiteboard => as_string($bug->status_whiteboard), + } }; + + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'alias') { + $item{alias} = as_string_array($bug->alias); + } + if (filter_wants $params, 'assigned_to') { + $item{'assigned_to'} = as_email($bug->assigned_to->login); + $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); + } + if (filter_wants $params, 'blocks') { + $item{'blocks'} = as_int_array($bug->blocked); + } + if (filter_wants $params, 'classification') { + $item{classification} = as_string($bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = as_string($bug->component); + } + if (filter_wants $params, 'cc') { + $item{'cc'} = as_email_array($bug->cc); + $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ]; + } + if (filter_wants $params, 'creation_time') { + $item{'creation_time'} = as_datetime($bug->creation_ts); + } + if (filter_wants $params, 'creator') { + $item{'creator'} = as_email($bug->reporter->login); + $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); + } + if (filter_wants $params, 'depends_on') { + $item{'depends_on'} = as_int_array($bug->dependson); + } + if (filter_wants $params, 'dupe_of') { + $item{'dupe_of'} = as_int($bug->dup_id); + } + if (filter_wants $params, 'groups') { + $item{'groups'} = as_name_array($bug->groups_in); + } + if (filter_wants $params, 'is_open') { + $item{'is_open'} = as_boolean($bug->status->is_open); + } + if (filter_wants $params, 'keywords') { + $item{'keywords'} = as_name_array($bug->keyword_objects); + } + if (filter_wants $params, 'last_change_time') { + $item{'last_change_time'} = as_datetime($bug->delta_ts); + } + if (filter_wants $params, 'product') { + $item{product} = as_string($bug->product); + } + if (filter_wants $params, 'qa_contact') { + my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; + $item{'qa_contact'} = as_email($qa_login); + if ($bug->qa_contact) { + $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); + } + } + if (filter_wants $params, 'see_also') { + $item{'see_also'} = as_string_array($bug->see_also); + } + if (filter_wants $params, 'flags') { + $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + } + if (filter_wants $params, 'tags', 'extra') { + $item{'tags'} = $bug->tags; + } + + # 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, ['default', 'custom']); + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = as_int($bug->$name); + } + elsif ($field->type == FIELD_TYPE_DATETIME + || $field->type == FIELD_TYPE_DATE) + { + $item{$name} = as_datetime($bug->$name); + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $item{$name} = as_string_array($bug->$name); + } + else { + $item{$name} = as_string($bug->$name); + } + } + + # Timetracking fields are only sent if the user can see them. + if (Bugzilla->user->is_timetracker) { + if (filter_wants $params, 'estimated_time') { + $item{'estimated_time'} = as_double($bug->estimated_time); + } + if (filter_wants $params, 'remaining_time') { + $item{'remaining_time'} = as_double($bug->remaining_time); + } + if (filter_wants $params, 'actual_time') { + $item{'actual_time'} = as_double($bug->actual_time); + } + } + + # The "accessible" bits go here because they have long names and it + # makes the code look nicer to separate them out. + if (filter_wants $params, 'is_cc_accessible') { + $item{'is_cc_accessible'} = as_boolean($bug->cclist_accessible); + } + if (filter_wants $params, 'is_creator_accessible') { + $item{'is_creator_accessible'} = as_boolean($bug->reporter_accessible); + } + + return \%item; +} + +sub _user_to_hash { + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, { + id => as_int($user->id), + real_name => as_string($user->name), + name => as_email($user->login), + email => as_email($user->email), + }, $types, $prefix; + return $item; +} + +sub _attachment_to_hash { + my ($self, $attach, $filters, $types, $prefix) = @_; + + my $item = filter $filters, { + creation_time => as_datetime($attach->attached), + last_change_time => as_datetime($attach->modification_time), + id => as_int($attach->id), + bug_id => as_int($attach->bug_id), + file_name => as_string($attach->filename), + summary => as_string($attach->description), + content_type => as_string($attach->contenttype), + is_private => as_boolean($attach->isprivate), + is_obsolete => as_boolean($attach->isobsolete), + is_patch => as_boolean($attach->ispatch), + }, $types, $prefix; + + # creator requires an extra lookup, so we only send them if + # the filter wants them. + if (filter_wants $filters, 'creator', $types, $prefix) { + $item->{'creator'} = as_email($attach->attacher->login); + } + + if (filter_wants $filters, 'data', $types, $prefix) { + $item->{'data'} = as_base64($attach->data); + } + + if (filter_wants $filters, 'size', $types, $prefix) { + $item->{'size'} = as_int($attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; + } + + return $item; +} + +sub _flag_to_hash { + my ($self, $flag) = @_; + + my $item = { + id => as_int($flag->id), + name => as_string($flag->name), + type_id => as_int($flag->type_id), + creation_date => as_datetime($flag->creation_date), + modification_date => as_datetime($flag->modification_date), + status => as_string($flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = as_email($flag->$field->login) + if $flag->$field_id; + } + + return $item; +} + +sub _add_update_tokens { + my ($self, $params, $bugs, $hashes) = @_; + + return if !Bugzilla->user->id; + return if !filter_wants($params, 'update_token'); + + for(my $i = 0; $i < @$bugs; $i++) { + my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); + $hashes->[$i]->{'update_token'} = as_string($token); + } +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::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 USAGE + +Full documentation on how to use the Bugzilla API can be found at +L<https://bugzilla.readthedocs.org/en/latest/api/core/v1/bug.html>. + +=head1 METHODS + +=head2 fields + +=over + +=item B<Description> + +Get information about valid bug fields, including the lists of legal values +for each field. + +=item B<REST> + +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 /rest/field/bug + +To get information related to a single field: + +GET /rest/field/bug/<id_or_name> + +The returned data format is the same as below. + +=item B<Params> + +You can pass either field ids or field names. + +B<Note>: If neither C<ids> nor C<names> is specified, then all +non-obsolete fields will be returned. + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +=over + +=item C<ids> (array) - An array of integer field ids. + +=item C<names> (array) - An array of strings representing field names. + +=back + +=item B<Returns> + +A hash containing a single element, C<fields>. This is an array of hashes, +containing the following keys: + +=over + +=item C<id> + +C<int> An integer id uniquely identifying this field in this installation only. + +=item C<type> + +C<int> 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") + +=item C<8> Keywords + +=item C<9> Date + +=item C<10> Integer value + +=back + +=item C<is_custom> + +C<boolean> True when this is a custom field, false otherwise. + +=item C<name> + +C<string> 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<display_name> + +C<string> The name of the field, as it is shown in the user interface. + +=item C<is_mandatory> + +C<boolean> 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<is_on_bug_entry> + +C<boolean> 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</create>.) + +=item C<visibility_field> + +C<string> 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<visibility_values>. +Can be null. + +=item C<visibility_values> + +C<array> of C<string>s This field is only shown when C<visibility_field> +matches one of these values. When C<visibility_field> is null, +then this is an empty array. + +=item C<value_field> + +C<string> 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<values> + +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<component>, C<version>, C<target_milestone>, and C<keywords> +fields, but not for the C<product> field (you must use +L<Product.get_accessible_products|Bugzilla::API::1_0::Resource::Product/get_accessible_products> +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<name> + +C<string> The actual value--this is what you would specify for this +field in L</create>, etc. + +=item C<sort_key> + +C<int> Values, when displayed in a list, are sorted first by this integer +and then secondly by their name. + +=item C<sortkey> + +B<DEPRECATED> - Use C<sort_key> instead. + +=item C<visibility_values> + +If C<value_field> is defined for this field, then this value is only shown +if the C<value_field> is set to one of the values listed in this array. +Note that for per-product fields, C<value_field> is set to C<'product'> +and C<visibility_values> will reflect which product(s) this value appears in. + +=item C<is_active> + +C<boolean> 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<description> + +C<string> The description of the value. This item is only included for the +C<keywords> field. + +=item C<is_open> + +C<boolean> For C<bug_status> values, determines whether this status +specifies that the bug is "open" (true) or "closed" (false). This item +is only included for the C<bug_status> field. + +=item C<can_change_to> + +For C<bug_status> 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<bug_status> field.) + +Each hash contains the following items: + +=over + +=item C<name> + +the name of the new status + +=item C<comment_required> + +this C<boolean> True if a comment is required when you change a bug into +this status using this transition. + +=back + +=back + +=back + +=item B<Errors> + +=over + +=item 51 (Invalid Field Name or Id) + +You specified an invalid field name or id. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.6>. + +=item The C<is_mandatory> return value was added in Bugzilla B<4.0>. + +=item C<sortkey> was renamed to C<sort_key> in Bugzilla B<4.2>. + +=item C<is_active> return key for C<values> was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0> + +=back + +=back + +=head2 legal_values + +B<DEPRECATED> - Use L</fields> instead. + +=over + +=item B<Description> + +Tells you what values are allowed for a particular field. + +=item B<REST> + +To get information on the values for a field based on field name: + +GET /rest/field/bug/<field_name>/values + +To get information based on field name and a specific product: + +GET /rest/field/bug/<field_name>/<product_id>/values + +The returned data format is the same as below. + +=item B<Params> + +=over + +=item C<field> - The name of the field you want information about. +This should be the same as the name you would use in L</create>, below. + +=item C<product_id> - 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<Returns> + +C<values> - An array of strings: the legal values for this field. +The values will be sorted as they normally would be in Bugzilla. + +=item B<Errors> + +=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<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 Bug Information + +=head2 attachments + +=over + +=item B<Description> + +It allows you to get data about attachments, given a list of bugs +and/or attachment ids. + +B<Note>: Private attachments will only be returned if you are in the +insidergroup or if you are the submitter of the attachment. + +=item B<REST> + +To get all current attachments for a bug: + +GET /rest/bug/<bug_id>/attachment + +To get a specific attachment based on attachment ID: + +GET /rest/bug/attachment/<attachment_id> + +The returned data format is the same as below. + +=item B<Params> + +B<Note>: At least one of C<ids> or C<attachment_ids> is required. + +=over + +=item C<ids> + +See the description of the C<ids> parameter in the L</get> method. + +=item C<attachment_ids> + +C<array> An array of integer attachment ids. + +=back + +Also accepts the L<include_fields|Bugzilla::API::1_0::Resource/include_fields>, +and L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +=item B<Returns> + +A hash containing two elements: C<bugs> and C<attachments>. 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<ids> argument in +input are returned in C<bugs> on output. C<bugs> 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<attachment_ids>, they +are returned in C<attachments> 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<data> + +C<base64> The raw data of the attachment, encoded as Base64. + +=item C<size> + +C<int> The length (in bytes) of the attachment. + +=item C<creation_time> + +C<dateTime> The time the attachment was created. + +=item C<last_change_time> + +C<dateTime> The last time the attachment was modified. + +=item C<id> + +C<int> The numeric id of the attachment. + +=item C<bug_id> + +C<int> The numeric id of the bug that the attachment is attached to. + +=item C<file_name> + +C<string> The file name of the attachment. + +=item C<summary> + +C<string> A short string describing the attachment. + +=item C<content_type> + +C<string> The MIME type of the attachment. + +=item C<is_private> + +C<boolean> True if the attachment is private (only visible to a certain +group called the "insidergroup"), False otherwise. + +=item C<is_obsolete> + +C<boolean> True if the attachment is obsolete, False otherwise. + +=item C<is_patch> + +C<boolean> True if the attachment is a patch, False otherwise. + +=item C<creator> + +C<string> The login name of the user that created the attachment. + +=item C<flags> + +An array of hashes containing the information about flags currently set +for each attachment. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> 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<Errors> + +This method can throw all the same errors as L</get>. 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<attachment_ids> +argument, and you are not in the "insider group" that can see +private attachments. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.6>. + +=item In Bugzilla B<4.0>, the C<attacher> return value was renamed to +C<creator>. + +=item In Bugzilla B<4.0>, the C<description> return value was renamed to +C<summary>. + +=item The C<data> return value was added in Bugzilla B<4.0>. + +=item In Bugzilla B<4.2>, the C<is_url> return value was removed +(this attribute no longer exists for attachments). + +=item The C<size> return value was added in Bugzilla B<4.4>. + +=item The C<flags> array was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + + +=head2 comments + +=over + +=item B<Description> + +This allows you to get data about comments, given a list of bugs +and/or comment ids. + +=item B<REST> + +To get all comments for a particular bug using the bug ID or alias: + +GET /rest/bug/<id_or_alias>/comment + +To get a specific comment based on the comment ID: + +GET /rest/bug/comment/<comment_id> + +The returned data format is the same as below. + +=item B<Params> + +B<Note>: At least one of C<ids> or C<comment_ids> is required. + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +=over + +=item C<ids> + +C<array> 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<comment_ids> + +C<array> An array of integer comment_ids. These comments will be +returned individually, separate from any other comments in their +respective bugs. + +=item C<new_since> + +C<dateTime> If specified, the method will only return comments I<newer> +than this time. This only affects comments returned from the C<ids> +argument. You will always be returned all comments you request in the +C<comment_ids> argument, even if they are older than this date. + +=back + +=item B<Returns> + +Two items are returned: + +=over + +=item C<bugs> + +This is used for bugs specified in C<ids>. 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<comments>, 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<ids>, it will still only be +returned once. + +=item C<comments> + +Each individual comment requested in C<comment_ids> 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<int> The globally unique ID for the comment. + +=item bug_id + +C<int> The ID of the bug that this comment is on. + +=item attachment_id + +C<int> If the comment was made on an attachment, this will be the +ID of that attachment. Otherwise it will be null. + +=item count + +C<int> The number of the comment local to the bug. The Description is 0, +comments start with 1. + +=item text + +C<string> The actual text of the comment. + +=item creator + +C<string> The login name of the comment's author. + +=item time + +C<dateTime> The time (in Bugzilla's timezone) that the comment was added. + +=item creation_time + +C<dateTime> This is exactly same as the C<time> key. Use this field instead of +C<time> for consistency with other methods including L</get> and L</attachments>. +For compatibility, C<time> is still usable. However, please note that C<time> +may be deprecated and removed in a future release. + +=item is_private + +C<boolean> True if this comment is private (only visible to a certain +group called the "insidergroup"), False otherwise. + +=item is_markdown + +C<boolean> True if this comment needs Markdown processing, false otherwise. + +=back + +=item B<Errors> + +This method can throw all the same errors as L</get>. In addition, +it can also throw the following errors: + +=over + +=item 110 (Comment Is Private) + +You specified the id of a private comment in the C<comment_ids> +argument, and you are not in the "insider group" that can see +private comments. + +=item 111 (Invalid Comment ID) + +You specified an id in the C<comment_ids> argument that is invalid--either +you specified something that wasn't a number, or there is no comment with +that id. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item C<attachment_id> was added to the return value in Bugzilla B<3.6>. + +=item In Bugzilla B<4.0>, the C<author> return value was renamed to +C<creator>. + +=item C<count> was added to the return value in Bugzilla B<4.4>. + +=item C<creation_time> was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + + +=head2 get + +=over + +=item B<Description> + +Gets information about particular bugs in the database. + +Note: Can also be called as "get_bugs" for compatibilty with Bugzilla 3.0 API. + +=item B<REST> + +To get information about a particular bug using its ID or alias: + +GET /rest/bug/<id_or_alias> + +The returned data format is the same as below. + +=item B<Params> + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +=over + +=item C<ids> + +An array of numbers and strings. + +If an element in the array is entirely numeric, it represents a bug_id +from the Bugzilla database to fetch. If it contains any non-numeric +characters, it is considered to be a bug alias instead, and the bug with +that alias will be loaded. + +=item C<permissive> + +C<boolean> Normally, if you request any inaccessible or invalid bug ids, +Bug.get will throw an error. If this parameter is True, instead of throwing an +error we return an array of hashes with a C<id>, C<faultString> and C<faultCode> +for each bug that fails, and return normal information for the other bugs that +were accessible. + +=back + +=item B<Returns> + +Two items are returned: + +=over + +=item C<bugs> + +An array of hashes that contains information about the bugs with +the valid ids. Each hash contains the following items: + +These fields are returned by default or by specifying C<_default> +in C<include_fields>. + +=over + +=item C<actual_time> + +C<double> The total number of hours that this bug has taken (so far). + +If you are not in the time-tracking group, this field will not be included +in the return value. + +=item C<alias> + +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. + +=item C<assigned_to> + +C<string> The login name of the user to whom the bug is assigned. + +=item C<assigned_to_detail> + +C<hash> A hash containing detailed user information for the assigned_to. To see the +keys included in the user detail hash, see below. + +=item C<blocks> + +C<array> of C<int>s. The ids of bugs that are "blocked" by this bug. + +=item C<cc> + +C<array> of C<string>s. The login names of users on the CC list of this +bug. + +=item C<cc_detail> + +C<array> of hashes containing detailed user information for each of the cc list +members. To see the keys included in the user detail hash, see below. + +=item C<classification> + +C<string> The name of the current classification the bug is in. + +=item C<component> + +C<string> The name of the current component of this bug. + +=item C<creation_time> + +C<dateTime> When the bug was created. + +=item C<creator> + +C<string> The login name of the person who filed this bug (the reporter). + +=item C<creator_detail> + +C<hash> A hash containing detailed user information for the creator. To see the +keys included in the user detail hash, see below. + +=item C<deadline> + +C<string> The day that this bug is due to be completed, in the format +C<YYYY-MM-DD>. + +=item C<depends_on> + +C<array> of C<int>s. The ids of bugs that this bug "depends on". + +=item C<dupe_of> + +C<int> The bug ID of the bug that this bug is a duplicate of. If this bug +isn't a duplicate of any bug, this will be null. + +=item C<estimated_time> + +C<double> The number of hours that it was estimated that this bug would +take. + +If you are not in the time-tracking group, this field will not be included +in the return value. + +=item C<flags> + +An array of hashes containing the information about flags currently set +for the bug. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> 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 + +=item C<groups> + +C<array> of C<string>s. The names of all the groups that this bug is in. + +=item C<id> + +C<int> The unique numeric id of this bug. + +=item C<is_cc_accessible> + +C<boolean> If true, this bug can be accessed by members of the CC list, +even if they are not in the groups the bug is restricted to. + +=item C<is_confirmed> + +C<boolean> True if the bug has been confirmed. Usually this means that +the bug has at some point been moved out of the C<UNCONFIRMED> status +and into another open status. + +=item C<is_open> + +C<boolean> True if this bug is open, false if it is closed. + +=item C<is_creator_accessible> + +C<boolean> If true, this bug can be accessed by the creator (reporter) +of the bug, even if they are not a member of the groups the bug +is restricted to. + +=item C<keywords> + +C<array> of C<string>s. Each keyword that is on this bug. + +=item C<last_change_time> + +C<dateTime> When the bug was last changed. + +=item C<op_sys> + +C<string> The name of the operating system that the bug was filed against. + +=item C<platform> + +C<string> The name of the platform (hardware) that the bug was filed against. + +=item C<priority> + +C<string> The priority of the bug. + +=item C<product> + +C<string> The name of the product this bug is in. + +=item C<qa_contact> + +C<string> The login name of the current QA Contact on the bug. + +=item C<qa_contact_detail> + +C<hash> A hash containing detailed user information for the qa_contact. To see the +keys included in the user detail hash, see below. + +=item C<remaining_time> + +C<double> The number of hours of work remaining until work on this bug +is complete. + +If you are not in the time-tracking group, this field will not be included +in the return value. + +=item C<resolution> + +C<string> The current resolution of the bug, or an empty string if the bug +is open. + +=item C<see_also> + +C<array> of C<string>s. The URLs in the See Also field on the bug. + +=item C<severity> + +C<string> The current severity of the bug. + +=item C<status> + +C<string> The current status of the bug. + +=item C<summary> + +C<string> The summary of this bug. + +=item C<target_milestone> + +C<string> The milestone that this bug is supposed to be fixed by, or for +closed bugs, the milestone that it was fixed for. + +=item C<update_token> + +C<string> The token that you would have to pass to the F<process_bug.cgi> +page in order to update this bug. This changes every time the bug is +updated. + +This field is not returned to logged-out users. + +=item C<url> + +C<string> A URL that demonstrates the problem described in +the bug, or is somehow related to the bug report. + +=item C<version> + +C<string> The version the bug was reported against. + +=item C<whiteboard> + +C<string> The value of the "status whiteboard" field on the bug. + +=item I<custom fields> + +Every custom field in this installation will also be included in the +return value. Most fields are returned as C<string>s. However, some +field types have different return values. + +Normally custom fields are returned by default similar to normal bug +fields or you can specify only custom fields by using C<_custom> in +C<include_fields>. + +=over + +=item Bug ID Fields - C<int> + +=item Multiple-Selection Fields - C<array> of C<string>s. + +=item Date/Time Fields - C<dateTime> + +=back + +=item I<user detail hashes> + +Each user detail hash contains the following items: + +=over + +=item C<id> + +C<int> The user id for this user. + +=item C<real_name> + +C<string> The 'real' name for this user, if any. + +=item C<name> + +C<string> The user's Bugzilla login. + +=item C<email> + +C<string> The user's email address. Currently this is the same value as the name. + +=back + +=back + +These fields are returned only by specifying "_extra" or the field name in "include_fields". + +=over + +=item C<tags> + +C<array> of C<string>s. Each array item is a tag name. + +Note that tags are personal to the currently logged in user. + +=back + +=item C<faults> + +An array of hashes that contains invalid bug ids with error messages +returned for them. Each hash contains the following items: + +=over + +=item id + +C<int> The numeric bug_id of this bug. + +=item faultString + +C<string> This will only be returned for invalid bugs if the C<permissive> +argument was set when calling Bug.get, and it is an error indicating that +the bug id was invalid. + +=item faultCode + +C<int> This will only be returned for invalid bugs if the C<permissive> +argument was set when calling Bug.get, and it is the error code for the +invalid bug error. + +=back + +=back + +=item B<Errors> + +=over + +=item 100 (Invalid Bug Alias) + +If you specified an alias and there is no bug with that alias. + +=item 101 (Invalid Bug ID) + +The bug_id you specified doesn't exist in the database. + +=item 102 (Access Denied) + +You do not have access to the bug_id you specified. + +=back + +=item B<History> + +=over + +=item C<permissive> argument added to this method's params in Bugzilla B<3.4>. + +=item The following properties were added to this method's return values +in Bugzilla B<3.4>: + +=over + +=item For C<bugs> + +=over + +=item assigned_to + +=item component + +=item dupe_of + +=item is_open + +=item priority + +=item product + +=item resolution + +=item severity + +=item status + +=back + +=item C<faults> + +=back + +=item In Bugzilla B<4.0>, the following items were added to the C<bugs> +return value: C<blocks>, C<cc>, C<classification>, C<creator>, +C<deadline>, C<depends_on>, C<estimated_time>, C<is_cc_accessible>, +C<is_confirmed>, C<is_creator_accessible>, C<groups>, C<keywords>, +C<op_sys>, C<platform>, C<qa_contact>, C<remaining_time>, C<see_also>, +C<target_milestone>, C<update_token>, C<url>, C<version>, C<whiteboard>, +and all custom fields. + +=item The C<flags> array was added in Bugzilla B<4.4>. + +=item The C<actual_time> item was added to the C<bugs> return value +in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=item In Bugzilla B<5.0>, the following items were added to the bugs return value: C<assigned_to_detail>, C<creator_detail>, C<qa_contact_detail>. + +=back + +=back + +=head2 history + +=over + +=item B<Description> + +Gets the history of changes for particular bugs in the database. + +=item B<REST> + +To get the history for a specific bug ID: + +GET /rest/bug/<bug_id>/history + +The returned data format will be the same as below. + +=item B<Params> + +=over + +=item C<ids> + +An array of numbers and strings. + +If an element in the array is entirely numeric, it represents a bug_id +from the Bugzilla database to fetch. If it contains any non-numeric +characters, it is considered to be a bug alias instead, and the data bug +with that alias will be loaded. + +item C<new_since> + +C<dateTime> If specified, the method will only return changes I<newer> +than this time. + +=back + +=item B<Returns> + +A hash containing a single element, C<bugs>. This is an array of hashes, +containing the following keys: + +=over + +=item id + +C<int> The numeric id of the bug. + +=item alias + +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. + +=item history + +C<array> An array of hashes, each hash having the following keys: + +=over + +=item when + +C<dateTime> The date the bug activity/change happened. + +=item who + +C<string> The login name of the user who performed the bug change. + +=item changes + +C<array> An array of hashes which contain all the changes that happened +to the bug at this time (as specified by C<when>). Each hash contains +the following items: + +=over + +=item field_name + +C<string> The name of the bug field that has changed. + +=item removed + +C<string> The previous value of the bug field which has been deleted +by the change. + +=item added + +C<string> The new value of the bug field which has been added by the change. + +=item attachment_id + +C<int> The id of the attachment that was changed. This only appears if +the change was to an attachment, otherwise C<attachment_id> will not be +present in this hash. + +=back + +=back + +=back + +=item B<Errors> + +The same as L</get>. + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item Field names returned by the C<field_name> field changed to be +consistent with other methods. Since Bugzilla B<4.4>, they now match +names used by L<Bug.update|/"update"> for consistency. + +=item REST API call added Bugzilla B<5.0>. + +=item Added C<new_since> parameter if Bugzilla B<5.0>. + +=back + +=back + +=head2 possible_duplicates + +=over + +=item B<Description> + +Allows a user to find possible duplicate bugs based on a set of keywords +such as a user may use as a bug summary. Optionally the search can be +narrowed down to specific products. + +=item B<Params> + +=over + +=item C<summary> (string) B<Required> - A string of keywords defining +the type of bug you are trying to report. + +=item C<products> (array) - One or more product names to narrow the +duplicate search to. If omitted, all bugs are searched. + +=back + +=item B<Returns> + +The same as L</get>. + +Note that you will only be returned information about bugs that you +can see. Bugs that you can't see will be entirely excluded from the +results. So, if you want to see private bugs, you will have to first +log in and I<then> call this method. + +=item B<Errors> + +=over + +=item 50 (Param Required) + +You must specify a value for C<summary> containing a string of keywords to +search for duplicates. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.0>. + +=item The C<product> parameter has been renamed to C<products> in +Bugzilla B<5.0>. + +=back + +=back + +=head2 search + +=over + +=item B<Description> + +Allows you to search for bugs based on particular criteria. + +=item <REST> + +To search for bugs: + +GET /bug + +The URL parameters and the returned data format are the same as below. + +=item B<Params> + +Unless otherwise specified in the description of a parameter, bugs are +returned if they match I<exactly> the criteria you specify in these +parameters. That is, we don't match against substrings--if a bug is in +the "Widgets" product and you ask for bugs in the "Widg" product, you +won't get anything. + +Criteria are joined in a logical AND. That is, you will be returned +bugs that match I<all> of the criteria, not bugs that match I<any> of +the criteria. + +Each parameter can be either the type it says, or an array of the types +it says. If you pass an array, it means "Give me bugs with I<any> of +these values." For example, if you wanted bugs that were in either +the "Foo" or "Bar" products, you'd pass: + + product => ['Foo', 'Bar'] + +Some Bugzillas may treat your arguments case-sensitively, depending +on what database system they are using. Most commonly, though, Bugzilla is +not case-sensitive with the arguments passed (because MySQL is the +most-common database to use with Bugzilla, and MySQL is not case sensitive). + +In addition to the fields listed below, you may also use criteria that +is similar to what is used in the Advanced Search screen of the Bugzilla +UI. This includes fields specified by C<Search by Change History> and +C<Custom Search>. The easiest way to determine what the field names are and what +format Bugzilla expects, is to first construct your query using the +Advanced Search UI, execute it and use the query parameters in they URL +as your key/value pairs for the WebService call. With REST, you can +just reuse the query parameter portion in the REST call itself. + +=over + +=item C<alias> + +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. + +=item C<assigned_to> + +C<string> The login name of a user that a bug is assigned to. + +=item C<component> + +C<string> The name of the Component that the bug is in. Note that +if there are multiple Components with the same name, and you search +for that name, bugs in I<all> those Components will be returned. If you +don't want this, be sure to also specify the C<product> argument. + +=item C<creation_time> + +C<dateTime> Searches for bugs that were created at this time or later. +May not be an array. + +=item C<creator> + +C<string> The login name of the user who created the bug. + +You can also pass this argument with the name C<reporter>, for +backwards compatibility with older Bugzillas. + +=item C<id> + +C<int> The numeric id of the bug. + +=item C<last_change_time> + +C<dateTime> Searches for bugs that were modified at this time or later. +May not be an array. + +=item C<limit> + +C<int> Limit the number of results returned to C<int> records. If the limit +is more than zero and higher than the maximum limit set by the administrator, +then the maximum limit will be used instead. If you set the limit equal to zero, +then all matching results will be returned instead. + +=item C<offset> + +C<int> Used in conjunction with the C<limit> argument, C<offset> defines +the starting position for the search. For example, given a search that +would return 100 bugs, setting C<limit> to 10 and C<offset> to 10 would return +bugs 11 through 20 from the set of 100. + +=item C<op_sys> + +C<string> The "Operating System" field of a bug. + +=item C<platform> + +C<string> The Platform (sometimes called "Hardware") field of a bug. + +=item C<priority> + +C<string> The Priority field on a bug. + +=item C<product> + +C<string> The name of the Product that the bug is in. + +=item C<resolution> + +C<string> The current resolution--only set if a bug is closed. You can +find open bugs by searching for bugs with an empty resolution. + +=item C<severity> + +C<string> The Severity field on a bug. + +=item C<status> + +C<string> The current status of a bug (not including its resolution, +if it has one, which is a separate field above). + +=item C<summary> + +C<string> Searches for substrings in the single-line Summary field on +bugs. If you specify an array, then bugs whose summaries match I<any> of the +passed substrings will be returned. + +Note that unlike searching in the Bugzilla UI, substrings are not split +on spaces. So searching for C<foo bar> will match "This is a foo bar" +but not "This foo is a bar". C<['foo', 'bar']>, would, however, match +the second item. + +=item C<tags> + +C<string> Searches for a bug with the specified tag. If you specify an +array, then any bugs that match I<any> of the tags will be returned. + +Note that tags are personal to the currently logged in user. + +=item C<target_milestone> + +C<string> The Target Milestone field of a bug. Note that even if this +Bugzilla does not have the Target Milestone field enabled, you can +still search for bugs by Target Milestone. However, it is likely that +in that case, most bugs will not have a Target Milestone set (it +defaults to "---" when the field isn't enabled). + +=item C<qa_contact> + +C<string> The login name of the bug's QA Contact. Note that even if +this Bugzilla does not have the QA Contact field enabled, you can +still search for bugs by QA Contact (though it is likely that no bug +will have a QA Contact set, if the field is disabled). + +=item C<url> + +C<string> The "URL" field of a bug. + +=item C<version> + +C<string> The Version field of a bug. + +=item C<whiteboard> + +C<string> Search the "Status Whiteboard" field on bugs for a substring. +Works the same as the C<summary> field described above, but searches the +Status Whiteboard field. + +=item C<quicksearch> + +C<string> Search for bugs using quicksearch syntax. + +=back + +=item B<Returns> + +The same as L</get>. + +Note that you will only be returned information about bugs that you +can see. Bugs that you can't see will be entirely excluded from the +results. So, if you want to see private bugs, you will have to first +log in and I<then> call this method. + +=item B<Errors> + +If you specify an invalid value for a particular field, you just won't +get any results for that value. + +=over + +=item 1000 (Parameters Required) + +You may not search without any search terms. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item Searching by C<votes> was removed in Bugzilla B<4.0>. + +=item The C<reporter> input parameter was renamed to C<creator> +in Bugzilla B<4.0>. + +=item In B<4.2.6> and newer, added the ability to return all results if +C<limit> is set equal to zero. Otherwise maximum results returned are limited +by system configuration. + +=item REST API call added in Bugzilla B<5.0>. + +=item Updated to allow for full search capability similar to the Bugzilla UI +in Bugzilla B<5.0>. + +=item Updated to allow quicksearch capability in Bugzilla B<5.0>. + +=back + +=back + +=head1 Bug Creation and Modification + +=head2 create + +=over + +=item B<Description> + +This allows you to create a new bug in Bugzilla. If you specify any +invalid fields, an error will be thrown stating which field is invalid. +If you specify any fields you are not allowed to set, they will just be +set to their defaults or ignored. + +You cannot currently set all the items here that you can set on enter_bug.cgi. + +=item B<REST> + +To create a new bug in Bugzilla: + +POST /rest/bug + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +Some params must be set, or an error will be thrown. These params are +marked B<Required>. + +Some parameters can have defaults set in Bugzilla, by the administrator. +If these parameters have defaults set, you can omit them. These parameters +are marked B<Defaulted>. + +Clients that want to be able to interact uniformly with multiple +Bugzillas should always set both the params marked B<Required> and those +marked B<Defaulted>, because some Bugzillas may not have defaults set +for B<Defaulted> parameters, and then this method will throw an error +if you don't specify them. + +The descriptions of the parameters below are what they mean when Bugzilla is +being used to track software bugs. They may have other meanings in some +installations. + +=over + +=item C<product> (string) B<Required> - The name of the product the bug +is being filed against. + +=item C<component> (string) B<Required> - The name of a component in the +product above. + +=item C<summary> (string) B<Required> - A brief description of the bug being +filed. + +=item C<version> (string) B<Required> - A version of the product above; +the version the bug was found in. + +=item C<description> (string) B<Defaulted> - The initial description for +this bug. Some Bugzilla installations require this to not be blank. + +=item C<op_sys> (string) B<Defaulted> - The operating system the bug was +discovered on. + +=item C<platform> (string) B<Defaulted> - What type of hardware the bug was +experienced on. + +=item C<priority> (string) B<Defaulted> - What order the bug will be fixed +in by the developer, compared to the developer's other bugs. + +=item C<severity> (string) B<Defaulted> - How severe the bug is. + +=item C<alias> (array) - A brief alias for the bug that can be used +instead of a bug number when accessing this bug. Must be unique in +all of this Bugzilla. + +=item C<assigned_to> (username) - A user to assign this bug to, if you +don't want it to be assigned to the component owner. + +=item C<cc> (array) - An array of usernames to CC on this bug. + +=item C<comment_is_private> (boolean) - If set to true, the description +is private, otherwise it is assumed to be public. + +=item C<is_markdown> (boolean) - If set to true, the description +has Markdown structures, otherwise it is a normal text. + +=item C<groups> (array) - An array of group names to put this +bug into. You can see valid group names on the Permissions +tab of the Preferences screen, or, if you are an administrator, +in the Groups control panel. +If you don't specify this argument, then the bug will be added into +all the groups that are set as being "Default" for this product. (If +you want to avoid that, you should specify C<groups> as an empty array.) + +=item C<qa_contact> (username) - If this installation has QA Contacts +enabled, you can set the QA Contact here if you don't want to use +the component's default QA Contact. + +=item C<status> (string) - The status that this bug should start out as. +Note that only certain statuses can be set on bug creation. + +=item C<resolution> (string) - If you are filing a closed bug, then +you will have to specify a resolution. You cannot currently specify +a resolution of C<DUPLICATE> for new bugs, though. That must be done +with L</update>. + +=item C<target_milestone> (string) - A valid target milestone for this +product. + +=item C<flags> + +C<array> An array of hashes with flags to add to the bug. To create a flag, +at least the status and the type_id or name must be provided. An optional +requestee can be passed if the flag type is requestable to a specific user. + +=over + +=item C<name> + +C<string> The name of the flag type. + +=item C<type_id> + +C<int> The internal flag type id. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=back + +=back + +In addition to the above parameters, if your installation has any custom +fields, you can set them just by passing in the name of the field and +its value as a string. + +=item B<Returns> + +A hash with one element, C<id>. This is the id of the newly-filed bug. + +=item B<Errors> + +=over + +=item 51 (Invalid Object) + +You specified a field value that is invalid. The error message will have +more details. + +=item 103 (Invalid Alias) + +The alias you specified is invalid for some reason. See the error message +for more details. + +=item 104 (Invalid Field) + +One of the drop-down fields has an invalid value, or a value entered in a +text field is too long. The error message will have more detail. + +=item 105 (Invalid Component) + +You didn't specify a component. + +=item 106 (Invalid Product) + +Either you didn't specify a product, this product doesn't exist, or +you don't have permission to enter bugs in this product. + +=item 107 (Invalid Summary) + +You didn't specify a summary for the bug. + +=item 116 (Dependency Loop) + +You specified values in the C<blocks> or C<depends_on> fields +that would cause a circular dependency between bugs. + +=item 120 (Group Restriction Denied) + +You tried to restrict the bug to a group which does not exist, or which +you cannot use with this product. + +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + +=item 504 (Invalid User) + +Either the QA Contact, Assignee, or CC lists have some invalid user +in them. The error message will have more details. + +=back + +=item B<History> + +=over + +=item Before B<3.0.4>, parameters marked as B<Defaulted> were actually +B<Required>, due to a bug in Bugzilla. + +=item The C<groups> argument was added in Bugzilla B<4.0>. Before +Bugzilla 4.0, bugs were only added into Mandatory groups by this +method. Since Bugzilla B<4.0.2>, passing an illegal group name will +throw an error. In Bugzilla 4.0 and 4.0.1, illegal group names were +silently ignored. + +=item The C<comment_is_private> argument was added in Bugzilla B<4.0>. +Before Bugzilla 4.0, you had to use the undocumented C<commentprivacy> +argument. + +=item Error 116 was added in Bugzilla B<4.0>. Before that, dependency +loop errors had a generic code of C<32000>. + +=item The ability to file new bugs with a C<resolution> was added in +Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=item C<is_markdown> option added in Bugzilla B<6.0>. + +=back + +=back + + +=head2 add_attachment + +=over + +=item B<Description> + +This allows you to add an attachment to a bug in Bugzilla. + +=item B<REST> + +To create attachment on a current bug: + +POST /rest/bug/<bug_id>/attachment + +The params to include in the POST body, as well as the returned +data format are the same as below. The C<ids> param will be +overridden as it it pulled from the URL path. + +=item B<Params> + +=over + +=item C<ids> + +B<Required> C<array> An array of ints and/or strings--the ids +or aliases of bugs that you want to add this attachment to. +The same attachment and comment will be added to all +these bugs. + +=item C<data> + +B<Required> C<base64> or C<string> The content of the attachment. +If the content of the attachment is not ASCII text, you must encode +it in base64 and declare it as the C<base64> type. + +=item C<file_name> + +B<Required> C<string> The "file name" that will be displayed +in the UI for this attachment. + +=item C<summary> + +B<Required> C<string> A short string describing the +attachment. + +=item C<content_type> + +B<Required> C<string> The MIME type of the attachment, like +C<text/plain> or C<image/png>. + +=item C<comment> + +C<string> or hash. A comment to add along with this attachment. If C<comment> +is a hash, it has the following keys: + +=over + +=item C<body> + +C<string> The body of the comment. + +=item C<is_markdown> + +C<boolean> If set to true, the comment has Markdown structures; otherwise, it +is an ordinary text. + +=back + +=item C<is_patch> + +C<boolean> True if Bugzilla should treat this attachment as a patch. +If you specify this, you do not need to specify a C<content_type>. +The C<content_type> of the attachment will be forced to C<text/plain>. + +Defaults to False if not specified. + +=item C<is_private> + +C<boolean> True if the attachment should be private (restricted +to the "insidergroup"), False if the attachment should be public. + +Defaults to False if not specified. + +=item C<flags> + +C<array> An array of hashes with flags to add to the attachment. to create a flag, +at least the status and the type_id or name must be provided. An optional requestee +can be passed if the flag type is requestable to a specific user. + +=over + +=item C<name> + +C<string> The name of the flag type. + +=item C<type_id> + +C<int> The internal flag type id. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=back + +=item C<minor_update> + +C<boolean> If set to true, this is considered a minor update and no mail is sent +to users who do not want minor update emails. If current user is not in the +minor_update_group, this parameter is simply ignored. + +=back + +=item B<Returns> + +A single item C<ids>, which contains an array of the +attachment id(s) created. + +=item B<Errors> + +This method can throw all the same errors as L</get>, plus: + +=over + +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + +=item 140 (Markdown Disabled) + +You tried to set the C<is_markdown> flag of the comment to true but the Markdown feature is not enabled. + +=item 600 (Attachment Too Large) + +You tried to attach a file that was larger than Bugzilla will accept. + +=item 601 (Invalid MIME Type) + +You specified a C<content_type> argument that was blank, not a valid +MIME type, or not a MIME type that Bugzilla accepts for attachments. + +=item 603 (File Name Not Specified) + +You did not specify a valid for the C<file_name> argument. + +=item 604 (Summary Required) + +You did not specify a value for the C<summary> argument. + +=item 606 (Empty Data) + +You set the "data" field to an empty string. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.0>. + +=item The C<is_url> parameter was removed in Bugzilla B<4.2>. + +=item The return value has changed in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=item C<is_markdown> added in Bugzilla B<6.0>. + +=back + +=back + + +=head2 update_attachment + +=over + +=item B<Description> + +This allows you to update attachment metadata in Bugzilla. + +=item B<REST> + +To update attachment metadata on a current attachment: + +PUT /rest/bug/attachment/<attach_id> + +The params to include in the POST body, as well as the returned +data format are the same as below. The C<ids> param will be +overridden as it it pulled from the URL path. + +=item B<Params> + +=over + +=item C<ids> + +B<Required> C<array> An array of integers -- the ids of the attachments you +want to update. + +=item C<file_name> + +C<string> The "file name" that will be displayed +in the UI for this attachment. + +=item C<summary> + +C<string> A short string describing the +attachment. + +=item C<comment> + +C<string> or hash: An optional comment to add to the attachment's bug. If C<comment> is +a hash, it has the following keys: + +=over + +=item C<body> + +C<string> The body of the comment to be added. + +=item C<is_markdown> + +C<boolean> If set to true, the comment has Markdown structures; otherwise it is a normal +text. + +=back + +=item C<content_type> + +C<string> The MIME type of the attachment, like +C<text/plain> or C<image/png>. + +=item C<is_patch> + +C<boolean> True if Bugzilla should treat this attachment as a patch. +If you specify this, you do not need to specify a C<content_type>. +The C<content_type> of the attachment will be forced to C<text/plain>. + +=item C<is_private> + +C<boolean> True if the attachment should be private (restricted +to the "insidergroup"), False if the attachment should be public. + +=item C<is_obsolete> + +C<boolean> True if the attachment is obsolete, False otherwise. + +=item C<flags> + +C<array> An array of hashes with changes to the flags. The following values +can be specified. At least the status and one of type_id, id, or name must +be specified. If a type_id or name matches a single currently set flag, +the flag will be updated unless new is specified. + +=over + +=item C<name> + +C<string> The name of the flag that will be created or updated. + +=item C<type_id> + +C<int> The internal flag type id that will be created or updated. You will +need to specify the C<type_id> if more than one flag type of the same name exists. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=item C<id> + +C<int> Use id to specify the flag to be updated. You will need to specify the C<id> +if more than one flag is set of the same name. + +=item C<new> + +C<boolean> Set to true if you specifically want a new flag to be created. + +=back + +=item C<minor_update> + +C<boolean> If set to true, this is considered a minor update and no mail is sent +to users who do not want minor update emails. If current user is not in the +minor_update_group, this parameter is simply ignored. + +=back + +=item B<Returns> + +A C<hash> with a single field, "attachments". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the attachment that was updated. + +=item C<last_change_time> + +C<dateTime> The exact time that this update was done at, for this attachment. +If no update was done (that is, no fields had their values changed and +no comment was added) then this will instead be the last time the attachment +was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this bug. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> (C<string>) The values that were added to this field. +possibly a comma-and-space-separated list if multiple values were added. + +=item C<removed> (C<string>) The values that were removed from this +field. + +=back + +=back + +Here's an example of what a return value might look like: + + { + attachments => [ + { + id => 123, + last_change_time => '2010-01-01T12:34:56', + changes => { + summary => { + removed => 'Sample ptach', + added => 'Sample patch' + }, + is_obsolete => { + removed => '0', + added => '1', + } + }, + } + ] + } + +=item B<Errors> + +This method can throw all the same errors as L</get>, plus: + +=over + +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 132 (Flag not Unique) + +The flag specified has been set multiple times. You must specify the id +value to update the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + +=item 140 (Markdown Disabled) + +You tried to set the C<is_markdown> flag of the C<comment> to true but Markdown feature is +not enabled. + +=item 601 (Invalid MIME Type) + +You specified a C<content_type> argument that was blank, not a valid +MIME type, or not a MIME type that Bugzilla accepts for attachments. + +=item 603 (File Name Not Specified) + +You did not specify a valid for the C<file_name> argument. + +=item 604 (Summary Required) + +You did not specify a value for the C<summary> argument. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 add_comment + +=over + +=item B<Description> + +This allows you to add a comment to a bug in Bugzilla. + +=item B<REST> + +To create a comment on a current bug: + +POST /rest/bug/<bug_id>/comment + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +=over + +=item C<id> (int or string) B<Required> - The id or alias of the bug to append a +comment to. + +=item C<comment> (string) B<Required> - The comment to append to the bug. +If this is empty or all whitespace, an error will be thrown saying that +you did not set the C<comment> parameter. + +=item C<is_private> (boolean) - If set to true, the comment is private, +otherwise it is assumed to be public. + +=item C<is_markdown> (boolean) - If set to true, the comment has Markdown +structures, otherwise it is a normal text. + +=item C<work_time> (double) - Adds this many hours to the "Hours Worked" +on the bug. If you are not in the time tracking group, this value will +be ignored. + +=item C<minor_update> (boolean) - If set to true, this is considered a minor update +and no mail is sent to users who do not want minor update emails. If current user +is not in the minor_update_group, this parameter is simply ignored. + +=back + +=item B<Returns> + +A hash with one element, C<id> whose value is the id of the newly-created comment. + +=item B<Errors> + +=over + +=item 54 (Hours Worked Too Large) + +You specified a C<work_time> larger than the maximum allowed value of +C<99999.99>. + +=item 100 (Invalid Bug Alias) + +If you specified an alias and there is no bug with that alias. + +=item 101 (Invalid Bug ID) + +The id you specified doesn't exist in the database. + +=item 109 (Bug Edit Denied) + +You did not have the necessary rights to edit the bug. + +=item 113 (Can't Make Private Comments) + +You tried to add a private comment, but don't have the necessary rights. + +=item 114 (Comment Too Long) + +You tried to add a comment longer than the maximum allowed length +(65,535 characters). + +=item 140 (Markdown Disabled) + +You tried to set the C<is_markdown> flag to true but the Markdown feature +is not enabled. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.2>. + +=item Modified to return the new comment's id in Bugzilla B<3.4> + +=item Modified to throw an error if you try to add a private comment +but can't, in Bugzilla B<3.4>. + +=item Before Bugzilla B<3.6>, the C<is_private> argument was called +C<private>, and you can still call it C<private> for backwards-compatibility +purposes if you wish. + +=item Before Bugzilla B<3.6>, error 54 and error 114 had a generic error +code of 32000. + +=item REST API call added in Bugzilla B<5.0>. + +=item C<is_markdown> option added in Bugzilla B<6.0>. + +=back + +=back + + +=head2 update + +=over + +=item B<Description> + +Allows you to update the fields of a bug. Automatically sends emails +out about the changes. + +=item B<REST> + +To update the fields of a current bug: + +PUT /rest/bug/<bug_id> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> param will be overridden as it is +pulled from the URL path. + +=item B<Params> + +=over + +=item C<ids> + +Array of C<int>s or C<string>s. The ids or aliases of the bugs that +you want to modify. + +=item C<minor_update> + +C<boolean> If set to true, this is considered a minor update and no mail is sent +to users who do not want minor update emails. If current user is not in the +minor_update_group, this parameter is simply ignored. + +=back + +B<Note>: All following fields specify the values you want to set on the +bugs you are updating. + +=over + +=item C<alias> + +C<hash> These specify the aliases of a bug that can be used instead of a bug +number when acessing this bug. To set these, you should pass a hash as the +value. The hash may contain the following fields: + +=over + +=item C<add> An array of C<string>s. Aliases to add to this field. + +=item C<remove> An array of C<string>s. Aliases to remove from this field. +If the aliases are not already in the field, they will be ignored. + +=item C<set> An array of C<string>s. An exact set of aliases to set this +field to, overriding the current value. If you specify C<set>, then C<add> +and C<remove> will be ignored. + +=back + +You can only set this if you are modifying a single bug. If there is more +than one bug specified in C<ids>, passing in a value for C<alias> will cause +an error to be thrown. + +For backwards compatibility, you can also specify a single string. This will +be treated as if you specified the set key above. + +=item C<assigned_to> + +C<string> The full login name of the user this bug is assigned to. + +=item C<blocks> + +=item C<depends_on> + +C<hash> These specify the bugs that this bug blocks or depends on, +respectively. To set these, you should pass a hash as the value. The hash +may contain the following fields: + +=over + +=item C<add> An array of C<int>s. Bug ids to add to this field. + +=item C<remove> An array of C<int>s. Bug ids to remove from this field. +If the bug ids are not already in the field, they will be ignored. + +=item C<set> An array of C<int>s. An exact set of bug ids to set this +field to, overriding the current value. If you specify C<set>, then C<add> +and C<remove> will be ignored. + +=back + +=item C<cc> + +C<hash> The users on the cc list. To modify this field, pass a hash, which +may have the following fields: + +=over + +=item C<add> Array of C<string>s. User names to add to the CC list. +They must be full user names, and an error will be thrown if you pass +in an invalid user name. + +=item C<remove> Array of C<string>s. User names to remove from the CC +list. They must be full user names, and an error will be thrown if you +pass in an invalid user name. + +=back + +=item C<is_cc_accessible> + +C<boolean> Whether or not users in the CC list are allowed to access +the bug, even if they aren't in a group that can normally access the bug. + +=item C<comment> + +C<hash>. A comment on the change. The hash may contain the following fields: + +=over + +=item C<body> C<string> The actual text of the comment. +B<Note>: For compatibility with the parameters to L</add_comment>, +you can also call this field C<comment>, if you want. + +=item C<is_private> C<boolean> Whether the comment is private or not. +If you try to make a comment private and you don't have the permission +to, an error will be thrown. + +=back + +=item C<comment_is_private> + +C<hash> This is how you update the privacy of comments that are already +on a bug. This is a hash, where the keys are the C<int> id of comments (not +their count on a bug, like #1, #2, #3, but their globally-unique id, +as returned by L</comments>) and the value is a C<boolean> which specifies +whether that comment should become private (C<true>) or public (C<false>). + +The comment ids must be valid for the bug being updated. Thus, it is not +practical to use this while updating multiple bugs at once, as a single +comment id will never be valid on multiple bugs. + +=item C<component> + +C<string> The Component the bug is in. + +=item C<deadline> + +C<string> The Deadline field--a date specifying when the bug must +be completed by, in the format C<YYYY-MM-DD>. + +=item C<dupe_of> + +C<int> The bug that this bug is a duplicate of. If you want to mark +a bug as a duplicate, the safest thing to do is to set this value +and I<not> set the C<status> or C<resolution> fields. They will +automatically be set by Bugzilla to the appropriate values for +duplicate bugs. + +=item C<estimated_time> + +C<double> The total estimate of time required to fix the bug, in hours. +This is the I<total> estimate, not the amount of time remaining to fix it. + +=item C<flags> + +C<array> An array of hashes with changes to the flags. The following values +can be specified. At least the status and one of type_id, id, or name must +be specified. If a type_id or name matches a single currently set flag, +the flag will be updated unless new is specified. + +=over + +=item C<name> + +C<string> The name of the flag that will be created or updated. + +=item C<type_id> + +C<int> The internal flag type id that will be created or updated. You will +need to specify the C<type_id> if more than one flag type of the same name exists. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=item C<id> + +C<int> Use id to specify the flag to be updated. You will need to specify the C<id> +if more than one flag is set of the same name. + +=item C<new> + +C<boolean> Set to true if you specifically want a new flag to be created. + +=back + +=item C<groups> + +C<hash> The groups a bug is in. To modify this field, pass a hash, which +may have the following fields: + +=over + +=item C<add> Array of C<string>s. The names of groups to add. Passing +in an invalid group name or a group that you cannot add to this bug will +cause an error to be thrown. + +=item C<remove> Array of C<string>s. The names of groups to remove. Passing +in an invalid group name or a group that you cannot remove from this bug +will cause an error to be thrown. + +=back + +=item C<keywords> + +C<hash> Keywords on the bug. To modify this field, pass a hash, which +may have the following fields: + +=over + +=item C<add> An array of C<strings>s. The names of keywords to add to +the field on the bug. Passing something that isn't a valid keyword name +will cause an error to be thrown. + +=item C<remove> An array of C<string>s. The names of keywords to remove +from the field on the bug. Passing something that isn't a valid keyword +name will cause an error to be thrown. + +=item C<set> An array of C<strings>s. An exact set of keywords to set the +field to, on the bug. Passing something that isn't a valid keyword name +will cause an error to be thrown. Specifying C<set> overrides C<add> and +C<remove>. + +=back + +=item C<op_sys> + +C<string> The Operating System ("OS") field on the bug. + +=item C<platform> + +C<string> The Platform or "Hardware" field on the bug. + +=item C<priority> + +C<string> The Priority field on the bug. + +=item C<product> + +C<string> The name of the product that the bug is in. If you change +this, you will probably also want to change C<target_milestone>, +C<version>, and C<component>, since those have different legal +values in every product. + +If you cannot change the C<target_milestone> field, it will be reset to +the default for the product, when you move a bug to a new product. + +You may also wish to add or remove groups, as which groups are +valid on a bug depends on the product. Groups that are not valid +in the new product will be automatically removed, and groups which +are mandatory in the new product will be automaticaly added, but no +other automatic group changes will be done. + +Note that users can only move a bug into a product if they would +normally have permission to file new bugs in that product. + +=item C<qa_contact> + +C<string> The full login name of the bug's QA Contact. + +=item C<is_creator_accessible> + +C<boolean> Whether or not the bug's reporter is allowed to access +the bug, even if they aren't in a group that can normally access +the bug. + +=item C<remaining_time> + +C<double> How much work time is remaining to fix the bug, in hours. +If you set C<work_time> but don't explicitly set C<remaining_time>, +then the C<work_time> will be deducted from the bug's C<remaining_time>. + +=item C<reset_assigned_to> + +C<boolean> If true, the C<assigned_to> field will be reset to the +default for the component that the bug is in. (If you have set the +component at the same time as using this, then the component used +will be the new component, not the old one.) + +=item C<reset_qa_contact> + +C<boolean> If true, the C<qa_contact> field will be reset to the +default for the component that the bug is in. (If you have set the +component at the same time as using this, then the component used +will be the new component, not the old one.) + +=item C<resolution> + +C<string> The current resolution. May only be set if you are closing +a bug or if you are modifying an already-closed bug. Attempting to set +the resolution to I<any> value (even an empty or null string) on an +open bug will cause an error to be thrown. + +If you change the C<status> field to an open status, the resolution +field will automatically be cleared, so you don't have to clear it +manually. + +=item C<see_also> + +C<hash> The See Also field on a bug, specifying URLs to bugs in other +bug trackers. To modify this field, pass a hash, which may have the +following fields: + +=over + +=item C<add> An array of C<strings>s. URLs to add to the field. +Each URL must be a valid URL to a bug-tracker, or an error will +be thrown. + +=item C<remove> An array of C<string>s. URLs to remove from the field. +Invalid URLs will be ignored. + +=back + +=item C<severity> + +C<string> The Severity field of a bug. + +=item C<status> + +C<string> The status you want to change the bug to. Note that if +a bug is changing from open to closed, you should also specify +a C<resolution>. + +=item C<summary> + +C<string> The Summary field of the bug. + +=item C<target_milestone> + +C<string> The bug's Target Milestone. + +=item C<url> + +C<string> The "URL" field of a bug. + +=item C<version> + +C<string> The bug's Version field. + +=item C<whiteboard> + +C<string> The Status Whiteboard field of a bug. + +=item C<work_time> + +C<double> The number of hours worked on this bug as part of this change. +If you set C<work_time> but don't explicitly set C<remaining_time>, +then the C<work_time> will be deducted from the bug's C<remaining_time>. + +=back + +You can also set the value of any custom field by passing its name as +a parameter, and the value to set the field to. For multiple-selection +fields, the value should be an array of strings. + +=item B<Returns> + +A C<hash> with a single field, "bugs". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the bug that was updated. + +=item C<alias> + +C<array> of C<string>s The aliases of the bug that was updated, if this bug +has any alias. + +=item C<last_change_time> + +C<dateTime> The exact time that this update was done at, for this bug. +If no update was done (that is, no fields had their values changed and +no comment was added) then this will instead be the last time the bug +was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this bug. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> (C<string>) The values that were added to this field, +possibly a comma-and-space-separated list if multiple values were added. + +=item C<removed> (C<string>) The values that were removed from this +field, possibly a comma-and-space-separated list if multiple values were +removed. + +=back + +=back + +Here's an example of what a return value might look like: + + { + bugs => [ + { + id => 123, + alias => [ 'foo' ], + last_change_time => '2010-01-01T12:34:56', + changes => { + status => { + removed => 'NEW', + added => 'ASSIGNED' + }, + keywords => { + removed => 'bar', + added => 'qux, quo, qui', + } + }, + } + ] + } + +Currently, some fields are not tracked in changes: C<comment>, +C<comment_is_private>, and C<work_time>. This means that they will not +show up in the return value even if they were successfully updated. +This may change in a future version of Bugzilla. + +=item B<Errors> + +This function can throw all of the errors that L</get>, L</create>, +and L</add_comment> can throw, plus: + +=over + +=item 50 (Empty Field) + +You tried to set some field to be empty, but that field cannot be empty. +The error message will have more details. + +=item 52 (Input Not A Number) + +You tried to set a numeric field to a value that wasn't numeric. + +=item 54 (Number Too Large) + +You tried to set a numeric field to a value larger than that field can +accept. + +=item 55 (Number Too Small) + +You tried to set a negative value in a numeric field that does not accept +negative values. + +=item 56 (Bad Date/Time) + +You specified an invalid date or time in a date/time field (such as +the C<deadline> field or a custom date/time field). + +=item 112 (See Also Invalid) + +You attempted to add an invalid value to the C<see_also> field. + +=item 115 (Permission Denied) + +You don't have permission to change a particular field to a particular value. +The error message will have more detail. + +=item 116 (Dependency Loop) + +You specified a value in the C<blocks> or C<depends_on> fields that causes +a dependency loop. + +=item 117 (Invalid Comment ID) + +You specified a comment id in C<comment_is_private> that isn't on this bug. + +=item 118 (Duplicate Loop) + +You specified a value for C<dupe_of> that causes an infinite loop of +duplicates. + +=item 119 (dupe_of Required) + +You changed the resolution to C<DUPLICATE> but did not specify a value +for the C<dupe_of> field. + +=item 120 (Group Add/Remove Denied) + +You tried to add or remove a group that you don't have permission to modify +for this bug, or you tried to add a group that isn't valid in this product. + +=item 121 (Resolution Required) + +You tried to set the C<status> field to a closed status, but you didn't +specify a resolution. + +=item 122 (Resolution On Open Status) + +This bug has an open status, but you specified a value for the C<resolution> +field. + +=item 123 (Invalid Status Transition) + +You tried to change from one status to another, but the status workflow +rules don't allow that change. + +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 132 (Flag not Unique) + +The flag specified has been set multiple times. You must specify the id +value to update the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.0>. + +=item REST API call added Bugzilla B<5.0>. + +=back + +=back + + +=head2 update_see_also + +=over + +=item B<Description> + +Adds or removes URLs for the "See Also" field on bugs. These URLs must +point to some valid bug in some Bugzilla installation or in Launchpad. + +=item B<Params> + +=over + +=item C<ids> + +Array of C<int>s or C<string>s. The ids or aliases of bugs that you want +to modify. + +=item C<add> + +Array of C<string>s. URLs to Bugzilla bugs. These URLs will be added to +the See Also field. They must be valid URLs to C<show_bug.cgi> in a +Bugzilla installation or to a bug filed at launchpad.net. + +If the URLs don't start with C<http://> or C<https://>, it will be assumed +that C<http://> should be added to the beginning of the string. + +It is safe to specify URLs that are already in the "See Also" field on +a bug--they will just be silently ignored. + +=item C<remove> + +Array of C<string>s. These URLs will be removed from the See Also field. +You must specify the full URL that you want removed. However, matching +is done case-insensitively, so you don't have to specify the URL in +exact case, if you don't want to. + +If you specify a URL that is not in the See Also field of a particular bug, +it will just be silently ignored. Invaild URLs are currently silently ignored, +though this may change in some future version of Bugzilla. + +=item C<minor_update> + +C<boolean> If set to true, this is considered a minor update and no mail is sent +to users who do not want minor update emails. If current user is not in the +minor_update_group, this parameter is simply ignored. + +=back + +NOTE: If you specify the same URL in both C<add> and C<remove>, it will +be I<added>. (That is, C<add> overrides C<remove>.) + +=item B<Returns> + +C<changes>, a hash where the keys are numeric bug ids and the contents +are a hash with one key, C<see_also>. C<see_also> points to a hash, which +contains two keys, C<added> and C<removed>. These are arrays of strings, +representing the actual changes that were made to the bug. + +Here's a diagram of what the return value looks like for updating +bug ids 1 and 2: + + { + changes => { + 1 => { + see_also => { + added => (an array of bug URLs), + removed => (an array of bug URLs), + } + }, + 2 => { + see_also => { + added => (an array of bug URLs), + removed => (an array of bug URLs), + } + } + } + } + +This return value allows you to tell what this method actually did. It is in +this format to be compatible with the return value of a future C<Bug.update> +method. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws, plus: + +=over + +=item 109 (Bug Edit Denied) + +You did not have the necessary rights to edit the bug. + +=item 112 (Invalid Bug URL) + +One of the URLs you provided did not look like a valid bug URL. + +=item 115 (See Also Edit Denied) + +You did not have the necessary rights to edit the See Also field for +this bug. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item Before Bugzilla B<3.6>, error 115 had a generic error code of 32000. + +=back + +=back + + +=head2 update_tags + +=over + +=item B<Description> + +Adds or removes tags on bugs. + +=item B<Params> + +=over + +=item C<ids> + +B<Required> C<array> An array of ints and/or strings--the ids +or aliases of bugs that you want to add or remove tags to. All the tags +will be added or removed to all these bugs. + +=item C<tags> + +B<Required> C<hash> A hash representing tags to be added and/or removed. +The hash has the following fields: + +=over + +=item C<add> An array of C<string>s representing tag names +to be added to the bugs. + +It is safe to specify tags that are already associated with the +bugs--they will just be silently ignored. + +=item C<remove> An array of C<string>s representing tag names +to be removed from the bugs. + +It is safe to specify tags that are not associated with any +bugs--they will just be silently ignored. + +=back + +=back + +=item B<Returns> + +C<changes>, a hash containing bug IDs as keys and one single value +name "tags" which is also a hash, with C<added> and C<removed> as keys. +See L</update_see_also> for an example of how it looks like. + +=item B<Errors> + +This method can throw the same errors as L</get>. + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=back + +=back + +=head2 search_comment_tags + +=over + +=item B<Description> + +Searches for tags which contain the provided substring. + +=item B<REST> + +To search for comment tags: + +GET /rest/bug/comment/tags/<query> + +=item B<Params> + +=over + +=item C<query> + +B<Required> C<string> Only tags containg this substring will be returned. + +=item C<limit> + +C<int> If provided will return no more than C<limit> tags. Defaults to C<10>. + +=back + +=item B<Returns> + +An C<array of strings> of matching tags. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws, plus: + +=over + +=item 125 (Comment Tagging Disabled) + +Comment tagging support is not available or enabled. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update_comment_tags + +=over + +=item B<Description> + +Adds or removes tags from a comment. + +=item B<REST> + +To update the tags comments attached to a comment: + +PUT /rest/bug/comment/tags + +The params to include in the PUT body as well as the returned data format, +are the same as below. + +=item B<Params> + +=over + +=item C<comment_id> + +B<Required> C<int> The ID of the comment to update. + +=item C<add> + +C<array of strings> The tags to attach to the comment. + +=item C<remove> + +C<array of strings> The tags to detach from the comment. + +=back + +=item B<Returns> + +An C<array of strings> containing the comment's updated tags. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws, plus: + +=over + +=item 125 (Comment Tagging Disabled) + +Comment tagging support is not available or enabled. + +=item 126 (Invalid Comment Tag) + +The comment tag provided was not valid (eg. contains invalid characters). + +=item 127 (Comment Tag Too Short) + +The comment tag provided is shorter than the minimum length. + +=item 128 (Comment Tag Too Long) + +The comment tag provided is longer than the maximum length. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 render_comment + +=over + +=item B<Description> + +Returns the HTML rendering of the provided comment text. + +=item B<Params> + +=over + +=item C<text> + +B<Required> C<strings> Text comment text to render. + +=item C<id> + +C<int> The ID of the bug to render the comment against. + +=back + +=item B<Returns> + +C<html> containing the HTML rendering. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws. + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head1 SEE ALSO + +=over + +=item L<Bugzilla::API::1_0::Resource> + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=item get_bugs + +=item get_history + +=back diff --git a/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm b/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm new file mode 100644 index 000000000..7568fc30f --- /dev/null +++ b/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm @@ -0,0 +1,239 @@ +# 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::1_0::Resource::BugUserLastVisit; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Util; + +use Bugzilla::Bug; +use Bugzilla::Error; +use Bugzilla::Constants; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant READ_ONLY => qw( + get +); + +use constant PUBLIC_METHODS => qw( + get + update +); + +sub REST_RESOURCES { + return [ + # bug-id + qr{^/bug_user_last_visit/(\d+)$}, { + GET => { + method => 'get', + params => sub { + return { ids => $_[0] }; + }, + }, + POST => { + method => 'update', + params => sub { + return { ids => $_[0] }; + }, + }, + }, + ]; +} + +############ +# Methods # +############ + +sub update { + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $user->login(LOGIN_REQUIRED); + + my $ids = $params->{ids} // []; + ThrowCodeError('param_required', { param => 'ids' }) unless @$ids; + + # Cache permissions for bugs. This highly reduces the number of calls to the + # DB. visible_bugs() is only able to handle bug IDs, so we have to skip + # aliases. + $user->visible_bugs([grep /^[0-9]$/, @$ids]); + + $dbh->bz_start_transaction(); + my @results; + my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }); + + ThrowUserError('user_not_involved', { bug_id => $bug->id }) + unless $user->is_involved_in_bug($bug); + + $bug->update_user_last_visit($user, $last_visit_ts); + + push( + @results, + $self->_bug_user_last_visit_to_hash( + $bug, $last_visit_ts, $params + )); + } + $dbh->bz_commit_transaction(); + + return \@results; +} + +sub get { + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $ids = $params->{ids}; + + $user->login(LOGIN_REQUIRED); + + if ($ids) { + # Cache permissions for bugs. This highly reduces the number of calls to + # the DB. visible_bugs() is only able to handle bug IDs, so we have to + # skip aliases. + $user->visible_bugs([grep /^[0-9]$/, @$ids]); + } + + my @last_visits = @{ $user->last_visited }; + + if ($ids) { + # remove bugs that we are not interested in if ids is passed in. + my %id_set = map { ($_ => 1) } @$ids; + @last_visits = grep { $id_set{ $_->bug_id } } @last_visits; + } + + return [ + map { + $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, + $params) + } @last_visits + ]; +} + +sub _bug_user_last_visit_to_hash { + my ($self, $bug_id, $last_visit_ts, $params) = @_; + + my %result = (id => as_int($bug_id), + last_visit_ts => as_datetime($last_visit_ts)); + + return filter($params, \%result); +} + +1; + +__END__ +=head1 NAME + +Bugzilla::API::1_0::Resource::BugUserLastVisit - Find and Store the last time a +user visited a bug. + +=head1 METHODS + +=head2 update + +=over + +=item B<Description> + +Update the last visit time for the specified bug and current user. + +=item B<REST> + +To add a single bug id: + + POST /rest/bug_user_last_visit/<bug-id> + +Tp add one or more bug ids at once: + + POST /rest/bug_user_last_visit + +The returned data format is the same as below. + +=item B<Params> + +=over + +=item C<ids> (array) - One or more bug ids to add. + +=back + +=item B<Returns> + +=over + +=item C<array> - An array of hashes containing the following: + +=over + +=item C<id> - (int) The bug id. + +=item C<last_visit_ts> - (string) The timestamp the user last visited the bug. + +=back + +=back + +=back + +=head2 get + +=over + +=item B<Description> + +Get the last visited timestamp for one or more specified bug ids. + +=item B<REST> + +To return the last visited timestamp for a single bug id: + + GET /rest/bug_user_last_visit/<bug-id> + +=item B<Params> + +=over + +=item C<ids> (integer) - One or more optional bug ids to get. + +=back + +=item B<Returns> + +=over + +=item C<array> - An array of hashes containing the following: + +=over + +=item C<id> - (int) The bug id. + +=item C<last_visit_ts> - (string) The timestamp the user last visited the bug. + +=back + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/Bugzilla.pm b/Bugzilla/API/1_0/Resource/Bugzilla.pm new file mode 100644 index 000000000..8a0c6baac --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Bugzilla.pm @@ -0,0 +1,547 @@ +# 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::1_0::Resource::Bugzilla; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Util; + +use Bugzilla::Constants; +use Bugzilla::Util qw(datetime_from); +use Bugzilla::Util qw(trick_taint); + +use DateTime; +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +# Basic info that is needed before logins +use constant LOGIN_EXEMPT => { + parameters => 1, + timezone => 1, + version => 1, +}; + +use constant READ_ONLY => qw( + extensions + parameters + timezone + time + version +); + +use constant PUBLIC_METHODS => qw( + extensions + last_audit_time + parameters + time + timezone + version +); + +# Logged-out users do not need to know more than that. +use constant PARAMETERS_LOGGED_OUT => qw( + maintainer + requirelogin +); + +# These parameters are guessable from the web UI when the user +# is logged in. So it's safe to access them. +use constant PARAMETERS_LOGGED_IN => qw( + allowemailchange + attachment_base + commentonchange_resolution + commentonduplicate + cookiepath + defaultopsys + defaultplatform + defaultpriority + defaultseverity + duplicate_or_move_bug_status + emailregexpdesc + emailsuffix + letsubmitterchoosemilestone + letsubmitterchoosepriority + mailfrom + maintainer + maxattachmentsize + maxlocalattachment + musthavemilestoneonaccept + noresolveonopenblockers + password_complexity + rememberlogin + requirelogin + search_allow_no_criteria + urlbase + use_see_also + useclassification + usemenuforusers + useqacontact + usestatuswhiteboard + usetargetmilestone +); + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/version$}, { + GET => { + method => 'version' + } + }, + qr{^/extensions$}, { + GET => { + method => 'extensions' + } + }, + qr{^/timezone$}, { + GET => { + method => 'timezone' + } + }, + qr{^/time$}, { + GET => { + method => 'time' + } + }, + qr{^/last_audit_time$}, { + GET => { + method => 'last_audit_time' + } + }, + qr{^/parameters$}, { + GET => { + method => 'parameters' + } + } + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub version { + my $self = shift; + return { version => as_string(BUGZILLA_VERSION) }; +} + +sub extensions { + my $self = shift; + + my %retval; + foreach my $extension (@{ Bugzilla->extensions }) { + my $version = $extension->VERSION || 0; + my $name = $extension->NAME; + $retval{$name}->{version} = as_string($version); + } + return { extensions => \%retval }; +} + +sub timezone { + my $self = shift; + # All Webservices return times in UTC; Use UTC here for backwards compat. + return { timezone => as_string("+0000") }; +} + +sub time { + my ($self) = @_; + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + my $dbh = Bugzilla->dbh; + + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC'); + my $now_utc = DateTime->now(); + + return { + db_time => as_datetime($db_time), + web_time => as_datetime($now_utc), + }; +} + +sub last_audit_time { + my ($self, $params) = validate(@_, 'class'); + my $dbh = Bugzilla->dbh; + + my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; + my $class_values = $params->{class}; + my @class_values_quoted; + foreach my $class_value (@$class_values) { + push (@class_values_quoted, $dbh->quote($class_value)) + if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; + } + + if (@class_values_quoted) { + $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); + } + + my $last_audit_time = $dbh->selectrow_array("$sql_statement"); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + $last_audit_time = datetime_from($last_audit_time, 'UTC'); + + return { + last_audit_time => as_datetime($last_audit_time) + }; +} + +sub parameters { + my ($self, $args) = @_; + my $user = Bugzilla->login(); + my $params = Bugzilla->params; + $args ||= {}; + + my @params_list = $user->in_group('tweakparams') + ? keys(%$params) + : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT; + + my %parameters; + foreach my $param (@params_list) { + next unless filter_wants($args, $param); + $parameters{$param} = as_string($params->{$param}); + } + + return { parameters => \%parameters }; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Bugzilla - Global functions for the webservice interface. + +=head1 DESCRIPTION + +This provides functions that tell you about Bugzilla in general. + +=head1 METHODS + +=head2 version + +=over + +=item B<Description> + +Returns the current version of Bugzilla. + +=item B<REST> + +GET /rest/version + +The returned data format is the same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash with a single item, C<version>, that is the version as a +string. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 extensions + +=over + +=item B<Description> + +Gets information about the extensions that are currently installed and enabled +in this Bugzilla. + +=item B<REST> + +GET /rest/extensions + +The returned data format is the same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash with a single item, C<extensions>. This points to a hash. I<That> hash +contains the names of extensions as keys, and the values are a hash. +That hash contains a single key C<version>, which is the version of the +extension, or C<0> if the extension hasn't defined a version. + +The return value looks something like this: + + extensions => { + Example => { + version => '3.6', + }, + BmpConvert => { + version => '1.0', + }, + } + +=item B<History> + +=over + +=item Added in Bugzilla B<3.2>. + +=item As of Bugzilla B<3.6>, the names of extensions are canonical names +that the extensions define themselves. Before 3.6, the names of the +extensions depended on the directory they were in on the Bugzilla server. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 timezone + +B<DEPRECATED> This method may be removed in a future version of Bugzilla. +Use L</time> instead. + +=over + +=item B<Description> + +Returns the timezone that Bugzilla expects dates and times in. + +=item B<REST> + +GET /rest/timezone + +The returned data format is the same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash with a single item, C<timezone>, that is the timezone offset as a +string in (+/-)XXXX (RFC 2822) format. + +=item B<History> + +=over + +=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000> +(the UTC timezone). + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + + +=head2 time + +=over + +=item B<Description> + +Gets information about what time the Bugzilla server thinks it is, and +what timezone it's running in. + +=item B<REST> + +GET /rest/time + +The returned data format is the same as below. + +=item B<Params> (none) + +=item B<Returns> + +A struct with the following items: + +=over + +=item C<db_time> + +C<dateTime> The current time in UTC, according to the Bugzilla +I<database server>. + +Note that Bugzilla assumes that the database and the webserver are running +in the same time zone. However, if the web server and the database server +aren't synchronized for some reason, I<this> is the time that you should +rely on for doing searches and other input to the WebService. + +=item C<web_time> + +C<dateTime> This is the current time in UTC, according to Bugzilla's +I<web server>. + +This might be different by a second from C<db_time> since this comes from +a different source. If it's any more different than a second, then there is +likely some problem with this Bugzilla instance. In this case you should +rely on the C<db_time>, not the C<web_time>. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item As of Bugzilla B<3.6>, this method returns all data as though the server +were in the UTC timezone, instead of returning information in the server's +local timezone. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 parameters + +=over + +=item B<Description> + +Returns parameter values currently used in this Bugzilla. + +=item B<REST> + +GET /rest/parameters + +The returned data format is the same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash with a single item C<parameters> which contains a hash with +the name of the parameters as keys and their value as values. All +values are returned as strings. +The list of parameters returned by this method depends on the user +credentials: + +A logged-out user can only access the C<maintainer> and C<requirelogin> parameters. + +A logged-in user can access the following parameters (listed alphabetically): + C<allowemailchange>, + C<attachment_base>, + C<commentonchange_resolution>, + C<commentonduplicate>, + C<cookiepath>, + C<defaultopsys>, + C<defaultplatform>, + C<defaultpriority>, + C<defaultseverity>, + C<duplicate_or_move_bug_status>, + C<emailregexpdesc>, + C<emailsuffix>, + C<letsubmitterchoosemilestone>, + C<letsubmitterchoosepriority>, + C<mailfrom>, + C<maintainer>, + C<maxattachmentsize>, + C<maxlocalattachment>, + C<musthavemilestoneonaccept>, + C<noresolveonopenblockers>, + C<password_complexity>, + C<rememberlogin>, + C<requirelogin>, + C<search_allow_no_criteria>, + C<urlbase>, + C<use_see_also>, + C<useclassification>, + C<usemenuforusers>, + C<useqacontact>, + C<usestatuswhiteboard>, + C<usetargetmilestone>. + +A user in the tweakparams group can access all existing parameters. +New parameters can appear or obsolete parameters can disappear depending +on the version of Bugzilla and on extensions being installed. +The list of parameters returned by this method is not stable and will +never be stable. + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 last_audit_time + +=over + +=item B<Description> + +Gets the latest time of the audit_log table. + +=item B<REST> + +GET /rest/last_audit_time + +The returned data format is the same as below. + +=item B<Params> + +You can pass the optional parameter C<class> to get the maximum for only +the listed classes. + +=over + +=item C<class> (array) - An array of strings representing the class names. + +B<Note:> The class names are defined as "Bugzilla::<class_name>". For the product +use Bugzilla:Product. + +=back + +=item B<Returns> + +A hash with a single item, C<last_audit_time>, that is the maximum of the +at_time from the audit_log. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/Classification.pm b/Bugzilla/API/1_0/Resource/Classification.pm new file mode 100644 index 000000000..7a59e381d --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Classification.pm @@ -0,0 +1,235 @@ +# 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::1_0::Resource::Classification; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Util; + +use Bugzilla::Classification; +use Bugzilla::Error; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant READ_ONLY => qw( + get +); + +use constant PUBLIC_METHODS => qw( + get +); + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/classification/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + } + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub get { + my ($self, $params) = validate(@_, 'names', 'ids'); + + defined $params->{names} || defined $params->{ids} + || ThrowCodeError('params_required', { function => 'Classification.get', + params => ['names', 'ids'] }); + + my $user = Bugzilla->user; + + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); + + Bugzilla->switch_to_shadow_db; + + my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; + unless ($user->in_group('editclassifications')) { + my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } + + my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; + + return { classifications => \@classifications }; +} + +sub _classification_to_hash { + my ($self, $classification, $params) = @_; + + my $user = Bugzilla->user; + return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications')); + + my $products = $user->in_group('editclassifications') ? + $classification->products : $user->get_selectable_products($classification->id); + + return filter $params, { + id => as_int($classification->id), + name => as_string($classification->name), + description => as_string($classification->description), + sort_key => as_int($classification->sortkey), + products => [ map { $self->_product_to_hash($_, $params) } @$products ], + }; +} + +sub _product_to_hash { + my ($self, $product, $params) = @_; + + return filter $params, { + id => as_int($product->id), + name => as_string($product->name), + description => as_string($product->description), + }, undef, 'products'; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Classification - The Classification API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to deal with the available Classifications. +You will be able to get information about them as well as manipulate them. + +=head1 METHODS + +=head2 get + +=over + +=item B<Description> + +Returns a hash containing information about a set of classifications. + +=item B<REST> + +To return information on a single classification: + +GET /rest/classification/<classification_id_or_name> + +The returned data format will be the same as below. + +=item B<Params> + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +You could get classifications info by supplying their names and/or ids. +So, this method accepts the following parameters: + +=over + +=item C<ids> + +An array of classification ids. + +=item C<names> + +An array of classification names. + +=back + +=item B<Returns> + +A hash with the key C<classifications> and an array of hashes as the corresponding value. +Each element of the array represents a classification that the user is authorized to see +and has the following keys: + +=over + +=item C<id> + +C<int> The id of the classification. + +=item C<name> + +C<string> The name of the classification. + +=item C<description> + +C<string> The description of the classificaion. + +=item C<sort_key> + +C<int> The value which determines the order the classification is sorted. + +=item C<products> + +An array of hashes. The array contains the products the user is authorized to +access within the classification. Each hash has the following keys: + +=over + +=item C<name> + +C<string> The name of the product. + +=item C<id> + +C<int> The id of the product. + +=item C<description> + +C<string> The description of the product. + +=back + +=back + +=item B<Errors> + +=over + +=item 900 (Classification not enabled) + +Classification is not enabled on this installation. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/Component.pm b/Bugzilla/API/1_0/Resource/Component.pm new file mode 100644 index 000000000..c3bdc0386 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Component.pm @@ -0,0 +1,639 @@ +# 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::1_0::Resource::Component; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Error; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant PUBLIC_METHODS => qw( + create +); + +use constant CREATE_MAPPED_FIELDS => { + default_assignee => 'initialowner', + default_qa_contact => 'initialqacontact', + default_cc => 'initial_cc', + is_open => 'isactive', +}; + +use constant MAPPED_FIELDS => { + is_open => 'is_active', +}; + +use constant MAPPED_RETURNS => { + initialowner => 'default_assignee', + initialqacontact => 'default_qa_contact', + cc_list => 'default_cc', + isactive => 'isopen', +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/component$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/component/(\d+)$}, { + PUT => { + method => 'update', + params => sub { + return { ids => [ $_[0] ] }; + } + }, + DELETE => { + method => 'delete', + params => sub { + return { ids => [ $_[0] ] }; + } + }, + }, + qr{^/component/([^/]+)/([^/]+)$}, { + PUT => { + method => 'update', + params => sub { + return { names => [ { product => $_[0], component => $_[1] } ] }; + } + }, + DELETE => { + method => 'delete', + params => sub { + return { names => [ { product => $_[0], component => $_[1] } ] }; + } + }, + }, + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub create { + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar @{ $user->get_products_by_permission('editcomponents') } + || ThrowUserError('auth_failure', { group => 'editcomponents', + action => 'edit', + object => 'components' }); + + my $product = $user->check_can_admin_product($params->{product}); + + # Translate the fields + my $values = translate($params, CREATE_MAPPED_FIELDS); + $values->{product} = $product; + + # Create the component and return the newly created id. + my $component = Bugzilla::Component->create($values); + return { id => as_int($component->id) }; +} + +sub _component_params_to_objects { + # We can't use Util's _param_to_objects since name is a hash + my $params = shift; + my $user = Bugzilla->user; + + my @components = (); + + if (defined $params->{ids}) { + push @components, @{ Bugzilla::Component->new_from_list($params->{ids}) }; + } + + if (defined $params->{names}) { + # To get the component objects for product/component combination + # first obtain the product object from the passed product name + foreach my $name_hash (@{$params->{names}}) { + my $product = $user->can_admin_product($name_hash->{product}); + push @components, @{ Bugzilla::Component->match({ + product_id => $product->id, + name => $name_hash->{component} + })}; + } + } + + my %seen_component_ids = (); + + my @accessible_components; + foreach my $component (@components) { + # Skip if we already included this component + next if $seen_component_ids{$component->id}++; + + # Can the user see and admin this product? + my $product = $component->product; + $user->check_can_admin_product($product->name); + + push @accessible_components, $component; + } + + return \@accessible_components; +} + +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 => "components" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'Component.update', params => ['ids', 'names'] }); + + my $component_objects = _component_params_to_objects($params); + + # If the user tries to change component name for several + # components of the same product then throw an error + if ($params->{name}) { + my %unique_product_comps; + foreach my $comp (@$component_objects) { + if($unique_product_comps{$comp->product_id}) { + ThrowUserError("multiple_components_update_not_allowed"); + } + else { + $unique_product_comps{$comp->product_id} = 1; + } + } + } + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $component (@$component_objects) { + $component->set_all($values); + } + + my %changes; + foreach my $component (@$component_objects) { + my $returned_changes = $component->update(); + $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $component (@$component_objects) { + my %hash = ( + id => $component->id, + changes => {}, + ); + + foreach my $field (keys %{ $changes{$component->id} }) { + my $change = $changes{$component->id}->{$field}; + + if ($field eq 'default_assignee' + || $field eq 'default_qa_contact' + || $field eq 'default_cc' + ) { + # We need to convert user ids to login names + my @old_user_ids = split(/[,\s]+/, $change->[0]); + my @new_user_ids = split(/[,\s]+/, $change->[1]); + + my @old_users = map { $_->login } + @{Bugzilla::User->new_from_list(\@old_user_ids)}; + my @new_users = map { $_->login } + @{Bugzilla::User->new_from_list(\@new_user_ids)}; + + $hash{changes}{$field} = { + removed => as_string(join(', ', @old_users)), + added => as_string(join(', ', @new_users)), + }; + } + else { + $hash{changes}{$field} = { + removed => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + } + + push(@result, \%hash); + } + + return { components => \@result }; +} + +sub delete { + 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 => "components" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'Component.delete', params => ['ids', 'names'] }); + + my $component_objects = _component_params_to_objects($params); + + $dbh->bz_start_transaction(); + my %changes; + foreach my $component (@$component_objects) { + my $returned_changes = $component->remove_from_db(); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $component (@$component_objects) { + push @result, { id => $component->id }; + } + + return { components => \@result }; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Component - The Component API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to deal with the available product components. +You will be able to get information about them as well as manipulate them. + +=head1 METHODS + +=head2 create + +=over + +=item B<Description> + +This allows you to create a new component in Bugzilla. + +=item B<Params> + +Some params must be set, or an error will be thrown. These params are +marked B<Required>. + +=over + +=item C<name> + +B<Required> C<string> The name of the new component. + +=item C<product> + +B<Required> C<string> The name of the product that the component must be +added to. This product must already exist, and the user have the necessary +permissions to edit components for it. + +=item C<description> + +B<Required> C<string> The description of the new component. + +=item C<default_assignee> + +B<Required> C<string> The login name of the default assignee of the component. + +=item C<default_cc> + +C<array> An array of strings with each element representing one login name of the default CC list. + +=item C<default_qa_contact> + +C<string> The login name of the default QA contact for the component. + +=item C<is_open> + +C<boolean> 1 if you want to enable the component for bug creations. 0 otherwise. Default is 1. + +=back + +=item B<Returns> + +A hash with one key: C<id>. This will represent the ID of the newly-added +component. + +=item B<Errors> + +=over + +=item 304 (Authorization Failure) + +You are not authorized to create a new component. + +=item 1200 (Component already exists) + +The name that you specified for the new component already exists in the +specified product. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B<Description> + +This allows you to update one or more components in Bugzilla. + +=item B<REST> + +PUT /rest/component/<component_id> + +PUT /rest/component/<product_name>/<component_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + +=item B<Params> + +B<Note:> The following parameters specify which components you are updating. +You must set one or both of these parameters. + +=over + +=item C<ids> + +C<array> of C<int>s. Numeric ids of the components that you wish to update. + +=item C<names> + +C<array> of C<hash>es. Names of the components that you wish to update. The +hash keys are C<product> and C<component>, representing the name of the product +and the component you wish to change. + +=back + +B<Note:> The following parameters specify the new values you want to set for +the components you are updating. + +=over + +=item C<name> + +C<string> A new name for this component. If you try to set this while updating +more than one component for a product, an error will occur, as component names +must be unique per product. + +=item C<description> + +C<string> Update the long description for these components to this value. + +=item C<default_assignee> + +C<string> The login name of the default assignee of the component. + +=item C<default_cc> + +C<array> An array of strings with each element representing one login name of the default CC list. + +=item C<default_qa_contact> + +C<string> The login name of the default QA contact for the component. + +=item C<is_open> + +C<boolean> True if the component is currently allowing bugs to be entered +into it, False otherwise. + +=back + +=item B<Returns> + +A C<hash> with a single field "components". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the component that was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this component. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> + +C<string> The value that this field was changed to. + +=item C<removed> + +C<string> 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: + + { + components => [ + { + id => 123, + changes => { + name => { + removed => 'FooName', + added => 'BarName' + }, + default_assignee => { + removed => 'foo@company.com', + added => 'bar@company.com', + } + } + } + ] + } + +=back + +=item B<Errors> + +=over + +=item 51 (User does not exist) + +One of the contact e-mail addresses is not a valid Bugzilla user. + +=item 106 (Product access denied) + +The product you are trying to modify does not exist or you don't have access to it. + +=item 706 (Product admin denied) + +You do not have the permission to change components for this product. + +=item 105 (Component name too long) + +The name specified for this component was longer than the maximum +allowed length. + +=item 1200 (Component name already exists) + +You specified the name of a component that already exists. +(Component names must be unique per product in Bugzilla.) + +=item 1210 (Component blank name) + +You must specify a non-blank name for this component. + +=item 1211 (Component must have description) + +You must specify a description for this component. + +=item 1212 (Component name is not unique) + +You have attempted to set more than one component in the same product with the +same name. Component names must be unique in each product. + +=item 1213 (Component needs a default assignee) + +A default assignee is required for this component. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 delete + +=over + +=item B<Description> + +This allows you to delete one or more components in Bugzilla. + +=item B<REST> + +DELETE /rest/component/<component_id> + +DELETE /rest/component/<product_name>/<component_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + +=item B<Params> + +B<Note:> The following parameters specify which components you are deleting. +You must set one or both of these parameters. + +=over + +=item C<ids> + +C<array> of C<int>s. Numeric ids of the components that you wish to delete. + +=item C<names> + +C<array> of C<hash>es. Names of the components that you wish to delete. The +hash keys are C<product> and C<component>, representing the name of the product +and the component you wish to delete. + +=back + +=item B<Returns> + +A C<hash> with a single field "components". This points to an array of hashes +with the following field: + +=over + +=item C<id> + +C<int> The id of the component that was deleted. + +=back + +=item B<Errors> + +=over + +=item 106 (Product access denied) + +The product you are trying to modify does not exist or you don't have access to it. + +=item 706 (Product admin denied) + +You do not have the permission to delete components for this product. + +=item 1202 (Component has bugs) + +The component you are trying to delete currently has bugs assigned to it. +You must move these bugs before trying to delete the component. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0> + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/FlagType.pm b/Bugzilla/API/1_0/Resource/FlagType.pm new file mode 100644 index 000000000..297be1510 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/FlagType.pm @@ -0,0 +1,890 @@ +# 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::1_0::Resource::FlagType; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +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); +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant READ_ONLY => qw( + get +); + +use constant PUBLIC_METHODS => qw( + create + get + update +); + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/flag_type$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/flag_type/([^/]+)/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + return { product => $_[0], + component => $_[1] }; + } + } + }, + qr{^/flag_type/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + return { product => $_[0] }; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + }, + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub get { + 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 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 => as_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 => as_int($flag_type_id), + name => as_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 => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + + push(@result, \%hash); + } + + return { flagtypes => \@result }; +} + +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 => as_int($flagtype->id), + name => as_string($flagtype->name), + description => as_string($flagtype->description), + type => as_string($flagtype->target_type), + values => \@values, + is_active => as_boolean($flagtype->is_active), + is_requesteeble => as_boolean($flagtype->is_requesteeble), + is_multiplicable => as_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 _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::API::1_0::Resource::FlagType - API for creating flags. + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to create new flags + +=head1 METHODS + +=head2 Get Flag Types + +=over + +=item C<get> + +=item B<Description> + +Get information about valid flag types that can be set for bugs and attachments. + +=item B<REST> + +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 /rest/flag_type/<product> + +To get information about flag_types for a product and component: + +GET /rest/flag_type/<product>/<component> + +The returned data format is the same as below. + +=item B<Params> + +You must pass a product name and an optional component name. + +=over + +=item C<product> (string) - The name of a valid product. + +=item C<component> (string) - An optional valid component name associated with the product. + +=back + +=item B<Returns> + +A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes, +containing the following keys: + +=over + +=item C<id> + +C<int> An integer id uniquely identifying this flag type. + +=item C<name> + +C<string> The name for the flag type. + +=item C<type> + +C<string> The target of the flag type which is either C<bug> or C<attachment>. + +=item C<description> + +C<string> The description of the flag type. + +=item C<values> + +C<array> An array of string values that the user can set on the flag type. + +=item C<is_requesteeble> + +C<boolean> Users can ask specific other users to set flags of this type. + +=item C<is_multiplicable> + +C<boolean> Multiple flags of this type can be set for the same bug or attachment. + +=back + +=item B<Errors> + +=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<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 Create Flag + +=over + +=item C<create> + +=item B<Description> + +Creates a new FlagType + +=item B<REST> + +POST /rest/flag_type + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +At a minimum the following two arguments must be supplied: + +=over + +=item C<name> (string) - The name of the new Flag Type. + +=item C<description> (string) - A description for the Flag Type object. + +=back + +=item B<Returns> + +C<int> flag_id + +The ID of the new FlagType object is returned. + +=item B<Params> + +=over + +=item name B<required> + +C<string> A short name identifying this type. + +=item description B<required> + +C<string> A comprehensive description of this type. + +=item inclusions B<optional> + +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<All> components of I<FooProduct>, +components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>. + +=item exclusions B<optional> + +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<optional> + +C<int> 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<optional> + +C<boolean> Flag of this type appear in the UI and can be set. Default is B<true>. + +=item is_requestable B<optional> + +C<boolean> Users can ask for flags of this type to be set. Default is B<true>. + +=item cc_list B<optional> + +C<array> 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<optional> + +C<boolean> Users can ask specific other users to set flags of this type as +opposed to just asking the wind. Default is B<true>. + +=item is_multiplicable B<optional> + +C<boolean> Multiple flags of this type can be set on the same bug. Default is B<true>. + +=item grant_group B<optional> + +C<string> 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<no group>. + +=item request_group B<optional> + +C<string> 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<no group>. + +=back + +=item B<Errors> + +=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<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B<Description> + +This allows you to update a flag type in Bugzilla. + +=item B<REST> + +PUT /rest/flag_type/<product_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + +=item B<Params> + +B<Note:> The following parameters specify which products you are updating. +You must set one or both of these parameters. + +=over + +=item C<ids> + +C<array> of C<int>s. Numeric ids of the flag types that you wish to update. + +=item C<names> + +C<array> of C<string>s. 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<Note:> The following parameters specify the new values you want to set for +the products you are updating. + +=over + +=item name + +C<string> A short name identifying this type. + +=item description + +C<string> A comprehensive description of this type. + +=item inclusions B<optional> + +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<All> components of I<FooProduct>, +components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>. + +=item exclusions B<optional> + +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<int> 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<boolean> Flag of this type appear in the UI and can be set. + +=item is_requestable + +C<boolean> Users can ask for flags of this type to be set. + +=item cc_list + +C<array> 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<boolean> Users can ask specific other users to set flags of this type as +opposed to just asking the wind. + +=item is_multiplicable + +C<boolean> Multiple flags of this type can be set on the same bug. + +=item grant_group + +C<string> 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<string> 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<Returns> + +A C<hash> with a single field "flagtypes". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the product that was updated. + +=item C<name> + +C<string> The name of the product that was updated. + +=item C<changes> + +C<hash> 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<added> + +C<string> The value that this field was changed to. + +=item C<removed> + +C<string> 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<Errors> + +=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<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/Group.pm b/Bugzilla/API/1_0/Resource/Group.pm new file mode 100644 index 000000000..aee8a7492 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Group.pm @@ -0,0 +1,636 @@ +# 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::1_0::Resource::Group; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Constants; +use Bugzilla::Error; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant PUBLIC_METHODS => qw( + create + get + update +); + +use constant MAPPED_RETURNS => { + userregexp => 'user_regexp', + isactive => 'is_active' +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/group$}, { + GET => { + method => 'get' + }, + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/group/([^/]+)$}, { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + } + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub create { + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", { group => "creategroups", + action => "add", + object => "group"}); + # Create group + my $group = Bugzilla::Group->create({ + name => $params->{name}, + description => $params->{description}, + userregexp => $params->{user_regexp}, + isactive => $params->{is_active}, + isbuggroup => 1, + icon_url => $params->{icon_url} + }); + return { id => as_int($group->id) }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", { group => "creategroups", + action => "edit", + object => "group" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'Group.update', params => ['ids', 'names'] }); + + my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + + my %values = %$params; + + # We delete names and ids to keep only new values to set. + delete $values{names}; + delete $values{ids}; + + $dbh->bz_start_transaction(); + foreach my $group (@$group_objects) { + $group->set_all(\%values); + } + + my %changes; + foreach my $group (@$group_objects) { + my $returned_changes = $group->update(); + $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $group (@$group_objects) { + my %hash = ( + id => $group->id, + changes => {}, + ); + foreach my $field (keys %{ $changes{$group->id} }) { + my $change = $changes{$group->id}->{$field}; + $hash{changes}{$field} = { + removed => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + push(@result, \%hash); + } + + return { groups => \@result }; +} + +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + + Bugzilla->login(LOGIN_REQUIRED); + + # Reject access if there is no sense in continuing. + my $user = Bugzilla->user; + my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups'); + if (!$all_groups && !$user->can_bless) { + ThrowUserError('group_cannot_view'); + } + + Bugzilla->switch_to_shadow_db(); + + my $groups = []; + + if (defined $params->{ids}) { + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } + + if (defined $params->{names}) { + # Get the groups by name. Check will throw an error if a bad name is given + foreach my $name (@{$params->{names}}) { + # Skip if we got this from params->{id} + next if grep { $_->name eq $name } @$groups; + + push @$groups, Bugzilla::Group->check({ name => $name }); + } + } + + if (!defined $params->{ids} && !defined $params->{names}) { + if ($all_groups) { + @$groups = Bugzilla::Group->get_all; + } + else { + # Get only groups the user has bless groups too + $groups = $user->bless_groups; + } + } + + # Now create a result entry for each. + my @groups = map { $self->_group_to_hash($params, $_) } @$groups; + return { groups => \@groups }; +} + +sub _group_to_hash { + my ($self, $params, $group) = @_; + my $user = Bugzilla->user; + + my $field_data = { + id => as_int($group->id), + name => as_string($group->name), + description => as_string($group->description), + }; + + if ($user->in_group('creategroups')) { + $field_data->{is_active} = as_boolean($group->is_active); + $field_data->{is_bug_group} = as_boolean($group->is_bug_group); + $field_data->{user_regexp} = as_string($group->user_regexp); + } + + if ($params->{membership}) { + $field_data->{membership} = $self->_get_group_membership($group, $params); + } + return $field_data; +} + +sub _get_group_membership { + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; + + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); + + my $query = 'SELECT userid FROM profiles'; + my $visibleGroups; + + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_inherited; + + if (scalar @$visibleGroups) { + $query .= qq{, user_group_map AS ugm + WHERE ugm.user_id = profiles.userid + AND ugm.isbless = 0 + AND } . $dbh->sql_in('ugm.group_id', $visibleGroups); + } + } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) { + $visibleGroups = 1; + $query .= qq{, user_group_map AS ugm + WHERE ugm.user_id = profiles.userid + AND ugm.isbless = 0 + }; + } + if (!$visibleGroups) { + ThrowUserError('group_not_visible', { group => $group }); + } + + my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); + $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); + + my $userids = $dbh->selectcol_arrayref($query); + my $user_objects = Bugzilla::User->new_from_list($userids); + my @users = + map {{ + id => as_int($_->id), + real_name => as_string($_->name), + name => as_string($_->login), + email => as_string($_->email), + can_login => as_boolean($_->is_enabled), + email_enabled => as_boolean($_->email_enabled), + login_denied_text => as_string($_->disabledtext), + }} @$user_objects; + + return \@users; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Group - The API for creating, changing, and getting +information about Groups. + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to create Groups and +get information about them. + +=head1 METHODS + +=head2 create + +=over + +=item B<Description> + +This allows you to create a new group in Bugzilla. + +=item B<REST> + +POST /rest/group + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +Some params must be set, or an error will be thrown. These params are +marked B<Required>. + +=over + +=item C<name> + +B<Required> C<string> A short name for this group. Must be unique. This +is not usually displayed in the user interface, except in a few places. + +=item C<description> + +B<Required> C<string> A human-readable name for this group. Should be +relatively short. This is what will normally appear in the UI as the +name of the group. + +=item C<user_regexp> + +C<string> A regular expression. Any user whose Bugzilla username matches +this regular expression will automatically be granted membership in this group. + +=item C<is_active> + +C<boolean> C<True> if new group can be used for bugs, C<False> if this +is a group that will only contain users and no bugs will be restricted +to it. + +=item C<icon_url> + +C<string> A URL pointing to a small icon used to identify the group. +This icon will show up next to users' names in various parts of Bugzilla +if they are in this group. + +=back + +=item B<Returns> + +A hash with one element, C<id>. This is the id of the newly-created group. + +=item B<Errors> + +=over + +=item 800 (Empty Group Name) + +You must specify a value for the C<name> field. + +=item 801 (Group Exists) + +There is already another group with the same C<name>. + +=item 802 (Group Missing Description) + +You must specify a value for the C<description> field. + +=item 803 (Group Regexp Invalid) + +You specified an invalid regular expression in the C<user_regexp> field. + +=back + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B<Description> + +This allows you to update a group in Bugzilla. + +=item B<REST> + +PUT /rest/group/<group_name_or_id> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> param will be overridden as it is pulled +from the URL path. + +=item B<Params> + +At least C<ids> or C<names> must be set, or an error will be thrown. + +=over + +=item C<ids> + +B<Required> C<array> Contain ids of groups to update. + +=item C<names> + +B<Required> C<array> Contain names of groups to update. + +=item C<name> + +C<string> A new name for group. + +=item C<description> + +C<string> A new description for groups. This is what will appear in the UI +as the name of the groups. + +=item C<user_regexp> + +C<string> A new regular expression for email. Will automatically grant +membership to these groups to anyone with an email address that matches +this perl regular expression. + +=item C<is_active> + +C<boolean> Set if groups are active and eligible to be used for bugs. +True if bugs can be restricted to this group, false otherwise. + +=item C<icon_url> + +C<string> A URL pointing to an icon that will appear next to the name of +users who are in this group. + +=back + +=item B<Returns> + +A C<hash> with a single field "groups". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the group that was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this group. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> + +C<string> The values that were added to this field, +possibly a comma-and-space-separated list if multiple values were added. + +=item C<removed> + +C<string> The values that were removed from this field, possibly a +comma-and-space-separated list if multiple values were removed. + +=back + +=back + +=item B<Errors> + +The same as L</create>. + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 Group Information + +=head2 get + +=over + +=item B<Description> + +Returns information about L<Bugzilla::Group|Groups>. + +=item B<REST> + +To return information about a specific group by C<id> or C<name>: + +GET /rest/group/<group_id_or_name> + +You can also return information about more than one specific group +by using the following in your query string: + +GET /rest/group?ids=1&ids=2&ids=3 or GET /group?names=ProductOne&names=Product2 + +the returned data format is same as below. + +=item B<Params> + +If neither ids or names is passed, and you are in the creategroups or +editusers group, then all groups will be retrieved. Otherwise, only groups +that you have bless privileges for will be returned. + +=over + +=item C<ids> + +C<array> Contain ids of groups to update. + +=item C<names> + +C<array> Contain names of groups to update. + +=item C<membership> + +C<boolean> Set to 1 then a list of members of the passed groups' names and +ids will be returned. + +=back + +=item B<Returns> + +If the user is a member of the "creategroups" group they will receive +information about all groups or groups matching the criteria that they passed. +You have to be in the creategroups group unless you're requesting membership +information. + +If the user is not a member of the "creategroups" group, but they are in the +"editusers" group or have bless privileges to the groups they require +membership information for, the is_active, is_bug_group and user_regexp values +are not supplied. + +The return value will be a hash containing group names as the keys, each group +name will point to a hash that describes the group and has the following items: + +=over + +=item id + +C<int> The unique integer ID that Bugzilla uses to identify this group. +Even if the name of the group changes, this ID will stay the same. + +=item name + +C<string> The name of the group. + +=item description + +C<string> The description of the group. + +=item is_bug_group + +C<int> Whether this groups is to be used for bug reports or is only administrative specific. + +=item user_regexp + +C<string> A regular expression that allows users to be added to this group if their login matches. + +=item is_active + +C<int> Whether this group is currently active or not. + +=item users + +C<array> An array of hashes, each hash contains a user object for one of the +members of this group, only returned if the user sets the C<membership> +parameter to 1, the user hash has the following items: + +=over + +=item id + +C<int> The id of the user. + +=item real_name + +C<string> The actual name of the user. + +=item email + +C<string> The email address of the user. + +=item name + +C<string> The login name of the user. Note that in some situations this is +different than their email. + +=item can_login + +C<boolean> A boolean value to indicate if the user can login into bugzilla. + +=item email_enabled + +C<boolean> A boolean value to indicate if bug-related mail will be sent +to the user or not. + +=item disabled_text + +C<string> A text field that holds the reason for disabling a user from logging +into bugzilla, if empty then the user account is enabled otherwise it is +disabled/closed. + +=back + +=back + +=item B<Errors> + +=over + +=item 51 (Invalid Object) + +A non existing group name was passed to the function, as a result no +group object existed for that invalid name. + +=item 805 (Cannot view groups) + +Logged-in users are not authorized to edit bugzilla groups as they are not +members of the creategroups group in bugzilla, or they are not authorized to +access group member's information as they are not members of the "editusers" +group or can bless the group. + +=back + +=item B<History> + +=over + +=item This function was added in Bugzilla B<5.0>. + +=back + +=back + +=cut + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Resource/Product.pm b/Bugzilla/API/1_0/Resource/Product.pm new file mode 100644 index 000000000..0f393e207 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/Product.pm @@ -0,0 +1,1013 @@ +# 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::1_0::Resource::Product; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Product; +use Bugzilla::User; +use Bugzilla::Error; +use Bugzilla::Constants; + +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +use constant READ_ONLY => qw( + get + get_accessible_products + get_enterable_products + get_selectable_products +); + +use constant PUBLIC_METHODS => qw( + create + get + get_accessible_products + get_enterable_products + get_selectable_products + update +); + +use constant MAPPED_FIELDS => { + has_unconfirmed => 'allows_unconfirmed', + is_open => 'is_active', +}; + +use constant MAPPED_RETURNS => { + allows_unconfirmed => 'has_unconfirmed', + defaultmilestone => 'default_milestone', + isactive => 'is_open', +}; + +use constant FIELD_MAP => { + has_unconfirmed => 'allows_unconfirmed', + is_open => 'isactive', +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/product_accessible$}, { + GET => { + method => 'get_accessible_products' + } + }, + qr{^/product_enterable$}, { + GET => { + method => 'get_enterable_products' + } + }, + qr{^/product_selectable$}, { + GET => { + method => 'get_selectable_products' + } + }, + qr{^/product$}, { + GET => { + method => 'get' + }, + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/product/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + }, + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +BEGIN { *get_products = \&get } + +# Get the ids of the products the user can search +sub get_selectable_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; +} + +# Get the ids of the products the user can enter bugs against +sub get_enterable_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; +} + +# Get the union of the products the user can search and enter bugs against. +sub get_accessible_products { + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; +} + +# Get a list of actual products, based on list of ids or names +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} || defined $params->{names} || defined $params->{type} + || ThrowCodeError("params_required", { function => "Product.get", + params => ['ids', 'names', 'type'] }); + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{ $params->{type} }) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', + { type => $type }); + } + map { $product_hash{$_->id} = $_ } @$result; + } + $products = [ values %product_hash ]; + } + else { + $products = $user->get_accessible_products; + } + + my @requested_products; + + if (defined $params->{ids}) { + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; + + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, + grep { $ids{$_->id} } @$products); + } + + if (defined $params->{names}) { + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } + @$products) { + next if grep { $_->id == $product->id } + @requested_products; + push @requested_products, $product; + } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } + + # Now create a result entry for each. + my @products = map { $self->_product_to_hash($params, $_) } + @requested_products; + return { products => \@products }; +} + +sub create { + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "add", + object => "products"}); + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + # create_series has no default value. + create_series => defined $params->{create_series} ? + $params->{create_series} : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; + } + } + my $product = Bugzilla::Product->create($args); + return { id => as_int($product->id) }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "edit", + object => "products" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'Product.update', params => ['ids', 'names'] }); + + my $product_objects = params_to_objects($params, 'Bugzilla::Product'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $product (@$product_objects) { + $product->set_all($values); + } + + my %changes; + foreach my $product (@$product_objects) { + my $returned_changes = $product->update(); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $product (@$product_objects) { + my %hash = ( + id => $product->id, + changes => {}, + ); + + foreach my $field (keys %{ $changes{$product->id} }) { + my $change = $changes{$product->id}->{$field}; + $hash{changes}{$field} = { + removed => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + + push(@result, \%hash); + } + + return { products => \@result }; +} + +sub _product_to_hash { + my ($self, $params, $product) = @_; + + my $field_data = { + id => as_int($product->id), + name => as_string($product->name), + description => as_string($product->description), + is_active => as_boolean($product->is_active), + default_milestone => as_string($product->default_milestone), + has_unconfirmed => as_boolean($product->allows_unconfirmed), + classification => as_string($product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} = [map { + $self->_component_to_hash($_, $params) + } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} = [map { + $self->_version_to_hash($_, $params) + } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} = [map { + $self->_milestone_to_hash($_, $params) + } @{$product->milestones}]; + } + return filter($params, $field_data); +} + +sub _component_to_hash { + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => as_int($component->id), + name => as_string($component->name), + description => as_string($component->description), + default_assigned_to => + as_email($component->default_assignee->login), + default_qa_contact => + as_email($component->default_qa_contact ? + $component->default_qa_contact->login : ""), + sort_key => 0, # sort_key is returned to match Bug.fields + is_active => as_boolean($component->is_active), + }, undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => + [map { + $self->_flag_type_to_hash($_) + } @{$component->flag_types->{'bug'}}], + attachment => + [map { + $self->_flag_type_to_hash($_) + } @{$component->flag_types->{'attachment'}}], + }; + } + + return $field_data; +} + +sub _flag_type_to_hash { + my ($self, $flag_type, $params) = @_; + return filter $params, { + id => as_int($flag_type->id), + name => as_string($flag_type->name), + description => as_string($flag_type->description), + cc_list => as_string($flag_type->cc_list), + sort_key => as_int($flag_type->sortkey), + is_active => as_boolean($flag_type->is_active), + is_requestable => as_boolean($flag_type->is_requestable), + is_requesteeble => as_boolean($flag_type->is_requesteeble), + is_multiplicable => as_boolean($flag_type->is_multiplicable), + grant_group => as_int($flag_type->grant_group_id), + request_group => as_int($flag_type->request_group_id), + }, undef, 'flag_types'; +} + +sub _version_to_hash { + my ($self, $version, $params) = @_; + return filter $params, { + id => as_int($version->id), + name => as_string($version->name), + sort_key => 0, # sort_key is returened to match Bug.fields + is_active => as_boolean($version->is_active), + }, undef, 'versions'; +} + +sub _milestone_to_hash { + my ($self, $milestone, $params) = @_; + return filter $params, { + id => as_int($milestone->id), + name => as_string($milestone->name), + sort_key => as_int($milestone->sortkey), + is_active => as_boolean($milestone->is_active), + }, undef, 'milestones'; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::Product - The Product API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to list the available Products and +get information about them. + +=head1 METHODS + +=head2 get_selectable_products + +=over + +=item B<Description> + +Returns a list of the ids of the products the user can search on. + +=item B<REST> + +GET /rest/product_selectable + +the returned data format is same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash containing one item, C<ids>, that contains an array of product +ids. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get_enterable_products + +=over + +=item B<Description> + +Returns a list of the ids of the products the user can enter bugs +against. + +=item B<REST> + +GET /rest/product_enterable + +the returned data format is same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash containing one item, C<ids>, that contains an array of product +ids. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get_accessible_products + +=over + +=item B<Description> + +Returns a list of the ids of the products the user can search or enter +bugs against. + +=item B<REST> + +GET /rest/product_accessible + +the returned data format is same as below. + +=item B<Params> (none) + +=item B<Returns> + +A hash containing one item, C<ids>, that contains an array of product +ids. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 get + +=over + +=item B<Description> + +Returns a list of information about the products passed to it. + +B<Note>: You must at least specify one of C<ids> or C<names>. + +B<Note>: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API. + +=item B<REST> + +To return information about a specific groups of products such as +C<accessible>, C<selectable>, or C<enterable>: + +GET /rest/product?type=accessible + +To return information about a specific product by C<id> or C<name>: + +GET /rest/product/<product_id_or_name> + +You can also return information about more than one specific product +by using the following in your query string: + +GET /rest/product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2 + +the returned data format is same as below. + +=item B<Params> + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +This RPC call supports sub field restrictions. + +=over + +=item C<ids> + +An array of product ids + +=item C<names> + +An array of product names + +=item C<type> + +The group of products to return. Valid values are: C<accessible> (default), +C<selectable>, and C<enterable>. C<type> can be a single value or an array +of values if more than one group is needed with duplicates removed. + +=back + +=item B<Returns> + +A hash containing one item, C<products>, that is an array of +hashes. Each hash describes a product, and has the following items: + +=over + +=item C<id> + +C<int> An integer id uniquely identifying the product in this installation only. + +=item C<name> + +C<string> The name of the product. This is a unique identifier for the +product. + +=item C<description> + +C<string> A description of the product, which may contain HTML. + +=item C<is_active> + +C<boolean> A boolean indicating if the product is active. + +=item C<default_milestone> + +C<string> The name of the default milestone for the product. + +=item C<has_unconfirmed> + +C<boolean> Indicates whether the UNCONFIRMED bug status is available +for this product. + +=item C<classification> + +C<string> The classification name for the product. + +=item C<components> + +C<array> An array of hashes, where each hash describes a component, and has the +following items: + +=over + +=item C<id> + +C<int> An integer id uniquely identifying the component in this installation +only. + +=item C<name> + +C<string> The name of the component. This is a unique identifier for this +component. + +=item C<description> + +C<string> A description of the component, which may contain HTML. + +=item C<default_assigned_to> + +C<string> The login name of the user to whom new bugs will be assigned by +default. + +=item C<default_qa_contact> + +C<string> The login name of the user who will be set as the QA Contact for +new bugs by default. Empty string if the QA contact is not defined. + +=item C<sort_key> + +C<int> Components, when displayed in a list, are sorted first by this integer +and then secondly by their name. + +=item C<is_active> + +C<boolean> A boolean indicating if the component is active. Inactive +components are not enabled for new bugs. + +=item C<flag_types> + +A hash containing the two items C<bug> and C<attachment> that each contains an +array of hashes, where each hash describes a flagtype, and has the +following items: + +=over + +=item C<id> + +C<int> Returns the ID of the flagtype. + +=item C<name> + +C<string> Returns the name of the flagtype. + +=item C<description> + +C<string> Returns the description of the flagtype. + +=item C<cc_list> + +C<string> Returns the concatenated CC list for the flagtype, as a single string. + +=item C<sort_key> + +C<int> Returns the sortkey of the flagtype. + +=item C<is_active> + +C<boolean> Returns whether the flagtype is active or disabled. Flags being +in a disabled flagtype are not deleted. It only prevents you from +adding new flags to it. + +=item C<is_requestable> + +C<boolean> Returns whether you can request for the given flagtype +(i.e. whether the '?' flag is available or not). + +=item C<is_requesteeble> + +C<boolean> Returns whether you can ask someone specifically or not. + +=item C<is_multiplicable> + +C<boolean> Returns whether you can have more than one flag for the given +flagtype in a given bug/attachment. + +=item C<grant_group> + +C<int> the group id that is allowed to grant/deny flags of this type. +If the item is not included all users are allowed to grant/deny this +flagtype. + +=item C<request_group> + +C<int> the group id that is allowed to request the flag if the flag +is of the type requestable. If the item is not included all users +are allowed request this flagtype. + +=back + +=back + +=item C<versions> + +C<array> An array of hashes, where each hash describes a version, and has the +following items: C<name>, C<sort_key> and C<is_active>. + +=item C<milestones> + +C<array> An array of hashes, where each hash describes a milestone, and has the +following items: C<name>, C<sort_key> and C<is_active>. + +=back + +Note, that if the user tries to access a product that is not in the +list of accessible products for the user, or a product that does not +exist, that is silently ignored, and no information about that product +is returned. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item In Bugzilla B<4.2>, C<names> was added as an input parameter. + +=item In Bugzilla B<4.2>, C<classification>, C<components>, C<versions>, +C<milestones>, C<default_milestone> and C<has_unconfirmed> were added to +the fields returned by C<get> as a replacement for C<internals>, which has +been removed. + +=item In Bugzilla B<4.4>, C<flag_types> was added to the fields returned +by C<get>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 Product Creation and Modification + +=head2 create + +=over + +=item B<Description> + +This allows you to create a new product in Bugzilla. + +=item B<REST> + +POST /rest/product + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +Some params must be set, or an error will be thrown. These params are +marked B<Required>. + +=over + +=item C<name> + +B<Required> C<string> The name of this product. Must be globally unique +within Bugzilla. + +=item C<description> + +B<Required> C<string> A description for this product. Allows some simple HTML. + +=item C<version> + +B<Required> C<string> The default version for this product. + +=item C<has_unconfirmed> + +C<boolean> Allow the UNCONFIRMED status to be set on bugs in this product. +Default: true. + +=item C<classification> + +C<string> The name of the Classification which contains this product. + +=item C<default_milestone> + +C<string> The default milestone for this product. Default '---'. + +=item C<is_open> + +C<boolean> True if the product is currently allowing bugs to be entered +into it. Default: true. + +=item C<create_series> + +C<boolean> True if you want series for New Charts to be created for this +new product. Default: true. + +=back + +=item B<Returns> + +A hash with one element, id. This is the id of the newly-filed product. + +=item B<Errors> + +=over + +=item 51 (Classification does not exist) + +You must specify an existing classification name. + +=item 700 (Product blank name) + +You must specify a non-blank name for this product. + +=item 701 (Product name too long) + +The name specified for this product was longer than the maximum +allowed length. + +=item 702 (Product name already exists) + +You specified the name of a product that already exists. +(Product names must be globally unique in Bugzilla.) + +=item 703 (Product must have description) + +You must specify a description for this product. + +=item 704 (Product must have version) + +You must specify a version for this product. + +=back + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B<Description> + +This allows you to update a product in Bugzilla. + +=item B<REST> + +PUT /rest/product/<product_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + +=item B<Params> + +B<Note:> The following parameters specify which products you are updating. +You must set one or both of these parameters. + +=over + +=item C<ids> + +C<array> of C<int>s. Numeric ids of the products that you wish to update. + +=item C<names> + +C<array> of C<string>s. Names of the products that you wish to update. + +=back + +B<Note:> The following parameters specify the new values you want to set for +the products you are updating. + +=over + +=item C<name> + +C<string> A new name for this product. If you try to set this while updating more +than one product, an error will occur, as product names must be unique. + +=item C<default_milestone> + +C<string> When a new bug is filed, what milestone does it get by default if the +user does not choose one? Must represent a milestone that is valid for this product. + +=item C<description> + +C<string> Update the long description for these products to this value. + +=item C<has_unconfirmed> + +C<boolean> Allow the UNCONFIRMED status to be set on bugs in products. + +=item C<is_open> + +C<boolean> True if the product is currently allowing bugs to be entered +into it, False otherwise. + +=back + +=item B<Returns> + +A C<hash> with a single field "products". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the product that was updated. + +=item C<changes> + +C<hash> 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<added> + +C<string> The value that this field was changed to. + +=item C<removed> + +C<string> 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 => 'FooName', + added => 'BarName' + }, + has_unconfirmed => { + removed => '1', + added => '0', + } + } + } + ] + } + +=item B<Errors> + +=over + +=item 700 (Product blank name) + +You must specify a non-blank name for this product. + +=item 701 (Product name too long) + +The name specified for this product was longer than the maximum +allowed length. + +=item 702 (Product name already exists) + +You specified the name of a product that already exists. +(Product names must be globally unique in Bugzilla.) + +=item 703 (Product must have description) + +You must specify a description for this product. + +=item 705 (Product must define a default milestone) + +You must define a default milestone. + +=back + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=item get_products + +=back diff --git a/Bugzilla/API/1_0/Resource/User.pm b/Bugzilla/API/1_0/Resource/User.pm new file mode 100644 index 000000000..d2c869907 --- /dev/null +++ b/Bugzilla/API/1_0/Resource/User.pm @@ -0,0 +1,1151 @@ +# 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::1_0::Resource::User; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util; + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::User; +use Bugzilla::Util qw(trim detaint_natural); + +use List::Util qw(first min); +use Moo; + +extends 'Bugzilla::API::1_0::Resource'; + +############## +# Constants # +############## + +# Don't need auth to login +use constant LOGIN_EXEMPT => { + login => 1, + offer_account_by_email => 1, +}; + +use constant READ_ONLY => qw( + get + login + logout + valid_login +); + +use constant PUBLIC_METHODS => qw( + create + get + login + logout + offer_account_by_email + update + valid_login +); + +use constant MAPPED_FIELDS => { + email => 'login', + full_name => 'name', + login_denied_text => 'disabledtext', + email_enabled => 'disable_mail' +}; + +use constant MAPPED_RETURNS => { + login_name => 'email', + realname => 'full_name', + disabledtext => 'login_denied_text', + disable_mail => 'email_enabled' +}; + +sub REST_RESOURCES { + my $rest_resources = [ + qr{^/login$}, { + GET => { + method => 'login' + }, + POST => { + method => 'login' + } + }, + qr{^/logout$}, { + GET => { + method => 'logout' + } + }, + qr{^/valid_login$}, { + GET => { + method => 'valid_login' + } + }, + qr{^/user$}, { + GET => { + method => 'get' + }, + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/user/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + } + ]; + return $rest_resources; +} + +############ +# Methods # +############ + +sub login { + my ($self, $params) = @_; + + # Check to see if we are already logged in + my $user = Bugzilla->user; + if ($user->id) { + return $self->_login_to_hash($user); + } + + # Username and password params are required + foreach my $param ("login", "password") { + (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) + || ThrowCodeError('param_required', { param => $param }); + } + + $user = Bugzilla->login(); + return $self->_login_to_hash($user); +} + +sub logout { + my $self = shift; + Bugzilla->logout; +} + +sub valid_login { + my ($self, $params) = @_; + defined $params->{login} + || ThrowCodeError('param_required', { param => 'login' }); + Bugzilla->login(); + if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { + return as_boolean(1); + } + return as_boolean(0); +} + +sub offer_account_by_email { + my $self = shift; + my ($params) = @_; + my $email = trim($params->{email}) + || ThrowCodeError('param_required', { param => 'email' }); + + Bugzilla->user->check_account_creation_enabled; + Bugzilla->user->check_and_send_account_creation_confirmation($email); + return undef; +} + +sub create { + my $self = shift; + my ($params) = @_; + + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", { group => "editusers", + action => "add", + object => "users"}); + + my $email = trim($params->{email}) + || ThrowCodeError('param_required', { param => 'email' }); + my $realname = trim($params->{full_name}); + my $password = trim($params->{password}) || '*'; + + my $user = Bugzilla::User->create({ + login_name => $email, + realname => $realname, + cryptpassword => $password + }); + + return { id => as_int($user->id) }; +} + + +# function to return user information by passing either user ids or +# login names or both together: +# $call = $rpc->call( 'User.get', { ids => [1,2,3], +# names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); +sub get { + my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); + + Bugzilla->switch_to_shadow_db(); + + defined($params->{names}) || defined($params->{ids}) + || defined($params->{match}) + || ThrowCodeError('params_required', + { function => 'User.get', params => ['ids', 'names', 'match'] }); + + my @user_objects; + @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} } + if $params->{names}; + + # start filtering to remove duplicate user ids + my %unique_users = map { $_->id => $_ } @user_objects; + @user_objects = values %unique_users; + + my @users; + + # If the user is not logged in: Return an error if they passed any user ids. + # Otherwise, return a limited amount of information based on login names. + if (!Bugzilla->user->id){ + if ($params->{ids}){ + ThrowUserError("user_access_by_id_denied"); + } + if ($params->{match}) { + ThrowUserError('user_access_by_match_denied'); + } + my $in_group = $self->_filter_users_by_group( + \@user_objects, $params); + @users = map { filter $params, { + id => as_int($_->id), + real_name => as_string($_->name), + name => as_email($_->login), + } } @$in_group; + + return { users => \@users }; + } + + my $obj_by_ids; + $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; + + # obj_by_ids are only visible to the user if they can see + # the otheruser, for non visible otheruser throw an error + foreach my $obj (@$obj_by_ids) { + if (Bugzilla->user->can_see_user($obj)){ + if (!$unique_users{$obj->id}) { + push (@user_objects, $obj); + $unique_users{$obj->id} = $obj; + } + } + else { + ThrowUserError('auth_failure', {reason => "not_visible", + action => "access", + object => "user", + userid => $obj->id}); + } + } + + # User Matching + my $limit = Bugzilla->params->{maxusermatches}; + if ($params->{limit}) { + detaint_natural($params->{limit}) + || ThrowCodeError('param_must_be_numeric', + { function => 'Bugzilla::API::1_0::Resource::User::match', + param => 'limit' }); + $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + + my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; + foreach my $match_string (@{ $params->{'match'} || [] }) { + my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); + foreach my $user (@$matched) { + if (!$unique_users{$user->id}) { + push(@user_objects, $user); + $unique_users{$user->id} = $user; + } + } + } + + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + foreach my $user (@$in_group) { + my $user_info = filter $params, { + id => as_int($user->id), + real_name => as_string($user->name), + name => as_email($user->login), + email => as_email($user->email), + can_login => as_boolean($user->is_enabled ? 1 : 0), + }; + + if (Bugzilla->user->in_group('editusers')) { + $user_info->{email_enabled} = as_boolean($user->email_enabled); + $user_info->{login_denied_text} = as_string($user->disabledtext); + } + + if (Bugzilla->user->id == $user->id) { + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} = [ + map { $self->_query_to_hash($_) } @{ $user->queries } + ]; + } + if (filter_wants($params, 'saved_reports')) { + $user_info->{saved_reports} = [ + map { $self->_report_to_hash($_) } @{ $user->reports } + ]; + } + } + + if (filter_wants($params, 'groups')) { + if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { + $user_info->{groups} = [ + map { $self->_group_to_hash($_) } @{ $user->groups } + ]; + } + else { + $user_info->{groups} = $self->_filter_bless_groups($user->groups); + } + } + + push(@users, $user_info); + } + + return { users => \@users }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Reject access if there is no sense in continuing. + $user->in_group('editusers') + || ThrowUserError("auth_failure", {group => "editusers", + action => "edit", + object => "users"}); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'User.update', params => ['ids', 'names'] }); + + my $user_objects = params_to_objects($params, 'Bugzilla::User'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $user (@$user_objects){ + $user->set_all($values); + } + + my %changes; + foreach my $user (@$user_objects){ + my $returned_changes = $user->update(); + $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $user (@$user_objects) { + my %hash = ( + id => $user->id, + changes => {}, + ); + + foreach my $field (keys %{ $changes{$user->id} }) { + my $change = $changes{$user->id}->{$field}; + # We normalize undef to an empty string, so that the API + # stays consistent for things that can become empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + # We also flatten arrays (used by groups and blessed_groups) + $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; + $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; + + $hash{changes}{$field} = { + removed => as_string($change->[0]), + added => as_string($change->[1]) + }; + } + + push(@result, \%hash); + } + + return { users => \@result }; +} + +sub _filter_users_by_group { + my ($self, $users, $params) = @_; + my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + + # If no groups are specified, we return all users. + return $users if (!$group_ids and !$group_names); + + my $user = Bugzilla->user; + my (@groups, %groups); + + if ($group_ids) { + @groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids; + $groups{$_->id} = $_ foreach @groups; + } + if ($group_names) { + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' }); + $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name }); + $groups{$group->id} = $group; + } + } + @groups = values %groups; + + my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; + return \@in_group; +} + +sub _user_in_any_group { + my ($self, $user, $groups) = @_; + foreach my $group (@$groups) { + return 1 if $user->in_group($group); + } + return 0; +} + +sub _filter_bless_groups { + my ($self, $groups) = @_; + my $user = Bugzilla->user; + + my @filtered_groups; + foreach my $group (@$groups) { + next unless $user->can_bless($group->id); + push(@filtered_groups, $self->_group_to_hash($group)); + } + + return \@filtered_groups; +} + +sub _group_to_hash { + my ($self, $group) = @_; + my $item = { + id => as_int($group->id), + name => as_string($group->name), + description => as_string($group->description), + }; + return $item; +} + +sub _query_to_hash { + my ($self, $query) = @_; + my $item = { + id => as_int($query->id), + name => as_string($query->name), + query => as_string($query->url), + }; + return $item; +} + +sub _report_to_hash { + my ($self, $report) = @_; + my $item = { + id => as_int($report->id), + name => as_string($report->name), + query => as_string($report->query), + }; + return $item; +} + +sub _login_to_hash { + my ($self, $user) = @_; + my $item = { id => as_int($user->id) }; + if ($user->{_login_token}) { + $item->{'token'} = $user->id . "-" . $user->{_login_token}; + } + return $item; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Resource::User - The User Account and Login API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to create User Accounts and +log in/out using an existing account. + +=head1 METHODS + +The authentication methods listed here are now deprecated, and will be removed +in the release after Bugzilla 5.0. The correct way to authenticate when making +calls is noted in L<Bugzilla::API::Server>. + +=head2 login + +B<DEPRECATED> + +=over + +=item B<Description> + +Logging in, with a username and password, is required for many +Bugzilla installations, in order to search for bugs, post new bugs, +etc. This method logs in an user. + +=item B<Params> + +=over + +=item C<login> (string) - The user's login name. + +=item C<password> (string) - The user's password. + +=item C<restrict_login> (bool) B<Optional> - If set to a true value, +the token returned by this method will only be valid from the IP address +which called this method. + +=back + +=item B<Returns> + +On success, a hash containing two items, C<id>, the numeric id of the +user that was logged in, and a C<token> which can be passed in +the parameters as authentication in other calls. The token can be sent +along with any future requests to the webservice, for the duration of the +session, i.e. till L<User.logout|/logout> is called. + +=item B<Errors> + +=over + +=item 300 (Invalid Username or Password) + +The username does not exist, or the password is wrong. + +=item 301 (Login Disabled) + +The ability to login with this account has been disabled. A reason may be +specified with the error. + +=item 305 (New Password Required) + +The current password is correct, but the user is asked to change +their password. + +=item 50 (Param Required) + +A login or password parameter was not provided. + +=back + +=item B<History> + +=over + +=item C<remember> was removed in Bugzilla B<5.0> as this method no longer +creates a login cookie. + +=item C<restrict_login> was added in Bugzilla B<5.0>. + +=item C<token> was added in Bugzilla B<4.4.3>. + +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. + +=back + +=back + +=head2 logout + +B<DEPRECATED> + +=over + +=item B<Description> + +Log out the user. Does nothing if there is no user logged in. + +=item B<Params> (none) + +=item B<Returns> (nothing) + +=item B<Errors> (none) + +=back + +=head2 valid_login + +B<DEPRECATED> + +=over + +=item B<Description> + +This method will verify whether a client's cookies or current login +token is still valid or have expired. A valid username must be provided +as well that matches. + +=item B<Params> + +=over + +=item C<login> + +The login name that matches the provided cookies or token. + +=item C<token> + +(string) Persistent login token current being used for authentication (optional). +Cookies passed by client will be used before the token if both provided. + +=back + +=item B<Returns> + +Returns true/false depending on if the current cookies or token are valid +for the provided username. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. + +=back + +=back + +=head1 Account Creation and Modification + +=head2 offer_account_by_email + +=over + +=item B<Description> + +Sends an email to the user, offering to create an account. The user +will have to click on a URL in the email, and choose their password +and real name. + +This is the recommended way to create a Bugzilla account. + +=item B<Param> + +=over + +=item C<email> (string) - the email to send the offer to. + +=back + +=item B<Returns> (nothing) + +=item B<Errors> + +=over + +=item 500 (Account Already Exists) + +An account with that email address already exists in Bugzilla. + +=item 501 (Illegal Email Address) + +This Bugzilla does not allow you to create accounts with the format of +email address you specified. Account creation may be entirely disabled. + +=back + +=back + +=head2 create + +=over + +=item B<Description> + +Creates a user account directly in Bugzilla, password and all. +Instead of this, you should use L</offer_account_by_email> when +possible, because that makes sure that the email address specified can +actually receive an email. This function does not check that. + +You must be logged in and have the C<editusers> privilege in order to +call this function. + +=item B<REST> + +POST /rest/user + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +=over + +=item C<email> (string) - The email address for the new user. + +=item C<full_name> (string) B<Optional> - The user's full name. Will +be set to empty if not specified. + +=item C<password> (string) B<Optional> - The password for the new user +account, in plain text. It will be stripped of leading and trailing +whitespace. If blank or not specified, the newly created account will +exist in Bugzilla, but will not be allowed to log in using DB +authentication until a password is set either by the user (through +resetting their password) or by the administrator. + +=back + +=item B<Returns> + +A hash containing one item, C<id>, the numeric id of the user that was +created. + +=item B<Errors> + +The same as L</offer_account_by_email>. If a password is specified, +the function may also throw: + +=over + +=item 502 (Password Too Short) + +The password specified is too short. (Usually, this means the +password is under three characters.) + +=back + +=item B<History> + +=over + +=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +=over + +=item B<Description> + +Updates user accounts in Bugzilla. + +=item B<REST> + +PUT /rest/user/<user_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params are overridden as they +are pulled from the URL path. + +=item B<Params> + +=over + +=item C<ids> + +C<array> Contains ids of user to update. + +=item C<names> + +C<array> Contains email/login of user to update. + +=item C<full_name> + +C<string> The new name of the user. + +=item C<email> + +C<string> The email of the user. Note that email used to login to bugzilla. +Also note that you can only update one user at a time when changing the +login name / email. (An error will be thrown if you try to update this field +for multiple users at once.) + +=item C<password> + +C<string> The password of the user. + +=item C<email_enabled> + +C<boolean> A boolean value to enable/disable sending bug-related mail to the user. + +=item C<login_denied_text> + +C<string> A text field that holds the reason for disabling a user from logging +into bugzilla, if empty then the user account is enabled otherwise it is +disabled/closed. + +=item C<groups> + +C<hash> These specify the groups that this user is directly a member of. +To set these, you should pass a hash as the value. The hash may contain +the following fields: + +=over + +=item C<add> An array of C<int>s or C<string>s. The group ids or group names +that the user should be added to. + +=item C<remove> An array of C<int>s or C<string>s. The group ids or group names +that the user should be removed from. + +=item C<set> An array of C<int>s or C<string>s. An exact set of group ids +and group names that the user should be a member of. NOTE: This does not +remove groups from the user where the person making the change does not +have the bless privilege for. + +If you specify C<set>, then C<add> and C<remove> will be ignored. A group in +both the C<add> and C<remove> list will be added. Specifying a group that the +user making the change does not have bless rights will generate an error. + +=back + +=item C<bless_groups> + +C<hash> - This is the same as groups, but affects what groups a user +has direct membership to bless that group. It takes the same inputs as +groups. + +=back + +=item B<Returns> + +A C<hash> with a single field "users". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the user that was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this user. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> + +C<string> The values that were added to this field, +possibly a comma-and-space-separated list if multiple values were added. + +=item C<removed> + +C<string> The values that were removed from this field, possibly a +comma-and-space-separated list if multiple values were removed. + +=back + +=back + +=item B<Errors> + +=over + +=item 51 (Bad Login Name) + +You passed an invalid login name in the "names" array. + +=item 304 (Authorization Required) + +Logged-in users are not authorized to edit other users. + +=back + +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 User Info + +=head2 get + +=over + +=item B<Description> + +Gets information about user accounts in Bugzilla. + +=item B<REST> + +To get information about a single user: + +GET /rest/user/<user_id_or_name> + +To search for users by name, group using URL params same as below: + +GET /rest/user + +The returned data format is the same as below. + +=item B<Params> + +B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified. + +B<Note>: Users will not be returned more than once, so even if a user +is matched by more than one argument, only one user will be returned. + +In addition to the parameters below, this method also accepts the +standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and +L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments. + +=over + +=item C<ids> (array) + +An array of integers, representing user ids. + +Logged-out users cannot pass this parameter to this function. If they try, +they will get an error. Logged-in users will get an error if they specify +the id of a user they cannot see. + +=item C<names> (array) + +An array of login names (strings). + +=item C<match> (array) + +An array of strings. This works just like "user matching" in +Bugzilla itself. Users will be returned whose real name or login name +contains any one of the specified strings. Users that you cannot see will +not be included in the returned list. + +Most installations have a limit on how many matches are returned for +each string, which defaults to 1000 but can be changed by the Bugzilla +administrator. + +Logged-out users cannot use this argument, and an error will be thrown +if they try. (This is to make it harder for spammers to harvest email +addresses from Bugzilla, and also to enforce the user visibility +restrictions that are implemented on some Bugzillas.) + +=item C<limit> (int) + +Limit the number of users matched by the C<match> parameter. If value +is greater than the system limit, the system limit will be used. This +parameter is only used when user matching using the C<match> parameter +is being performed. + +=item C<group_ids> (array) + +=item C<groups> (array) + +C<group_ids> is an array of numeric ids for groups that a user can be in. +C<groups> is an array of names of groups that a user can be in. +If these are specified, they limit the return value to users who are +in I<any> of the groups specified. + +=item C<include_disabled> (boolean) + +By default, when using the C<match> parameter, disabled users are excluded +from the returned results unless their full username is identical to the +match string. Setting C<include_disabled> to C<true> will include disabled +users in the returned results even if their username doesn't fully match +the input string. + +=back + +=item B<Returns> + +A hash containing one item, C<users>, that is an array of +hashes. Each hash describes a user, and has the following items: + +=over + +=item id + +C<int> The unique integer ID that Bugzilla uses to represent this user. +Even if the user's login name changes, this will not change. + +=item real_name + +C<string> The actual name of the user. May be blank. + +=item email + +C<string> The email address of the user. + +=item name + +C<string> The login name of the user. Note that in some situations this is +different than their email. + +=item can_login + +C<boolean> A boolean value to indicate if the user can login into bugzilla. + +=item email_enabled + +C<boolean> A boolean value to indicate if bug-related mail will be sent +to the user or not. + +=item login_denied_text + +C<string> A text field that holds the reason for disabling a user from logging +into bugzilla, if empty then the user account is enabled. Otherwise it is +disabled/closed. + +=item groups + +C<array> An array of group hashes the user is a member of. If the currently +logged in user is querying their own account or is a member of the 'editusers' +group, the array will contain all the groups that the user is a +member of. Otherwise, the array will only contain groups that the logged in +user can bless. Each hash describes the group and contains the following items: + +=over + +=item id + +C<int> The group id + +=item name + +C<string> The name of the group + +=item description + +C<string> The description for the group + +=back + +=item saved_searches + +C<array> An array of hashes, each of which represents a user's saved search and has +the following keys: + +=over + +=item id + +C<int> An integer id uniquely identifying the saved search. + +=item name + +C<string> The name of the saved search. + +=item query + +C<string> The CGI parameters for the saved search. + +=back + +=item saved_reports + +C<array> An array of hashes, each of which represents a user's saved report and has +the following keys: + +=over + +=item id + +C<int> An integer id uniquely identifying the saved report. + +=item name + +C<string> The name of the saved report. + +=item query + +C<string> The CGI parameters for the saved report. + +=back + +B<Note>: If you are not logged in to Bugzilla when you call this function, you +will only be returned the C<id>, C<name>, and C<real_name> items. If you are +logged in and not in editusers group, you will only be returned the C<id>, C<name>, +C<real_name>, C<email>, C<can_login>, and C<groups> items. The groups returned are +filtered based on your permission to bless each group. +The C<saved_searches> and C<saved_reports> items are only returned if you are +querying your own account, even if you are in the editusers group. + +=back + +=item B<Errors> + +=over + +=item 51 (Bad Login Name or Group ID) + +You passed an invalid login name in the "names" array or a bad +group ID in the C<group_ids> argument. + +=item 52 (Invalid Parameter) + +The value used must be an integer greater than zero. + +=item 304 (Authorization Required) + +You are logged in, but you are not authorized to see one of the users you +wanted to get information about by user id. + +=item 505 (User Access By Id or User-Matching Denied) + +Logged-out users cannot use the "ids" or "match" arguments to this +function. + +=item 804 (Invalid Group Name) + +You passed a group name in the C<groups> argument which either does not +exist or you do not belong to it. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<3.4>. + +=item C<group_ids> and C<groups> were added in Bugzilla B<4.0>. + +=item C<include_disabled> was added in Bugzilla B<4.0>. Default +behavior for C<match> was changed to only return enabled accounts. + +=item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now +illegal to pass a group name you don't belong to. + +=item C<groups>, C<saved_searches>, and C<saved_reports> were added +in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 B<Methods in need of POD> + +=over + +=item REST_RESOURCES + +=back diff --git a/Bugzilla/API/1_0/Server.pm b/Bugzilla/API/1_0/Server.pm new file mode 100644 index 000000000..ed147dc2a --- /dev/null +++ b/Bugzilla/API/1_0/Server.pm @@ -0,0 +1,451 @@ +# 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::1_0::Server; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::API::1_0::Util qw(taint_data fix_credentials api_include_exclude); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Hook; +use Bugzilla::Util qw(datetime_from trick_taint); + +use File::Basename qw(basename); +use File::Glob qw(bsd_glob); +use List::MoreUtils qw(none uniq); +use MIME::Base64 qw(decode_base64 encode_base64); +use Moo; +use Scalar::Util qw(blessed); + +extends 'Bugzilla::API::Server'; + +############ +# Start up # +############ + +has api_version => (is => 'ro', default => '1_0', init_arg => undef); +has api_namespace => (is => 'ro', default => 'core', init_arg => undef); + +sub _build_content_type { + # Determine how the data should be represented. We do this early so + # errors will also be returned with the proper content type. + # If no accept header was sent or the content types specified were not + # matched, we default to the first type in the whitelist. + return $_[0]->_best_content_type( + @{ $_[0]->constants->{REST_CONTENT_TYPE_WHITELIST} }); +} + +################## +# Public Methods # +################## + +sub handle { + my ($self) = @_; + + # Using current path information, decide which class/method to + # use to serve the request. Throw error if no resource was found + # unless we were looking for OPTIONS + if (!$self->_find_resource) { + if ($self->request->method eq 'OPTIONS' + && $self->api_options) + { + my $response = $self->response_header($self->constants->{STATUS_OK}, ""); + my $options_string = join(', ', @{ $self->api_options }); + $response->header('Allow' => $options_string, + 'Access-Control-Allow-Methods' => $options_string); + return $self->print_response($response); + } + + ThrowUserError("rest_invalid_resource", + { path => $self->cgi->path_info, + method => $self->request->method }); + } + + my $params = $self->_retrieve_json_params; + $self->_params_check($params); + + fix_credentials($params); + + # Fix includes/excludes for each call + api_include_exclude($params); + + # Set callback name if exists + $self->callback($params->{'callback'}) if $params->{'callback'}; + + Bugzilla->input_params($params); + + # Let's try to authenticate before executing + $self->handle_login; + + # Execute the handler + my $result = $self->_handle; + + $self->response($result); +} + +sub response { + my ($self, $result) = @_; + + # Error data needs to be formatted differently + my $status_code; + if (my $error = $self->return_error) { + $status_code = delete $error->{status_code}; + $error->{documentation} = REST_DOC; + $result = $error; + } + else { + $status_code = $self->success_code; + } + + Bugzilla::Hook::process('webservice_rest_result', + { api => $self, result => \$result }); + + # ETag support + my $etag = $self->etag; + $self->etag($result) if !$etag; + + # If accessing through web browser, then display in readable format + my $content; + if ($self->content_type eq 'text/html') { + $result = $self->json->pretty->canonical->allow_nonref->encode($result); + my $template = Bugzilla->template; + $template->process("rest.html.tmpl", { result => $result }, \$content) + || ThrowTemplateError($template->error()); + } + else { + $content = $self->json->encode($result); + } + + if (my $callback = $self->callback) { + # Prepend the response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $content = "/**/$callback($content)"; + } + + my $response = $self->response_header($status_code, $content); + + Bugzilla::Hook::process('webservice_rest_response', + { api => $self, response => $response }); + + $self->print_response($response); +} + +sub print_response { + my ($self, $response) = @_; + + # Access Control + my @allowed_headers = qw(accept content-type origin x-requested-with); + foreach my $header (keys %{ API_AUTH_HEADERS() }) { + # We want to lowercase and replace _ with - + my $translated_header = $header; + $translated_header =~ tr/A-Z_/a-z\-/; + push(@allowed_headers, $translated_header); + } + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers)); + + # Use $cgi->header properly instead of just printing text directly. + # This fixes various problems, including sending Bugzilla's cookies + # properly. + my $headers = $response->headers; + my @header_args; + foreach my $name ($headers->header_field_names) { + my @values = $headers->header($name); + $name =~ s/-/_/g; + foreach my $value (@values) { + push(@header_args, "-$name", $value); + } + } + + # ETag support + my $etag = $self->etag; + if ($etag && $self->cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $self->cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $self->cgi->header(-status => $response->code, @header_args); + print $response->content; + } +} + +sub handle_login { + my $self = shift; + my $controller = $self->controller; + my $method = $self->method_name; + + return if ($controller->login_exempt($method) + and !defined Bugzilla->input_params->{Bugzilla_login}); + + Bugzilla->login(); + + Bugzilla::Hook::process('webservice_before_call', + { rpc => $self, controller => $controller }); +} + +################### +# Private Methods # +################### + +sub _handle { + my ($self) = shift; + my $method = $self->method_name; + my $controller = $self->controller; + my $params = Bugzilla->input_params; + + unless ($controller->can($method)) { + return $self->return_error(302, "No such a method : '$method'."); + } + + my $result = eval q| $controller->$method($params) |; + + if ($@) { + return $self->return_error(500, "Procedure error: $@"); + } + + # Set the ETag if not already set in the webservice methods. + my $etag = $self->etag; + if (!$etag && ref $result) { + $self->etag($result); + } + + return $result; +} + +sub _params_check { + my ($self, $params) = @_; + my $method = $self->method_name; + my $controller = $self->controller; + + taint_data($params); + + # Now, convert dateTime fields on input. + my @date_fields = @{ $controller->DATE_FIELDS->{$method} || [] }; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = + [ map { $self->datetime_format_inbound($_) } @$value ]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } + } + } + my @base64_fields = @{ $controller->BASE64_FIELDS->{$method} || [] }; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); + } + } + + if ($self->request->method eq 'POST') { + # CSRF is possible via XMLHttpRequest when the Content-Type header + # is not application/json (for example: text/plain or + # application/x-www-form-urlencoded). + # application/json is the single official MIME type, per RFC 4627. + my $content_type = $self->cgi->content_type; + # The charset can be appended to the content type, so we use a regexp. + if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { + ThrowUserError('json_rpc_illegal_content_type', + { content_type => $content_type }); + } + } + else { + # When being called using GET, we don't allow calling + # methods that can change data. This protects us against cross-site + # request forgeries. + if (!grep($_ eq $method, $controller->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', + { method => $self->method_name }); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method} $controller->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', { method => $self->method_name }); + } +} + +sub _retrieve_json_params { + my $self = shift; + + # Make a copy of the current input_params rather than edit directly + my $params = {}; + %{$params} = %{ Bugzilla->input_params }; + + # First add any parameters we were able to pull out of the path + # based on the resource regexp and combine with the normal URL + # parameters. + if (my $api_params = $self->api_params) { + foreach my $param (keys %$api_params) { + # If the param does not already exist or if the + # rest param is a single value, add it to the + # global params. + if (!exists $params->{$param} || !ref $api_params->{$param}) { + $params->{$param} = $api_params->{$param}; + } + # If param is a list then add any extra values to the list + elsif (ref $api_params->{$param}) { + my @extra_values = ref $params->{$param} + ? @{ $params->{$param} } + : ($params->{$param}); + $params->{$param} + = [ uniq (@{ $api_params->{$param} }, @extra_values) ]; + } + } + } + + # Any parameters passed in in the body of a non-GET request will override + # any parameters pull from the url path. Otherwise non-unique keys are + # combined. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + # We do this manually because CGI.pm doesn't understand JSON strings. + my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; + if ($json) { + eval { $extra_params = $self->json->decode($json); }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); + } + } + + # Allow parameters in the query string if request was non-GET. + # Note: parameters in query string body override any matching + # parameters in the request body. + foreach my $param ($self->cgi->url_param()) { + $extra_params->{$param} = $self->cgi->url_param($param); + } + + %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + } + + return $params; +} + +sub _find_resource { + my ($self) = @_; + my $api_version = $self->api_version; + my $api_ext_version = $self->api_ext_version; + my $api_namespace = $self->api_namespace; + my $api_path = $self->api_path; + my $request_method = $self->request->method; + my $resource_found = 0; + + my $resource_modules; + if ($api_ext_version) { + $resource_modules = File::Spec->catdir(bz_locations()->{extensionsdir}, + $api_namespace, 'API', $api_ext_version, 'Resource', '*.pm'); + } + else { + $resource_modules = File::Spec->catdir('Bugzilla','API', $api_version, + 'Resource', '*.pm'); + } + + # Load in the WebService modules from the appropriate version directory + # and then call $module->REST_RESOURCES to get the resources array ref. + foreach my $module_file (bsd_glob($resource_modules)) { + # Create a controller object + trick_taint($module_file); + my $module_basename = basename($module_file, '.pm'); + eval { require "$module_file"; } || die $@; + my $module_class = "Bugzilla::API::${api_version}::Resource::${module_basename}"; + my $controller = $module_class->new; + next if !$controller || !$controller->can('REST_RESOURCES'); + + # The resource data for each module needs to be an array ref with an + # even number of elements to work correctly. + my $this_resources = $controller->REST_RESOURCES; + next if (ref $this_resources ne 'ARRAY' || scalar @$this_resources % 2 != 0); + + while (my ($regex, $options_data) = splice(@$this_resources, 0, 2)) { + next if ref $options_data ne 'HASH'; + + if (my @matches = ($self->api_path =~ $regex)) { + # If a specific path is accompanied by a OPTIONS request + # method, the user is asking for a list of possible request + # methods for a specific path. + $self->api_options([ keys %$options_data ]); + + if ($options_data->{$request_method}) { + my $resource_data = $options_data->{$request_method}; + + # The method key/value can be a simple scalar method name + # or a anonymous subroutine so we execute it here. + my $method = ref $resource_data->{method} eq 'CODE' + ? $resource_data->{method}->($self) + : $resource_data->{method}; + $self->method_name($method); + + # Pull out any parameters parsed from the URL path + # and store them for use by the method. + if ($resource_data->{params}) { + $self->api_params($resource_data->{params}->(@matches)); + } + + # If a special success code is needed for this particular + # method, then store it for later when generating response. + if ($resource_data->{success_code}) { + $self->success_code($resource_data->{success_code}); + } + + # Stash away for later + $self->controller($controller); + + # No need to look further + $resource_found = 1; + last; + } + } + } + last if $resource_found; + } + + return $resource_found; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Server - The API 1.0 Interface to Bugzilla + +=head1 DESCRIPTION + +This documentation describes version 1.0 of the Bugzilla API. This +module inherits from L<Bugzilla::API::Server> and overrides specific +methods to make this version distinct from other versions of the API. +New versions of the API may make breaking changes by implementing +these methods in a different way. + +=head1 SEE ALSO + +L<Bugzilla::API::Server> + +=head1 B<Methods in need of POD> + +=over + +=item handle + +=item response + +=item print_response + +=item handle_login + +=back + diff --git a/Bugzilla/API/1_0/Util.pm b/Bugzilla/API/1_0/Util.pm new file mode 100644 index 000000000..2d83e1d2d --- /dev/null +++ b/Bugzilla/API/1_0/Util.pm @@ -0,0 +1,540 @@ +# 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::1_0::Util; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::API::1_0::Constants; +use Bugzilla::Error; +use Bugzilla::Flag; +use Bugzilla::FlagType; +use Bugzilla::Util qw(datetime_from email_filter); + +use JSON; +use MIME::Base64 qw(decode_base64 encode_base64); +use Storable qw(dclone); +use Test::Taint (); +use URI::Escape qw(uri_unescape); + +use parent qw(Exporter); + +our @EXPORT = qw( + api_include_exclude + as_base64 + as_boolean + as_datetime + as_double + as_email + as_email_array + as_int + as_int_array + as_name_array + as_string + as_string_array + datetime_format_inbound + datetime_format_outbound + extract_flags + filter + filter_wants + fix_credentials + params_to_objects + taint_data + translate + validate +); + +sub extract_flags { + my ($flags, $bug, $attachment) = @_; + my (@new_flags, @old_flags); + + my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; + my $current_flags = $attachment ? $attachment->flags : $bug->flags; + + # Copy the user provided $flags as we may call extract_flags more than + # once when editing multiple bugs or attachments. + my $flags_copy = dclone($flags); + + foreach my $flag (@$flags_copy) { + my $id = $flag->{id}; + my $type_id = $flag->{type_id}; + + my $new = delete $flag->{new}; + my $name = delete $flag->{name}; + + if ($id) { + my $flag_obj = grep($id == $_->id, @$current_flags); + $flag_obj || ThrowUserError('object_does_not_exist', + { class => 'Bugzilla::Flag', id => $id }); + } + elsif ($type_id) { + my $type_obj = grep($type_id == $_->id, @$flag_types); + $type_obj || ThrowUserError('object_does_not_exist', + { class => 'Bugzilla::FlagType', id => $type_id }); + if (!$new) { + my @flag_matches = grep($type_id == $_->type->id, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', + { value => $type_id }); + if (!@flag_matches) { + delete $flag->{id}; + } + else { + delete $flag->{type_id}; + $flag->{id} = $flag_matches[0]->id; + } + } + } + elsif ($name) { + my @type_matches = grep($name eq $_->name, @$flag_types); + @type_matches > 1 && ThrowUserError('flag_type_not_unique', + { value => $name }); + @type_matches || ThrowUserError('object_does_not_exist', + { class => 'Bugzilla::FlagType', name => $name }); + if ($new) { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + else { + my @flag_matches = grep($name eq $_->type->name, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name }); + if (@flag_matches) { + $flag->{id} = $flag_matches[0]->id; + } + else { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + } + } + + if ($flag->{id}) { + push(@old_flags, $flag); + } + else { + push(@new_flags, $flag); + } + } + + return (\@old_flags, \@new_flags); +} + +sub filter($$;$$) { + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; + + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); + } + + return \%newhash; +} + +sub filter_wants($$;$$) { + my ($params, $field, $types, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + + # Mimic old behavior if no types provided + my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); + + my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; + my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; + + my %include_types; + my %exclude_types; + + # Only return default fields if nothing is specified + $include_types{default} = 1 if !%include; + + # Look for any field types requested + foreach my $key (keys %include) { + next if $key !~ /^_(.*)$/; + $include_types{$1} = 1; + delete $include{$key}; + } + foreach my $key (keys %exclude) { + next if $key !~ /^_(.*)$/; + $exclude_types{$1} = 1; + delete $exclude{$key}; + } + + # Explicit inclusion/exclusion + return $cache->{$field} = 0 if $exclude{$field}; + return $cache->{$field} = 1 if $include{$field}; + + # If the user has asked to include all or exclude all + return $cache->{$field} = 0 if $exclude_types{'all'}; + return $cache->{$field} = 1 if $include_types{'all'}; + + # If the user has not asked for any fields specifically or if the user has asked + # for one or more of the field's types (and not excluded them) + foreach my $type (keys %field_types) { + return $cache->{$field} = 0 if $exclude_types{$type}; + return $cache->{$field} = 1 if $include_types{$type}; + } + + my $wants = 0; + if ($prefix) { + # Include the field if the parent is include (and this one is not excluded) + $wants = 1 if $include{$prefix}; + } + else { + # We want to include this if one of the sub keys is included + my $key = $field . '.'; + my $len = length($key); + $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; + } + + return $cache->{$field} = $wants; +} + +sub taint_data { + my @params = @_; + return if !@params; + # Though this is a private function, it hasn't changed since 2004 and + # should be safe to use, and prevents us from having to write it ourselves + # or require another module to do it. + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); +} + +sub _delete_bad_keys { + foreach my $item (@_) { + next if ref $item ne 'HASH'; + foreach my $key (keys %$item) { + # Making something a hash key always untaints it, in Perl. + # However, we need to validate our argument names in some way. + # We know that all hash keys passed in to the WebService wil + # match \w+, contain '.' or '-', so we delete any key that + # doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { + delete $item->{$key}; + } + } + } + return @_; +} + +sub api_include_exclude { + my ($params) = @_; + + if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; + } + if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { + $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; + } + + return $params; +} + +sub validate { + my ($self, $params, @keys) = @_; + + # If $params is defined but not a reference, then we weren't + # sent any parameters at all, and we're getting @keys where + # $params should be. + return ($self, undef) if (defined $params and !ref $params); + + # If @keys is not empty then we convert any named + # parameters that have scalar values to arrayrefs + # that match. + foreach my $key (@keys) { + if (exists $params->{$key}) { + $params->{$key} = ref $params->{$key} + ? $params->{$key} + : [ $params->{$key} ]; + } + } + + return ($self, $params); +} + +sub translate { + my ($params, $mapped) = @_; + my %changes; + while (my ($key,$value) = each (%$params)) { + my $new_field = $mapped->{$key} || $key; + $changes{$new_field} = $value; + } + return \%changes; +} + +sub params_to_objects { + my ($params, $class) = @_; + my (@objects, @objects_by_ids); + + @objects = map { $class->check($_) } + @{ $params->{names} } if $params->{names}; + + @objects_by_ids = map { $class->check({ id => $_ }) } + @{ $params->{ids} } if $params->{ids}; + + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; +} + +sub fix_credentials { + my ($params) = @_; + my $cgi = Bugzilla->cgi; + + # Allow user to pass in authentication details in X-Headers + # This allows callers to keep credentials out of GET request query-strings + if ($cgi) { + foreach my $field (keys %{ API_AUTH_HEADERS() }) { + next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq ''; + $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field)); + } + } + + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = delete $params->{'login'}; + $params->{'Bugzilla_password'} = delete $params->{'password'}; + } + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } + # Allow user to pass token=12345678 as a convenience which becomes + # "Bugzilla_token" which is what the auth code looks for. + if (exists $params->{'token'}) { + $params->{'Bugzilla_token'} = delete $params->{'token'}; + } + + # Allow extensions to modify the credential data before login + Bugzilla::Hook::process('webservice_fix_credentials', { params => $params }); +} + +sub datetime_format_inbound { + my ($time) = @_; + + my $converted = datetime_from($time, Bugzilla->local_timezone); + if (!defined $converted) { + ThrowUserError('illegal_date', { date => $time }); + } + $time = $converted->ymd() . ' ' . $converted->hms(); + return $time +} + +sub datetime_format_outbound { + my ($date) = @_; + + return undef if (!defined $date or $date eq ''); + + my $time = $date; + if (blessed($date)) { + # We expect this to mean we were sent a datetime object + $time->set_time_zone('UTC'); + } else { + # We always send our time in UTC, for consistency. + # passed in value is likely a string, create a datetime object + $time = datetime_from($date, 'UTC'); + } + return $time->iso8601() . 'Z'; +} + + +# simple types + +sub as_boolean { $_[0] ? JSON::true : JSON::false } +sub as_double { defined $_[0] ? $_[0] + 0.0 : JSON::null } +sub as_int { defined $_[0] ? int($_[0]) : JSON::null } +sub as_string { defined $_[0] ? $_[0] . '' : JSON::null } + +# array types + +sub as_email_array { [ map { as_email($_) } @{ $_[0] // [] } ] } +sub as_int_array { [ map { as_int($_) } @{ $_[0] // [] } ] } +sub as_name_array { [ map { as_string($_->name) } @{ $_[0] // [] } ] } +sub as_string_array { [ map { as_string($_) } @{ $_[0] // [] } ] } + +# complex types + +sub as_datetime { + return defined $_[0] + ? datetime_from($_[0], 'UTC')->iso8601() . 'Z' + : JSON::null; +} + +sub as_email { + defined $_[0] + ? ( Bugzilla->params->{webservice_email_filter} ? email_filter($_[0]) : $_[0] . '' ) + : JSON::null +} + +sub as_base64 { + utf8::encode($_[0]) if utf8::is_utf8($_[0]); + return encode_base64($_[0], ''); +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::1_0::Util - Utility functions used inside of the WebSercvice +API code. These are B<not> functions that can be called via the API. + +=head1 DESCRIPTION + +This is somewhat like L<Bugzilla::Util>, but these functions are only used +internally in the API code. + +=head1 SYNOPSIS + + filter({ include_fields => ['id', 'name'], + exclude_fields => ['name'] }, $hash); + my $wants = filter_wants $params, 'field_name'; + validate(@_, 'ids'); + +=head1 METHODS + +=head2 api_include_exclude + +The API allows for values for C<include_fields> and C<exclude_fields> to be +passed from the client in the URI string in a comma delimited format. This +converts that format into proper arrays used by other API code such as +C<filter>, etc. + +=head2 filter + +This helps implement the C<include_fields> and C<exclude_fields> arguments +of WebService methods. Given a hash (the second argument to this subroutine), +this will remove any keys that are I<not> in C<include_fields> and then remove +any keys that I<are> in C<exclude_fields>. + +An optional third option can be passed that prefixes the field name to allow +filtering of data two or more levels deep. + +For example, if you want to filter out the C<id> key/value in components returned +by Product.get, you would use the value C<component.id> in your C<exclude_fields> +list. + +=head2 filter_wants + +Returns C<1> if a filter would preserve the specified field when passing +a hash to L</filter>, C<0> otherwise. + +=head2 validate + +This helps in the validation of parameters passed into the WebService +methods. Currently it converts listed parameters into an array reference +if the client only passed a single scalar value. It modifies the parameters +hash in place so other parameters should be unaltered. + +=head2 translate + +WebService methods frequently take parameters with different names than +the ones that we use internally in Bugzilla. This function takes a hashref +that has field names for keys and returns a hashref with those keys renamed +according to the mapping passed in with the second parameter (which is also +a hashref). + +=head2 params_to_objects + +Creates objects of the type passed in as the second parameter, using the +parameters passed to a WebService method (the first parameter to this function). +Helps make life simpler for WebService methods that internally create objects +via both "ids" and "names" fields. Also de-duplicates objects that were loaded +by both "ids" and "names". Returns an arrayref of objects. + +=head2 fix_credentials + +Allows for certain parameters related to authentication such as Bugzilla_login, +Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in. +This function converts the shorter versions to their respective internal names. + +=head2 extract_flags + +Subroutine that takes a list of hashes that are potential flag changes for +both bugs and attachments. Then breaks the list down into two separate lists +based on if the change is to add a new flag or to update an existing flag. + +=head2 as_base64 + +Returns a base64 encoded value based on the parameter passed in. + +=head2 as_boolean + +If a true value is passed as a parameter, the method will return a JSON::true. +If not returns JSON::false. + +=head2 as_datetime + +Formats an internal datetime value into a 'UTC' string suitable for returning to +the client. If parameter is undefined, returns JSON::null. + +=head2 as_double + +Takes a number value passed as a parameter, and adds 0.0 to it converting to a +double value. If parameter is undefined, returns JSON::null. + +=head2 as_email + +Takes an email address as a parameter if filters it if C<webservice_email_filter> is +enabled in the system settings. If parameter is undefined, returns JSON::null. + +=head2 as_email_array + +Similar to C<as_email>, but takes an array reference to a list of values and +returns an array reference with the converted values. + +=head2 as_int + +Takes a string or number passed as a parameter and converts it to an integer +value. If parameter is undefined, returns JSON::null. + +=head2 as_int_array + +Similar to C<as_int>, but takes an array reference to a list of values and +returns an array reference with the converted values. + +=head2 as_name_array + +Takes a list of L<Bugzilla::Object> values and returns an array of new values +by calling '$object->name' for each value. + +=head2 as_string + +Returns whatever parameter is passed in unchanged, unless undefined, then it +returns JSON::null. + +=head2 as_string_array + +Similar to C<as_string>, but takes an array reference to a list of values and +returns an array reference with the converted values. + +=head2 datetime_format_inbound + +Takes a datetime string passed in from the client and converts into the format +'%Y-%m-%d %T' to be used by the internal Bugzilla code. + +=head2 datetime_format_outbound + +Formats the current datetime value from the internal formal into 'UTC' before +turning to the client. + +=head2 taint_data + +Walks the data structure passed in by the client for an API call and taints +any values that it finds for security purposes. diff --git a/Bugzilla/API/Server.pm b/Bugzilla/API/Server.pm new file mode 100644 index 000000000..c2682ab8a --- /dev/null +++ b/Bugzilla/API/Server.pm @@ -0,0 +1,654 @@ +# 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::Server; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Constants; +use Bugzilla::Util qw(trick_taint trim disable_utf8); + +use Digest::MD5 qw(md5_base64); +use File::Spec qw(catfile); +use HTTP::Request; +use HTTP::Response; +use JSON; +use Moo; +use Module::Runtime qw(require_module); +use Scalar::Util qw(blessed); +use Storable qw(freeze); + +############# +# Constants # +############# + +use constant DEFAULT_API_VERSION => '1_0'; +use constant DEFAULT_API_NAMESPACE => 'core'; + +################################# +# Set up basic accessor methods # +################################# + +has api_ext => (is => 'rw', default => 0); +has api_ext_version => (is => 'rw', default => ''); +has api_options => (is => 'rw', default => sub { [] }); +has api_params => (is => 'rw', default => sub { {} }); +has api_path => (is => 'rw', default => ''); +has cgi => (is => 'lazy'); +has content_type => (is => 'lazy'); +has controller => (is => 'rw', default => undef); +has json => (is => 'lazy'); +has load_error => (is => 'rw', default => undef); +has method_name => (is => 'rw', default => ''); +has request => (is => 'lazy'); +has success_code => (is => 'rw', default => 200); + +################## +# Public methods # +################## + +sub server { + my ($class) = @_; + + my $api_namespace = DEFAULT_API_NAMESPACE; + my $api_version = DEFAULT_API_VERSION; + + # First load the default server in case something fails + # we still have something to return. + my $server_class = "Bugzilla::API::${api_version}::Server"; + require_module($server_class); + my $self = $server_class->new; + + my $path_info = Bugzilla->cgi->path_info; + + # If we do not match /<namespace>/<version>/ then we assume legacy calls + # and use the default namespace and version. + if ($path_info =~ m|^/([^/]+)/(\d+\.\d+(?:\.\d+)?)/|) { + # First figure out the namespace we are accessing (core is native) + $api_namespace = $1 if $path_info =~ s|^/([^/]+)||; + $api_namespace = $self->_check_namespace($api_namespace); + + # Figure out which version we are looking for based on path + $api_version = $1 if $path_info =~ s|^/(\d+\.\d+(?:\.\d+)?)(/.*)$|$2|; + $api_version = $self->_check_version($api_version, $api_namespace); + } + + # If the version pulled from the path is different than + # what the server is currently, then reload as the new version. + if ($api_version ne $self->api_version) { + my $server_class = "Bugzilla::API::${api_version}::Server"; + require_module($server_class); + $self = $server_class->new; + } + + # Stuff away for later + $self->api_path($path_info); + + return $self; +} + +sub constants { + my ($self) = @_; + my $api_version = $self->api_version; + + no strict 'refs'; + + my $class = "Bugzilla::API::${api_version}::Constants"; + require_module($class); + + my %constants; + foreach my $constant (@{$class . "::EXPORT"}, @{$class . "::EXPORT_OK"}) { + if (ref $class->$constant) { + $constants{$constant} = $class->$constant; + } + else { + my @list = ($class->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + + return \%constants; +} + +sub response_header { + my ($self, $code, $result) = @_; + # The HTTP body needs to be bytes (not a utf8 string) for recent + # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this + # properly. $_[1] is the HTTP body content we're going to be sending. + if (utf8::is_utf8($_[2])) { + utf8::encode($_[2]); + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + my $h = HTTP::Headers->new; + $h->header('Content-Type' => $self->content_type . '; charset=UTF-8'); + return HTTP::Response->new($code => undef, $h, $result); +} + +################################### +# Public methods to be overridden # +################################### + +sub handle { } +sub response { } +sub print_response { } +sub handle_login { } + +################### +# Utility methods # +################### + +sub return_error { + my ($self, $status_code, $message, $error_code) = @_; + if ($status_code && $message) { + $self->{_return_error} = { + status_code => $status_code, + error => JSON::true, + message => $message + }; + $self->{_return_error}->{code} = $error_code if $error_code; + } + return $self->{_return_error}; +} + +sub callback { + my ($self, $value) = @_; + if (defined $value) { + $value = trim($value); + # We don't use \w because we don't want to allow Unicode here. + if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { + ThrowUserError('json_rpc_invalid_callback', { callback => $value }); + } + $self->{_callback} = $value; + # JSONP needs to be parsed by a JS parser, not by a JSON parser. + $self->content_type('text/javascript'); + } + return $self->{_callback}; +} + +# ETag support +sub etag { + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + if (defined $data) { + # Serialize the data if passed a reference + local $Storable::canonical = 1; + $data = freeze($data) if ref $data; + + # Wide characters cause md5_base64() to die. + utf8::encode($data) if utf8::is_utf8($data); + + # Append content_type to the end of the data + # string as we want the etag to be unique to + # the content_type. We do not need this for + # XMLRPC as text/xml is always returned. + if (blessed($self) && $self->can('content_type')) { + $data .= $self->content_type if $self->content_type; + } + + $cache->{'_etag'} = md5_base64($data); + } + return $cache->{'_etag'}; +} + +# HACK: Allow error tag checking to work with t/012throwables.t +sub ThrowUserError { + my ($error, $self, $vars) = @_; + $self->load_error({ type => 'user', + error => $error, + vars => $vars }); +} + +sub ThrowCodeError { + my ($error, $self, $vars) = @_; + $self->load_error({ type => 'code', + error => $error, + vars => $vars }); +} + +################### +# Private methods # +################### + +sub _build_cgi { + return Bugzilla->cgi; +} + +sub _build_content_type { + return 'application/json'; +} + +sub _build_json { + # This may seem a little backwards to set utf8(0), but what this really + # means is "don't convert our utf8 into byte strings, just leave it as a + # utf8 string." + return JSON->new->utf8(0) + ->allow_blessed(1) + ->convert_blessed(1); +} + +sub _build_request { + return HTTP::Request->new($_[0]->cgi->request_method, $_[0]->cgi->url); +} + +sub _check_namespace { + my ($self, $namespace) = @_; + + # No need to do anything else if native api + return $namespace if lc($namespace) eq lc(DEFAULT_API_NAMESPACE); + + # Check if namespace matches an extension name + my $found = 0; + foreach my $extension (@{ Bugzilla->extensions }) { + $found = 1 if lc($extension->NAME) eq lc($namespace); + } + # Make sure we have this namespace available + if (!$found) { + ThrowUserError('unknown_api_namespace', $self, + { api_namespace => $namespace }); + return DEFAULT_API_NAMESPACE; + } + + return $namespace; +} + +sub _check_version { + my ($self, $version, $namespace) = @_; + + return DEFAULT_API_VERSION if !defined $version; + + my $old_version = $version; + $version =~ s/\./_/g; + + my $version_dir; + if (lc($namespace) eq 'core') { + $version_dir = File::Spec->catdir('Bugzilla', 'API', $version); + } + else { + $version_dir = File::Spec->catdir(bz_locations()->{extensionsdir}, + $namespace, 'API', $version); + } + + # Make sure we actual have this version installed + if (!-d $version_dir) { + ThrowUserError('unknown_api_version', $self, + { api_version => $old_version, + api_namespace => $namespace }); + return DEFAULT_API_VERSION; + } + + # If we using an extension API, we need to determing which version of + # the Core API it was written for. + if (lc($namespace) ne 'core') { + my $core_api_version; + foreach my $extension (@{ Bugzilla->extensions }) { + next if lc($extension->NAME) ne lc($namespace); + if ($extension->API_VERSION_MAP + && $extension->API_VERSION_MAP->{$version}) + { + $self->api_ext_version($version); + $version = $extension->API_VERSION_MAP->{$version}; + } + } + } + + return $version; +} + +sub _best_content_type { + my ($self, @types) = @_; + my @accept_types = $self->_get_content_prefs(); + # Return the types as-is if no accept header sent, since sorting will be a no-op. + if (!@accept_types) { + return $types[0]; + } + my $score = sub { $self->_score_type(shift, @accept_types) }; + my @scored_types = sort {$score->($b) <=> $score->($a)} @types; + return $scored_types[0] || '*/*'; +} + +sub _score_type { + my ($self, $type, @accept_types) = @_; + my $score = scalar(@accept_types); + for my $accept_type (@accept_types) { + return $score if $type eq $accept_type; + $score--; + } + return 0; +} + +sub _get_content_prefs { + my $self = shift; + my $default_weight = 1; + my @prefs; + + # Parse the Accept header, and save type name, score, and position. + my @accept_types = split /,/, $self->cgi->http('accept') || ''; + my $order = 0; + for my $accept_type (@accept_types) { + my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); + my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); + next unless $name; + push @prefs, { name => $name, order => $order++}; + if (defined $weight) { + $prefs[-1]->{score} = $weight; + } else { + $prefs[-1]->{score} = $default_weight; + $default_weight -= 0.001; + } + } + + # Sort the types by score, subscore by order, and pull out just the name + @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} || + $a->{order} <=> $b->{order}} @prefs; + return @prefs; +} + +#################################### +# Private methods to be overridden # +#################################### + +sub _handle { } +sub _params_check { } +sub _retrieve_json_params { } +sub _find_resource { } + +1; + +__END__ + +=head1 NAME + +Bugzilla::API::Server - The Web Service API interface to Bugzilla + +=head1 DESCRIPTION + +This is the standard API for external programs that want to interact +with Bugzilla. It provides various resources in various modules. + +You interact with this API using L<REST|Bugzilla::API::Server>. + +Full client documentation for the Bugzilla API can be found at +L<https://bugzilla.readthedocs.org/en/latest/api/index.html>. + +=head1 USAGE + +Methodl are grouped into "namespaces", like C<core> for +native Bugzilla API methods. Extensions reside in their own +I<namespaces> such as C<Example>. So, for example: + +GET /example/1.0/bug1 + +calls + +GET /bug/1 + +in the C<Example> namespace. + +The endpoint for the API interface is the C<rest.cgi> script in +your Bugzilla installation. For example, if your Bugzilla is at +C<bugzilla.yourdomain.com>, to access the API and load a bug, +you would use C<http://bugzilla.yourdomain.com/rest.cgi/core/1.0/bug/35>. + +If using Apache and mod_rewrite is installed and enabled, you can +simplify the endpoint by changing /rest.cgi/ to something like /api/ +or something similar. So the same example from above would be: +C<http://bugzilla.yourdomain.com/api/core/1.0/bug/35> which is simpler +to remember. + +Add this to your .htaccess file: + + <IfModule mod_rewrite.c> + RewriteEngine On + RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE] + </IfModule> + +=head1 BROWSING + +If the Accept: header of a request is set to text/html (as it is by an +ordinary web browser) then the API will return the JSON data as a HTML +page which the browser can display. In other words, you can play with the +API using just your browser and see results in a human-readable form. +This is a good way to try out the various GET calls, even if you can't use +it for POST or PUT. + +=head1 DATA FORMAT + +The API only supports JSON input, and either JSON and JSONP output. +So objects sent and received must be in JSON format. + +On every request, you must set both the "Accept" and "Content-Type" HTTP +headers to the MIME type of the data format you are using to communicate with +the API. Content-Type tells the API how to interpret your request, and Accept +tells it how you want your data back. "Content-Type" must be "application/json". +"Accept" can be either that, or "application/javascript" for JSONP - add a "callback" +parameter to name your callback. + +Parameters may also be passed in as part of the query string for non-GET requests +and will override any matching parameters in the request body. + +=head1 AUTHENTICATION + +Along with viewing data as an anonymous user, you may also see private information +if you have a Bugzilla account by providing your login credentials. + +=over + +=item Login name and password + +Pass in as query parameters of any request: + +login=fred@example.com&password=ilovecheese + +Remember to URL encode any special characters, which are often seen in passwords and to +also enable SSL support. + +=item Login token + +By calling GET /login?login=fred@example.com&password=ilovecheese, you get back +a C<token> value which can then be passed to each subsequent call as +authentication. This is useful for third party clients that cannot use cookies +and do not want to store a user's login and password in the client. You can also +pass in "token" as a convenience. + +=item API Key + +You can also authenticate by passing an C<api_key> value as part of the query +parameters which is setup using the I<API Keys> tab in C<userprefs.cgi>. + +=back + +=head1 ERRORS + +When an API error occurs, a data structure is returned with the key C<error> +set to C<true>. + +The error contents look similar to: + + { "error": true, "message": "Some message here", "code": 123 } + +=head1 CONSTANTS + +=over + +=item DEFAULT_API_VERSION + +The default API version that is used by C<server>. +Current default is L<1.0> which is the first version of the API implemented in this way.. + +=item DEFAULT_API_NAMESPACE + +The default API namespace that is used if C<server> is called before C<init_serber>. +Current default is L<core> which is the native API methods (non-extension). + +=back + +=head1 METHODS + +The L<Bugzilla::API::Server> has the following methods used by various +code in Bugzilla. + +=over + +=item server + +Returns a L<Bugzilla::API::Server> object after looking at the cgi path to +determine which version of the API is being requested and which namespace to +load methods from. A new server instance of the proper version is returned. + +=item constants + +A method return a hash containing the constants from the Constants.pm module +in the API version directory. The calling code will not need to know which +version of the API is being used to access the constant values. + +=item json + +Returns a L<JSON> encode/decoder object. + +=item cgi + +Returns a L<Bugzilla::CGI> object. + +=item request + +Returns a L<HTTP::Request> object. + +=item response_header + +Returns a L<HTTP::Response> object with the appropriate content-type set. +Requires that a status code and content data to be passed in. + +=item handle + +Handles the current request by finding the correct resource, setting the parameters, +authentication, executing the resource, and forming an appropriate response. + +=item response + +Encodes the return data in the requested content-type and also does some other +changes such as conversion to JSONP and setting status_code. Also sets the eTag +header values based on the result content. + +=item print_response + +Prints the final response headers and content to STDOUT. + +=item handle_login + +Authenticates the user and performs additional checks. + +=item return_error + +If an error occurs, this method will return a data structure describing the error +with a code and message. + +=item callback + +When calling the API over GET, you can use the "JSONP" method of doing cross-domain +requests, if you want to access the API directly on a web page from another site. +JSONP is described at L<http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/>. + +To use JSONP with Bugzilla's API, simply specify a C<callback> parameter when +using it via GET as described above. For example, here's some HTML you could use +to get the time on a remote Bugzilla website, using JSONP: + + <script type="text/javascript" src="http://bugzilla.example.com/time?callback=foo"> + +That would call the API path for C<time> and pass its value to a function +called C<foo> as the only argument. All the other URL parameters (such as for +passing in arguments to methods) that can be passed during GET requests are also +available, of course. The above is just the simplest possible example. + +The values returned when using JSONP are identical to the values returned +when not using JSONP, so you will also get error messages if there is an +error. + +The C<callback> URL parameter may only contain letters, numbers, periods, and +the underscore (C<_>) character. Including any other characters will cause +Bugzilla to throw an error. (This error will be a normal API response, not JSONP.) + +=item etag + +Using the data structure passed to the subroutine, we convert the data to a string +and then md5 hash the string to creates a value for the eTag header. This allows +a user to include the value in seubsequent requests and only return the full data +if it has changed. + +=item api_ext + +A boolean value signifying if the current request is for an API method is exported +by an extension or is part of the core methods. + +=item api_ext_version + +If the current request is for an extension API method, this is the version of the +extension API that should be used. + +=item api_namespace + +The current namespace of the API method being requested as determined by the +cgi path. If a namespace is not provided, we default to L<core>. + +=item api_options + +Once a resource has been matched to the current request, this the available options +to the client such as GET, PUT, etc. + +=item api_params + +Once a resource has been matched, this is the params that were pulled from the +regex used to match the resource. This could be a resource id or name such as +a bug id, etc. + +=item api_path + +The final cgi path after namespace and version have been removed. This is the +path used to locate a matching resource from the controller modules. + +=item api_version + +The current version of the L<core> API that is being used for processing the +request. Note that this version may be different from C<api_ext_version> if +the client requested a method in an extension's namespace. + +=item content_type + +The content-type of the data that will be returned. The current default is +L<application/json>. If a caller is msking a request using a browser, it will +most likely be L<text/html>. + +=item controller + +Once a resource has been matched, this is the controller module that contains +the method that will be executed. + +=item method_name + +The method in the controller module that will be executed to handle the request. + +=item success_code + +The success code to be used when creating the L<response> object to be returned. +It can be different depending on if the request was successful, a resource was +created, or an error occurred. + +=back + +=head1 B<Methods in need of POD> + +=over + +=item ThrowCodeError + +=item ThrowUserError + +=back + diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index ef6320d15..e730022db 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -123,19 +123,13 @@ sub _throw_error { if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { die SOAP::Fault->faultcode($code)->faultstring($message); } - else { + elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) { my $server = Bugzilla->_json_server; - my $status_code = 0; - if (Bugzilla->error_mode == ERROR_MODE_REST) { - my %status_code_map = %{ REST_STATUS_CODE_MAP() }; - $status_code = $status_code_map{$code} || $status_code_map{'_default'}; - } # Technically JSON-RPC isn't allowed to have error numbers # higher than 999, but we do this to avoid conflicts with # the internal JSON::RPC error codes. $server->raise_error(code => 100000 + $code, - status_code => $status_code, message => $message, id => $server->{_bz_request_id}, version => $server->version); @@ -146,6 +140,13 @@ sub _throw_error { die if _in_eval(); $server->response($server->error_response_header); } + else { + my $server = Bugzilla->api_server; + my %status_code_map = %{ $server->constants->{REST_STATUS_CODE_MAP} }; + my $status_code = $status_code_map{$code} || $status_code_map{'_default'}; + $server->return_error($status_code, $message, $code); + $server->response; + } } exit; } diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index 101bc2205..4ffae3448 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -301,7 +301,7 @@ sub OPTIONAL_MODULES { package => 'JSON-RPC', module => 'JSON::RPC', version => 0, - feature => ['jsonrpc', 'rest'], + feature => ['jsonrpc'], }, { package => 'Test-Taint', @@ -311,6 +311,36 @@ sub OPTIONAL_MODULES { feature => ['jsonrpc', 'xmlrpc', 'rest'], }, { + package => 'Moo', + module => 'Moo', + version => 2, + feature => ['rest'] + }, + { + package => 'Module-Runtime', + module => 'Module::Runtime', + version => 0, + feature => ['rest'] + }, + { + package => 'HTTP-Request', + module => 'HTTP::Request', + version => 0, + feature => ['rest'] + }, + { + package => 'HTTP-Response', + module => 'HTTP::Response', + version => 0, + feature => ['rest'] + }, + { + package => 'URI-Escape', + module => 'URI::Escape', + version => 0, + feature => ['rest'] + }, + { # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber. package => 'HTML-Parser', module => 'HTML::Parser', diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm deleted file mode 100644 index d646dd795..000000000 --- a/Bugzilla/WebService/Server/REST.pm +++ /dev/null @@ -1,664 +0,0 @@ -# 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; - -use 5.10.1; -use strict; -use warnings; - -use parent qw(Bugzilla::WebService::Server::JSONRPC); - -use Bugzilla::Constants; -use Bugzilla::Error; -use Bugzilla::Hook; -use Bugzilla::Util qw(correct_urlbase html_quote); -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(taint_data fix_credentials); - -# Load resource modules -use Bugzilla::WebService::Server::REST::Resources::Bug; -use Bugzilla::WebService::Server::REST::Resources::Bugzilla; -use Bugzilla::WebService::Server::REST::Resources::Classification; -use Bugzilla::WebService::Server::REST::Resources::Component; -use Bugzilla::WebService::Server::REST::Resources::FlagType; -use Bugzilla::WebService::Server::REST::Resources::Group; -use Bugzilla::WebService::Server::REST::Resources::Product; -use Bugzilla::WebService::Server::REST::Resources::User; -use Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit; - -use List::MoreUtils qw(uniq); -use Scalar::Util qw(blessed reftype); -use MIME::Base64 qw(decode_base64); - -########################### -# Public Method Overrides # -########################### - -sub handle { - my ($self) = @_; - - # Determine how the data should be represented. We do this early so - # errors will also be returned with the proper content type. - # If no accept header was sent or the content types specified were not - # matched, we default to the first type in the whitelist. - $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); - - # Using current path information, decide which class/method to - # use to serve the request. Throw error if no resource was found - # unless we were looking for OPTIONS - if (!$self->_find_resource($self->cgi->path_info)) { - if ($self->request->method eq 'OPTIONS' - && $self->bz_rest_options) - { - my $response = $self->response_header(STATUS_OK, ""); - my $options_string = join(', ', @{ $self->bz_rest_options }); - $response->header('Allow' => $options_string, - 'Access-Control-Allow-Methods' => $options_string); - return $self->response($response); - } - - ThrowUserError("rest_invalid_resource", - { path => $self->cgi->path_info, - method => $self->request->method }); - } - - # Dispatch to the proper module - my $class = $self->bz_class_name; - my ($path) = $class =~ /::([^:]+)$/; - $self->path_info($path); - delete $self->{dispatch_path}; - $self->dispatch({ $path => $class }); - - my $params = $self->_retrieve_json_params; - - fix_credentials($params, $self->cgi); - - # Fix includes/excludes for each call - rest_include_exclude($params); - - # Set callback name if exists - $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; - - Bugzilla->input_params($params); - - # Set the JSON version to 1.1 and the id to the current urlbase - # also set up the correct handler method - my $obj = { - version => '1.1', - id => correct_urlbase(), - method => $self->bz_method_name, - params => $params - }; - - # Execute the handler - my $result = $self->_handle($obj); - - if (!$self->error_response_header) { - return $self->response( - $self->response_header($self->bz_success_code || STATUS_OK, $result)); - } - - $self->response($self->error_response_header); -} - -sub response { - my ($self, $response) = @_; - - # If we have thrown an error, the 'error' key will exist - # otherwise we use 'result'. JSONRPC returns other data - # along with the result/error such as version and id which - # we will strip off for REST calls. - my $content = $response->content; - my $json_data = {}; - if ($content) { - $json_data = $self->json->decode($content); - } - - my $result = {}; - if (exists $json_data->{error}) { - $result = $json_data->{error}; - $result->{error} = $self->type('boolean', 1); - $result->{documentation} = REST_DOC; - delete $result->{'name'}; # Remove JSONRPCError - } - elsif (exists $json_data->{result}) { - $result = $json_data->{result}; - } - - Bugzilla::Hook::process('webservice_rest_response', - { rpc => $self, result => \$result, response => $response }); - - # Access Control - my @allowed_headers = qw(accept content-type origin x-requested-with); - foreach my $header (keys %{ API_AUTH_HEADERS() }) { - # We want to lowercase and replace _ with - - my $translated_header = $header; - $translated_header =~ tr/A-Z_/a-z\-/; - push(@allowed_headers, $translated_header); - } - $response->header("Access-Control-Allow-Origin", "*"); - $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers)); - - # ETag support - my $etag = $self->bz_etag; - $self->bz_etag($result) if !$etag; - - # If accessing through web browser, then display in readable format - if ($self->content_type eq 'text/html') { - $result = $self->json->pretty->canonical->allow_nonref->encode($result); - - my $template = Bugzilla->template; - $content = ""; - $template->process("rest.html.tmpl", { result => $result }, \$content) - || ThrowTemplateError($template->error()); - - $response->content_type('text/html'); - } - else { - $content = $self->json->encode($result); - } - - $response->content($content); - - $self->SUPER::response($response); -} - -####################################### -# Bugzilla::WebService Implementation # -####################################### - -sub handle_login { - my $self = shift; - my $class = $self->bz_class_name; - my $method = $self->bz_method_name; - my $full_method = $class . "." . $method; - - # Bypass JSONRPC::handle_login - Bugzilla::WebService::Server->handle_login($class, $method, $full_method); -} - -############################ -# Private Method Overrides # -############################ - -# We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure -# as it determines the method name differently. -sub _find_procedure { - my $self = shift; - if ($self->isa('JSON::RPC::Server::CGI')) { - return JSON::RPC::Server::_find_procedure($self, @_); - } - else { - return JSON::RPC::Legacy::Server::_find_procedure($self, @_); - } -} - -sub _argument_type_check { - my $self = shift; - my $params; - - if ($self->isa('JSON::RPC::Server::CGI')) { - $params = JSON::RPC::Server::_argument_type_check($self, @_); - } - else { - $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); - } - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - my $method = $self->bz_method_name; - my $pkg = $self->{dispatch_path}->{$self->path_info}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } - - # This is the best time to do login checks. - $self->handle_login(); - - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; - - # Allow extensions to modify the params post login - Bugzilla::Hook::process('webservice_rest_request', - { rpc => $self, params => $params }); - - if ($params_is_array) { - $params = [$params]; - } - - return $params; -} - -################### -# Utility Methods # -################### - -sub bz_method_name { - my ($self, $method) = @_; - $self->{_bz_method_name} = $method if $method; - return $self->{_bz_method_name}; -} - -sub bz_class_name { - my ($self, $class) = @_; - $self->{_bz_class_name} = $class if $class; - return $self->{_bz_class_name}; -} - -sub bz_success_code { - my ($self, $value) = @_; - $self->{_bz_success_code} = $value if $value; - return $self->{_bz_success_code}; -} - -sub bz_rest_params { - my ($self, $params) = @_; - $self->{_bz_rest_params} = $params if $params; - return $self->{_bz_rest_params}; -} - -sub bz_rest_options { - my ($self, $options) = @_; - $self->{_bz_rest_options} = $options if $options; - return $self->{_bz_rest_options}; -} - -sub rest_include_exclude { - my ($params) = @_; - - if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { - $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; - } - if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { - $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; - } - - return $params; -} - -########################## -# Private Custom Methods # -########################## - -sub _retrieve_json_params { - my $self = shift; - - # Make a copy of the current input_params rather than edit directly - my $params = {}; - %{$params} = %{ Bugzilla->input_params }; - - # First add any parameters we were able to pull out of the path - # based on the resource regexp and combine with the normal URL - # parameters. - if (my $rest_params = $self->bz_rest_params) { - foreach my $param (keys %$rest_params) { - # If the param does not already exist or if the - # rest param is a single value, add it to the - # global params. - if (!exists $params->{$param} || !ref $rest_params->{$param}) { - $params->{$param} = $rest_params->{$param}; - } - # If rest_param is a list then add any extra values to the list - elsif (ref $rest_params->{$param}) { - my @extra_values = ref $params->{$param} - ? @{ $params->{$param} } - : ($params->{$param}); - $params->{$param} - = [ uniq (@{ $rest_params->{$param} }, @extra_values) ]; - } - } - } - - # Any parameters passed in in the body of a non-GET request will override - # any parameters pull from the url path. Otherwise non-unique keys are - # combined. - if ($self->request->method ne 'GET') { - my $extra_params = {}; - # We do this manually because CGI.pm doesn't understand JSON strings. - my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; - if ($json) { - eval { $extra_params = $self->json->decode($json); }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); - } - } - - # Allow parameters in the query string if request was non-GET. - # Note: parameters in query string body override any matching - # parameters in the request body. - foreach my $param ($self->cgi->url_param()) { - $extra_params->{$param} = $self->cgi->url_param($param); - } - - %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; - } - - return $params; -} - -sub _find_resource { - my ($self, $path) = @_; - - # Load in the WebService module from the dispatch map and then call - # $module->rest_resources to get the resources array ref. - my $resources = {}; - foreach my $module (values %{ $self->{dispatch_path} }) { - eval("require $module") || die $@; - next if !$module->can('rest_resources'); - $resources->{$module} = $module->rest_resources; - } - - Bugzilla::Hook::process('webservice_rest_resources', - { rpc => $self, resources => $resources }); - - # Use the resources hash from each module loaded earlier to determine - # which handler to use based on a regex match of the CGI path. - # Also any matches found in the regex will be passed in later to the - # handler for possible use. - my $request_method = $self->request->method; - - my (@matches, $handler_found, $handler_method, $handler_class); - foreach my $class (keys %{ $resources }) { - # The resource data for each module needs to be - # an array ref with an even number of elements - # to work correctly. - next if (ref $resources->{$class} ne 'ARRAY' - || scalar @{ $resources->{$class} } % 2 != 0); - - while (my $regex = shift @{ $resources->{$class} }) { - my $options_data = shift @{ $resources->{$class} }; - next if ref $options_data ne 'HASH'; - - if (@matches = ($path =~ $regex)) { - # If a specific path is accompanied by a OPTIONS request - # method, the user is asking for a list of possible request - # methods for a specific path. - $self->bz_rest_options([ keys %{ $options_data } ]); - - if ($options_data->{$request_method}) { - my $resource_data = $options_data->{$request_method}; - $self->bz_class_name($class); - - # The method key/value can be a simple scalar method name - # or a anonymous subroutine so we execute it here. - my $method = ref $resource_data->{method} eq 'CODE' - ? $resource_data->{method}->($self) - : $resource_data->{method}; - $self->bz_method_name($method); - - # Pull out any parameters parsed from the URL path - # and store them for use by the method. - if ($resource_data->{params}) { - $self->bz_rest_params($resource_data->{params}->(@matches)); - } - - # If a special success code is needed for this particular - # method, then store it for later when generating response. - if ($resource_data->{success_code}) { - $self->bz_success_code($resource_data->{success_code}); - } - $handler_found = 1; - } - } - last if $handler_found; - } - last if $handler_found; - } - - return $handler_found; -} - -sub _best_content_type { - my ($self, @types) = @_; - return ($self->_simple_content_negotiation(@types))[0] || '*/*'; -} - -sub _simple_content_negotiation { - my ($self, @types) = @_; - my @accept_types = $self->_get_content_prefs(); - # Return the types as-is if no accept header sent, since sorting will be a no-op. - if (!@accept_types) { - return @types; - } - my $score = sub { $self->_score_type(shift, @accept_types) }; - return sort {$score->($b) <=> $score->($a)} @types; -} - -sub _score_type { - my ($self, $type, @accept_types) = @_; - my $score = scalar(@accept_types); - for my $accept_type (@accept_types) { - return $score if $type eq $accept_type; - $score--; - } - return 0; -} - -sub _get_content_prefs { - my $self = shift; - my $default_weight = 1; - my @prefs; - - # Parse the Accept header, and save type name, score, and position. - my @accept_types = split /,/, $self->cgi->http('accept') || ''; - my $order = 0; - for my $accept_type (@accept_types) { - my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); - my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); - next unless $name; - push @prefs, { name => $name, order => $order++}; - if (defined $weight) { - $prefs[-1]->{score} = $weight; - } else { - $prefs[-1]->{score} = $default_weight; - $default_weight -= 0.001; - } - } - - # Sort the types by score, subscore by order, and pull out just the name - @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} || - $a->{order} <=> $b->{order}} @prefs; - return @prefs; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::WebService::Server::REST - The REST Interface to Bugzilla - -=head1 DESCRIPTION - -This documentation describes things about the Bugzilla WebService that -are specific to REST. For a general overview of the Bugzilla WebServices, -see L<Bugzilla::WebService>. The L<Bugzilla::WebService::Server::REST> -module is a sub-class of L<Bugzilla::WebService::Server::JSONRPC> so any -method documentation not found here can be viewed in it's POD. - -Please note that I<everything> about this REST interface is -B<EXPERIMENTAL>. If you want a fully stable API, please use the -C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface. - -=head1 CONNECTING - -The endpoint for the REST interface is the C<rest.cgi> script in -your Bugzilla installation. For example, if your Bugzilla is at -C<bugzilla.yourdomain.com>, to access the API and load a bug, -you would use C<http://bugzilla.yourdomain.com/rest.cgi/bug/35>. - -If using Apache and mod_rewrite is installed and enabled, you can -simplify the endpoint by changing /rest.cgi/ to something like /rest/ -or something similar. So the same example from above would be: -C<http://bugzilla.yourdomain.com/rest/bug/35> which is simpler to remember. - -Add this to your .htaccess file: - - <IfModule mod_rewrite.c> - RewriteEngine On - RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE] - </IfModule> - -=head1 BROWSING - -If the Accept: header of a request is set to text/html (as it is by an -ordinary web browser) then the API will return the JSON data as a HTML -page which the browser can display. In other words, you can play with the -API using just your browser and see results in a human-readable form. -This is a good way to try out the various GET calls, even if you can't use -it for POST or PUT. - -=head1 DATA FORMAT - -The REST API only supports JSON input, and either JSON and JSONP output. -So objects sent and received must be in JSON format. Basically since -the REST API is a sub class of the JSONRPC API, you can refer to -L<JSONRPC|Bugzilla::WebService::Server::JSONRPC> for more information -on data types that are valid for REST. - -On every request, you must set both the "Accept" and "Content-Type" HTTP -headers to the MIME type of the data format you are using to communicate with -the API. Content-Type tells the API how to interpret your request, and Accept -tells it how you want your data back. "Content-Type" must be "application/json". -"Accept" can be either that, or "application/javascript" for JSONP - add a "callback" -parameter to name your callback. - -Parameters may also be passed in as part of the query string for non-GET requests -and will override any matching parameters in the request body. - -=head1 AUTHENTICATION - -Along with viewing data as an anonymous user, you may also see private information -if you have a Bugzilla account by providing your login credentials. - -=over - -=item Login name and password - -Pass in as query parameters of any request: - -login=fred@example.com&password=ilovecheese - -Remember to URL encode any special characters, which are often seen in passwords and to -also enable SSL support. - -=item Login token - -By calling GET /login?login=fred@example.com&password=ilovecheese, you get back -a C<token> value which can then be passed to each subsequent call as -authentication. This is useful for third party clients that cannot use cookies -and do not want to store a user's login and password in the client. You can also -pass in "token" as a convenience. - -=back - -=head1 ERRORS - -When an error occurs over REST, a hash structure is returned with the key C<error> -set to C<true>. - -The error contents look similar to: - - { "error": true, "message": "Some message here", "code": 123 } - -Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>. -Errors with a numeric C<code> higher than 100000 are errors thrown by -the JSON-RPC library that Bugzilla uses, not by Bugzilla. - -=head1 UTILITY FUNCTIONS - -=over - -=item B<handle> - -This method overrides the handle method provided by JSONRPC so that certain -actions related to REST such as determining the proper resource to use, -loading query parameters, etc. can be done before the proper WebService -method is executed. - -=item B<response> - -This method overrides the response method provided by JSONRPC so that -the response content can be altered for REST before being returned to -the client. - -=item B<handle_login> - -This method determines the proper WebService all to make based on class -and method name determined earlier. Then calls L<Bugzilla::WebService::Server::handle_login> -which will attempt to authenticate the client. - -=item B<bz_method_name> - -The WebService method name that matches the path used by the client. - -=item B<bz_class_name> - -The WebService class containing the method that matches the path used by the client. - -=item B<bz_rest_params> - -Each REST resource contains a hash key called C<params> that is a subroutine reference. -This subroutine will return a hash structure based on matched values from the path -information that is formatted properly for the WebService method that will be called. - -=item B<bz_rest_options> - -When a client uses the OPTIONS request method along with a specific path, they are -requesting the list of request methods that are valid for the path. Such as for the -path /bug, the valid request methods are GET (search) and POST (create). So the -client would receive in the response header, C<Access-Control-Allow-Methods: GET, POST>. - -=item B<bz_success_code> - -Each resource can specify a specific SUCCESS CODE if the operation completes successfully. -OTherwise STATUS OK (200) is the default returned. - -=item B<rest_include_exclude> - -Normally the WebService methods required C<include_fields> and C<exclude_fields> to be an -array of field names. REST allows for the values for these to be instead comma delimited -string of field names. This method converts the latter into the former so the WebService -methods will not complain. - -=back - -=head1 SEE ALSO - -L<Bugzilla::WebService> diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm deleted file mode 100644 index 3fa8b65cf..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ /dev/null @@ -1,179 +0,0 @@ -# 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::Bug; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Bug; - -BEGIN { - *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/bug$}, { - GET => { - method => 'search', - }, - POST => { - method => 'create', - status_code => STATUS_CREATED - } - }, - qr{^/bug/$}, { - GET => { - method => 'get' - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - return { ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - method => 'comments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_comment', - params => sub { - return { id => $_[0] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/comment/([^/]+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/comment/tags/([^/]+)$}, { - GET => { - method => 'search_comment_tags', - params => sub { - return { query => $_[0] }; - }, - }, - }, - qr{^/bug/comment/([^/]+)/tags$}, { - PUT => { - method => 'update_comment_tags', - params => sub { - return { comment_id => $_[0] }; - }, - }, - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - method => 'history', - params => sub { - return { ids => [ $_[0] ] }; - }, - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - method => 'attachments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_attachment', - params => sub { - return { ids => [ $_[0] ] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update_attachment', - params => sub { - 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] }; - } - } - }, - ]; - return $rest_resources; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::Webservice::Server::REST::Resources::Bug - The REST API for creating, -changing, and getting the details of bugs. - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to file a new bug in Bugzilla, -or get information about bugs that have already been filed. - -See L<Bugzilla::WebService::Bug> for more details on how to use this part of -the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm deleted file mode 100644 index a434d4bef..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm +++ /dev/null @@ -1,52 +0,0 @@ -# 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::BugUserLastVisit; - -use 5.10.1; -use strict; -use warnings; - -BEGIN { - *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; -} - -sub _rest_resources { - return [ - # bug-id - qr{^/bug_user_last_visit/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => $_[0] }; - }, - }, - POST => { - method => 'update', - params => sub { - return { ids => $_[0] }; - }, - }, - }, - ]; -} - -1; -__END__ - -=head1 NAME - -Bugzilla::Webservice::Server::REST::Resources::BugUserLastVisit - The -BugUserLastVisit REST API - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to lookup and update the last time -a user visited a bug. - -See L<Bugzilla::WebService::BugUserLastVisit> for more details on how to use -this part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm deleted file mode 100644 index a8f3f9330..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ /dev/null @@ -1,70 +0,0 @@ -# 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::Bugzilla; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Bugzilla; - -BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/version$}, { - GET => { - method => 'version' - } - }, - qr{^/extensions$}, { - GET => { - method => 'extensions' - } - }, - qr{^/timezone$}, { - GET => { - method => 'timezone' - } - }, - qr{^/time$}, { - GET => { - method => 'time' - } - }, - qr{^/last_audit_time$}, { - GET => { - method => 'last_audit_time' - } - }, - qr{^/parameters$}, { - GET => { - method => 'parameters' - } - } - ]; - return $rest_resources; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::WebService::Bugzilla - Global functions for the webservice interface. - -=head1 DESCRIPTION - -This provides functions that tell you about Bugzilla in general. - -See L<Bugzilla::WebService::Bugzilla> for more details on how to use this part -of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm deleted file mode 100644 index 3f8d32a03..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm +++ /dev/null @@ -1,50 +0,0 @@ -# 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::Classification; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Classification; - -BEGIN { - *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/classification/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - } - ]; - return $rest_resources; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::Webservice::Server::REST::Resources::Classification - The Classification REST API - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to deal with the available Classifications. -You will be able to get information about them as well as manipulate them. - -See L<Bugzilla::WebService::Classification> for more details on how to use this part -of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm deleted file mode 100644 index 47a8b9e0f..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Component.pm +++ /dev/null @@ -1,76 +0,0 @@ -# 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::Component; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Component; - -use Bugzilla::Error; - -BEGIN { - *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/component$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/component/(\d+)$}, { - PUT => { - method => 'update', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - DELETE => { - method => 'delete', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - }, - qr{^/component/([^/]+)/([^/]+)$}, { - PUT => { - method => 'update', - params => sub { - return { names => [ { product => $_[0], component => $_[1] } ] }; - } - }, - DELETE => { - method => 'delete', - params => sub { - return { names => [ { product => $_[0], component => $_[1] } ] }; - } - }, - }, - ]; - return $rest_resources; -} - -1; - -__END__ - -=head1 NAME - -Bugzilla::Webservice::Server::REST::Resources::Component - The Component REST API - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you create Components. - -See L<Bugzilla::WebService::Component> for more details on how to use this -part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm deleted file mode 100644 index 21dad0f73..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm +++ /dev/null @@ -1,72 +0,0 @@ -# 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 warnings; - -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{^/flag_type$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/flag_type/([^/]+)/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0], - component => $_[1] }; - } - } - }, - qr{^/flag_type/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0] }; - } - }, - 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<Bugzilla::WebService::FlagType> for more details on how to use this -part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm deleted file mode 100644 index b052e384b..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Group.pm +++ /dev/null @@ -1,60 +0,0 @@ -# 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::Group; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Group; - -BEGIN { - *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/group$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/group/([^/]+)$}, { - 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::Group - The REST API for -creating, changing, and getting information about Groups. - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to create Groups and -get information about them. - -See L<Bugzilla::WebService::Group> for more details on how to use this part -of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm deleted file mode 100644 index 607b94b53..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/Product.pm +++ /dev/null @@ -1,83 +0,0 @@ -# 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::Product; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Product; - -use Bugzilla::Error; - -BEGIN { - *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/product_accessible$}, { - GET => { - method => 'get_accessible_products' - } - }, - qr{^/product_enterable$}, { - GET => { - method => 'get_enterable_products' - } - }, - qr{^/product_selectable$}, { - GET => { - method => 'get_selectable_products' - } - }, - qr{^/product$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/product/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - 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::Product - The Product REST API - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to list the available Products and -get information about them. - -See L<Bugzilla::WebService::Product> for more details on how to use this part of -the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm deleted file mode 100644 index a83109e73..000000000 --- a/Bugzilla/WebService/Server/REST/Resources/User.pm +++ /dev/null @@ -1,81 +0,0 @@ -# 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::User; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::WebService::Constants; -use Bugzilla::WebService::User; - -BEGIN { - *Bugzilla::WebService::User::rest_resources = \&_rest_resources; -}; - -sub _rest_resources { - my $rest_resources = [ - qr{^/login$}, { - GET => { - method => 'login' - } - }, - qr{^/logout$}, { - GET => { - method => 'logout' - } - }, - qr{^/valid_login$}, { - GET => { - method => 'valid_login' - } - }, - qr{^/user$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/user/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - 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::User - The User Account REST API - -=head1 DESCRIPTION - -This part of the Bugzilla REST API allows you to get User information as well -as create User Accounts. - -See L<Bugzilla::WebService::User> for more details on how to use this part of -the REST API. |