File Coverage

blib/lib/Mojo/XMLRPC.pm
Criterion Covered Total %
statement 122 129 94.5
branch 77 88 87.5
condition 13 15 86.6
subroutine 18 18 100.0
pod 4 4 100.0
total 234 254 92.1


line stmt bran cond sub pod time code
1             package Mojo::XMLRPC;
2              
3 3     3   1226738 use Mojo::Base -strict;
  3         23  
  3         28  
4              
5 3     3   473 use B ();
  3         7  
  3         58  
6 3     3   514 use Mojo::ByteStream;
  3         3909  
  3         118  
7 3     3   525 use Mojo::Date;
  3         4421  
  3         29  
8 3     3   651 use Mojo::DOM;
  3         17560  
  3         87  
9 3     3   555 use Mojo::JSON;
  3         22744  
  3         120  
10 3     3   1095 use Mojo::Template;
  3         68085  
  3         33  
11 3     3   131 use Scalar::Util ();
  3         7  
  3         74  
12              
13 3     3   1563 use Mojo::XMLRPC::Base64;
  3         10  
  3         17  
14 3     3   1331 use Mojo::XMLRPC::Message::Call;
  3         8  
  3         24  
15 3     3   1351 use Mojo::XMLRPC::Message::Response;
  3         9  
  3         24  
16              
17 3     3   173 use Exporter 'import';
  3         8  
  3         5949  
