1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4# 5# This Source Code Form is "Incompatible With Secondary Licenses", as 6# defined by the Mozilla Public License, v. 2.0. 7 8package Bugzilla::WebService::Server::XMLRPC; 9 10use 5.10.1; 11use strict; 12use warnings; 13 14use XMLRPC::Transport::HTTP; 15use Bugzilla::WebService::Server; 16if ($ENV{MOD_PERL}) { 17 our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); 18} else { 19 our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); 20} 21 22use Bugzilla::WebService::Constants; 23use Bugzilla::Error; 24use Bugzilla::Util; 25 26use List::MoreUtils qw(none); 27 28BEGIN { 29 # Allow WebService methods to call XMLRPC::Lite's type method directly 30 *Bugzilla::WebService::type = sub { 31 my ($self, $type, $value) = @_; 32 if ($type eq 'dateTime') { 33 # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. 34 # Our "base" implementation is in Bugzilla::WebService::Server. 35 $value = Bugzilla::WebService::Server->datetime_format_outbound($value); 36 $value =~ s/-//g; 37 } 38 elsif ($type eq 'email') { 39 $type = 'string'; 40 if (Bugzilla->params->{'webservice_email_filter'}) { 41 $value = email_filter($value); 42 } 43 } 44 return XMLRPC::Data->type($type)->value($value); 45 }; 46 47 # Add support for ETags into XMLRPC WebServices 48 *Bugzilla::WebService::bz_etag = sub { 49 return Bugzilla::WebService::Server->bz_etag($_[1]); 50 }; 51} 52 53sub initialize { 54 my $self = shift; 55 my %retval = $self->SUPER::initialize(@_); 56 $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; 57 $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; 58 $retval{'dispatch_with'} = WS_DISPATCH; 59 return %retval; 60} 61 62sub make_response { 63 my $self = shift; 64 my $cgi = Bugzilla->cgi; 65 66 # Fix various problems with IIS. 67 if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { 68 $ENV{CONTENT_LENGTH} = 0; 69 binmode(STDOUT, ':bytes'); 70 } 71 72 $self->SUPER::make_response(@_); 73 74 # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around 75 # its cookies in Bugzilla::CGI, so we need to copy them over. 76 foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { 77 $self->response->headers->push_header('Set-Cookie', $cookie); 78 } 79 80 # Copy across security related headers from Bugzilla::CGI 81 foreach my $header (split(/[\r\n]+/, $cgi->header)) { 82 my ($name, $value) = $header =~ /^([^:]+): (.*)/; 83 if (!$self->response->headers->header($name)) { 84 $self->response->headers->header($name => $value); 85 } 86 } 87 88 # ETag support 89 my $etag = $self->bz_etag; 90 if (!$etag) { 91 my $data = $self->response->as_string; 92 $etag = $self->bz_etag($data); 93 } 94 95 if ($etag && $cgi->check_etag($etag)) { 96 $self->response->headers->push_header('ETag', $etag); 97 $self->response->headers->push_header('status', '304 Not Modified'); 98 } 99 elsif ($etag) { 100 $self->response->headers->push_header('ETag', $etag); 101 } 102} 103 104sub handle_login { 105 my ($self, $classes, $action, $uri, $method) = @_; 106 my $class = $classes->{$uri}; 107 my $full_method = $uri . "." . $method; 108 # Only allowed methods to be used from the module's whitelist 109 my $file = $class; 110 $file =~ s{::}{/}g; 111 $file .= ".pm"; 112 require $file; 113 if (none { $_ eq $method } $class->PUBLIC_METHODS) { 114 ThrowCodeError('unknown_method', { method => $full_method }); 115 } 116 117 $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; 118 $self->SUPER::handle_login($class, $method, $full_method); 119 return; 120} 121 1221; 123 124# This exists to validate input parameters (which XMLRPC::Lite doesn't do) 125# and also, in some cases, to more-usefully decode them. 126package Bugzilla::XMLRPC::Deserializer; 127 128use 5.10.1; 129use strict; 130use warnings; 131 132# We can't use "use parent" because XMLRPC::Serializer doesn't return 133# a true value. 134use XMLRPC::Lite; 135our @ISA = qw(XMLRPC::Deserializer); 136 137use Bugzilla::Error; 138use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST); 139use Bugzilla::WebService::Util qw(fix_credentials); 140use Scalar::Util qw(tainted); 141 142sub new { 143 my $self = shift->SUPER::new(@_); 144 # Initialise XML::Parser to not expand references to entities, to prevent DoS 145 require XML::Parser; 146 my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); 147 $self->{_parser}->parser($parser, $parser); 148 return $self; 149} 150 151sub deserialize { 152 my $self = shift; 153 154 # Only allow certain content types to protect against CSRF attacks 155 my $content_type = lc($ENV{'CONTENT_TYPE'}); 156 # Remove charset, etc, if provided 157 $content_type =~ s/^([^;]+);.*/$1/; 158 if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { 159 ThrowUserError('xmlrpc_illegal_content_type', 160 { content_type => $ENV{'CONTENT_TYPE'} }); 161 } 162 163 my ($xml) = @_; 164 my $som = $self->SUPER::deserialize(@_); 165 if (tainted($xml)) { 166 $som->{_bz_do_taint} = 1; 167 } 168 bless $som, 'Bugzilla::XMLRPC::SOM'; 169 my $params = $som->paramsin; 170 # This allows positional parameters for Testopia. 171 $params = {} if ref $params ne 'HASH'; 172 173 # Update the params to allow for several convenience key/values 174 # use for authentication 175 fix_credentials($params); 176 177 Bugzilla->input_params($params); 178 179 return $som; 180} 181 182# Some method arguments need to be converted in some way, when they are input. 183sub decode_value { 184 my $self = shift; 185 my ($type) = @{ $_[0] }; 186 my $value = $self->SUPER::decode_value(@_); 187 188 # We only validate/convert certain types here. 189 return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; 190 191 # Though the XML-RPC standard doesn't allow an empty <int>, 192 # <double>,or <dateTime.iso8601>, we do, and we just say 193 # "that's undef". 194 if (grep($type eq $_, qw(int double dateTime))) { 195 return undef if $value eq ''; 196 } 197 198 my $validator = $self->_validation_subs->{$type}; 199 if (!$validator->($value)) { 200 ThrowUserError('xmlrpc_invalid_value', 201 { type => $type, value => $value }); 202 } 203 204 # We convert dateTimes to a DB-friendly date format. 205 if ($type eq 'dateTime.iso8601') { 206 if ($value !~ /T.*[\-+Z]/i) { 207 # The caller did not specify a timezone, so we assume UTC. 208 # pass 'Z' specifier to datetime_from to force it 209 $value = $value . 'Z'; 210 } 211 $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); 212 } 213 214 return $value; 215} 216 217sub _validation_subs { 218 my $self = shift; 219 return $self->{_validation_subs} if $self->{_validation_subs}; 220 # The only place that XMLRPC::Lite stores any sort of validation 221 # regex is in XMLRPC::Serializer. We want to re-use those regexes here. 222 my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; 223 224 # $lookup is a hash whose values are arrayrefs, and whose keys are the 225 # names of types. The second item of each arrayref is a subroutine 226 # that will do our validation for us. 227 my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); 228 # Add a boolean validator 229 $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; 230 # Some types have multiple names, or have a different name in 231 # XMLRPC::Serializer than their standard XML-RPC name. 232 $validators{'dateTime.iso8601'} = $validators{'dateTime'}; 233 $validators{'i4'} = $validators{'int'}; 234 235 $self->{_validation_subs} = \%validators; 236 return \%validators; 237} 238 2391; 240 241package Bugzilla::XMLRPC::SOM; 242 243use 5.10.1; 244use strict; 245use warnings; 246 247use XMLRPC::Lite; 248our @ISA = qw(XMLRPC::SOM); 249use Bugzilla::WebService::Util qw(taint_data); 250 251sub paramsin { 252 my $self = shift; 253 if (!$self->{bz_params_in}) { 254 my @params = $self->SUPER::paramsin(@_); 255 if ($self->{_bz_do_taint}) { 256 taint_data(@params); 257 } 258 $self->{bz_params_in} = \@params; 259 } 260 my $params = $self->{bz_params_in}; 261 return wantarray ? @$params : $params->[0]; 262} 263 2641; 265 266# This package exists to fix a UTF-8 bug in SOAP::Lite. 267# See http://rt.cpan.org/Public/Bug/Display.html?id=32952. 268package Bugzilla::XMLRPC::Serializer; 269 270use 5.10.1; 271use strict; 272use warnings; 273 274use Scalar::Util qw(blessed reftype); 275# We can't use "use parent" because XMLRPC::Serializer doesn't return 276# a true value. 277use XMLRPC::Lite; 278our @ISA = qw(XMLRPC::Serializer); 279 280sub new { 281 my $class = shift; 282 my $self = $class->SUPER::new(@_); 283 # This fixes UTF-8. 284 $self->{'_typelookup'}->{'base64'} = 285 [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, 286 'as_base64']; 287 # This makes arrays work right even though we're a subclass. 288 # (See http://rt.cpan.org//Ticket/Display.html?id=34514) 289 $self->{'_encodingStyle'} = ''; 290 return $self; 291} 292 293# Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. 294sub encode_object { 295 my $self = shift; 296 my @encoded = $self->SUPER::encode_object(@_); 297 298 return $encoded[0]->[0] eq 'nil' 299 ? ['value', {}, [@encoded]] 300 : @encoded; 301} 302 303# Removes undefined values so they do not produce invalid XMLRPC. 304sub envelope { 305 my $self = shift; 306 my ($type, $method, $data) = @_; 307 # If the type isn't a successful response we don't want to change the values. 308 if ($type eq 'response') { 309 _strip_undefs($data); 310 } 311 return $self->SUPER::envelope($type, $method, $data); 312} 313 314# In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, 315# Bugzilla objects (reftype = 'HASH') and XMLRPC::Data objects. 316# The whole XMLRPC::Data object must be removed if its value key is undefined 317# so it cannot be recursed like the other hash type objects. 318sub _strip_undefs { 319 my ($initial) = @_; 320 my $type = reftype($initial) or return; 321 322 if ($type eq "HASH") { 323 while (my ($key, $value) = each(%$initial)) { 324 if ( !defined $value 325 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) 326 { 327 # If the value is undefined remove it from the hash. 328 delete $initial->{$key}; 329 } 330 else { 331 _strip_undefs($value); 332 } 333 } 334 } 335 elsif ($type eq "ARRAY") { 336 for (my $count = 0; $count < scalar @{$initial}; $count++) { 337 my $value = $initial->[$count]; 338 if ( !defined $value 339 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) 340 { 341 # If the value is undefined remove it from the array. 342 splice(@$initial, $count, 1); 343 $count--; 344 } 345 else { 346 _strip_undefs($value); 347 } 348 } 349 } 350} 351 352sub BEGIN { 353 no strict 'refs'; 354 for my $type (qw(double i4 int dateTime)) { 355 my $method = 'as_' . $type; 356 *$method = sub { 357 my ($self, $value) = @_; 358 if (!defined($value)) { 359 return as_nil(); 360 } 361 else { 362 my $super_method = "SUPER::$method"; 363 return $self->$super_method($value); 364 } 365 } 366 } 367} 368 369sub as_nil { 370 return ['nil', {}]; 371} 372 3731; 374 375__END__ 376 377=head1 NAME 378 379Bugzilla::WebService::Server::XMLRPC - The XML-RPC Interface to Bugzilla 380 381=head1 DESCRIPTION 382 383This documentation describes things about the Bugzilla WebService that 384are specific to XML-RPC. For a general overview of the Bugzilla WebServices, 385see L<Bugzilla::WebService>. 386 387=head1 XML-RPC 388 389The XML-RPC standard is described here: L<http://www.xmlrpc.com/spec> 390 391=head1 CONNECTING 392 393The endpoint for the XML-RPC interface is the C<xmlrpc.cgi> script in 394your Bugzilla installation. For example, if your Bugzilla is at 395C<bugzilla.yourdomain.com>, then your XML-RPC client would access the 396API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi> 397 398=head1 PARAMETERS 399 400C<dateTime> fields are the standard C<dateTime.iso8601> XML-RPC field. They 401should be in C<YYYY-MM-DDTHH:MM:SS> format (where C<T> is a literal T). As 402of Bugzilla B<3.6>, Bugzilla always expects C<dateTime> fields to be in the 403UTC timezone, and all returned C<dateTime> values are in the UTC timezone. 404 405All other fields are standard XML-RPC types. 406 407=head2 How XML-RPC WebService Methods Take Parameters 408 409All functions take a single argument, a C<< <struct> >> that contains all parameters. 410The names of the parameters listed in the API docs for each function are the 411C<< <name> >> element for the struct C<< <member> >>s. 412 413=head1 EXTENSIONS TO THE XML-RPC STANDARD 414 415=head2 Undefined Values 416 417Normally, XML-RPC does not allow empty values for C<int>, C<double>, or 418C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as 419C<undef> (called C<NULL> or C<None> in some programming languages). 420 421Bugzilla accepts a timezone specifier at the end of C<dateTime.iso8601> 422fields that are specified as method arguments. The format of the timezone 423specifier is specified in the ISO-8601 standard. If no timezone specifier 424is included, the passed-in time is assumed to be in the UTC timezone. 425Bugzilla will never output a timezone specifier on returned data, because 426doing so would violate the XML-RPC specification. All returned times are in 427the UTC timezone. 428 429Bugzilla also accepts an element called C<< <nil> >>, as specified by the 430XML-RPC extension here: L<http://ontosys.com/xml-rpc/extensions.php>, which 431is always considered to be C<undef>, no matter what it contains. 432 433Bugzilla does not use C<< <nil> >> values in returned data, because currently 434most clients do not support C<< <nil> >>. Instead, any fields with C<undef> 435values will be stripped from the response completely. Therefore 436B<the client must handle the fact that some expected fields may not be 437returned>. 438 439=begin private 440 441nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value 442in the CPAN SVN since 14th Dec 2008 443L<http://rt.cpan.org/Public/Bug/Display.html?id=20569> and in Fedora's 444perl-SOAP-Lite package in versions 0.68-1 and above. 445 446=end private 447 448=head1 SEE ALSO 449 450L<Bugzilla::WebService> 451 452=head1 B<Methods in need of POD> 453 454=over 455 456=item make_response 457 458=item initialize 459 460=item handle_login 461 462=back 463