summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla.pm19
-rw-r--r--Bugzilla/API/1_0/Constants.pm311
-rw-r--r--Bugzilla/API/1_0/Resource.pm147
-rw-r--r--Bugzilla/API/1_0/Resource/Bug.pm4881
-rw-r--r--Bugzilla/API/1_0/Resource/BugUserLastVisit.pm239
-rw-r--r--Bugzilla/API/1_0/Resource/Bugzilla.pm547
-rw-r--r--Bugzilla/API/1_0/Resource/Classification.pm235
-rw-r--r--Bugzilla/API/1_0/Resource/Component.pm639
-rw-r--r--Bugzilla/API/1_0/Resource/FlagType.pm890
-rw-r--r--Bugzilla/API/1_0/Resource/Group.pm636
-rw-r--r--Bugzilla/API/1_0/Resource/Product.pm1013
-rw-r--r--Bugzilla/API/1_0/Resource/User.pm1151
-rw-r--r--Bugzilla/API/1_0/Server.pm451
-rw-r--r--Bugzilla/API/1_0/Util.pm540
-rw-r--r--Bugzilla/API/Server.pm654
-rw-r--r--Bugzilla/Error.pm15
-rw-r--r--Bugzilla/Install/Requirements.pm32
-rw-r--r--Bugzilla/WebService/Server/REST.pm664
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm179
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm52
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm70
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Classification.pm50
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Component.pm76
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/FlagType.pm72
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Group.pm60
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Product.pm83
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/User.pm81
-rw-r--r--extensions/Example/API/1_0/Resource/Example.pm59
-rw-r--r--extensions/Example/Config.pm8
-rw-r--r--extensions/Example/Extension.pm27
-rwxr-xr-xrest.cgi11
-rw-r--r--template/en/default/global/user-error.html.tmpl7
32 files changed, 12484 insertions, 1415 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 3aa6e5489..4b19301aa 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -209,6 +209,20 @@ sub extensions {
return $cache->{extensions};
}
+sub api_server {
+ my $class = shift;
+ my $cache = $class->request_cache;
+ return $cache->{api_server} if defined $cache->{api_server};
+ require Bugzilla::API::Server;
+ $cache->{api_server} = Bugzilla::API::Server->server;
+ if (my $load_error = $cache->{api_server}->load_error) {
+ my @error_params = ($load_error->{error}, $load_error->{vars});
+ ThrowCodeError(@error_params) if $load_error->{type} eq 'code';
+ ThrowUserError(@error_params) if $load_error->{type} eq 'user';
+ }
+ return $cache->{api_server};
+}
+
sub feature {
my ($class, $feature) = @_;
my $cache = $class->request_cache;
@@ -980,6 +994,11 @@ this Bugzilla installation.
Tells you whether or not a specific feature is enabled. For names
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
+=item C<api_server>
+
+Returns a cached instance of the WebService API server object used for
+manipulating Bugzilla resources.
+
=back
=head1 B<CACHING>
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.
diff --git a/extensions/Example/API/1_0/Resource/Example.pm b/extensions/Example/API/1_0/Resource/Example.pm
new file mode 100644
index 000000000..c4c5f9a32
--- /dev/null
+++ b/extensions/Example/API/1_0/Resource/Example.pm
@@ -0,0 +1,59 @@
+# 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::Example;
+
+use 5.10.1;
+use strict;
+use warnings;
+use parent qw(Bugzilla::API::1_0::Resource);
+use Bugzilla::Error;
+
+#############
+# Constants #
+#############
+
+use constant READ_ONLY => qw(
+ hello
+ throw_an_error
+);
+
+use constant PUBLIC_METHODS => qw(
+ hello
+ throw_an_error
+);
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/hello$}, {
+ GET => {
+ method => 'hello'
+ }
+ },
+ qr{^/throw_an_error$}, {
+ GET => {
+ method => 'throw_an_error'
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+###########
+# Methods #
+###########
+
+# This can be called as Example.hello() from the WebService.
+sub hello {
+ return {
+ message => 'Hello!'
+ };
+}
+
+sub throw_an_error { ThrowUserError('example_my_error') }
+
+1;
diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm
index e7782ef6c..16708e3f8 100644
--- a/extensions/Example/Config.pm
+++ b/extensions/Example/Config.pm
@@ -29,4 +29,12 @@ use constant OPTIONAL_MODULES => [
},
];
+# The map determines which verion of
+# the Core API an extension's API modules
+# were written to work with.
+use constant API_VERSION_MAP => {
+ '1_0' => '1_0',
+ '2_0' => '1_0'
+};
+
__PACKAGE__->NAME;
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
index a866e85e6..1649957fd 100644
--- a/extensions/Example/Extension.pm
+++ b/extensions/Example/Extension.pm
@@ -1058,29 +1058,34 @@ sub webservice_rest_resources {
my $resources = $args->{'resources'};
# Add a new resource that allows for /rest/example/hello
# to call Example.hello
- $resources->{'Bugzilla::Extension::Example::WebService'} = [
- qr{^/example/hello$}, {
- GET => {
- method => 'hello',
- }
- }
- ];
+ #$resources->{'Bugzilla::Extension::Example::WebService'} = [
+ # qr{^/example/hello$}, {
+ # GET => {
+ # method => 'hello',
+ # }
+ # }
+ #];
}
-sub webservice_rest_response {
+sub webservice_rest_result {
my ($self, $args) = @_;
- my $rpc = $args->{'rpc'};
- my $result = $args->{'result'};
- my $response = $args->{'response'};
+ my $result = $args->{'result'};
# Convert a list of bug hashes to a single bug hash if only one is
# being returned.
if (ref $$result eq 'HASH'
&& exists $$result->{'bugs'}
+ && ref $$result->{'bugs'} eq 'ARRAY'
&& scalar @{ $$result->{'bugs'} } == 1)
{
$$result = $$result->{'bugs'}->[0];
}
}
+sub webservice_rest_response {
+ my ($self, $args) = @_;
+ my $response = $args->{'response'};
+ $response->header('X-Example-Header', 'This is an example header');
+}
+
# This must be the last line of your extension.
__PACKAGE__->NAME;
diff --git a/rest.cgi b/rest.cgi
index f12fb64c4..ab985ca79 100755
--- a/rest.cgi
+++ b/rest.cgi
@@ -15,17 +15,10 @@ use lib qw(. lib);
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
-use Bugzilla::WebService::Constants;
BEGIN {
- if (!Bugzilla->feature('rest')
- || !Bugzilla->feature('jsonrpc'))
- {
+ if (!Bugzilla->feature('rest')) {
ThrowUserError('feature_disabled', { feature => 'rest' });
}
}
-use Bugzilla::WebService::Server::REST;
Bugzilla->usage_mode(USAGE_MODE_REST);
-local @INC = (bz_locations()->{extensionsdir}, @INC);
-my $server = new Bugzilla::WebService::Server::REST;
-$server->version('1.1');
-$server->handle();
+Bugzilla->api_server->handle();
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 299e2a194..c74ba1efc 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -1150,6 +1150,13 @@
[% ELSIF error == "rest_invalid_resource" %]
A REST API resource was not found for '[% method FILTER html +%] [%+ path FILTER html %]'.
+ [% ELSIF error == "unknown_api_version" %]
+ A REST API version was not found for '[% api_version FILTER html +%]'
+ [%- IF api_namespace %] in namespace '[% api_namespace FILTER html %]'[% END %].
+
+ [% ELSIF error == "unknown_api_namespace" %]
+ A REST API namespace was not found for '[% api_namespace FILTER html +%]'.
+
[% ELSIF error == "get_products_invalid_type" %]
The product type '[% type FILTER html %]' is invalid. Valid choices
are 'accessible', 'selectable', and 'enterable'.