=head1 NAME XML::RPC -- Pure Perl implementation for an XML-RPC client and server. =head1 SYNOPSIS make a call to an XML-RPC server: use XML::RPC; my $xmlrpc = XML::RPC->new('http://betty.userland.com/RPC2'); my $result = $xmlrpc->call( 'examples.getStateStruct', { state1 => 12, state2 => 28 } ); create an XML-RPC service: use XML::RPC; use CGI; my $q = new CGI; my $xmlrpc = XML::RPC->new(); my $xml = $q->param('POSTDATA'); print $q->header( -type => 'text/xml', -charset => 'UTF-8' ); print $xmlrpc->receive( $xml, \&handler ); sub handler { my ( $methodname, @params ) = @_; return { you_called => $methodname, with_params => \@params }; } =head1 WARNING 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. 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. =head1 DESCRIPTION XML::RPC module provides simple Pure Perl methods for XML-RPC communication. It's goals are simplicity and flexibility. XML::RPC uses XML::TreePP for parsing. This version of XML::RPC merges the changes from XML::RPC::CustomUA. =head1 CONSTRUCTOR AND OPTIONS =head2 $xmlrpc = XML::RPC->new(); This constructor method returns a new XML::RPC object. Usable for XML-RPC servers. =head2 $xmlrpc = XML::RPC->new( 'http://betty.userland.com/RPC2', %options ); Its first argument is the full URL for your server. The second argument is for options passing to XML::TreePP, for example: output_encoding => 'ISO-8859-1' (default is UTF-8). You can also define the UserAgent string, for example: my $rpcfoo = XML::RPC->new($apiurl, ('User-Agent' => 'Baz/3000 (Mozilla/1.0; FooBar phone app)')); =head1 METHODS =head2 $xmlrpc->credentials( 'username', 'password ); Set Credentials for HTTP Basic Authentication. This is only secure over HTTPS. Please, please, please do not use this over unencrypted connections! =head2 $xmlrpc->call( 'method_name', @arguments ); This method calls the provides XML-RPC server's method_name with @arguments. It will return the server method's response. =head2 $xmlrpc->receive( $xml, \&handler ); This parses an incoming XML-RPC methodCall and call the \&handler subref with parameters: $methodName and @parameters. =head2 $xmlrpc->errstr(); Returns the last HTTP status code (200 when no remote call has happened yet). Can return 999 for some internal errors =head2 $xmlrpc->xml_in(); Returns the last XML that went in the client. =head2 $xmlrpc->xml_out(); Returns the last XML that went out the client. =head2 $xmlrpc->indent(indentsize); Sets the xmlout indentation =head1 CUSTOM TYPES =head2 $xmlrpc->call( 'method_name', { data => sub { { 'base64' => encode_base64($data) } } } ); When passing a CODEREF to a value XML::RPC will simply use the returned hashref as a type => value pair. =head1 TYPECASTING Sometimes a value type might not be clear from the value alone, typecasting provides a way to "force" a value to a certain type =head2 as_string Forces a value to be cast as string. $xmlrpc->call( 'gimmeallyourmoney', { cardnumber => as_string( 12345 ) } ); =head2 as_int Forces a value to be cast as int =head2 as_i4 Forces a value to be cast as i4 =head2 as_double Forces a value to be cast as double =head2 as_boolean Forces a value to be cast as boolean =head2 as_base64 Forces a value to be cast as base64 =head2 as_dateTime_iso8601 Forces a value to be cast as ISO8601 Datetime =head1 ERROR HANDLING To provide an error response you can simply die() in the \&handler function. Also you can set the $XML::RPC::faultCode variable to a (int) value just before dieing. =head1 PROXY SUPPORT Default XML::RPC will try to use LWP::Useragent for requests, you can set the environment variable: CGI_HTTP_PROXY to set a proxy. =head1 LIMITATIONS XML::RPC will not create "bool", "dateTime.iso8601" or "base64" types automatically. They will be parsed as "int" or "string". You can use the CODE ref to create these types. =head1 AUTHOR Original author: Niek Albers, http://www.daansystems.com/ Current author: Rene Schickbauer, https://cavac.at =head1 COPYRIGHT AND LICENSE Copyright (c) 2007-2008 Niek Albers. All rights reserved. This program Copyright (c) 2012-2022 Rene Schickbauer This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut package XML::RPC; use strict; use XML::TreePP; use MIME::Base64; use Time::Local; use vars qw($VERSION $faultCode); no strict 'refs'; $VERSION = 2.1; $faultCode = 0; sub new { my $package = shift; my $self = {}; bless $self, $package; $self->{url} = shift; $self->{tpp} = XML::TreePP->new(@_); $self->{laststatus} = '200'; return $self; } sub indent { my $self = shift || return; $self->{tpp}->set( indent => shift ); } sub credentials { my ($self, $username, $password) = @_; my $authtoken = 'Basic ' . encode_base64($username . ':' . $password, ''); $self->{authtoken} = $authtoken; return; } sub call { my $self = shift; my ( $methodname, @params ) = @_; die 'no url' if ( !$self->{url} ); $faultCode = 0; my $xml_out = $self->create_call_xml( $methodname, @params ); $self->{xml_out} = $xml_out; my %header = ( 'Content-Type' => 'text/xml', 'User-Agent' => defined($self->{tpp}->{'User-Agent'}) ? $self->{tpp}->{'User-Agent'} : 'XML-RPC/' . $VERSION, 'Content-Length' => length($xml_out) ); if(defined($self->{authtoken})) { $header{'Authorization'} = $self->{authtoken} } my ( $result, $xml_in, $httpstatus ); my $parseok = 0; eval { ( $result, $xml_in, $httpstatus ) = $self->{tpp}->parsehttp( POST => $self->{url}, $xml_out, \%header, ); $parseok = 1; }; if(!$parseok) { my $err = $@; $httpstatus = '999'; die($err); }; $self->{laststatus} = $httpstatus; $self->{xml_in} = $xml_in; my @data = $self->unparse_response($result); return @data == 1 ? $data[0] : @data; } sub errstr { my $self = shift; return $self->{laststatus}; } sub receive { my $self = shift; my $result = eval { my $xml_in = shift || die 'no xml'; $self->{xml_in} = $xml_in; my $handler = shift || die 'no handler'; my $hash = $self->{tpp}->parse($xml_in); my ( $methodname, @params ) = $self->unparse_call($hash); $self->create_response_xml( $handler->( $methodname, @params ) ); }; $result = $self->create_fault_xml($@) if ($@); $self->{xml_out} = $result; return $result; } sub create_fault_xml { my $self = shift; my $error = shift; chomp($error); return $self->{tpp} ->write( { methodResponse => { fault => $self->parse( { faultString => $error, faultCode => int($faultCode) } ) } } ); } sub create_call_xml { my $self = shift; my ( $methodname, @params ) = @_; return $self->{tpp}->write( { methodCall => { methodName => $methodname, params => { param => [ map { $self->parse($_) } @params ] } } } ); } sub create_response_xml { my $self = shift; my @params = @_; return $self->{tpp}->write( { methodResponse => { params => { param => [ map { $self->parse($_) } @params ] } } } ); } sub parse { my $self = shift; my $p = shift; my $result; if ( ref($p) eq 'HASH' ) { $result = $self->parse_struct($p); } elsif ( ref($p) eq 'ARRAY' ) { $result = $self->parse_array($p); } elsif ( ref($p) eq 'CODE' ) { $result = $p->(); } else { $result = $self->parse_scalar($p); } return { value => $result }; } sub parse_scalar { my $self = shift; my $scalar = shift; local $^W = undef; if ( ( $scalar =~ m/^[\-+]?(0|[1-9]\d*)$/ ) && ( abs($scalar) <= ( 0xffffffff >> 1 ) ) ) { return { i4 => $scalar }; } elsif ( $scalar =~ m/^[\-+]?\d+\.\d+$/ ) { return { double => $scalar }; } else { return { string => \$scalar }; } } sub parse_struct { my $self = shift; my $hash = shift; return { struct => { member => [ map { { name => $_, %{ $self->parse( $hash->{$_} ) } } } keys(%$hash) ] } }; } sub parse_array { my $self = shift; my $array = shift; return { array => { data => { value => [ map { $self->parse($_)->{value} } $self->list($array) ] } } }; } sub unparse_response { my $self = shift; my $hash = shift; my $response = $hash->{methodResponse} || die 'no data'; if ( $response->{fault} ) { return $self->unparse_value( $response->{fault}->{value} ); } else { return map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} ); } } sub unparse_call { my $self = shift; my $hash = shift; my $response = $hash->{methodCall} || die 'no data'; my $methodname = $response->{methodName}; my @args = map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} ); return ( $methodname, @args ); } sub unparse_value { my $self = shift; my $value = shift; my $result; return $value if ( ref($value) ne 'HASH' ); # for unspecified params if ( $value->{struct} ) { $result = $self->unparse_struct( $value->{struct} ); return !%$result ? undef : $result; # fix for empty hashrefs from XML::TreePP } elsif ( $value->{array} ) { return $self->unparse_array( $value->{array} ); } else { return $self->unparse_scalar($value); } } sub unparse_scalar { my $self = shift; my $scalar = shift; my ($result) = values(%$scalar); return ( ref($result) eq 'HASH' && !%$result ) ? undef : $result; # fix for empty hashrefs from XML::TreePP } sub unparse_struct { my $self = shift; my $struct = shift; return { map { $_->{name} => $self->unparse_value( $_->{value} ) } $self->list( $struct->{member} ) }; } sub unparse_array { my $self = shift; my $array = shift; my $data = $array->{data}; return [ map { $self->unparse_value($_) } $self->list( $data->{value} ) ]; } sub list { my $self = shift; my $param = shift; return () if ( !$param ); return @$param if ( ref($param) eq 'ARRAY' ); return ($param); } sub xml_in { shift->{xml_in} } sub xml_out { shift->{xml_out} } # private helper function to create specialised closure sub _cast { my ($type, $val) = @_; return sub { return { "$type" => $val }; }; } sub as_string { return _cast( 'string', shift ); } sub as_int { return _cast( 'int', int shift ); } sub as_i4 { return _cast( 'i4', int shift ); } sub as_double { return _cast( 'double', sprintf('%g', shift) ); } sub as_boolean { return _cast( 'boolean', (shift) ? '1' : '0' ); } sub as_base64 { chomp( my $base64 = encode_base64( shift ) ); return _cast( 'base64', $base64 ); } # converts epoch (or current time if undef) to dateTime.iso8601 (UTC) sub as_dateTime_iso8601 { my $epoch = shift; $epoch = time() unless defined $epoch; # could be: "shift // time" with modern perl versions my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime( $epoch ); return _cast( 'dateTime.iso8601', sprintf('%4d%02d%02dT%02d:%02d:%02dZ', $year + 1900, $mon + 1, $mday, $hour, $min, $sec) ); } 1;