File Coverage

blib/lib/XML/RPC.pm
Criterion Covered Total %
statement 24 148 16.2
branch 0 40 0.0
condition 0 16 0.0
subroutine 7 36 19.4
pod 14 28 50.0
total 45 268 16.7


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             XML::RPC -- Pure Perl implementation for an XML-RPC client and server.
5              
6             =head1 SYNOPSIS
7              
8             make a call to an XML-RPC server:
9              
10             use XML::RPC;
11              
12             my $xmlrpc = XML::RPC->new('http://betty.userland.com/RPC2');
13             my $result = $xmlrpc->call( 'examples.getStateStruct', { state1 => 12, state2 => 28 } );
14              
15             create an XML-RPC service:
16              
17             use XML::RPC;
18             use CGI;
19              
20             my $q = new CGI;
21             my $xmlrpc = XML::RPC->new();
22             my $xml = $q->param('POSTDATA');
23              
24             print $q->header( -type => 'text/xml', -charset => 'UTF-8' );
25             print $xmlrpc->receive( $xml, \&handler );
26              
27             sub handler {
28             my ( $methodname, @params ) = @_;
29             return { you_called => $methodname, with_params => \@params };
30             }
31              
32             =head1 WARNING
33              
34             Very little maintainance goes into this module. While it continues to work, is has certain quirks that may or may not be fixable without breaking backward compatibility.
35              
36             I strongly recommend that, before deciding what to use in a new project, you look into Randy Ray's L module. This seems to be a much more modern approach.
37              
38             =head1 DESCRIPTION
39              
40             XML::RPC module provides simple Pure Perl methods for XML-RPC communication.
41             It's goals are simplicity and flexibility. XML::RPC uses XML::TreePP
42             for parsing.
43              
44             This version of XML::RPC merges the changes from XML::RPC::CustomUA.
45              
46             =head1 CONSTRUCTOR AND OPTIONS
47              
48             =head2 $xmlrpc = XML::RPC->new();
49              
50             This constructor method returns a new XML::RPC object. Usable for XML-RPC servers.
51              
52             =head2 $xmlrpc = XML::RPC->new( 'http://betty.userland.com/RPC2', %options );
53              
54             Its first argument is the full URL for your server. The second argument
55             is for options passing to XML::TreePP, for example: output_encoding => 'ISO-8859-1'
56             (default is UTF-8).
57              
58             You can also define the UserAgent string, for example:
59              
60             my $rpcfoo = XML::RPC->new($apiurl, ('User-Agent' => 'Baz/3000 (Mozilla/1.0; FooBar phone app)'));
61              
62             =head1 METHODS
63              
64             =head2 $xmlrpc->credentials( 'username', 'password );
65              
66             Set Credentials for HTTP Basic Authentication. This is only
67             secure over HTTPS.
68              
69             Please, please, please do not use this over unencrypted connections!
70              
71             =head2 $xmlrpc->call( 'method_name', @arguments );
72              
73             This method calls the provides XML-RPC server's method_name with
74             @arguments. It will return the server method's response.
75              
76             =head2 $xmlrpc->receive( $xml, \&handler );
77              
78             This parses an incoming XML-RPC methodCall and call the \&handler subref
79             with parameters: $methodName and @parameters.
80              
81             =head2 $xmlrpc->xml_in();
82              
83             Returns the last XML that went in the client.
84              
85             =head2 $xmlrpc->xml_out();
86              
87             Returns the last XML that went out the client.
88              
89             =head2 $xmlrpc->indent(indentsize);
90              
91             Sets the xmlout indentation
92              
93             =head1 CUSTOM TYPES
94              
95             =head2 $xmlrpc->call( 'method_name', { data => sub { { 'base64' => encode_base64($data) } } } );
96              
97             When passing a CODEREF to a value XML::RPC will simply use the returned hashref as a type => value pair.
98              
99             =head1 TYPECASTING
100              
101             Sometimes a value type might not be clear from the value alone, typecasting provides a way to "force" a value to a certain type
102              
103             =head2 as_string
104              
105             Forces a value to be cast as string.
106              
107             $xmlrpc->call( 'gimmeallyourmoney', { cardnumber => as_string( 12345 ) } );
108              
109             =head2 as_int
110              
111             Forces a value to be cast as int
112              
113             =head2 as_i4
114              
115             Forces a value to be cast as i4
116              
117             =head2 as_double
118              
119             Forces a value to be cast as double
120              
121             =head2 as_boolean
122              
123             Forces a value to be cast as boolean
124              
125             =head2 as_base64
126              
127             Forces a value to be cast as base64
128              
129             =head2 as_dateTime_iso8601
130              
131             Forces a value to be cast as ISO8601 Datetime
132              
133              
134             =head1 ERROR HANDLING
135              
136             To provide an error response you can simply die() in the \&handler
137             function. Also you can set the $XML::RPC::faultCode variable to a (int) value
138             just before dieing.
139              
140             =head1 PROXY SUPPORT
141              
142             Default XML::RPC will try to use LWP::Useragent for requests,
143             you can set the environment variable: CGI_HTTP_PROXY to
144             set a proxy.
145              
146             =head1 LIMITATIONS
147              
148             XML::RPC will not create "bool", "dateTime.iso8601" or "base64" types
149             automatically. They will be parsed as "int" or "string". You can use the
150             CODE ref to create these types.
151              
152             =head1 AUTHOR
153              
154             Original author: Niek Albers, http://www.daansystems.com/
155             Current author: Rene Schickbauer, https://cavac.at
156              
157             =head1 COPYRIGHT AND LICENSE
158              
159             Copyright (c) 2007-2008 Niek Albers. All rights reserved. This program
160              
161             Copyright (c) 2012-2022 Rene Schickbauer
162              
163             This program is free software; you can redistribute it and/or modify it under the same
164             terms as Perl itself.
165             =cut
166              
167             package XML::RPC;
168              
169 2     2   109384 use strict;
  2         12  
  2         49  
170 2     2   1048 use XML::TreePP;
  2         13625  
  2         63  
171 2     2   804 use MIME::Base64;
  2         1094  
  2         99  
172 2     2   804 use Time::Local;
  2         3858  
  2         98  
173 2     2   12 use vars qw($VERSION $faultCode);
  2         4  
  2         67  
174 2     2   8 no strict 'refs';
  2         5  
  2         3085  
175              
176             $VERSION = 2.0;
177             $faultCode = 0;
178              
179             sub new {
180 1     1 1 68 my $package = shift;
181 1         3 my $self = {};
182 1         2 bless $self, $package;
183 1         4 $self->{url} = shift;
184 1         5 $self->{tpp} = XML::TreePP->new(@_);
185 1         8 return $self;
186             }
187              
188             sub indent {
189 0   0 0 1   my $self = shift || return;
190 0           $self->{tpp}->set( indent => shift );
191             }
192              
193             sub credentials {
194 0     0 1   my ($self, $username, $password) = @_;
195              
196 0           my $authtoken = 'Basic ' . encode_base64($username . ':' . $password, '');
197              
198 0           $self->{authtoken} = $authtoken;
199              
200 0           return;
201             }
202              
203             sub call {
204 0     0 1   my $self = shift;
205 0           my ( $methodname, @params ) = @_;
206              
207 0 0         die 'no url' if ( !$self->{url} );
208              
209 0           $faultCode = 0;
210 0           my $xml_out = $self->create_call_xml( $methodname, @params );
211              
212 0           $self->{xml_out} = $xml_out;
213              
214             my %header = (
215             'Content-Type' => 'text/xml',
216 0 0         'User-Agent' => defined($self->{tpp}->{'User-Agent'}) ? $self->{tpp}->{'User-Agent'} : 'XML-RPC/' . $VERSION,
217             'Content-Length' => length($xml_out)
218             );
219              
220 0 0         if(defined($self->{authtoken})) {
221             $header{'Authorization'} = $self->{authtoken}
222 0           }
223              
224             my ( $result, $xml_in ) = $self->{tpp}->parsehttp(
225             POST => $self->{url},
226 0           $xml_out,
227             \%header,
228             );
229              
230 0           $self->{xml_in} = $xml_in;
231              
232 0           my @data = $self->unparse_response($result);
233 0 0         return @data == 1 ? $data[0] : @data;
234             }
235              
236             sub receive {
237 0     0 1   my $self = shift;
238 0           my $result = eval {
239 0   0       my $xml_in = shift || die 'no xml';
240 0           $self->{xml_in} = $xml_in;
241 0   0       my $handler = shift || die 'no handler';
242 0           my $hash = $self->{tpp}->parse($xml_in);
243 0           my ( $methodname, @params ) = $self->unparse_call($hash);
244 0           $self->create_response_xml( $handler->( $methodname, @params ) );
245             };
246              
247 0 0         $result = $self->create_fault_xml($@) if ($@);
248 0           $self->{xml_out} = $result;
249 0           return $result;
250              
251             }
252              
253             sub create_fault_xml {
254 0     0 0   my $self = shift;
255 0           my $error = shift;
256 0           chomp($error);
257             return $self->{tpp}
258 0           ->write( { methodResponse => { fault => $self->parse( { faultString => $error, faultCode => int($faultCode) } ) } } );
259             }
260              
261             sub create_call_xml {
262 0     0 0   my $self = shift;
263 0           my ( $methodname, @params ) = @_;
264              
265             return $self->{tpp}->write(
266             {
267             methodCall => {
268             methodName => $methodname,
269 0           params => { param => [ map { $self->parse($_) } @params ] }
  0            
270             }
271             }
272             );
273             }
274              
275             sub create_response_xml {
276 0     0 0   my $self = shift;
277 0           my @params = @_;
278              
279 0           return $self->{tpp}->write( { methodResponse => { params => { param => [ map { $self->parse($_) } @params ] } } } );
  0            
280             }
281              
282             sub parse {
283 0     0 0   my $self = shift;
284 0           my $p = shift;
285 0           my $result;
286              
287 0 0         if ( ref($p) eq 'HASH' ) {
    0          
    0          
288 0           $result = $self->parse_struct($p);
289             }
290             elsif ( ref($p) eq 'ARRAY' ) {
291 0           $result = $self->parse_array($p);
292             }
293             elsif ( ref($p) eq 'CODE' ) {
294 0           $result = $p->();
295             }
296             else {
297 0           $result = $self->parse_scalar($p);
298             }
299              
300 0           return { value => $result };
301             }
302              
303             sub parse_scalar {
304 0     0 0   my $self = shift;
305 0           my $scalar = shift;
306 0           local $^W = undef;
307              
308 0 0 0       if ( ( $scalar =~ m/^[\-+]?(0|[1-9]\d*)$/ )
    0          
309             && ( abs($scalar) <= ( 0xffffffff >> 1 ) ) )
310             {
311 0           return { i4 => $scalar };
312             }
313             elsif ( $scalar =~ m/^[\-+]?\d+\.\d+$/ ) {
314 0           return { double => $scalar };
315             }
316             else {
317 0           return { string => \$scalar };
318             }
319             }
320              
321             sub parse_struct {
322 0     0 0   my $self = shift;
323 0           my $hash = shift;
324              
325 0           return { struct => { member => [ map { { name => $_, %{ $self->parse( $hash->{$_} ) } } } keys(%$hash) ] } };
  0            
  0            
326             }
327              
328             sub parse_array {
329 0     0 0   my $self = shift;
330 0           my $array = shift;
331              
332 0           return { array => { data => { value => [ map { $self->parse($_)->{value} } $self->list($array) ] } } };
  0            
333             }
334              
335             sub unparse_response {
336 0     0 0   my $self = shift;
337 0           my $hash = shift;
338              
339 0   0       my $response = $hash->{methodResponse} || die 'no data';
340              
341 0 0         if ( $response->{fault} ) {
342 0           return $self->unparse_value( $response->{fault}->{value} );
343             }
344             else {
345 0           return map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} );
  0            
346             }
347             }
348              
349             sub unparse_call {
350 0     0 0   my $self = shift;
351 0           my $hash = shift;
352              
353 0   0       my $response = $hash->{methodCall} || die 'no data';
354              
355 0           my $methodname = $response->{methodName};
356             my @args =
357 0           map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} );
  0            