18              
19             our @EXPORT_OK = (qw[decode_xmlrpc encode_xmlrpc from_xmlrpc to_xmlrpc]);
20              
21             our $VERSION = '0.06';
22             $VERSION = eval $VERSION;
23              
24             my $message = Mojo::Template->new(
25             auto_escape => 1,
26             name => 'message',
27             namespace => __PACKAGE__,
28             )->parse(<<'TEMPLATE');
29             % my ($tag, $method, @args) = @_;
30            
31             <<%= $tag %>>
32             % if (defined $method) {
33             <%= $method %>
34             % }
35             % for my $arg (@args) {
36             %= $arg
37             % }
38             >
39             TEMPLATE
40              
41             my $fault = Mojo::Template->new(
42             auto_escape => 1,
43             name => 'fault',
44             namespace => __PACKAGE__,
45             )->parse(<<'TEMPLATE');
46             % my ($code, $string) = @_;
47            
48            
49            
50             faultCode
51             <%= $code // '' %>
52            
53            
54             faultString
55             <%= $string // '' %>
56            
57            
58            
59             TEMPLATE
60              
61             my $params = Mojo::Template->new(
62             auto_escape => 1,
63             name => 'params',
64             namespace => __PACKAGE__,
65             )->parse(<<'TEMPLATE');
66             % my (@params) = @_;
67            
68             % for my $param (@params) {
69             <%= $param %>
70             % }
71            
72             TEMPLATE
73              
74 4     4 1 29601 sub decode_xmlrpc { from_xmlrpc(Mojo::Util::decode 'UTF-8', $_[0]) }
75              
76 4     4 1 5277 sub encode_xmlrpc { Mojo::Util::encode 'UTF-8', to_xmlrpc(@_) }
77              
78             sub from_xmlrpc {
79 17     17 1 13955 my ($xml) = shift;
80              
81             # parse the XML document
82 17         86 my $dom = Mojo::DOM->new($xml);
83 17         14460 my $msg;
84              
85             # detect the message type
86 17         59 my $top = $dom->children->first;
87 17         1643 my $type = $top->tag;
88 17 100       288 if ($type eq 'methodCall') {
    50          
89 4         53 $msg = Mojo::XMLRPC::Message::Call->new;
90 4 50       32 if (defined(my $method = $top->children('methodName')->first)) {
91 4         2101 $msg->method_name($method->text);
92             }
93              
94             } elsif ($type eq 'methodResponse') {
95 13         59 $msg = Mojo::XMLRPC::Message::Response->new;
96 13 100       133 if (defined(my $fault = $top->children('fault')->first)) {
97 2         493 return $msg->fault(_decode_element($fault));
98             }
99              
100             } else {
101 0         0 die 'unknown type of message';
102             }
103              
104 15 50       2587 if (defined(my $params = $top->children('params')->first)) {
105 15         3610 $msg->parameters([ map { _decode_element($_) } @{ $params->children('param') } ]);
  15         3030  
  15         51  
106             }
107              
108 15         681 return $msg;
109             }
110              
111             sub to_xmlrpc {
112 41     41 1 109223 my ($type, @args) = @_;
113              
114 41 100 66     234 if (Scalar::Util::blessed($type) && $type->isa('Mojo::XMLRPC::Message')) {
115 3         9 my $obj = $type;
116 3 100       17 if ($obj->isa('Mojo::XMLRPC::Message::Call')) {
    50          
117 1         4 $type = 'call';
118 1         6 @args = ($obj->method_name, @{ $obj->parameters });
  1         13  
119             } elsif ($obj->isa('Mojo::XMLRPC::Message::Response')) {
120 2 100       8 $type = $obj->is_fault ? 'fault' : 'response';
121 2 100       19 @args = $obj->is_fault ? @{ $obj->fault }{qw/faultCode faultString/} : @{ $obj->parameters };
  1         7  
  1         11  
122             } else {
123 0         0 die 'Message type not understood';
124             }
125             }
126              
127 41 100       156 my $tag = $type eq 'call' ? 'methodCall' : 'methodResponse';
128 41 100       104 my $method = $type eq 'call' ? shift @args : undef;
129              
130             my $xml =
131             $type eq 'fault' ? $fault->process(@args) :
132 41 100       153 @args ? $params->process(map { _encode_item($_) } @args) :
  39 100       94  
133             undef;
134              
135 41 100       7665 return $message->process($tag, $method, defined($xml) ? Mojo::ByteStream->new($xml) : ());
136             }
137              
138             sub _decode_element {
139 71     71   4410 my $elem = shift;
140 71         143 my $tag = $elem->tag;
141              
142 71 100 100     1233 if ($tag eq 'param' || $tag eq 'value' || $tag eq 'fault') {
    100 100        
    100 100        
    100 66        
    100          
    50          
    100          
    100          
    100          
    50          
143 45         98 my $children = $elem->children;
144              
145             # elements with no children must be strings
146 45 100       2880 return $elem->text unless @$children;
147              
148             # otherwise they are another element
149 43         99 return _decode_element($children->first);
150              
151             } elsif ($tag eq 'array') {
152 1         4 my $data = $elem->children('data')->first;
153 1         191 return [ map { _decode_element($_) } @{ $data->children('value') } ];
  5         924  
  1         3  
154              
155             } elsif ($tag eq 'struct') {
156             return +{
157             map {;
158 6         1095 $_->children('name')->first->text, # key
159             _decode_element($_->children('value')->first) # value
160             }
161 3         6 @{ $elem->children('member') } # pairs
  3         11  
162             };
163              
164             } elsif ($tag eq 'int' || $tag eq 'i4') {
165 9         28 return $elem->text + 0;
166              
167             } elsif ($tag eq 'string' || $tag eq 'name') {
168 5         17 return $elem->text;
169              
170             } elsif ($tag eq 'double') {
171 0         0 return $elem->text / 1.0;
172              
173             } elsif ($tag eq 'boolean') {
174 3 100       11 return $elem->text ? Mojo::JSON::true : Mojo::JSON::false;
175              
176             } elsif ($tag eq 'nil') {
177 1         8 return undef;
178              
179             } elsif ($tag eq 'dateTime.iso8601') {
180 3         10 my $date = Mojo::Date->new($elem->text);
181 3 100       337 unless ($date->epoch) {
182 2         765 require Time::Piece;
183 2         7293 my $text = $elem->text;
184 2         72 PARSE: for my $time_format ('%H:%M:%S', '%H%M%S') {
185 3         6 for my $calendar_format ('%Y%m%d', '%Y-%m-%d') {
186 5         10 my $format = $calendar_format . 'T' . $time_format;
187 5         7 eval {
188 5         14 $date->epoch(Time::Piece->strptime($text, $format)->epoch);
189             };
190 5 100       265 last PARSE unless $@;
191             }
192             }
193             }
194 3         34 return $date;
195             } elsif ($tag eq 'base64') {
196 1         4 return Mojo::XMLRPC::Base64->new(encoded => $elem->text);
197             }
198              
199 0         0 die "unknown tag: $tag";
200             }
201              
202             sub _encode_item {
203 44     44   68 my $item = shift;
204 44         64 my $ret;
205              
206 44 100       109 if (ref $item) {
207 9 100       45 if (Scalar::Util::blessed $item) {
    100          
    100          
    50          
208 5 50       85 if ($item->can('TO_XMLRPC')) {
    100          
    100          
    50          
    0          
209 0         0 return _encode_item($item->TO_XMLRPC);
210              
211             } elsif ($item->isa('JSON::PP::Boolean')) {
212 2 100       38 my $val = $item ? 1 : 0;
213 2         26 $ret = "$val";
214              
215             } elsif ($item->isa('Mojo::Date')) {
216 1         8 my $date = $item->to_datetime;
217 1         35 $ret = "$date";
218              
219             } elsif ($item->isa('Mojo::XMLRPC::Base64')) {
220 2         37 $ret = "$item";
221              
222             } elsif ($item->can('TO_JSON')) {
223 0         0 return _encode_item($item->TO_JSON);
224              
225             } else {
226 0         0 return _encode_item("$item");
227             }
228             } elsif (ref $item eq 'ARRAY') {
229 1         4 $ret = join '', map { '' . _encode_item($_) . '' } @$item;
  3         28  
230 1         16 $ret = "$ret";
231              
232             } elsif (ref $item eq 'HASH') {
233             $ret = join '', map {
234 1         5 my $name = Mojo::Util::xml_escape($_);
  2         20  
235 2         25 "$name" . _encode_item($item->{$_}) . '';
236             } keys %$item;
237 1         16 $ret = "$ret";
238              
239             } elsif (ref $item eq 'SCALAR') {
240 2 100       9 my $val = $$item ? 1 : 0;
241 2         7 $ret = "$val";
242             }
243             }
244             else {
245 35         136 my $sv = B::svref_2object(\$item);
246              
247 35 100       354 $ret =
    100          
    100          
248             !defined $item ? '' :
249             $sv->FLAGS & B::SVf_NOK ? "$item" :
250             $sv->FLAGS & B::SVf_IOK ? "$item" :
251             '' . Mojo::Util::xml_escape($item) . '';
252             }
253              
254 44         378 return Mojo::ByteStream->new($ret);
255             }
256              
257             1;
258              
259             =encoding utf8
260              
261             =head1 NAME
262              
263             Mojo::XMLRPC - An XMLRPC message parser/encoder using the Mojo stack
264              
265             =head1 SYNOPSIS
266              
267             use Mojo::UserAgent;
268             use Mojo::XMLRPC qw[to_xmlrpc from_xmlrpc];
269              
270             my $ua = Mojo::UserAgent->new;
271             my $url = ...;
272             my $tx = $ua->post($url, encode_xmlrpc(call => 'mymethod', 'myarg'));
273             my $res = decode_xmlrpc($tx->res->body)
274              
275             =head1 DESCRIPTION
276              
277             L is a pure-perl XML-RPC message parser and encoder.
278             It uses tools from the L toolkit to do all of the work.
279              
280             This does not mean that it must only be used in conjunction with a L app, far from it.
281             Feel free to use it in any circumstance that needs XML-RPC messages.
282              
283             =head1 MAPPING
284              
285             The mapping between Perl types and XMLRPC types is not perfectly one-to-one, especially given Perl's scalar types.
286             The following is a description of the procedure used to encode and decode XMLRPC message from/to Perl.
287              
288             =head2 Perl to XMLRPC
289              
290             If the item is a blessed reference:
291              
292             =over
293              
294             =item *
295              
296             If the item/object implements a C method, it is called and the result is encoded.
297              
298             =item *
299              
300             If the item is a L, as the L booleans are, it is encoded as a C.
301              
302             =item *
303              
304             If the item is a L then it is encoded as a C.
305              
306             =item *
307              
308             If the item is a L then it is encode as a C.
309             This wrapper class is used to distinguish a string from a base64 and aid in encoding/decoding.
310              
311             =item
312              
313             If the item/object implements a C method, it is called and the result is encoded.
314              
315             =item
316              
317             If none of the above cases are true, the item is stringified and encoded as a C.
318              
319             =back
320              
321             If the item is an unblessed reference:
322              
323             =over
324              
325             =item *
326              
327             An array reference is encoded as an C.
328              
329             =item *
330              
331             A hash reference is encoded as a C.
332              
333             =item *
334              
335             A scalar reference is encoded as a C depending on the truthiness of the referenced value.
336             This is the standard shortcut seen in JSON modules allowing C<\1> for true and C<\0> for false.
337              
338             =back
339              
340             If the item is a non-reference scalar:
341              
342             =over
343              
344             =item *
345              
346             If the item is undefined it is encoded as C<< >>.
347              
348             =item *
349              
350             If the item has C (it has been used as a floating point number) it is encoded as C.
351              
352             =item *
353              
354             If the item has C (it has been used as an integer (and not a float)) it is encoded as an C.
355              
356             =item *
357              
358             All other values are encoded as C.
359              
360             =back
361              
362             =head2 XMLRPC to Perl
363              
364             Most values decode back into Perl in a manner that would survive a round trip.
365             The exceptions are blessed objects that implement C or C or are stringified.
366             The shortcuts for booleans will round-trip to being L booleans objects.
367              
368             Values encoded as integers will not be truncated via C however no attempt is made to upgrade them to C or C.
369             Values encoded as floating point C will be forcably upgraded to C (by dividing by 1.0).
370             This is so that an integer value encoded as a floating point will round trip, the reverse case isn't as useful and thus isn't handled.
371              
372             =head1 FUNCTIONS
373              
374             =head2 decode_xmlrpc
375              
376             Like L but first decodes from UTF-8 encoded bytes.
377              
378             =head2 encode_xmlrpc
379              
380             Like L but encodes the result to UTF-8 encoded bytes.
381              
382             =head2 from_xmlrpc
383              
384             Takes a character string, interprets it, and returns a L containing the result.
385             If the input is UTF-8 encoded bytes, you can use L instead.
386              
387             =head2 to_xmlrpc
388              
389             Generates an XMLRPC message from data passed to the function.
390             The input may be a L or it could be of the following form.
391              
392             =over
393              
394             =item *
395              
396             A message type, one of C, C, C.
397              
398             =item *
399              
400             If the message type is C, then the method name.
401              
402             =item *
403              
404             If the message is not a C, then all remaining arguments are parameters.
405             If the message is a C, then the fault code followed by the fault string, all remaining arguments are ignored.
406              
407             =back
408              
409             The return value is a character string.
410             To generate UTF-8 encoded bytes, you can use L instead.
411              
412             =head1 THANKS
413              
414             This module was inspired by L written by Sébastien Aperghis-Tramoni.
415              
416             L was a port of that module initially to use the L module rather than L.
417             By the time port to the Mojo stack was complete, the module was entirely rewritten.
418             That said, the algorithm still owes a debt of gratitude to that one.
419              
420             =head1 SOURCE REPOSITORY
421              
422             L
423              
424             =head1 AUTHOR
425              
426             Joel Berger, Ejoel.a.berger@gmail.comE
427              
428             =head1 CONTRIBUTORS
429              
430             Andreas Vögele (voegelas)
431              
432             =head1 COPYRIGHT AND LICENSE
433              
434             Copyright (C) 2017 by L and L
435             This library is free software; you can redistribute it and/or modify
436             it under the same terms as Perl itself.