diff options
Diffstat (limited to 'Bugzilla/WebService/Server')
-rw-r--r-- | Bugzilla/WebService/Server/JSONRPC.pm | 39 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST.pm | 659 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bug.pm | 190 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm | 52 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm | 69 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Classification.pm | 49 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Group.pm | 56 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Product.pm | 82 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/User.pm | 80 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/XMLRPC.pm | 34 |
10 files changed, 1301 insertions, 9 deletions
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 373aa4fe0..0df4240e0 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -37,8 +37,8 @@ BEGIN { use Bugzilla::Error; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(taint_data); -use Bugzilla::Util qw(correct_urlbase trim disable_utf8); +use Bugzilla::WebService::Util qw(taint_data fix_credentials); +use Bugzilla::Util; use HTTP::Message; use MIME::Base64 qw(decode_base64 encode_base64); @@ -87,6 +87,7 @@ sub response_header { sub response { my ($self, $response) = @_; + my $cgi = $self->cgi; # Implement JSONP. if (my $callback = $self->_bz_callback) { @@ -108,9 +109,18 @@ sub response { push(@header_args, "-$name", $value); } } - my $cgi = $self->cgi; - print $cgi->header(-status => $response->code, @header_args); - print $response->content; + + # ETag support + my $etag = $self->bz_etag; + if ($etag && $cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $cgi->header(-status => $response->code, @header_args); + print $response->content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -222,6 +232,9 @@ sub type { utf8::encode($value) if utf8::is_utf8($value); $retval = encode_base64($value, ''); } + elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { + $retval = email_filter($value); + } return $retval; } @@ -267,7 +280,17 @@ sub _handle { my $self = shift; my ($obj) = @_; $self->{_bz_request_id} = $obj->{id}; - return $self->SUPER::_handle(@_); + + my $result = $self->SUPER::_handle(@_); + + # Set the ETag if not already set in the webservice methods. + my $etag = $self->bz_etag; + if (!$etag && ref $result) { + my $data = $self->json->decode($result)->{'result'}; + $self->bz_etag($data); + } + + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 @@ -364,6 +387,10 @@ sub _argument_type_check { } } + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + Bugzilla->input_params($params); if ($self->request->method eq 'POST') { diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm new file mode 100644 index 000000000..69f97ef08 --- /dev/null +++ b/Bugzilla/WebService/Server/REST.pm @@ -0,0 +1,659 @@ +# 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 parent qw(Bugzilla::WebService::Server::JSONRPC); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Hook; +use Bugzilla::Util qw(correct_urlbase html_quote disable_utf8 enable_utf8); +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::Group; +use Bugzilla::WebService::Server::REST::Resources::Product; +use Bugzilla::WebService::Server::REST::Resources::User; +use Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit; + +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); + + # Fix includes/excludes for each call + rest_include_exclude($params); + + # Set callback name if content-type is 'application/javascript' + if ($params->{'callback'} + || $self->content_type eq 'application/javascript') + { + $self->_bz_callback($params->{'callback'} || '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) { + # Content is in bytes at this point and needs to be converted + # back to utf8 string. + enable_utf8(); + utf8::decode($content) if !utf8::is_utf8($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 + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", "origin, content-type, accept"); + + # 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); + } + + utf8::encode($content) if utf8::is_utf8($content); + disable_utf8(); + + $response->content($content); + + $self->SUPER::response($response); +} + +####################################### +# Bugzilla::WebService Implementation # +####################################### + +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.) We do allow this for GET /login as we need to + # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we + # can also use for Bugzilla_token support. This is OK as it requires + # a login and password to be supplied and will fail if they are not + # valid for the user. + if (!grep($_ eq $self->request->method, ('POST', 'PUT')) + && !($self->bz_class_name eq 'Bugzilla::WebService::User' + && $self->bz_method_name eq 'login')) + { + # 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 $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 [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ]; +} + +sub rest_include_exclude { + my ($params) = @_; + + if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; + } + if (exists $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 params we were able to pull out of the path + # based on the resource regexp + %{$params} = (%{$params}, %{$self->bz_rest_params}) if $self->bz_rest_params; + + # Merge any additional query key/values with $obj->{params} if not a GET request + # We do this manually cause CGI.pm doesn't understand JSON strings. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; + if ($json) { + eval { $extra_params = $self->json->utf8(0)->decode($json); }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); + } + } + + # Allow parameters in the query string if request was not GET. + # Note: query string parameters will override any matching params + # also specified 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. If using Apache and mod_rewrite is installed +and enabled, you can also use /rest/ as your endpoint. For example, if your +Bugzilla is at C<bugzilla.yourdomain.com>, then your REST client would +access the API via: C<http://bugzilla.yourdomain.com/rest/bug/35> which +looks cleaner. + +=head1 BROWSING + +If the Accept: header of a request is set to text/html (as it is by an +ordinary web browser) then the API will return the JSON data as a HTML +page which the browser can display. In other words, you can play with the +API using just your browser and see results in a human-readable form. +This is a good way to try out the various GET calls, even if you can't use +it for POST or PUT. + +=head1 DATA FORMAT + +The REST API only supports JSON input, and either JSON and JSONP output. +So objects sent and received must be in JSON format. Basically since +the REST API is a sub class of the JSONRPC API, you can refer to +L<JSONRPC|Bugzilla::WebService::Server::JSONRPC> for more information +on data types that are valid for REST. + +On every request, you must set both the "Accept" and "Content-Type" HTTP +headers to the MIME type of the data format you are using to communicate with +the API. Content-Type tells the API how to interpret your request, and Accept +tells it how you want your data back. "Content-Type" must be "application/json". +"Accept" can be either that, or "application/javascript" for JSONP - add a "callback" +parameter to name your callback. + +Parameters may also be passed in as part of the query string for non-GET requests +and will override any matching parameters in the request body. + +=head1 AUTHENTICATION + +Along with viewing data as an anonymous user, you may also see private information +if you have a Bugzilla account by providing your login credentials. + +=over + +=item Login name and password + +Pass in as query parameters of any request: + +login=fred@example.com&password=ilovecheese + +Remember to URL encode any special characters, which are often seen in passwords and to +also enable SSL support. + +=item Login token + +By calling GET /login?login=fred@example.com&password=ilovecheese, you get back +a C<token> value which can then be passed to each subsequent call as +authentication. This is useful for third party clients that cannot use cookies +and do not want to store a user's login and password in the client. You can also +pass in "token" as a convenience. + +=back + +=head1 ERRORS + +When an error occurs over REST, a hash structure is returned with the key C<error> +set to C<true>. + +The error contents look similar to: + + { "error": true, "message": "Some message here", "code": 123 } + +Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>. +Errors with a numeric C<code> higher than 100000 are errors thrown by +the JSON-RPC library that Bugzilla uses, not by Bugzilla. + +=head1 UTILITY FUNCTIONS + +=over + +=item B<handle> + +This method overrides the handle method provided by JSONRPC so that certain +actions related to REST such as determining the proper resource to use, +loading query parameters, etc. can be done before the proper WebService +method is executed. + +=item B<response> + +This method overrides the response method provided by JSONRPC so that +the response content can be altered for REST before being returned to +the client. + +=item B<handle_login> + +This method determines the proper WebService all to make based on class +and method name determined earlier. Then calls L<Bugzilla::WebService::Server::handle_login> +which will attempt to authenticate the client. + +=item B<bz_method_name> + +The WebService method name that matches the path used by the client. + +=item B<bz_class_name> + +The WebService class containing the method that matches the path used by the client. + +=item B<bz_rest_params> + +Each REST resource contains a hash key called C<params> that is a subroutine reference. +This subroutine will return a hash structure based on matched values from the path +information that is formatted properly for the WebService method that will be called. + +=item B<bz_rest_options> + +When a client uses the OPTIONS request method along with a specific path, they are +requesting the list of request methods that are valid for the path. Such as for the +path /bug, the valid request methods are GET (search) and POST (create). So the +client would receive in the response header, C<Access-Control-Allow-Methods: GET, POST>. + +=item B<bz_success_code> + +Each resource can specify a specific SUCCESS CODE if the operation completes successfully. +OTherwise STATUS OK (200) is the default returned. + +=item B<rest_include_exclude> + +Normally the WebService methods required C<include_fields> and C<exclude_fields> to be an +array of field names. REST allows for the values for these to be instead comma delimited +string of field names. This method converts the latter into the former so the WebService +methods will not complain. + +=back + +=head1 SEE ALSO + +L<Bugzilla::WebService> diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm new file mode 100644 index 000000000..d0f470fcd --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -0,0 +1,190 @@ +# 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 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', + 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] }; + } + } + }, + qr{^/flag_types/([^/]+)/([^/]+)$}, { + GET => { + method => 'flag_types', + params => sub { + return { product => $_[0], + component => $_[1] }; + } + } + }, + qr{^/flag_types/([^/]+)$}, { + GET => { + method => 'flag_types', + params => sub { + return { product => $_[0] }; + } + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Bug - The REST API for creating, +changing, and getting the details of bugs. + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to file a new bug in Bugzilla, +or get information about bugs that have already been filed. + +See L<Bugzilla::WebService::Bug> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm new file mode 100644 index 000000000..a434d4bef --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit; + +use 5.10.1; +use strict; +use warnings; + +BEGIN { + *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; +} + +sub _rest_resources { + return [ + # bug-id + qr{^/bug_user_last_visit/(\d+)$}, { + GET => { + method => 'get', + params => sub { + return { ids => $_[0] }; + }, + }, + POST => { + method => 'update', + params => sub { + return { ids => $_[0] }; + }, + }, + }, + ]; +} + +1; +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::BugUserLastVisit - The +BugUserLastVisit REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to lookup and update the last time +a user visited a bug. + +See L<Bugzilla::WebService::BugUserLastVisit> for more details on how to use +this part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm new file mode 100644 index 000000000..1c86f77bc --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -0,0 +1,69 @@ +# 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 Bugzilla::WebService::Constants; +use Bugzilla::WebService::Bugzilla; + +BEGIN { + *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/version$}, { + GET => { + method => 'version' + } + }, + qr{^/extensions$}, { + GET => { + method => 'extensions' + } + }, + qr{^/timezone$}, { + GET => { + method => 'timezone' + } + }, + qr{^/time$}, { + GET => { + method => 'time' + } + }, + qr{^/last_audit_time$}, { + GET => { + method => 'last_audit_time' + } + }, + qr{^/parameters$}, { + GET => { + method => 'parameters' + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::WebService::Bugzilla - Global functions for the webservice interface. + +=head1 DESCRIPTION + +This provides functions that tell you about Bugzilla in general. + +See L<Bugzilla::WebService::Bugzilla> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm new file mode 100644 index 000000000..5bb697ac1 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -0,0 +1,49 @@ +# 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 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::Bug> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm new file mode 100644 index 000000000..9200d609d --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -0,0 +1,56 @@ +# 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 Bugzilla::WebService::Constants; +use Bugzilla::WebService::Group; + +BEGIN { + *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/group$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/group/([^/]+)$}, { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Group - The REST API for +creating, changing, and getting information about Groups. + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to create Groups and +get information about them. + +See L<Bugzilla::WebService::Group> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm new file mode 100644 index 000000000..acee3887b --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -0,0 +1,82 @@ +# 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 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::Bug> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm new file mode 100644 index 000000000..badbc94b2 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -0,0 +1,80 @@ +# 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 Bugzilla::WebService::Constants; +use Bugzilla::WebService::User; + +BEGIN { + *Bugzilla::WebService::User::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/valid_login$}, { + GET => { + method => 'valid_login' + } + }, + qr{^/login$}, { + GET => { + method => 'login' + } + }, + qr{^/logout$}, { + GET => { + method => 'logout' + } + }, + 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::Bug> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index fc297421a..8d9108122 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -30,9 +30,10 @@ if ($ENV{MOD_PERL}) { } use Bugzilla::WebService::Constants; +use Bugzilla::Util; -# Allow WebService methods to call XMLRPC::Lite's type method directly BEGIN { + # Allow WebService methods to call XMLRPC::Lite's type method directly *Bugzilla::WebService::type = sub { my ($self, $type, $value) = @_; if ($type eq 'dateTime') { @@ -41,8 +42,19 @@ BEGIN { $value = Bugzilla::WebService::Server->datetime_format_outbound($value); $value =~ s/-//g; } + elsif ($type eq 'email') { + $type = 'string'; + if (Bugzilla->params->{'webservice_email_filter'}) { + $value = email_filter($value); + } + } return XMLRPC::Data->type($type)->value($value); }; + + # Add support for ETags into XMLRPC WebServices + *Bugzilla::WebService::bz_etag = sub { + return Bugzilla::WebService::Server->bz_etag($_[1]); + }; } sub initialize { @@ -56,22 +68,38 @@ sub initialize { sub make_response { my $self = shift; + my $cgi = Bugzilla->cgi; $self->SUPER::make_response(@_); # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around # its cookies in Bugzilla::CGI, so we need to copy them over. - foreach my $cookie (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) { + foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { $self->response->headers->push_header('Set-Cookie', $cookie); } # Copy across security related headers from Bugzilla::CGI - foreach my $header (split(/[\r\n]+/, Bugzilla->cgi->header)) { + foreach my $header (split(/[\r\n]+/, $cgi->header)) { my ($name, $value) = $header =~ /^([^:]+): (.*)/; if (!$self->response->headers->header($name)) { $self->response->headers->header($name => $value); } } + + # ETag support + my $etag = $self->bz_etag; + if (!$etag) { + my $data = $self->response->as_string; + $etag = $self->bz_etag($data); + } + + if ($etag && $cgi->check_etag($etag)) { + $self->response->headers->push_header('ETag', $etag); + $self->response->headers->push_header('status', '304 Not Modified'); + } + elsif ($etag) { + $self->response->headers->push_header('ETag', $etag); + } } sub handle_login { |