Bug 1051056: The REST API needs to be versioned so that new changes can be made that do not break compatibility
r=dylan,a=glob git-svn-id: svn://10.0.0.236/trunk@265911 18797224-902f-48f8-a5cc-f745e15eee43
This commit is contained in:
parent
75cf9cff5e
commit
f8138e2440
@ -1 +1 @@
|
|||||||
9365
|
9366
|
||||||
@ -1 +1 @@
|
|||||||
e6d2fb75aa3c183323c534a214f3dd9be5638676
|
dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a
|
||||||
@ -209,6 +209,20 @@ sub extensions {
|
|||||||
return $cache->{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 {
|
sub feature {
|
||||||
my ($class, $feature) = @_;
|
my ($class, $feature) = @_;
|
||||||
my $cache = $class->request_cache;
|
my $cache = $class->request_cache;
|
||||||
@ -980,6 +994,11 @@ this Bugzilla installation.
|
|||||||
Tells you whether or not a specific feature is enabled. For names
|
Tells you whether or not a specific feature is enabled. For names
|
||||||
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
|
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
|
=back
|
||||||
|
|
||||||
=head1 B<CACHING>
|
=head1 B<CACHING>
|
||||||
|
|||||||
311
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Constants.pm
Normal file
311
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Constants.pm
Normal file
@ -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
|
||||||
147
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource.pm
Normal file
147
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource.pm
Normal file
@ -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
|
||||||
4881
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Bug.pm
Normal file
4881
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Bug.pm
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
547
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Bugzilla.pm
Normal file
547
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Bugzilla.pm
Normal file
@ -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
|
||||||
@ -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
|
||||||
639
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Component.pm
Normal file
639
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Component.pm
Normal file
@ -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
|
||||||
890
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/FlagType.pm
Normal file
890
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/FlagType.pm
Normal file
@ -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
|
||||||
636
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Group.pm
Normal file
636
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Group.pm
Normal file
@ -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
|
||||||
1013
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Product.pm
Normal file
1013
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/Product.pm
Normal file
File diff suppressed because it is too large
Load Diff
1151
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/User.pm
Normal file
1151
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Resource/User.pm
Normal file
File diff suppressed because it is too large
Load Diff
451
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Server.pm
Normal file
451
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Server.pm
Normal file
@ -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
|
||||||
|
|
||||||
540
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Util.pm
Normal file
540
mozilla/webtools/bugzilla/Bugzilla/API/1_0/Util.pm
Normal file
@ -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.
|
||||||
654
mozilla/webtools/bugzilla/Bugzilla/API/Server.pm
Normal file
654
mozilla/webtools/bugzilla/Bugzilla/API/Server.pm
Normal file
@ -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
|
||||||
|
|
||||||
@ -123,19 +123,13 @@ sub _throw_error {
|
|||||||
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
|
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
|
||||||
die SOAP::Fault->faultcode($code)->faultstring($message);
|
die SOAP::Fault->faultcode($code)->faultstring($message);
|
||||||
}
|
}
|
||||||
else {
|
elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) {
|
||||||
my $server = Bugzilla->_json_server;
|
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
|
# Technically JSON-RPC isn't allowed to have error numbers
|
||||||
# higher than 999, but we do this to avoid conflicts with
|
# higher than 999, but we do this to avoid conflicts with
|
||||||
# the internal JSON::RPC error codes.
|
# the internal JSON::RPC error codes.
|
||||||
$server->raise_error(code => 100000 + $code,
|
$server->raise_error(code => 100000 + $code,
|
||||||
status_code => $status_code,
|
|
||||||
message => $message,
|
message => $message,
|
||||||
id => $server->{_bz_request_id},
|
id => $server->{_bz_request_id},
|
||||||
version => $server->version);
|
version => $server->version);
|
||||||
@ -146,6 +140,13 @@ sub _throw_error {
|
|||||||
die if _in_eval();
|
die if _in_eval();
|
||||||
$server->response($server->error_response_header);
|
$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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -301,7 +301,7 @@ sub OPTIONAL_MODULES {
|
|||||||
package => 'JSON-RPC',
|
package => 'JSON-RPC',
|
||||||
module => 'JSON::RPC',
|
module => 'JSON::RPC',
|
||||||
version => 0,
|
version => 0,
|
||||||
feature => ['jsonrpc', 'rest'],
|
feature => ['jsonrpc'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
package => 'Test-Taint',
|
package => 'Test-Taint',
|
||||||
@ -310,6 +310,36 @@ sub OPTIONAL_MODULES {
|
|||||||
version => 1.06,
|
version => 1.06,
|
||||||
feature => ['jsonrpc', 'xmlrpc', 'rest'],
|
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.
|
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
|
||||||
package => 'HTML-Parser',
|
package => 'HTML-Parser',
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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.
|
|
||||||
@ -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;
|
||||||
@ -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;
|
__PACKAGE__->NAME;
|
||||||
|
|||||||
@ -1058,29 +1058,34 @@ sub webservice_rest_resources {
|
|||||||
my $resources = $args->{'resources'};
|
my $resources = $args->{'resources'};
|
||||||
# Add a new resource that allows for /rest/example/hello
|
# Add a new resource that allows for /rest/example/hello
|
||||||
# to call Example.hello
|
# to call Example.hello
|
||||||
$resources->{'Bugzilla::Extension::Example::WebService'} = [
|
#$resources->{'Bugzilla::Extension::Example::WebService'} = [
|
||||||
qr{^/example/hello$}, {
|
# qr{^/example/hello$}, {
|
||||||
GET => {
|
# GET => {
|
||||||
method => 'hello',
|
# method => 'hello',
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
];
|
#];
|
||||||
}
|
}
|
||||||
|
|
||||||
sub webservice_rest_response {
|
sub webservice_rest_result {
|
||||||
my ($self, $args) = @_;
|
my ($self, $args) = @_;
|
||||||
my $rpc = $args->{'rpc'};
|
|
||||||
my $result = $args->{'result'};
|
my $result = $args->{'result'};
|
||||||
my $response = $args->{'response'};
|
|
||||||
# Convert a list of bug hashes to a single bug hash if only one is
|
# Convert a list of bug hashes to a single bug hash if only one is
|
||||||
# being returned.
|
# being returned.
|
||||||
if (ref $$result eq 'HASH'
|
if (ref $$result eq 'HASH'
|
||||||
&& exists $$result->{'bugs'}
|
&& exists $$result->{'bugs'}
|
||||||
|
&& ref $$result->{'bugs'} eq 'ARRAY'
|
||||||
&& scalar @{ $$result->{'bugs'} } == 1)
|
&& scalar @{ $$result->{'bugs'} } == 1)
|
||||||
{
|
{
|
||||||
$$result = $$result->{'bugs'}->[0];
|
$$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.
|
# This must be the last line of your extension.
|
||||||
__PACKAGE__->NAME;
|
__PACKAGE__->NAME;
|
||||||
|
|||||||
@ -15,17 +15,10 @@ use lib qw(. lib);
|
|||||||
use Bugzilla;
|
use Bugzilla;
|
||||||
use Bugzilla::Constants;
|
use Bugzilla::Constants;
|
||||||
use Bugzilla::Error;
|
use Bugzilla::Error;
|
||||||
use Bugzilla::WebService::Constants;
|
|
||||||
BEGIN {
|
BEGIN {
|
||||||
if (!Bugzilla->feature('rest')
|
if (!Bugzilla->feature('rest')) {
|
||||||
|| !Bugzilla->feature('jsonrpc'))
|
|
||||||
{
|
|
||||||
ThrowUserError('feature_disabled', { feature => 'rest' });
|
ThrowUserError('feature_disabled', { feature => 'rest' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use Bugzilla::WebService::Server::REST;
|
|
||||||
Bugzilla->usage_mode(USAGE_MODE_REST);
|
Bugzilla->usage_mode(USAGE_MODE_REST);
|
||||||
local @INC = (bz_locations()->{extensionsdir}, @INC);
|
Bugzilla->api_server->handle();
|
||||||
my $server = new Bugzilla::WebService::Server::REST;
|
|
||||||
$server->version('1.1');
|
|
||||||
$server->handle();
|
|
||||||
|
|||||||
@ -1150,6 +1150,13 @@
|
|||||||
[% ELSIF error == "rest_invalid_resource" %]
|
[% ELSIF error == "rest_invalid_resource" %]
|
||||||
A REST API resource was not found for '[% method FILTER html +%] [%+ path FILTER html %]'.
|
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" %]
|
[% ELSIF error == "get_products_invalid_type" %]
|
||||||
The product type '[% type FILTER html %]' is invalid. Valid choices
|
The product type '[% type FILTER html %]' is invalid. Valid choices
|
||||||
are 'accessible', 'selectable', and 'enterable'.
|
are 'accessible', 'selectable', and 'enterable'.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user