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:
bzrmirror%bugzilla.org 2015-04-08 18:01:07 +00:00
parent 75cf9cff5e
commit f8138e2440
34 changed files with 12486 additions and 1417 deletions

View File

@ -1 +1 @@
9365
9366

View File

@ -1 +1 @@
e6d2fb75aa3c183323c534a214f3dd9be5638676
dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a

View File

@ -209,6 +209,20 @@ sub extensions {
return $cache->{extensions};
}
sub api_server {
my $class = shift;
my $cache = $class->request_cache;
return $cache->{api_server} if defined $cache->{api_server};
require Bugzilla::API::Server;
$cache->{api_server} = Bugzilla::API::Server->server;
if (my $load_error = $cache->{api_server}->load_error) {
my @error_params = ($load_error->{error}, $load_error->{vars});
ThrowCodeError(@error_params) if $load_error->{type} eq 'code';
ThrowUserError(@error_params) if $load_error->{type} eq 'user';
}
return $cache->{api_server};
}
sub feature {
my ($class, $feature) = @_;
my $cache = $class->request_cache;
@ -980,6 +994,11 @@ this Bugzilla installation.
Tells you whether or not a specific feature is enabled. For names
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
=item C<api_server>
Returns a cached instance of the WebService API server object used for
manipulating Bugzilla resources.
=back
=head1 B<CACHING>

View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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

View 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

View File

@ -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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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.

View 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

View File

@ -123,19 +123,13 @@ sub _throw_error {
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
die SOAP::Fault->faultcode($code)->faultstring($message);
}
else {
elsif (Bugzilla->error_mode == ERROR_MODE_JSON_RPC) {
my $server = Bugzilla->_json_server;
my $status_code = 0;
if (Bugzilla->error_mode == ERROR_MODE_REST) {
my %status_code_map = %{ REST_STATUS_CODE_MAP() };
$status_code = $status_code_map{$code} || $status_code_map{'_default'};
}
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
# the internal JSON::RPC error codes.
$server->raise_error(code => 100000 + $code,
status_code => $status_code,
message => $message,
id => $server->{_bz_request_id},
version => $server->version);
@ -146,6 +140,13 @@ sub _throw_error {
die if _in_eval();
$server->response($server->error_response_header);
}
else {
my $server = Bugzilla->api_server;
my %status_code_map = %{ $server->constants->{REST_STATUS_CODE_MAP} };
my $status_code = $status_code_map{$code} || $status_code_map{'_default'};
$server->return_error($status_code, $message, $code);
$server->response;
}
}
exit;
}

View File

@ -301,7 +301,7 @@ sub OPTIONAL_MODULES {
package => 'JSON-RPC',
module => 'JSON::RPC',
version => 0,
feature => ['jsonrpc', 'rest'],
feature => ['jsonrpc'],
},
{
package => 'Test-Taint',
@ -310,6 +310,36 @@ sub OPTIONAL_MODULES {
version => 1.06,
feature => ['jsonrpc', 'xmlrpc', 'rest'],
},
{
package => 'Moo',
module => 'Moo',
version => 2,
feature => ['rest']
},
{
package => 'Module-Runtime',
module => 'Module::Runtime',
version => 0,
feature => ['rest']
},
{
package => 'HTTP-Request',
module => 'HTTP::Request',
version => 0,
feature => ['rest']
},
{
package => 'HTTP-Response',
module => 'HTTP::Response',
version => 0,
feature => ['rest']
},
{
package => 'URI-Escape',
module => 'URI::Escape',
version => 0,
feature => ['rest']
},
{
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
package => 'HTML-Parser',

View File

@ -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>

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -1058,29 +1058,34 @@ sub webservice_rest_resources {
my $resources = $args->{'resources'};
# Add a new resource that allows for /rest/example/hello
# to call Example.hello
$resources->{'Bugzilla::Extension::Example::WebService'} = [
qr{^/example/hello$}, {
GET => {
method => 'hello',
}
}
];
#$resources->{'Bugzilla::Extension::Example::WebService'} = [
# qr{^/example/hello$}, {
# GET => {
# method => 'hello',
# }
# }
#];
}
sub webservice_rest_response {
sub webservice_rest_result {
my ($self, $args) = @_;
my $rpc = $args->{'rpc'};
my $result = $args->{'result'};
my $response = $args->{'response'};
my $result = $args->{'result'};
# Convert a list of bug hashes to a single bug hash if only one is
# being returned.
if (ref $$result eq 'HASH'
&& exists $$result->{'bugs'}
&& ref $$result->{'bugs'} eq 'ARRAY'
&& scalar @{ $$result->{'bugs'} } == 1)
{
$$result = $$result->{'bugs'}->[0];
}
}
sub webservice_rest_response {
my ($self, $args) = @_;
my $response = $args->{'response'};
$response->header('X-Example-Header', 'This is an example header');
}
# This must be the last line of your extension.
__PACKAGE__->NAME;

View File

@ -15,17 +15,10 @@ use lib qw(. lib);
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::WebService::Constants;
BEGIN {
if (!Bugzilla->feature('rest')
|| !Bugzilla->feature('jsonrpc'))
{
if (!Bugzilla->feature('rest')) {
ThrowUserError('feature_disabled', { feature => 'rest' });
}
}
use Bugzilla::WebService::Server::REST;
Bugzilla->usage_mode(USAGE_MODE_REST);
local @INC = (bz_locations()->{extensionsdir}, @INC);
my $server = new Bugzilla::WebService::Server::REST;
$server->version('1.1');
$server->handle();
Bugzilla->api_server->handle();

View File

@ -1150,6 +1150,13 @@
[% ELSIF error == "rest_invalid_resource" %]
A REST API resource was not found for '[% method FILTER html +%] [%+ path FILTER html %]'.
[% ELSIF error == "unknown_api_version" %]
A REST API version was not found for '[% api_version FILTER html +%]'
[%- IF api_namespace %] in namespace '[% api_namespace FILTER html %]'[% END %].
[% ELSIF error == "unknown_api_namespace" %]
A REST API namespace was not found for '[% api_namespace FILTER html +%]'.
[% ELSIF error == "get_products_invalid_type" %]
The product type '[% type FILTER html %]' is invalid. Valid choices
are 'accessible', 'selectable', and 'enterable'.