358 0           return ( $methodname, @args );
359             }
360              
361             sub unparse_value {
362 0     0 0   my $self = shift;
363 0           my $value = shift;
364 0           my $result;
365              
366 0 0         return $value if ( ref($value) ne 'HASH' ); # for unspecified params
367 0 0         if ( $value->{struct} ) {
    0          
368 0           $result = $self->unparse_struct( $value->{struct} );
369 0 0         return !%$result
370             ? undef
371             : $result; # fix for empty hashrefs from XML::TreePP
372             }
373             elsif ( $value->{array} ) {
374 0           return $self->unparse_array( $value->{array} );
375             }
376             else {
377 0           return $self->unparse_scalar($value);
378             }
379             }
380              
381             sub unparse_scalar {
382 0     0 0   my $self = shift;
383 0           my $scalar = shift;
384 0           my ($result) = values(%$scalar);
385 0 0 0       return ( ref($result) eq 'HASH' && !%$result )
386             ? undef
387             : $result; # fix for empty hashrefs from XML::TreePP
388             }
389              
390             sub unparse_struct {
391 0     0 0   my $self = shift;
392 0           my $struct = shift;
393              
394 0           return { map { $_->{name} => $self->unparse_value( $_->{value} ) } $self->list( $struct->{member} ) };
  0            
395             }
396              
397             sub unparse_array {
398 0     0 0   my $self = shift;
399 0           my $array = shift;
400 0           my $data = $array->{data};
401              
402 0           return [ map { $self->unparse_value($_) } $self->list( $data->{value} ) ];
  0            
403             }
404              
405             sub list {
406 0     0 0   my $self = shift;
407 0           my $param = shift;
408 0 0         return () if ( !$param );
409 0 0         return @$param if ( ref($param) eq 'ARRAY' );
410 0           return ($param);
411             }
412              
413 0     0 1   sub xml_in { shift->{xml_in} }
414              
415 0     0 1   sub xml_out { shift->{xml_out} }
416              
417             # private helper function to create specialised closure
418             sub _cast {
419 0     0     my ($type, $val) = @_;
420 0     0     return sub { return { "$type" => $val }; };
  0            
421             }
422              
423             sub as_string {
424 0     0 1   return _cast( 'string', shift );
425             }
426              
427             sub as_int {
428 0     0 1   return _cast( 'int', int shift );
429             }
430              
431             sub as_i4 {
432 0     0 1   return _cast( 'i4', int shift );
433             }
434              
435             sub as_double {
436 0     0 1   return _cast( 'double', sprintf('%g', shift) );
437             }
438              
439             sub as_boolean {
440 0 0   0 1   return _cast( 'boolean', (shift) ? '1' : '0' );
441             }
442              
443             sub as_base64 {
444 0     0 1   chomp( my $base64 = encode_base64( shift ) );
445 0           return _cast( 'base64', $base64 );
446             }
447              
448             # converts epoch (or current time if undef) to dateTime.iso8601 (UTC)
449             sub as_dateTime_iso8601 {
450 0     0 1   my $epoch = shift;
451 0 0         $epoch = time() unless defined $epoch; # could be: "shift // time" with modern perl versions
452 0           my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime( $epoch );
453              
454 0           return _cast( 'dateTime.iso8601',
455             sprintf('%4d%02d%02dT%02d:%02d:%02dZ',
456             $year + 1900, $mon + 1, $mday, $hour, $min, $sec)
457             );
458             }
459              
460             1;