File Coverage

blib/lib/BZ/Client/XMLRPC.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             #!/bin/false
2             # PODNAME: BZ::Client::XMLRPC
3             # ABSTRACT: Performs XML-RPC calls on behalf of the client.
4             # vim: softtabstop=4 tabstop=4 shiftwidth=4 ft=perl expandtab smarttab
5              
6 1     1   1886 use strict;
  1         2  
  1         34  
7 1     1   5 use warnings 'all';
  1         2  
  1         54  
8              
9             package BZ::Client::XMLRPC;
10             $BZ::Client::XMLRPC::VERSION = '4.4002';
11              
12 1     1   429 use URI;
  1         3483  
  1         26  
13 1     1   419 use Encode qw/ encode_utf8 /;
  1         7267  
  1         59  
14 1     1   427 use XML::Writer;
  1         9409  
  1         24  
15 1     1   564 use HTTP::Tiny;
  1         28839  
  1         33  
16 1     1   7 use File::Spec;
  1         2  
  1         20  
17 1     1   407 use BZ::Client::Exception;
  1         2  
  1         22  
18 1     1   332 use BZ::Client::XMLRPC::Parser;
  0            
  0            
19             use DateTime::Format::Strptime;
20             use DateTime::TimeZone;
21              
22              
23              
24             my $counter;
25             my $fmt = DateTime::Format::Strptime->new(
26             pattern => '%C%Y-%m-%dT%T',
27             time_zone => 'UTC' );
28             my $tz = DateTime::TimeZone->new( name => 'UTC' );
29              
30              
31             sub new {
32             my $class = shift;
33             my $self = { @_ };
34             bless($self, ref($class) || $class);
35             return $self
36             }
37              
38             sub url {
39             my $self = shift;
40             if (@_) {
41             $self->{'url'} = shift;
42             }
43             else {
44             return $self->{'url'};
45             }
46             }
47              
48             sub web_agent {
49             my $self = shift;
50             if (@_) {
51             $self->{'web_agent'} = shift;
52             }
53             else {
54             my $wa = $self->{'web_agent'};
55             my $connect = $self->{'connect'} || {};
56             $self->error(q/'connect' parameter in new() not a hashref/)
57             unless ref $connect eq 'HASH';
58             if (!defined($wa)) {
59             $wa = HTTP::Tiny->new(
60             %$connect,
61             agent => sprintf('BZ::Client::XMLRPC %s (perl %s; %s)',
62             $BZ::Client::XMLRPC::VERSION, $^V, $^O)
63             );
64             $self->web_agent($wa);
65             }
66             return $wa;
67             }
68             }
69              
70             sub error {
71             my($self, $message, $http_code, $xmlrpc_code) = @_;
72             BZ::Client::Exception->throw('message' => $message,
73             'http_code' => $http_code,
74             'xmlrpc_code' => $xmlrpc_code)
75             }
76              
77             {
78              
79             my %actions = (
80              
81             'HASH' => sub {
82             my($self, $writer, $value) = @_;
83             $writer->startTag('value');
84             $writer->startTag('struct');
85             for my $key (sort keys %$value) {
86             $writer->startTag('member');
87             $writer->startTag('name');
88             $writer->characters($key);
89             $writer->endTag('name');
90             $self->value($writer, $value->{$key});
91             $writer->endTag('member');
92             }
93             $writer->endTag('struct');
94             $writer->endTag('value');
95             },
96              
97             'ARRAY' => sub {
98             my($self, $writer, $value) = @_;
99             $writer->startTag('value');
100             $writer->startTag('array');
101             $writer->startTag('data');
102             for my $val (@$value) {
103             $self->value($writer, $val);
104             }
105             $writer->endTag('data');
106             $writer->endTag('array');
107             $writer->endTag('value');
108             },
109              
110             'BZ::Client::XMLRPC::int' => sub {
111             my($self, $writer, $value) = @_;
112             $writer->startTag('value');
113             $writer->startTag('i4');
114             $writer->characters( $value->stringify() );
115             $writer->endTag('i4');
116             $writer->endTag('value');
117             },
118              
119             'BZ::Client::XMLRPC::base64' => sub {
120             my($self, $writer, $value) = @_;
121             $writer->startTag('value');
122             $writer->startTag('base64');
123             $writer->characters( $value->base64() );
124             $writer->endTag('base64');
125             $writer->endTag('value');
126             },
127              
128             'BZ::Client::XMLRPC::boolean' => sub {
129             my($self, $writer, $value) = @_;
130             $writer->startTag('value');
131             $writer->startTag('boolean');
132             $writer->characters( $value->stringify() );
133             $writer->endTag('boolean');
134             $writer->endTag('value');
135             },
136              
137             'BZ::Client::XMLRPC::double' => sub {
138             my($self, $writer, $value) = @_;
139             $writer->startTag('value');
140             $writer->startTag('double');
141             $writer->characters( $value->stringify() );
142             $writer->endTag('double');
143             $writer->endTag('value');
144             },
145              
146             'DateTime' => sub {
147             my($self, $writer, $value) = @_;
148             my $clone = $value->clone();
149             $clone->set_time_zone($tz);
150             $clone->set_formatter($fmt);
151             $writer->startTag('value');
152             $writer->startTag('dateTime.iso8601');
153             $writer->characters($clone->iso8601(). 'Z');
154             $writer->endTag('dateTime.iso8601');
155             $writer->endTag('value');
156             },
157              
158             );
159              
160             sub value {
161             my($self, $writer, $value) = @_;
162              
163             if ($actions{ ref($value) }) {
164             $actions{ ref($value) }->($self, $writer, $value);
165             }
166             else {
167             $writer->startTag('value');
168             $writer->characters($value);
169             $writer->endTag('value');
170             }
171             }
172              
173             }
174              
175             sub create_request {
176             my($self, $methodName, $params) = @_;
177             my $contents;
178             my $writer = XML::Writer->new(
179             OUTPUT => \$contents,
180             ENCODING => 'UTF-8' );
181             $writer->startTag('methodCall');
182             $writer->startTag('methodName');
183             $writer->characters($methodName);
184             $writer->endTag('methodName');
185             $writer->startTag('params');
186             for my $param (@$params) {
187             $writer->startTag('param');
188             $self->value($writer, $param);
189             $writer->endTag('param');
190             }
191             $writer->endTag('params');
192             $writer->endTag('methodCall');
193             $writer->end();
194             return encode_utf8($contents)
195             }
196              
197             sub get_response {
198             my($self, $contents) = @_;
199             return _get_response(
200             $self,
201             {
202             'url' => $self->url() . '/xmlrpc.cgi',
203             'contentType' => 'text/xml',
204             'contents' => encode_utf8($contents)
205             }
206             )
207             }
208              
209             sub _get_response {
210             my($self, $params) = @_;
211             my $url = $params->{'url'};
212             my $contentType = $params->{'contentType'};
213             my $contents = $params->{'contents'};
214             if (ref($contents) eq 'ARRAY') {
215             my $uri = URI->new('http:');
216             $uri->query_form($contents);
217             $contents = $uri->query();
218             }
219              
220             my %options = (
221             headers => {
222             'Content-Type' => $contentType,
223             },
224             content => $contents, # carefull of the s
225             );
226              
227             my $wa = $self->web_agent();
228              
229             my($logDir,$logId) = $self->logDirectory();
230              
231             if ($logDir) {
232             $logId = ++$counter;
233             my $fileName = File::Spec->catfile($logDir, "$$.$logId.request.log");
234             if (open(my $fh, '>', $fileName)) {
235             while (my($header,$value) = each %{$options{headers}} ) {
236             print $fh "$header: $value\n";
237             }
238             print $fh 'user-agent: ', $wa->agent(), "\n";
239             if ($wa->{cookie_jar}) {
240             print $fh join("\n", $wa->{cookie_jar}->dump_cookies());
241             }
242             print $fh "\n";
243             print $fh $contents;
244             close($fh);
245             }
246             }
247              
248             my $res = $wa->request(POST => $url, \%options);
249             my $response = $res->{success} ? $res->{content} : undef;
250             if ($logDir) {
251             my $fileName = File::Spec->catfile($logDir, "$$.$logId.response.log");
252             if (open(my $fh, '>', $fileName)) {
253             for my $header (sort keys %{$res->{headers}}) {
254             my $value = $res->{headers}->{$header};
255             if (ref $value) {
256             print $fh "$header: $_\n" for @$value;
257             }
258             else {
259             print $fh "$header: $value\n";
260             }
261             }
262             print $fh "\n";
263             print $fh $res->{content} if $res->{content};
264             close($fh);
265             }
266             }
267             if (!$res->{success}) {
268             my $code = $res->{status};
269             if ($code == 401) {
270             $self->error('Authorization error, perhaps invalid user name and/or password', $code);
271             }
272             elsif ($code == 404) {
273             $self->error('Bugzilla server not found, perhaps invalid URL.', $code);
274             }
275             else {
276             my $msg = $res->{reason};
277             $msg .= ' : ' . $res->{content} if $res->{content};
278             $self->error("Unknown error: $msg", $code);
279             }
280             }
281              
282             return $response
283             }
284              
285             sub parse_response {
286             my($self, $contents) = @_;
287             my $parser = BZ::Client::XMLRPC::Parser->new();
288             return $parser->parse($contents)
289             }
290              
291             sub request {
292             my $self = shift;
293             my %args = @_;
294             my $methodName = $args{'methodName'};
295             $self->error('Missing argument: methodName')
296             unless defined($methodName);
297             my $params = $args{'params'};
298             $self->error('Missing argument: params')
299             unless defined($params);
300             $self->error('Invalid argument: params (Expected array)')
301             unless ref($params) eq 'ARRAY';
302             my $contents = $self->create_request($methodName, $params);
303             $self->log('debug', "BZ::Client::XMLRPC::request: Sending method $methodName to " . $self->url());
304             my $response = $self->get_response($contents);
305             $self->log('debug', "BZ::Client::XMLRPC::request: Got result for method $methodName");
306             return $self->parse_response($response)
307             }
308              
309             sub log {
310             my($self, $level, $msg) = @_;
311             my $logger = $self->logger();
312             if ($logger) {
313             &$logger($level, $msg);
314             }
315             }
316              
317             sub logger {
318             my($self) = shift;
319             if (@_) {
320             $self->{'logger'} = shift;
321             }
322             else {
323             return $self->{'logger'};
324             }
325             }
326              
327             sub logDirectory {
328             my($self) = shift;
329             if (@_) {
330             $self->{'logDirectory'} = shift;
331             }
332             else {
333             return $self->{'logDirectory'};
334             }
335             }
336              
337             ### Objects to represent data types
338              
339             package BZ::Client::XMLRPC::int;
340             $BZ::Client::XMLRPC::int::VERSION = '4.4002';
341             sub new {
342             my($class, $value) = @_;
343             return bless(\$value, (ref($class) || $class))
344             }
345              
346             sub stringify {
347             my $self = shift;
348             return $$self
349             }
350              
351             package BZ::Client::XMLRPC::base64;
352             $BZ::Client::XMLRPC::base64::VERSION = '4.4002';
353             use MIME::Base64 qw( encode_base64 decode_base64 );
354              
355             sub new {
356             my($class, $value) = @_;
357             return bless(
358             {
359             raw => $value,
360             base64 => encode_base64($value, '')
361             },
362             (ref($class) || $class))
363             }
364              
365             sub new64 {
366             my($class, $value) = @_;
367             return bless(
368             {
369             raw => decode_base64($value),
370             base64 => $value
371             },
372             (ref($class) || $class))
373             }
374              
375             sub base64 {
376             my $self = shift;
377             return $self->{base64}
378             }
379              
380             sub raw {
381             my $self = shift;
382             return $self->{raw}
383             }
384              
385             package BZ::Client::XMLRPC::boolean;
386             $BZ::Client::XMLRPC::boolean::VERSION = '4.4002';
387             sub new {
388             my($class, $value) = @_;
389             return bless(\$value, (ref($class) || $class))
390             }
391              
392             sub stringify {
393             my $self = shift;
394             return $$self ? '1' : '0';
395             }
396              
397             {
398              
399             my $true = BZ::Client::XMLRPC::boolean->new(1);
400             my $false = BZ::Client::XMLRPC::boolean->new(0);
401              
402             sub TRUE { $true }
403             sub FALSE { $false }
404              
405             }
406              
407             package BZ::Client::XMLRPC::double;
408             $BZ::Client::XMLRPC::double::VERSION = '4.4002';
409             sub new {
410             my($class, $value) = @_;
411             return bless(\$value, (ref($class) || $class))
412             }
413              
414             sub stringify {
415             my $self = shift;
416             return $$self
417             }
418              
419             1;
420              
421             __END__
422              
423             =pod
424              
425             =encoding UTF-8
426              
427             =head1 NAME
428              
429             BZ::Client::XMLRPC - Performs XML-RPC calls on behalf of the client.
430              
431             =head1 VERSION
432              
433             version 4.4002
434              
435             =head1 SYNOPSIS
436              
437             my $xmlrpc = BZ::Client::XMLRPC->new( url => $url );
438             my $result = $xmlrpc->request( methodName => $methodName, params => $params );
439              
440             An instance of BZ::Client::XMLRPC is able to perform XML-RPC calls against the
441             given URL. A request is performed by passing the method name and the method
442             parameters to the method L</request>. The request result is returned.
443              
444             =head1 CLASS METHODS
445              
446             This section lists the possible class methods.
447              
448             =head2 new
449              
450             my $xmlrpc = BZ::Client::XMLRPC->new( url => $url );
451              
452             Creates a new instance with the given URL.
453              
454             =head1 INSTANCE METHODS
455              
456             This section lists the possible instance methods.
457              
458             =head2 url
459              
460             my $url = $xmlrpc->url();
461             $xmlrpc->url( $url );
462              
463             Returns or sets the XML-RPC servers URL.
464              
465             =head2 request
466              
467             my $result = $xmlrpc->request( methodName => $methodName, params => $params );
468              
469             Calls the XML-RPC servers method C<$methodCall>, passing the parameters given by
470             C<$params>, an array of parameters. Parameters may be hash refs, array refs, or
471             atomic values. Array refs and hash refs may recursively contain array or hash
472             refs as values. An instance of L<BZ::Client::Exception> is thrown in case of
473             errors.
474              
475             =head1 SEE ALSO
476              
477             L<BZ::Client>, L<BZ::Client::Exception>
478              
479             =head1 AUTHORS
480              
481             =over 4
482              
483             =item *
484              
485             Dean Hamstead <dean@bytefoundry.com.au>
486              
487             =item *
488              
489             Jochen Wiedmann <jochen.wiedmann@gmail.com>
490              
491             =back
492              
493             =head1 COPYRIGHT AND LICENSE
494              
495             This software is copyright (c) 2017 by Dean Hamstad.
496              
497             This is free software; you can redistribute it and/or modify it under
498             the same terms as the Perl 5 programming language system itself.
499              
500             =cut