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