diff options
Diffstat (limited to 'Bugzilla/WebService/Server')
-rw-r--r-- | Bugzilla/WebService/Server/JSONRPC.pm | 696 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST.pm | 778 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bug.pm | 274 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm | 35 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm | 46 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Classification.pm | 27 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Component.pm | 18 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/FlagType.pm | 67 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Group.pm | 41 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Product.pm | 78 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/User.pm | 76 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/XMLRPC.pm | 491 |
12 files changed, 1304 insertions, 1323 deletions
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 70b8fd96c..66640beb7 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -12,16 +12,17 @@ use strict; use warnings; use Bugzilla::WebService::Server; -BEGIN { - our @ISA = qw(Bugzilla::WebService::Server); - if (eval { require JSON::RPC::Server::CGI }) { - unshift(@ISA, 'JSON::RPC::Server::CGI'); - } - else { - require JSON::RPC::Legacy::Server::CGI; - unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); - } +BEGIN { + our @ISA = qw(Bugzilla::WebService::Server); + + if (eval { require JSON::RPC::Server::CGI }) { + unshift(@ISA, 'JSON::RPC::Server::CGI'); + } + else { + require JSON::RPC::Legacy::Server::CGI; + unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); + } } use Bugzilla::Error; @@ -38,79 +39,83 @@ use List::MoreUtils qw(none); ##################################### 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; + 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); - # 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; + my $self = shift; + my $json = $self->SUPER::create_json_coder(@_); + $json->allow_blessed(1); + $json->convert_blessed(1); + + # 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_header { - my $self = shift; - # 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($_[1])) { - utf8::encode($_[1]); - # Since we're going to just be sending raw bytes, we need to - # set STDOUT to not expect utf8. - disable_utf8(); - } - return $self->SUPER::response_header(@_); + my $self = shift; + + # 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($_[1])) { + utf8::encode($_[1]); + + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + return $self->SUPER::response_header(@_); } sub response { - my ($self, $response) = @_; - my $cgi = $self->cgi; - - # Implement JSONP. - if (my $callback = $self->_bz_callback) { - my $content = $response->content; - # Prepend the JSONP response with /**/ in order to protect - # against possible encoding attacks (e.g., affecting Flash). - $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); - } - } - - # 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; - } + my ($self, $response) = @_; + my $cgi = $self->cgi; + + # Implement JSONP. + if (my $callback = $self->_bz_callback) { + my $content = $response->content; + + # Prepend the JSONP response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $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); + } + } + + # 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 @@ -122,70 +127,69 @@ sub response { # Base64 encoded, because that is ridiculous and obnoxious for JavaScript # clients. sub retrieve_json_from_get { - my $self = shift; - my $cgi = $self->cgi; - - my %input; - - # Both version and id must be set before any errors are thrown. - if ($cgi->param('version')) { - $self->version(scalar $cgi->param('version')); - $input{version} = $cgi->param('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 $cgi->param('id')) { - $id = $cgi->param('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(scalar $cgi->param('callback')); - - if (!$cgi->param('method')) { - ThrowUserError('json_rpc_get_method_required'); - } - $input{method} = $cgi->param('method'); - - my $params; - if (defined $cgi->param('params')) { - local $@; - $params = eval { - $self->json->decode(scalar $cgi->param('params')) - }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', - { params => scalar $cgi->param('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; + my $self = shift; + my $cgi = $self->cgi; + + my %input; + + # Both version and id must be set before any errors are thrown. + if ($cgi->param('version')) { + $self->version(scalar $cgi->param('version')); + $input{version} = $cgi->param('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 $cgi->param('id')) { + $id = $cgi->param('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(scalar $cgi->param('callback')); + + if (!$cgi->param('method')) { + ThrowUserError('json_rpc_get_method_required'); + } + $input{method} = $cgi->param('method'); + + my $params; + if (defined $cgi->param('params')) { + local $@; + $params = eval { $self->json->decode(scalar $cgi->param('params')) }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', + {params => scalar $cgi->param('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; } ####################################### @@ -193,72 +197,76 @@ sub retrieve_json_from_get { ####################################### 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, ''); - } - elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { - $retval = email_filter($value); - } - - return $retval; + 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, ''); + } + elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { + $retval = email_filter($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'; + 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); + 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); } ###################################### @@ -267,165 +275,165 @@ sub handle_login { # 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}; + my $self = shift; + my ($obj) = @_; + $self->{_bz_request_id} = $obj->{id}; - my $result = $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); - } + # 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; + return $result; } # 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; + 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 $self = shift; - my $method = shift; - $self->{_bz_method_name} = $method; + 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); + # This tricks SUPER::_find_procedure into finding the right class. + $method =~ /^(\S+)\.(\S+)$/; + $self->path_info($1); + unshift(@_, $2); - return $self->SUPER::_find_procedure(@_); + 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}); - } - } - - # 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') { - # 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, $pkg->READ_ONLY)) { - ThrowUserError('json_rpc_post_only', - { method => $self->_bz_method_name }); - } - } - - # Only allowed methods to be used from our whitelist - if (none { $_ eq $method} $pkg->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { 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; + 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}); + } + } + + # 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') { + + # 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, $pkg->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name}); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method } $pkg->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {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; } ########################## @@ -434,22 +442,24 @@ sub _argument_type_check { # _bz_method_name is stored by _find_procedure for later use. sub _bz_method_name { - return $_[0]->{_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-9_\.\[\]]+$/) { - 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'); + 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}); } - return $self->{_bz_callback}; + $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; diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 8450a7a28..8108e1d4f 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -40,134 +40,134 @@ use MIME::Base64 qw(decode_base64); ########################### 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 }); + 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); } - # 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 }); + ThrowUserError("rest_invalid_resource", + {path => $self->cgi->path_info, method => $self->request->method}); + } - my $params = $self->_retrieve_json_params; + # 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}); - fix_credentials($params); + my $params = $self->_retrieve_json_params; - # Fix includes/excludes for each call - rest_include_exclude($params); + fix_credentials($params); - # Set callback name if exists - $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; + # Fix includes/excludes for each call + rest_include_exclude($params); - Bugzilla->input_params($params); + # Set callback name if exists + $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; - # 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 - }; + Bugzilla->input_params($params); - # Execute the handler - my $result = $self->_handle($obj); + # 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 + }; - if (!$self->error_response_header) { - return $self->response( - $self->response_header($self->bz_success_code || STATUS_OK, $result)); - } + # 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); + $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}; - } - - # The result needs to be a valid JSON data structure - # and not a undefined or scalar value. - if (!ref $result - || blessed($result) - || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) - { - $result = { result => $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, x-requested-with"); - - # 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); + 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}; + } + + # The result needs to be a valid JSON data structure + # and not a undefined or scalar value. + if ( !ref $result + || blessed($result) + || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) + { + $result = {result => $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, x-requested-with"); + + # 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); } ####################################### @@ -175,36 +175,40 @@ sub response { ####################################### 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); + 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); } ############################ @@ -214,79 +218,78 @@ sub handle_login { # 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, @_); - } + 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, @_); + 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); + } } - else { - $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); } + } - # 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; - } + # This is the best time to do login checks. + $self->handle_login(); - 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}); - } - } + # 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; - # This is the best time to do login checks. - $self->handle_login(); + # Allow extensions to modify the params post login + Bugzilla::Hook::process('webservice_rest_request', + {rpc => $self, params => $params}); - # 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]; + } - # 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; + return $params; } ################### @@ -294,46 +297,46 @@ sub _argument_type_check { ################### sub bz_method_name { - my ($self, $method) = @_; - $self->{_bz_method_name} = $method if $method; - return $self->{_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}; + 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}; + 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}; + 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}; + my ($self, $options) = @_; + $self->{_bz_rest_options} = $options if $options; + return $self->{_bz_rest_options}; } sub rest_include_exclude { - my ($params) = @_; + 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'}) ]; - } + 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; + return $params; } ########################## @@ -341,184 +344,191 @@ sub rest_include_exclude { ########################## 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) ]; - } - } + 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 => $@}); + } } - # 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}; + # 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); } - return $params; + %{$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; + 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; } + last if $handler_found; + } - return $handler_found; + return $handler_found; } sub _best_content_type { - my ($self, @types) = @_; - return ($self->_simple_content_negotiation(@types))[0] || '*/*'; + 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; + 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; + 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; - } + 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; + # 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm index 3fa8b65cf..5cc25f432 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -15,150 +15,150 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bug; BEGIN { - *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; -}; + *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] ] }; - } - } + 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]}; }, - qr{^/field/bug$}, { - GET => { - method => 'fields', - } + 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{^/field/bug/([^/]+)$}, { - GET => { - method => 'fields', - params => sub { - my $value = $_[0]; - my $param = 'names'; - $param = 'ids' if $value =~ /^\d+$/; - return { $param => [ $_[0] ] }; - } - } + }, + }, + qr{^/bug/comment/([^/]+)/tags$}, + { + PUT => { + method => 'update_comment_tags', + params => sub { + return {comment_id => $_[0]}; }, - qr{^/field/bug/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0] }; - } - } + }, + }, + qr{^/bug/([^/]+)/history$}, + { + GET => { + method => 'history', + params => sub { + return {ids => [$_[0]]}; }, - qr{^/field/bug/([^/]+)/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0], - product_id => $_[1] }; - } - } + } + }, + qr{^/bug/([^/]+)/attachment$}, + { + GET => { + method => 'attachments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_attachment', + params => sub { + return {ids => [$_[0]]}; }, - ]; - return $rest_resources; + 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm index 8502d6b3b..806c3f9c7 100644 --- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -12,27 +12,28 @@ use strict; use warnings; BEGIN { - *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; + *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]] }; - }, - }, + 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm index a8f3f9330..072cfe2f6 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -15,43 +15,19 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bugzilla; BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -}; + *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; + 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm index 3f8d32a03..ed65aea5c 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Classification; BEGIN { - *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; -}; + *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] ] }; - } - } + my $rest_resources = [ + qr{^/classification/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm index 198c09332..8870a0f04 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Component.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm @@ -17,19 +17,15 @@ use Bugzilla::WebService::Component; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/component$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/component$}, + {POST => {method => 'create', success_code => STATUS_CREATED}}, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm index 21dad0f73..438c8fb30 100644 --- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm +++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm @@ -17,43 +17,40 @@ use Bugzilla::WebService::FlagType; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; -}; + *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; + 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm index b052e384b..7f607b7d1 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Group.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -15,31 +15,28 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Group; BEGIN { - *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; -}; + *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] ] }; - } - } + 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; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm index 607b94b53..eabe19681 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Product.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -17,53 +17,41 @@ use Bugzilla::WebService::Product; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; -}; + *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; + 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; diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm index a83109e73..4555b4dbc 100644 --- a/Bugzilla/WebService/Server/REST/Resources/User.pm +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -15,53 +15,41 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::User; BEGIN { - *Bugzilla::WebService::User::rest_resources = \&_rest_resources; -}; + *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] ] }; - } - } + 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; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 8deb253ad..b0eae8e19 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -14,9 +14,10 @@ use warnings; use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; if ($ENV{MOD_PERL}) { - our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); -} else { - our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); + our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); +} +else { + our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); } use Bugzilla::WebService::Constants; @@ -26,97 +27,99 @@ use Bugzilla::Util; use List::MoreUtils qw(none); BEGIN { - # Allow WebService methods to call XMLRPC::Lite's type method directly - *Bugzilla::WebService::type = sub { - my ($self, $type, $value) = @_; - if ($type eq 'dateTime') { - # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. - # Our "base" implementation is in Bugzilla::WebService::Server. - $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]); - }; + # Allow WebService methods to call XMLRPC::Lite's type method directly + *Bugzilla::WebService::type = sub { + my ($self, $type, $value) = @_; + if ($type eq 'dateTime') { + + # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. + # Our "base" implementation is in Bugzilla::WebService::Server. + $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 { - my $self = shift; - my %retval = $self->SUPER::initialize(@_); - $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; - $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; - $retval{'dispatch_with'} = WS_DISPATCH; - return %retval; + my $self = shift; + my %retval = $self->SUPER::initialize(@_); + $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; + $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; + $retval{'dispatch_with'} = WS_DISPATCH; + return %retval; } sub make_response { - my $self = shift; - my $cgi = Bugzilla->cgi; - - # Fix various problems with IIS. - if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { - $ENV{CONTENT_LENGTH} = 0; - binmode(STDOUT, ':bytes'); - } - - $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 (@{$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]+/, $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); + my $self = shift; + my $cgi = Bugzilla->cgi; + + # Fix various problems with IIS. + if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { + $ENV{CONTENT_LENGTH} = 0; + binmode(STDOUT, ':bytes'); + } + + $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 (@{$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]+/, $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 { - my ($self, $classes, $action, $uri, $method) = @_; - my $class = $classes->{$uri}; - my $full_method = $uri . "." . $method; - # Only allowed methods to be used from the module's whitelist - my $file = $class; - $file =~ s{::}{/}g; - $file .= ".pm"; - require $file; - if (none { $_ eq $method } $class->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $full_method }); - } - - $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; - $self->SUPER::handle_login($class, $method, $full_method); - return; + my ($self, $classes, $action, $uri, $method) = @_; + my $class = $classes->{$uri}; + my $full_method = $uri . "." . $method; + + # Only allowed methods to be used from the module's whitelist + my $file = $class; + $file =~ s{::}{/}g; + $file .= ".pm"; + require $file; + if (none { $_ eq $method } $class->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $full_method}); + } + + $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; + $self->SUPER::handle_login($class, $method, $full_method); + return; } 1; @@ -140,100 +143,111 @@ use Bugzilla::WebService::Util qw(fix_credentials); use Scalar::Util qw(tainted); sub new { - my $self = shift->SUPER::new(@_); - # Initialise XML::Parser to not expand references to entities, to prevent DoS - require XML::Parser; - my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); - $self->{_parser}->parser($parser, $parser); - return $self; + my $self = shift->SUPER::new(@_); + + # Initialise XML::Parser to not expand references to entities, to prevent DoS + require XML::Parser; + my $parser = XML::Parser->new( + NoExpand => 1, + Handlers => { + Default => sub { } + } + ); + $self->{_parser}->parser($parser, $parser); + return $self; } sub deserialize { - my $self = shift; - - # Only allow certain content types to protect against CSRF attacks - my $content_type = lc($ENV{'CONTENT_TYPE'}); - # Remove charset, etc, if provided - $content_type =~ s/^([^;]+);.*/$1/; - if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { - ThrowUserError('xmlrpc_illegal_content_type', - { content_type => $ENV{'CONTENT_TYPE'} }); - } + my $self = shift; - my ($xml) = @_; - my $som = $self->SUPER::deserialize(@_); - if (tainted($xml)) { - $som->{_bz_do_taint} = 1; - } - bless $som, 'Bugzilla::XMLRPC::SOM'; - my $params = $som->paramsin; - # This allows positional parameters for Testopia. - $params = {} if ref $params ne 'HASH'; + # Only allow certain content types to protect against CSRF attacks + my $content_type = lc($ENV{'CONTENT_TYPE'}); + + # Remove charset, etc, if provided + $content_type =~ s/^([^;]+);.*/$1/; + if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { + ThrowUserError('xmlrpc_illegal_content_type', + {content_type => $ENV{'CONTENT_TYPE'}}); + } - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); + my ($xml) = @_; + my $som = $self->SUPER::deserialize(@_); + if (tainted($xml)) { + $som->{_bz_do_taint} = 1; + } + bless $som, 'Bugzilla::XMLRPC::SOM'; + my $params = $som->paramsin; - Bugzilla->input_params($params); + # This allows positional parameters for Testopia. + $params = {} if ref $params ne 'HASH'; - return $som; + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + return $som; } # Some method arguments need to be converted in some way, when they are input. sub decode_value { - my $self = shift; - my ($type) = @{ $_[0] }; - my $value = $self->SUPER::decode_value(@_); - - # We only validate/convert certain types here. - return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; - - # Though the XML-RPC standard doesn't allow an empty <int>, - # <double>,or <dateTime.iso8601>, we do, and we just say - # "that's undef". - if (grep($type eq $_, qw(int double dateTime))) { - return undef if $value eq ''; - } - - my $validator = $self->_validation_subs->{$type}; - if (!$validator->($value)) { - ThrowUserError('xmlrpc_invalid_value', - { type => $type, value => $value }); - } - - # We convert dateTimes to a DB-friendly date format. - if ($type eq 'dateTime.iso8601') { - if ($value !~ /T.*[\-+Z]/i) { - # The caller did not specify a timezone, so we assume UTC. - # pass 'Z' specifier to datetime_from to force it - $value = $value . 'Z'; - } - $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + my $self = shift; + my ($type) = @{$_[0]}; + my $value = $self->SUPER::decode_value(@_); + + # We only validate/convert certain types here. + return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; + + # Though the XML-RPC standard doesn't allow an empty <int>, + # <double>,or <dateTime.iso8601>, we do, and we just say + # "that's undef". + if (grep($type eq $_, qw(int double dateTime))) { + return undef if $value eq ''; + } + + my $validator = $self->_validation_subs->{$type}; + if (!$validator->($value)) { + ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value}); + } + + # We convert dateTimes to a DB-friendly date format. + if ($type eq 'dateTime.iso8601') { + if ($value !~ /T.*[\-+Z]/i) { + + # The caller did not specify a timezone, so we assume UTC. + # pass 'Z' specifier to datetime_from to force it + $value = $value . 'Z'; } + $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + } - return $value; + return $value; } sub _validation_subs { - my $self = shift; - return $self->{_validation_subs} if $self->{_validation_subs}; - # The only place that XMLRPC::Lite stores any sort of validation - # regex is in XMLRPC::Serializer. We want to re-use those regexes here. - my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; - - # $lookup is a hash whose values are arrayrefs, and whose keys are the - # names of types. The second item of each arrayref is a subroutine - # that will do our validation for us. - my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); - # Add a boolean validator - $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; - # Some types have multiple names, or have a different name in - # XMLRPC::Serializer than their standard XML-RPC name. - $validators{'dateTime.iso8601'} = $validators{'dateTime'}; - $validators{'i4'} = $validators{'int'}; - - $self->{_validation_subs} = \%validators; - return \%validators; + my $self = shift; + return $self->{_validation_subs} if $self->{_validation_subs}; + + # The only place that XMLRPC::Lite stores any sort of validation + # regex is in XMLRPC::Serializer. We want to re-use those regexes here. + my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; + + # $lookup is a hash whose values are arrayrefs, and whose keys are the + # names of types. The second item of each arrayref is a subroutine + # that will do our validation for us. + my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); + + # Add a boolean validator + $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ }; + + # Some types have multiple names, or have a different name in + # XMLRPC::Serializer than their standard XML-RPC name. + $validators{'dateTime.iso8601'} = $validators{'dateTime'}; + $validators{'i4'} = $validators{'int'}; + + $self->{_validation_subs} = \%validators; + return \%validators; } 1; @@ -249,16 +263,16 @@ our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); sub paramsin { - my $self = shift; - if (!$self->{bz_params_in}) { - my @params = $self->SUPER::paramsin(@_); - if ($self->{_bz_do_taint}) { - taint_data(@params); - } - $self->{bz_params_in} = \@params; + my $self = shift; + if (!$self->{bz_params_in}) { + my @params = $self->SUPER::paramsin(@_); + if ($self->{_bz_do_taint}) { + taint_data(@params); } - my $params = $self->{bz_params_in}; - return wantarray ? @$params : $params->[0]; + $self->{bz_params_in} = \@params; + } + my $params = $self->{bz_params_in}; + return wantarray ? @$params : $params->[0]; } 1; @@ -272,43 +286,46 @@ use strict; use warnings; use Scalar::Util qw(blessed reftype); + # We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - # This fixes UTF-8. - $self->{'_typelookup'}->{'base64'} = - [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, - 'as_base64']; - # This makes arrays work right even though we're a subclass. - # (See http://rt.cpan.org//Ticket/Display.html?id=34514) - $self->{'_encodingStyle'} = ''; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + # This fixes UTF-8. + $self->{'_typelookup'}->{'base64'} = [ + 10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ }, + 'as_base64' + ]; + + # This makes arrays work right even though we're a subclass. + # (See http://rt.cpan.org//Ticket/Display.html?id=34514) + $self->{'_encodingStyle'} = ''; + return $self; } # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. sub encode_object { - my $self = shift; - my @encoded = $self->SUPER::encode_object(@_); + my $self = shift; + my @encoded = $self->SUPER::encode_object(@_); - return $encoded[0]->[0] eq 'nil' - ? ['value', {}, [@encoded]] - : @encoded; + return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded; } # Removes undefined values so they do not produce invalid XMLRPC. sub envelope { - my $self = shift; - my ($type, $method, $data) = @_; - # If the type isn't a successful response we don't want to change the values. - if ($type eq 'response') { - _strip_undefs($data); - } - return $self->SUPER::envelope($type, $method, $data); + my $self = shift; + my ($type, $method, $data) = @_; + + # If the type isn't a successful response we don't want to change the values. + if ($type eq 'response') { + _strip_undefs($data); + } + return $self->SUPER::envelope($type, $method, $data); } # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, @@ -316,58 +333,58 @@ sub envelope { # The whole XMLRPC::Data object must be removed if its value key is undefined # so it cannot be recursed like the other hash type objects. sub _strip_undefs { - my ($initial) = @_; - my $type = reftype($initial) or return; - - if ($type eq "HASH") { - while (my ($key, $value) = each(%$initial)) { - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the hash. - delete $initial->{$key}; - } - else { - _strip_undefs($value); - } - } + my ($initial) = @_; + my $type = reftype($initial) or return; + + if ($type eq "HASH") { + while (my ($key, $value) = each(%$initial)) { + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the hash. + delete $initial->{$key}; + } + else { + _strip_undefs($value); + } } - elsif ($type eq "ARRAY") { - for (my $count = 0; $count < scalar @{$initial}; $count++) { - my $value = $initial->[$count]; - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the array. - splice(@$initial, $count, 1); - $count--; - } - else { - _strip_undefs($value); - } - } + } + elsif ($type eq "ARRAY") { + for (my $count = 0; $count < scalar @{$initial}; $count++) { + my $value = $initial->[$count]; + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the array. + splice(@$initial, $count, 1); + $count--; + } + else { + _strip_undefs($value); + } } + } } sub BEGIN { - no strict 'refs'; - for my $type (qw(double i4 int dateTime)) { - my $method = 'as_' . $type; - *$method = sub { - my ($self, $value) = @_; - if (!defined($value)) { - return as_nil(); - } - else { - my $super_method = "SUPER::$method"; - return $self->$super_method($value); - } - } - } + no strict 'refs'; + for my $type (qw(double i4 int dateTime)) { + my $method = 'as_' . $type; + *$method = sub { + my ($self, $value) = @_; + if (!defined($value)) { + return as_nil(); + } + else { + my $super_method = "SUPER::$method"; + return $self->$super_method($value); + } + } + } } sub as_nil { - return ['nil', {}]; + return ['nil', {}]; } 1; |