549 lines
19 KiB
Perl
549 lines
19 KiB
Perl
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
|
#
|
|
# The contents of this file are subject to the Mozilla Public
|
|
# License Version 1.1 (the "License"); you may not use this file
|
|
# except in compliance with the License. You may obtain a copy of
|
|
# the License at http://www.mozilla.org/MPL/
|
|
#
|
|
# Software distributed under the License is distributed on an "AS
|
|
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
|
# implied. See the License for the specific language governing
|
|
# rights and limitations under the License.
|
|
#
|
|
# The Original Code is the Bugzilla JSON Webservices Interface.
|
|
#
|
|
# The Initial Developer of the Original Code is the San Jose State
|
|
# University Foundation. Portions created by the Initial Developer
|
|
# are Copyright (C) 2008 the Initial Developer. All Rights Reserved.
|
|
#
|
|
# Contributor(s):
|
|
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
|
|
|
package Bugzilla::WebService::Server::JSONRPC;
|
|
|
|
use strict;
|
|
use base qw(JSON::RPC::Legacy::Server::CGI Bugzilla::WebService::Server);
|
|
|
|
use Bugzilla::Error;
|
|
use Bugzilla::WebService::Constants;
|
|
use Bugzilla::WebService::Util qw(taint_data);
|
|
use Bugzilla::Util qw(correct_urlbase trim);
|
|
|
|
use MIME::Base64 qw(decode_base64 encode_base64);
|
|
|
|
#####################################
|
|
# Public JSON::RPC Method Overrides #
|
|
#####################################
|
|
|
|
sub new {
|
|
my $class = shift;
|
|
my $self = $class->SUPER::new(@_);
|
|
Bugzilla->_json_server($self);
|
|
$self->dispatch(WS_DISPATCH);
|
|
$self->return_die_message(1);
|
|
return $self;
|
|
}
|
|
|
|
sub create_json_coder {
|
|
my $self = shift;
|
|
my $json = $self->SUPER::create_json_coder(@_);
|
|
$json->allow_blessed(1);
|
|
$json->convert_blessed(1);
|
|
#*********************************************************************#
|
|
# ïÒÉÇÉÎÁÌØÎÏÅ ÒÅÛÅÎÉÅ, ÎÏ ÐÒÉ×ÏÄÉÔ Ë ÏÛÉÂËÁÍ #
|
|
# JSON::RPC::Server at line 170 (HTTP::Message content must be bytes) #
|
|
## This may seem a little backwards, but what this really means is #
|
|
## "don't convert our utf8 into byte strings, just leave it as a #
|
|
## utf8 string." #
|
|
#$json->utf8(0) if Bugzilla->params->{'utf8'}; #
|
|
#*********************************************************************#
|
|
return $json;
|
|
}
|
|
|
|
# Override the JSON::RPC method to return our CGI object instead of theirs.
|
|
sub cgi { return Bugzilla->cgi; }
|
|
|
|
sub response {
|
|
my ($self, $response) = @_;
|
|
|
|
# Implement JSONP.
|
|
if (my $callback = $self->_bz_callback) {
|
|
my $content = $response->content;
|
|
$response->content("$callback($content)");
|
|
|
|
}
|
|
|
|
# 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);
|
|
}
|
|
}
|
|
Bugzilla->cgi->send_header(-status => $response->code, @header_args);
|
|
print $response->content;
|
|
}
|
|
|
|
# The JSON-RPC 1.1 GET specification is not so great--you can't specify
|
|
# data structures as parameters. However, the JSON-RPC 2.0 "JSON-RPC over
|
|
# HTTP" spec is excellent, so we are using that for GET requests, instead.
|
|
# Spec: http://groups.google.com/group/json-rpc/web/json-rpc-over-http
|
|
#
|
|
# The one exception is that we don't require the "params" argument to be
|
|
# Base64 encoded, because that is ridiculous and obnoxious for JavaScript
|
|
# clients.
|
|
sub retrieve_json_from_get {
|
|
my $self = shift;
|
|
my $ARGS = Bugzilla->input_params;
|
|
|
|
my %input;
|
|
|
|
# Both version and id must be set before any errors are thrown.
|
|
if ($ARGS->{version}) {
|
|
$self->version($ARGS->{version});
|
|
$input{version} = $ARGS->{version};
|
|
}
|
|
else {
|
|
$self->version('1.0');
|
|
}
|
|
|
|
# The JSON-RPC 2.0 spec says that any request that omits an id doesn't
|
|
# want a response. However, in an HTTP GET situation, it's stupid to
|
|
# expect all clients to specify some id parameter just to get a response,
|
|
# so we don't require it.
|
|
my $id;
|
|
if (defined $ARGS->{id}) {
|
|
$id = $ARGS->{id};
|
|
}
|
|
# However, JSON::RPC does require that an id exist in most cases, in
|
|
# order to throw proper errors. We use the installation's urlbase as
|
|
# the id, in this case.
|
|
else {
|
|
$id = correct_urlbase();
|
|
}
|
|
# Setting _bz_request_id here is required in case we throw errors early,
|
|
# before _handle.
|
|
$self->{_bz_request_id} = $input{id} = $id;
|
|
|
|
# _bz_callback can throw an error, so we have to set it here, after we're
|
|
# ready to throw errors.
|
|
$self->_bz_callback($ARGS->{callback});
|
|
|
|
if (!$ARGS->{method}) {
|
|
ThrowUserError('json_rpc_get_method_required');
|
|
}
|
|
$input{method} = $ARGS->{method};
|
|
|
|
my $params;
|
|
if (defined $ARGS->{params}) {
|
|
local $@;
|
|
$params = eval {
|
|
$self->json->decode($ARGS->{params})
|
|
};
|
|
if ($@) {
|
|
ThrowUserError('json_rpc_invalid_params',
|
|
{ params => $ARGS->{params},
|
|
err_msg => $@ });
|
|
}
|
|
}
|
|
elsif (!$self->version or $self->version ne '1.1') {
|
|
$params = [];
|
|
}
|
|
else {
|
|
$params = {};
|
|
}
|
|
|
|
$input{params} = $params;
|
|
|
|
my $json = $self->json->encode(\%input);
|
|
return $json;
|
|
}
|
|
|
|
#######################################
|
|
# Bugzilla::WebService Implementation #
|
|
#######################################
|
|
|
|
sub type {
|
|
my ($self, $type, $value) = @_;
|
|
|
|
# This is the only type that does something special with undef.
|
|
if ($type eq 'boolean') {
|
|
return $value ? JSON::true : JSON::false;
|
|
}
|
|
|
|
return JSON::null if !defined $value;
|
|
|
|
my $retval = $value;
|
|
|
|
if ($type eq 'int') {
|
|
$retval = int($value);
|
|
}
|
|
if ($type eq 'double') {
|
|
$retval = 0.0 + $value;
|
|
}
|
|
elsif ($type eq 'string') {
|
|
# Forces string context, so that JSON will make it a string.
|
|
$retval = "$value";
|
|
}
|
|
elsif ($type eq 'dateTime') {
|
|
# ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
|
|
$retval = $self->datetime_format_outbound($value);
|
|
}
|
|
elsif ($type eq 'base64') {
|
|
utf8::encode($value) if utf8::is_utf8($value);
|
|
$retval = encode_base64($value, '');
|
|
}
|
|
|
|
return $retval;
|
|
}
|
|
|
|
sub datetime_format_outbound {
|
|
my $self = shift;
|
|
# YUI expects ISO8601 in UTC time; including TZ specifier
|
|
return $self->SUPER::datetime_format_outbound(@_) . 'Z';
|
|
}
|
|
|
|
sub handle_login {
|
|
my $self = shift;
|
|
|
|
# If we're being called using GET, we don't allow cookie-based or Env
|
|
# login, because GET requests can be done cross-domain, and we don't
|
|
# want private data showing up on another site unless the user
|
|
# explicitly gives that site their username and password. (This is
|
|
# particularly important for JSONP, which would allow a remote site
|
|
# to use private data without the user's knowledge, unless we had this
|
|
# protection in place.)
|
|
if ($self->request->method ne 'POST') {
|
|
# XXX There's no particularly good way for us to get a parameter
|
|
# to Bugzilla->login at this point, so we pass this information
|
|
# around using request_cache, which is a bit of a hack. The
|
|
# implementation of it is in Bugzilla::Auth::Login::Stack.
|
|
Bugzilla->request_cache->{auth_no_automatic_login} = 1;
|
|
}
|
|
|
|
my $path = $self->path_info;
|
|
my $class = $self->{dispatch_path}->{$path};
|
|
my $full_method = $self->_bz_method_name;
|
|
$full_method =~ /^\S+\.(\S+)/;
|
|
my $method = $1;
|
|
$self->SUPER::handle_login($class, $method, $full_method);
|
|
}
|
|
|
|
######################################
|
|
# Private JSON::RPC Method Overrides #
|
|
######################################
|
|
|
|
# Store the ID of the current call, because Bugzilla::Error will need it.
|
|
sub _handle {
|
|
my $self = shift;
|
|
my ($obj) = @_;
|
|
$self->{_bz_request_id} = $obj->{id};
|
|
return $self->SUPER::_handle(@_);
|
|
}
|
|
|
|
# Make all error messages returned by JSON::RPC go into the 100000
|
|
# range, and bring down all our errors into the normal range.
|
|
sub _error {
|
|
my ($self, $id, $code) = (shift, shift, shift);
|
|
# All JSON::RPC errors are less than 1000.
|
|
if ($code < 1000) {
|
|
$code += 100000;
|
|
}
|
|
# Bugzilla::Error adds 100,000 to all *our* errors, so
|
|
# we know they came from us.
|
|
elsif ($code > 100000) {
|
|
$code -= 100000;
|
|
}
|
|
|
|
# We can't just set $_[1] because it's not always settable,
|
|
# in JSON::RPC::Server.
|
|
unshift(@_, $id, $code);
|
|
my $json = $self->SUPER::_error(@_);
|
|
|
|
# We want to always send the JSON-RPC 1.1 error format, although
|
|
# If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
|
|
if (!$self->version or $self->version ne '1.1') {
|
|
my $object = $self->json->decode($json);
|
|
my $message = $object->{error};
|
|
# Just assure that future versions of JSON::RPC don't change the
|
|
# JSON-RPC 1.0 error format.
|
|
if (!ref $message) {
|
|
$object->{error} = {
|
|
code => $code,
|
|
message => $message,
|
|
};
|
|
$json = $self->json->encode($object);
|
|
}
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
# This handles dispatching our calls to the appropriate class based on
|
|
# the name of the method.
|
|
sub _find_procedure {
|
|
my $self = shift;
|
|
|
|
my $method = shift;
|
|
$self->{_bz_method_name} = $method;
|
|
|
|
# This tricks SUPER::_find_procedure into finding the right class.
|
|
$method =~ /^(\S+)\.(\S+)$/;
|
|
$self->path_info($1);
|
|
unshift(@_, $2);
|
|
|
|
return $self->SUPER::_find_procedure(@_);
|
|
}
|
|
|
|
# This is a hacky way to do something right before methods are called.
|
|
# This is the last thing that JSON::RPC::Server::_handle calls right before
|
|
# the method is actually called.
|
|
sub _argument_type_check {
|
|
my $self = shift;
|
|
my $params = $self->SUPER::_argument_type_check(@_);
|
|
|
|
# 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.
|
|
$self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
|
|
my ($class, $method) = ($1, $2);
|
|
my $pkg = $self->{dispatch_path}->{$class};
|
|
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});
|
|
}
|
|
}
|
|
|
|
Bugzilla->input_params($params);
|
|
|
|
if ($self->request->method ne 'POST') {
|
|
# When being called using GET, we don't allow calling
|
|
# methods that can change data. This protects us against cross-site
|
|
# request forgeries.
|
|
# FIXME: POST is not needed and does not protect against CSRF. We need tokens for this.
|
|
if (!grep($_ eq $method, $pkg->READ_ONLY)) {
|
|
ThrowUserError('json_rpc_post_only',
|
|
{ method => $self->_bz_method_name });
|
|
}
|
|
}
|
|
|
|
# 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;
|
|
|
|
if ($params_is_array) {
|
|
$params = [$params];
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
##########################
|
|
# Private Custom Methods #
|
|
##########################
|
|
|
|
# _bz_method_name is stored by _find_procedure for later use.
|
|
sub _bz_method_name {
|
|
return $_[0]->{_bz_method_name};
|
|
}
|
|
|
|
sub _bz_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-1_\.\[\]]+$/) {
|
|
ThrowUserError('json_rpc_invalid_callback', { callback => $value });
|
|
}
|
|
$self->{_bz_callback} = $value;
|
|
# JSONP needs to be parsed by a JS parser, not by a JSON parser.
|
|
$self->content_type('text/javascript');
|
|
}
|
|
return $self->{_bz_callback};
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Bugzilla::WebService::Server::JSONRPC - The JSON-RPC Interface to Bugzilla
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This documentation describes things about the Bugzilla WebService that
|
|
are specific to JSON-RPC. For a general overview of the Bugzilla WebServices,
|
|
see L<Bugzilla::WebService>.
|
|
|
|
Please note that I<everything> about this JSON-RPC interface is
|
|
B<EXPERIMENTAL>. If you want a fully stable API, please use the
|
|
C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface.
|
|
|
|
=head1 JSON-RPC
|
|
|
|
Bugzilla supports both JSON-RPC 1.0 and 1.1. We recommend that you use
|
|
JSON-RPC 1.0 instead of 1.1, though, because 1.1 is deprecated.
|
|
|
|
At some point in the future, Bugzilla may also support JSON-RPC 2.0.
|
|
|
|
The JSON-RPC standards are described at L<http://json-rpc.org/>.
|
|
|
|
=head1 CONNECTING
|
|
|
|
The endpoint for the JSON-RPC interface is the C<jsonrpc.cgi> script in
|
|
your Bugzilla installation. For example, if your Bugzilla is at
|
|
C<bugzilla.yourdomain.com>, then your JSON-RPC client would access the
|
|
API via: C<http://bugzilla.yourdomain.com/jsonrpc.cgi>
|
|
|
|
=head2 Connecting via GET
|
|
|
|
The most powerful way to access the JSON-RPC interface is by HTTP POST.
|
|
However, for convenience, you can also access certain methods by using GET
|
|
(a normal webpage load). Methods that modify the database or cause some
|
|
action to happen in Bugzilla cannot be called over GET. Only methods that
|
|
simply return data can be used over GET.
|
|
|
|
For security reasons, when you connect over GET, cookie authentication
|
|
is not accepted. If you want to authenticate using GET, you have to
|
|
use the C<Bugzilla_login> and C<Bugzilla_password> method described at
|
|
L<Bugzilla::WebService/LOGGING IN>.
|
|
|
|
To connect over GET, simply send the values that you'd normally send for
|
|
each JSON-RPC argument as URL parameters, with the C<params> item being
|
|
a JSON string.
|
|
|
|
The simplest example is a call to C<Bugzilla.time>:
|
|
|
|
jsonrpc.cgi?method=Bugzilla.time
|
|
|
|
Here's a call to C<User.get>, with several parameters:
|
|
|
|
jsonrpc.cgi?method=User.get¶ms=[ { "ids": [1,2], "names": ["user@domain.com"] } ]
|
|
|
|
Although in reality you would url-encode the C<params> argument, so it would
|
|
look more like this:
|
|
|
|
jsonrpc.cgi?method=User.get¶ms=%5B+%7B+%22ids%22%3A+%5B1%2C2%5D%2C+%22names%22%3A+%5B%22user%40domain.com%22%5D+%7D+%5D
|
|
|
|
You can also specify C<version> as a URL parameter, if you want to specify
|
|
what version of the JSON-RPC protocol you're using, and C<id> as a URL
|
|
parameter if you want there to be a specific C<id> value in the returned
|
|
JSON-RPC response.
|
|
|
|
=head2 JSONP
|
|
|
|
When calling the JSON-RPC WebService over GET, you can use the "JSONP"
|
|
method of doing cross-domain requests, if you want to access the WebService
|
|
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 JSON-RPC WebService, simply specify a
|
|
C<callback> parameter to jsonrpc.cgi when using it via GET as described above.
|
|
For example, here's some HTML you could use to get the data from
|
|
C<Bugzilla.time> on a remote website, using JSONP:
|
|
|
|
<script type="text/javascript"
|
|
src="http://bugzilla.example.com/jsonrpc.cgi?method=Bugzilla.time&callback=foo">
|
|
|
|
That would call the C<Bugzilla.time> method and pass its value to a function
|
|
called C<foo> as the only argument. All the other URL parameters (such as
|
|
C<params>, for passing in arguments to methods) that can be passed to
|
|
C<jsonrpc.cgi> 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 JSON-RPC response,
|
|
not JSONP.)
|
|
|
|
=head1 PARAMETERS
|
|
|
|
For JSON-RPC 1.0, the very first parameter should be an object containing
|
|
the named parameters. For example, if you were passing two named parameters,
|
|
one called C<foo> and the other called C<bar>, the C<params> element of
|
|
your JSON-RPC call would look like:
|
|
|
|
"params": [{ "foo": 1, "bar": "something" }]
|
|
|
|
For JSON-RPC 1.1, you can pass parameters either in the above fashion
|
|
or using the standard named-parameters mechanism of JSON-RPC 1.1.
|
|
|
|
C<dateTime> fields are strings in the standard ISO-8601 format:
|
|
C<YYYY-MM-DDTHH:MM:SSZ>, where C<T> and C<Z> are a literal T and Z,
|
|
respectively. The "Z" means that all times are in UTC timezone--times are
|
|
always returned in UTC, and should be passed in as UTC. (Note: The JSON-RPC
|
|
interface currently also accepts non-UTC times for any values passed in, if
|
|
they include a time-zone specifier that follows the ISO-8601 standard, instead
|
|
of "Z" at the end. This behavior is expected to continue into the future, but
|
|
to be fully safe for forward-compatibility with all future versions of
|
|
Bugzilla, it is safest to pass in all times as UTC with the "Z" timezone
|
|
specifier.)
|
|
|
|
C<base64> fields are strings that have been base64 encoded. Note that
|
|
although normal base64 encoding includes newlines to break up the data,
|
|
newlines within a string are not valid JSON, so you should not insert
|
|
newlines into your base64-encoded string.
|
|
|
|
All other types are standard JSON types.
|
|
|
|
=head1 ERRORS
|
|
|
|
JSON-RPC 1.0 and JSON-RPC 1.1 both return an C<error> element when they
|
|
throw an error. In Bugzilla, the error contents look like:
|
|
|
|
{ message: 'Some message here', code: 123 }
|
|
|
|
So, for example, in JSON-RPC 1.0, an error response would look like:
|
|
|
|
{
|
|
result: null,
|
|
error: { message: 'Some message here', code: 123 },
|
|
id: 1
|
|
}
|
|
|
|
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 SEE ALSO
|
|
|
|
L<Bugzilla::WebService>
|