summaryrefslogtreecommitdiffstats
path: root/Bugzilla/WebService/Server/XMLRPC.pm
blob: c6461abd68d158c70bb8d289c94a5d3821974489 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::WebService::Server::XMLRPC;

use 5.14.0;
use strict;
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);
}

use Bugzilla::WebService::Constants;
use Bugzilla::Error;
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 'login') {
            $type = 'string';
            $value = email_filter($value) if Bugzilla->params->{'use_email_as_login'};
        }
        elsif ($type eq 'email') {
            $type = 'string';
            $value = '' unless Bugzilla->user->in_group('editusers');
        }
        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;
}

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);
    }
}

sub handle_login {
    my ($self, $classes, $action, $uri, $method) = @_;
    my $class = $classes->{$uri};
    if (!$class) {
        ThrowCodeError('unknown_method', { method => $method eq 'methodName' ? '' : '.' . $method });
    }
    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;

# This exists to validate input parameters (which XMLRPC::Lite doesn't do)
# and also, in some cases, to more-usefully decode them.
package Bugzilla::XMLRPC::Deserializer;

use 5.14.0;
use strict;
use warnings;

# We can't use "use parent" because XMLRPC::Serializer doesn't return
# a true value.
use XMLRPC::Lite;
our @ISA = qw(XMLRPC::Deserializer);

use Bugzilla::Error;
use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST);
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;
}

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 ($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';

    # 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);
    }

    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;
}

1;

package Bugzilla::XMLRPC::SOM;

use 5.14.0;
use strict;
use warnings;

use XMLRPC::Lite;
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 $params = $self->{bz_params_in};
    return wantarray ? @$params : $params->[0];
}

1;

# This package exists to fix a UTF-8 bug in SOAP::Lite.
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
package Bugzilla::XMLRPC::Serializer;

use 5.14.0;
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;
}

# Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension.
sub encode_object {
    my $self = shift;
    my @encoded = $self->SUPER::encode_object(@_);

    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);
}

# In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
# Bugzilla objects (reftype = 'HASH') and XMLRPC::Data objects.
# 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);
            }
        }
    }
    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);
            }
        }
    }
}

sub as_nil {
    return ['nil', {}];
}

1;

__END__

=head1 NAME

Bugzilla::WebService::Server::XMLRPC - The XML-RPC Interface to Bugzilla

=head1 DESCRIPTION

This documentation describes things about the Bugzilla WebService that
are specific to XML-RPC. For a general overview of the Bugzilla WebServices,
see L<Bugzilla::WebService>.

=head1 XML-RPC

The XML-RPC standard is described here: L<http://www.xmlrpc.com/spec>

=head1 CONNECTING

The endpoint for the XML-RPC interface is the C<xmlrpc.cgi> script in
your Bugzilla installation. For example, if your Bugzilla is at
C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>

=head1 PARAMETERS

C<dateTime> fields are the standard C<dateTime.iso8601> XML-RPC field. They
should be in C<YYYY-MM-DDTHH:MM:SS> format (where C<T> is a literal T). As
of Bugzilla B<3.6>, Bugzilla always expects C<dateTime> fields to be in the
UTC timezone, and all returned C<dateTime> values are in the UTC timezone.

All other fields are standard XML-RPC types.

=head2 How XML-RPC WebService Methods Take Parameters

All functions take a single argument, a C<< <struct> >> that contains all parameters.
The names of the parameters listed in the API docs for each function are the
C<< <name> >> element for the struct C<< <member> >>s.

=head1 EXTENSIONS TO THE XML-RPC STANDARD

=head2 Undefined Values

Normally, XML-RPC does not allow empty values for C<int>, C<double>, or
C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as
C<undef> (called C<NULL> or C<None> in some programming languages).

Bugzilla accepts a timezone specifier at the end of C<dateTime.iso8601>
fields that are specified as method arguments. The format of the timezone
specifier is specified in the ISO-8601 standard. If no timezone specifier
is included, the passed-in time is assumed to be in the UTC timezone.
Bugzilla will never output a timezone specifier on returned data, because
doing so would violate the XML-RPC specification. All returned times are in
the UTC timezone.

Bugzilla also accepts an element called C<< <nil> >>, as specified by the
XML-RPC extension here: L<http://ontosys.com/xml-rpc/extensions.php>, which
is always considered to be C<undef>, no matter what it contains.

Bugzilla does not use C<< <nil> >> values in returned data, because currently
most clients do not support C<< <nil> >>. Instead, any fields with C<undef>
values will be stripped from the response completely. Therefore
B<the client must handle the fact that some expected fields may not be 
returned>.

=begin private

nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value
in the CPAN SVN since 14th Dec 2008
L<http://rt.cpan.org/Public/Bug/Display.html?id=20569> and in Fedora's
perl-SOAP-Lite package in versions 0.68-1 and above.

=end private

=head1 SEE ALSO

L<Bugzilla::WebService>

=head1 B<Methods in need of POD>

=over

=item make_response

=item initialize

=item handle_login

=back