summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormkanat%bugzilla.org <>2009-03-31 08:37:53 +0200
committermkanat%bugzilla.org <>2009-03-31 08:37:53 +0200
commita86ee3a1ca5213d5401c6425cfb0fcfc7bb15e78 (patch)
tree55ad97e67fa4322482f346289c113d211629338a
parent470f355df99acba2855b97619897d650e8dd09e0 (diff)
downloadbugzilla-a86ee3a1ca5213d5401c6425cfb0fcfc7bb15e78.tar.gz
bugzilla-a86ee3a1ca5213d5401c6425cfb0fcfc7bb15e78.tar.xz
Bug 432907: Create a JSON frontend for WebServices
Patch by Max Kanat-Alexander <mkanat@bugzilla.org> r=dkl, a=mkanat
-rw-r--r--Bugzilla.pm17
-rw-r--r--Bugzilla/CGI.pm4
-rw-r--r--Bugzilla/Constants.pm8
-rw-r--r--Bugzilla/Error.pm18
-rw-r--r--Bugzilla/Install/Requirements.pm6
-rw-r--r--Bugzilla/Object.pm7
-rwxr-xr-xBugzilla/WebService.pm141
-rwxr-xr-xBugzilla/WebService/Bug.pm5
-rwxr-xr-xBugzilla/WebService/Constants.pm1
-rw-r--r--Bugzilla/WebService/README18
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm266
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm53
-rw-r--r--extensions/example/lib/WSExample.pm2
-rw-r--r--jsonrpc.cgi38
-rw-r--r--template/en/default/global/code-error.html.tmpl7
-rw-r--r--template/en/default/global/user-error.html.tmpl4
-rwxr-xr-xxmlrpc.cgi2
17 files changed, 500 insertions, 97 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 324b3cc14..7bd40794a 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -360,6 +360,15 @@ sub error_mode {
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
}
+# This is used only by Bugzilla::Error to throw errors.
+sub _json_server {
+ my ($class, $newval) = @_;
+ if (defined $newval) {
+ $class->request_cache->{_json_server} = $newval;
+ }
+ return $class->request_cache->{_json_server};
+}
+
sub usage_mode {
my ($class, $newval) = @_;
if (defined $newval) {
@@ -369,9 +378,12 @@ sub usage_mode {
elsif ($newval == USAGE_MODE_CMDLINE) {
$class->error_mode(ERROR_MODE_DIE);
}
- elsif ($newval == USAGE_MODE_WEBSERVICE) {
+ elsif ($newval == USAGE_MODE_XMLRPC) {
$class->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
}
+ elsif ($newval == USAGE_MODE_JSON) {
+ $class->error_mode(ERROR_MODE_JSON_RPC);
+ }
elsif ($newval == USAGE_MODE_EMAIL) {
$class->error_mode(ERROR_MODE_DIE);
}
@@ -667,10 +679,11 @@ usage mode changes.
=item C<usage_mode>
Call either C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
-or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_WEBSERVICE)> near the
+or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_XMLRPC)> near the
beginning of your script to change this flag's default of
C<Bugzilla::Constants::USAGE_MODE_BROWSER> and to indicate that Bugzilla is
being called in a non-interactive manner.
+
This influences error handling because on usage mode changes, C<usage_mode>
calls C<Bugzilla->error_mode> to set an error mode which makes sense for the
usage mode.
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index d7934f89b..4af6a933d 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -347,10 +347,10 @@ sub require_https {
my ($self, $url) = @_;
# Do not create query string if data submitted via XMLRPC
# since we want the data to be resubmitted over POST method.
- my $query = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 0 : 1;
+ my $query = Bugzilla->usage_mode == USAGE_MODE_XMLRPC ? 0 : 1;
# XMLRPC clients (SOAP::Lite at least) requires 301 to redirect properly
# and do not work with 302.
- my $status = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 301 : 302;
+ my $status = Bugzilla->usage_mode == USAGE_MODE_XMLRPC ? 301 : 302;
if (defined $url) {
$url .= $self->url('-path_info' => 1, '-query' => $query, '-relative' => 1);
} else {
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 858464542..0150f3124 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -128,12 +128,14 @@ use File::Basename;
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
- USAGE_MODE_WEBSERVICE
+ USAGE_MODE_XMLRPC
USAGE_MODE_EMAIL
+ USAGE_MODE_JSON
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
+ ERROR_MODE_JSON_RPC
INSTALLATION_MODE_INTERACTIVE
INSTALLATION_MODE_NON_INTERACTIVE
@@ -378,14 +380,16 @@ use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
use constant USAGE_MODE_BROWSER => 0;
use constant USAGE_MODE_CMDLINE => 1;
-use constant USAGE_MODE_WEBSERVICE => 2;
+use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
+use constant USAGE_MODE_JSON => 4;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
use constant ERROR_MODE_WEBPAGE => 0;
use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
+use constant ERROR_MODE_JSON_RPC => 3;
# The various modes that checksetup.pl can run in.
use constant INSTALLATION_MODE_INTERACTIVE => 0;
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index d15336a81..661c72f74 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -101,7 +101,9 @@ sub _throw_error {
if (Bugzilla->error_mode == ERROR_MODE_DIE) {
die("$message\n");
}
- elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ {
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
require Bugzilla::Hook;
@@ -112,7 +114,19 @@ sub _throw_error {
$code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
$code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
}
- die SOAP::Fault->faultcode($code)->faultstring($message);
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die SOAP::Fault->faultcode($code)->faultstring($message);
+ }
+ else {
+ my $server = Bugzilla->_json_server;
+ # 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,
+ message => $message);
+ $server->response($server->error_response_header);
+ }
}
}
exit;
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 4f17ae053..dba588c8a 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -226,6 +226,12 @@ sub OPTIONAL_MODULES {
feature => 'XML-RPC Interface'
},
{
+ package => 'JSON-RPC',
+ module => 'JSON::RPC',
+ version => 0,
+ feature => 'JSON-RPC Interface'
+ },
+ {
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
package => 'HTML-Parser',
module => 'HTML::Parser',
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index adc96fa50..6cca49e45 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -219,7 +219,12 @@ sub _do_list_select {
$sql .= " $postamble" if $postamble;
my $dbh = Bugzilla->dbh;
- my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @$values);
+ # Sometimes the values are tainted, but we don't want to untaint them
+ # for the caller. So we copy the array. It's safe to untaint because
+ # they're only used in placeholders here.
+ my @untainted = @{ $values || [] };
+ trick_taint($_) foreach @untainted;
+ my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
bless ($_, $class) foreach @$objects;
return $objects
}
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 735291fc3..ffaf7c4b7 100755
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -22,17 +22,8 @@ use strict;
use Date::Parse;
use XMLRPC::Lite;
-sub datetime_format {
- my ($self, $date_string) = @_;
-
- my $time = str2time($date_string);
- my ($sec, $min, $hour, $mday, $mon, $year) = localtime $time;
- # This format string was stolen from SOAP::Utils->format_datetime,
- # which doesn't work but which has almost the right format string.
- my $iso_datetime = sprintf('%d%02d%02dT%02d:%02d:%02d',
- $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
- return $iso_datetime;
-}
+# Used by the JSON-RPC server to convert incoming date fields apprpriately.
+use constant DATE_FIELDS => {};
# For some methods, we shouldn't call Bugzilla->login before we call them
use constant LOGIN_EXEMPT => { };
@@ -63,85 +54,84 @@ Bugzilla::WebService - The Web Service interface to Bugzilla
This is the standard API for external programs that want to interact
with Bugzilla. It provides various methods in various modules.
-Currently the only method of accessing the API is via XML-RPC. The XML-RPC
-standard is described here: L<http://www.xmlrpc.com/spec>
-
-The endpoint for Bugzilla WebServices is the C<xmlrpc.cgi> script in
-your Bugzilla installation. For example, if your Bugzilla is at
-C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
-API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>
+You can interact with this API via
+L<XML-RPC|Bugzilla::WebService::Server::XMLRPC> or
+L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC>.
=head1 CALLING METHODS
-Methods are called in the normal XML-RPC fashion. Bugzilla does not currently
-implement any extensions to the standard method of XML-RPC method calling.
-
Methods are grouped into "packages", like C<Bug> for
L<Bugzilla::WebService::Bug>. So, for example,
-L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get> in XML-RPC.
+L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get>.
=head1 PARAMETERS
-In addition to the standard parameter types like C<int>, C<string>, etc.,
-XML-RPC has two data structures, a C<< <struct> >> and an C<< <array> >>.
+The Bugzilla API takes the following various types of parameters:
+
+=over
-=head2 Structs
+=item C<int>
-In Perl, we call a C<< <struct> >> a "hash" or a "hashref". You may see
-us refer to it that way in the API documentation.
+Integer. May be null.
-In example code, you will see the characters C<{> and C<}> used to represent
-the beginning and end of structs.
+=item C<double>
-For example, here's a struct in XML-RPC:
+A floating-point number. May be null.
- <struct>
- <member>
- <name>fruit</name>
- <value><string>oranges</string></value>
- </member>
- <member>
- <name>vegetable</name>
- <value><string>lettuce</string></value>
- </member>
- </struct>
+=item C<string>
-In our example code in these API docs, that would look like:
+A string. May be null.
- { fruit => 'oranges', vegetable => 'lettuce' }
+=item C<dateTime>
-=head2 Arrays
+A date/time. Represented differently in different interfaces to this API.
+May be null.
+
+=item C<boolean>
+
+True or false.
+
+=item C<array>
+
+An array. There may be mixed types in an array.
In example code, you will see the characters C<[> and C<]> used to
represent the beginning and end of arrays.
-For example, here's an array in XML-RPC:
+In our example code in these API docs, an array that contains the numbers
+1, 2, and 3 would look like:
- <array>
- <data>
- <value><i4>1</i4></value>
- <value><i4>2</i4></value>
- <value><i4>3</i4></value>
- </data>
- </array>
+ [1, 2, 3]
-In our example code in these API docs, that would look like:
+=item C<struct>
- [1, 2, 3]
+A mapping of keys to values. Called a "hash", "dict", or "map" in some
+other programming languages. We sometimes call this a "hash" in the API
+documentation.
+
+The keys are strings, and the values can be any type.
+
+In example code, you will see the characters C<{> and C<}> used to represent
+the beginning and end of structs.
+
+For example, a struct with an "fruit" key whose value is "oranges",
+and a "vegetable" key whose value is "lettuce" would look like:
+
+ { fruit => 'oranges', vegetable => 'lettuce' }
+
+=back
=head2 How Bugzilla WebService Methods Take Parameters
-B<All> Bugzilla WebServices functions take their parameters in
-a C<< <struct> >>. Another way of saying this would be: All functions
-take a single argument, a C<< <struct> >> that contains all parameters.
-The names of the parameters listed in the API docs for each function are
-the C<name> element for the struct C<member>s.
+B<All> Bugzilla WebService functions use I<named> parameters.
+The individual C<Bugzilla::WebService::Server> modules explain
+how this is implemented for those frontends.
=head1 LOGGING IN
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
user. This issues standard HTTP cookies that you must then use in future
-calls, so your XML-RPC client must be capable of receiving and transmitting
+calls, so your client must be capable of receiving and transmitting
cookies.
=head1 STABLE, EXPERIMENTAL, and UNSTABLE
@@ -164,18 +154,17 @@ Bugzilla versions.
=head1 ERRORS
-If a particular webservice call fails, it will throw a standard XML-RPC
-error. There will be a numeric error code, and then the description
-field will contain descriptive text of the error. Each error that Bugzilla
-can throw has a specific code that will not change between versions of
-Bugzilla.
+If a particular webservice call fails, it will throw an error in the
+appropriate format for the frontend that you are using. For all frontends,
+there is 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.
-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.
+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.
@@ -259,21 +248,3 @@ would return something like:
{ users => [{ id => 1, real_name => 'John Smith' }] }
=back
-
-
-=head1 EXTENSIONS TO THE XML-RPC STANDARD
-
-=head2 Undefined Values
-
-Normally, XML-RPC does not allow empty values for C<int>, C<double>, or
-C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as
-C<undef> (called C<NULL> or C<None> in some programming languages).
-
-Bugzilla also accepts a type called C<< <nil> >>, which is always considered
-to be C<undef>, no matter what it contains.
-
-=begin private
-
-nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value.
-
-=end private
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index f76d800fa..8ebad41d1 100755
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -56,6 +56,11 @@ use constant FIELD_MAP => {
use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
+use constant DATE_FIELDS => {
+ comments => ['new_since'],
+ search => ['last_change_time', 'creation_time'],
+};
+
######################################################
# Add aliases here for old method name compatibility #
######################################################
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 172d757ef..83b4197aa 100755
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -133,5 +133,4 @@ sub WS_DISPATCH {
return $dispatch;
};
-
1;
diff --git a/Bugzilla/WebService/README b/Bugzilla/WebService/README
new file mode 100644
index 000000000..bbe320979
--- /dev/null
+++ b/Bugzilla/WebService/README
@@ -0,0 +1,18 @@
+The class structure of these files is a little strange, and this README
+explains it.
+
+Our goal is to make JSON::RPC and XMLRPC::Lite both work with the same code.
+(That is, we want to have one WebService API, and have two frontends for it.)
+
+The problem is that these both pass different things for $self to WebService
+methods.
+
+When XMLRPC::Lite calls a method, $self is the name of the *class* the
+method is in. For example, if we call Bugzilla.version(), the first argument
+is Bugzilla::WebService::Bugzilla. So in order to have $self
+(our first argument) act correctly in XML-RPC, we make all WebService
+classes use base qw(Bugzilla::WebService).
+
+When JSON::RPC calls a method, $self is the JSON-RPC *server object*. In other
+words, it's an instance of Bugzilla::WebService::Server::JSONRPC. So we have
+Bugzilla::WebService::Server::JSONRPC inherit from Bugzilla::WebService.
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
new file mode 100644
index 000000000..b453c6196
--- /dev/null
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -0,0 +1,266 @@
+# -*- 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::Server::CGI Bugzilla::WebService::Server);
+
+use Bugzilla::Error;
+use Bugzilla::WebService::Constants;
+use Date::Parse;
+use DateTime;
+
+sub new {
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+ Bugzilla->_json_server($self);
+ $self->dispatch(WS_DISPATCH);
+ $self->return_die_message(1);
+ $self->json->allow_blessed(1);
+ $self->json->convert_blessed(1);
+ # Default to JSON-RPC 1.0
+ $self->version(0);
+ return $self;
+}
+
+# Override the JSON::RPC method to return our CGI object instead of theirs.
+sub cgi { return Bugzilla->cgi; }
+
+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') {
+ # str2time uses Time::Local internally, so I believe it should
+ # always return seconds based on the *Unix* epoch, even if the
+ # system doesn't use the Unix epoch.
+ $retval = str2time($value) * 1000;
+ }
+ # XXX Will have to implement base64 if Bugzilla starts using it.
+
+ return $retval;
+}
+
+##################
+# Login Handling #
+##################
+
+# This handles dispatching our calls to the appropriate class based on
+# the name of the method.
+sub _find_procedure {
+ my $self = shift;
+
+ # This is also a good place to deny GET requests, since we can
+ # safely call ThrowUserError at this point.
+ if ($self->request->method ne 'POST') {
+ ThrowUserError('json_rpc_post_only');
+ }
+
+ 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(@_);
+
+ # This is the best time to do login checks.
+ $self->handle_login();
+
+ # If there are no parameters, we don't need to parse them.
+ return $params if !ref $params;
+
+ # 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.
+ if (ref $params eq 'ARRAY') {
+ $params = $params->[0];
+ }
+
+ # 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->_bz_convert_datetime($_) } @$value ];
+ }
+ else {
+ $params->{$field} = $self->_bz_convert_datetime($value);
+ }
+ }
+ }
+
+ # 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;
+
+ return $params;
+}
+
+sub _bz_convert_datetime {
+ my ($self, $time) = @_;
+ my $dt = DateTime->from_epoch(epoch => ($time / 1000),
+ time_zone => Bugzilla->local_timezone);
+ return $dt->strftime('%Y-%m-%d %T');
+}
+
+sub handle_login {
+ my $self = shift;
+
+ 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);
+}
+
+# _bz_method_name is stored by _find_procedure for later use.
+sub _bz_method_name {
+ return $_[0]->{_bz_method_name};
+}
+
+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<UNSTABLE>. If you want a stable API, please use the
+C<XML-RPC|Bugzilla::WebService::Server::XMLRPC> 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>
+
+Bugzilla only allows JSON-RPC requests over C<POST>. C<GET> requests
+(or any other type of request, such as C<HEAD>) will be denied.
+
+=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 represented as an integer number of microseconds
+since the Unix epoch (January 1, 1970 UTC). They are always in the UTC
+timezone.
+
+All other types are standard JSON types.
+
+=head1 ERRORS
+
+All errors thrown by Bugzilla itself have 100000 added to their numeric
+code. So, if the documentation says that an error is C<302>, then
+it will be C<100302> when it is thrown via JSON-RPC.
+
+Errors less than 100000 are errors thrown by the JSON-RPC library that
+Bugzilla uses, not by Bugzilla.
+
+=head1 SEE ALSO
+
+=head2 Server Types
+
+=over
+
+=item L<Bugzilla::WebService::Server::XMLRPC>
+
+=item L<Bugzilla::WebService::Server::JSONRPC>
+
+=back
+
+=head2 WebService Methods
+
+=over
+
+=item L<Bugzilla::WebService::Bug>
+
+=item L<Bugzilla::WebService::Bugzilla>
+
+=item L<Bugzilla::WebService::Product>
+
+=item L<Bugzilla::WebService::User>
+
+=back
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 36b4e01fd..5c92532a9 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -168,3 +168,56 @@ sub as_string {
}
1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::WebService::Server::XMLRPC - The XML-RPC Interface to Bugzilla
+
+=head1 DESCRIPTION
+
+This documentation describes things about the Bugzilla WebService that
+are specific to XML-RPC. For a general overview of the Bugzilla WebServices,
+see L<Bugzilla::WebService>.
+
+=head1 XML-RPC
+
+The XML-RPC standard is described here: L<http://www.xmlrpc.com/spec>
+
+=head1 CONNECTING
+
+The endpoint for the XML-RPC interface is the C<xmlrpc.cgi> script in
+your Bugzilla installation. For example, if your Bugzilla is at
+C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
+API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>
+
+=head1 PARAMETERS
+
+C<dateTime> fields are the standard C<dateTime.iso8601> XML-RPC field. They
+should be in C<YYYY-MM-DDTHH:MM:SS> format (where C<T> is a literal T).
+
+All other fields are standard XML-RPC types.
+
+=head2 How XML-RPC WebService Methods Take Parameters
+
+All functions take a single argument, a C<< <struct> >> that contains all parameters.
+The names of the parameters listed in the API docs for each function are the
+C<< <name> >> element for the struct C<< <member> >>s.
+
+=head1 EXTENSIONS TO THE XML-RPC STANDARD
+
+=head2 Undefined Values
+
+Normally, XML-RPC does not allow empty values for C<int>, C<double>, or
+C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as
+C<undef> (called C<NULL> or C<None> in some programming languages).
+
+Bugzilla also accepts a type called C<< <nil> >>, which is always considered
+to be C<undef>, no matter what it contains.
+
+=begin private
+
+nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value.
+
+=end private \ No newline at end of file
diff --git a/extensions/example/lib/WSExample.pm b/extensions/example/lib/WSExample.pm
index 146867294..754d49f70 100644
--- a/extensions/example/lib/WSExample.pm
+++ b/extensions/example/lib/WSExample.pm
@@ -24,7 +24,7 @@ use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Error;
-# This can be called as Example.hello() from XML-RPC.
+# This can be called as Example.hello() from the WebService.
sub hello { return 'Hello!'; }
sub throw_an_error { ThrowUserError('example_my_error') }
diff --git a/jsonrpc.cgi b/jsonrpc.cgi
new file mode 100644
index 000000000..c04f20f6a
--- /dev/null
+++ b/jsonrpc.cgi
@@ -0,0 +1,38 @@
+#!/usr/bin/perl -wT
+# -*- 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>
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::WebService::Constants;
+# This eval allows runtests to pass even if JSON::RPC isn't
+# installed.
+eval { require Bugzilla::WebService::Server::JSONRPC; };
+$@ && ThrowCodeError('json_rpc_not_installed');
+
+Bugzilla->usage_mode(USAGE_MODE_JSON);
+
+local @INC = (bz_locations()->{extensionsdir}, @INC);
+my $server = new Bugzilla::WebService::Server::JSONRPC;
+$server->dispatch(WS_DISPATCH)->handle();
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index da8f902d4..9d182d25f 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -292,6 +292,13 @@
the job "[% job FILTER html %]". You need to add this job type
to the <code>JOB_MAP</code> constant in <code>Bugzilla::JobQueue</code>.
+ [% ELSIF error == "json_rpc_not_installed" %]
+ [% admindocslinks = { 'installation.html#install-perlmodules'
+ => 'Installing Perl modules' } %]
+ The JSON-RPC interface will not work without the <code>JSON::RPC</code>
+ Perl module being installed. Run <code>checksetup.pl</code> for
+ installation instructions.
+
[% ELSIF error == "ldap_bind_failed" %]
Failed to bind to the LDAP server. The error message was:
<code>[% errstr FILTER html %]</code>
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 58be9ea73..338088178 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -949,6 +949,10 @@
[% title = "Invalid Username Or Password" %]
The username or password you entered is not valid.
+ [% ELSIF error == "json_rpc_post_only" %]
+ For security reasons, you may only use JSON-RPC with the POST
+ HTTP method.
+
[% ELSIF error == "keyword_already_exists" %]
[% title = "Keyword Already Exists" %]
A keyword with the name [% name FILTER html %] already exists.
diff --git a/xmlrpc.cgi b/xmlrpc.cgi
index 10c245ef6..c98dd1b73 100755
--- a/xmlrpc.cgi
+++ b/xmlrpc.cgi
@@ -28,7 +28,7 @@ use Bugzilla::WebService::Constants;
eval { require Bugzilla::WebService::Server::XMLRPC; };
$@ && ThrowCodeError('soap_not_installed');
-Bugzilla->usage_mode(USAGE_MODE_WEBSERVICE);
+Bugzilla->usage_mode(USAGE_MODE_XMLRPC);
# Fix the error code that SOAP::Lite uses for Perl errors.
local $SOAP::Constants::FAULT_SERVER;