The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/bin/false

#
# BZ::Client::XMLRPC.pm - Performs XML-RPC calls on behalf of the client.
#

use strict;
use warnings 'all';

package BZ::Client::XMLRPC;

use LWP();
use XML::Writer();
use Encode;
use BZ::Client::XMLRPC::Parser();
use DateTime::Format::Strptime();
use DateTime::TimeZone();

our $VERSION = 1.02;
our $counter;
our $fmt = DateTime::Format::Strptime->new(pattern=> '%C%Y-%m-%dT%T', time_zone=>'UTC');
our $tz = DateTime::TimeZone->new(name => 'UTC');


sub new($%) {
    my $class = shift;
    my $self = { @_ };
    bless($self, ref($class) || $class);
    return $self;
}

sub url($;$) {
    my $self = shift;
    if (@_) {
        $self->{'url'} = shift;
    } else {
        return $self->{'url'};
    }
}

sub user_agent($;$) {
    my $self = shift;
    if (@_) {
        $self->{'user_agent'} = shift;
    } else {
        my $ua = $self->{'user_agent'};
        if (!defined($ua)) {
            $ua = LWP::UserAgent->new();
            $ua->agent("BZ::Client::XMLRPC $VERSION");
            $self->user_agent($ua);
        }
        return $ua;
    }
}

sub error($$;$$) {
    my($self, $message, $http_code, $xmlrpc_code) = @_;
    require BZ::Client::Exception;
    BZ::Client::Exception->throw('message' => $message,
                                 'http_code' => $http_code,
                                 'xmlrpc_code' => $xmlrpc_code);
}

sub value($$$) {
    my($self, $writer, $value) = @_;
    if (ref($value) eq 'HASH') {
        $writer->startTag('value');
        $writer->startTag('struct');
        for my $key (sort keys %$value) {
            $writer->startTag('member');
            $writer->startTag('name');
            $writer->characters($key);
            $writer->endTag('name');
            $self->value($writer, $value->{$key});
            $writer->endTag('member');
        }
        $writer->endTag('struct');
        $writer->endTag('value');
    } elsif (ref($value) eq 'ARRAY') {
        $writer->startTag('value');
        $writer->startTag('array');
        $writer->startTag('data');
        for my $val (@$value) {
            $self->value($writer, $val);
        }
        $writer->endTag('data');
        $writer->endTag('array');
        $writer->endTag('value');
    } elsif (ref($value) eq 'BZ::Client::XMLRPC::int') {
        $writer->startTag('value');
        $writer->startTag('i4');
        $writer->characters($$value);
        $writer->endTag('i4');
        $writer->endTag('value');
    } elsif (ref($value) eq 'BZ::Client::XMLRPC::boolean') {
        $writer->startTag('value');
        $writer->startTag('boolean');
        $writer->characters($$value ? '1' : '0');
        $writer->endTag('boolean');
        $writer->endTag('value');
    } elsif (ref($value) eq 'BZ::Client::XMLRPC::double') {
        $writer->startTag('value');
        $writer->startTag('double');
        $writer->characters($$value);
        $writer->endTag('double');
        $writer->endTag('value');
    } elsif (ref($value) eq 'DateTime') {
        my $clone = $value->clone();
        $clone->set_time_zone($tz);
        $clone->set_formatter($fmt);
        $writer->startTag('value');
        $writer->startTag('dateTime.iso8601');
        $writer->characters($clone->iso8601(). 'Z');
        $writer->endTag('dateTime.iso8601');
        $writer->endTag('value');
    } else {
        $writer->startTag('value');
        $writer->characters($value);
        $writer->endTag('value');
    }
}

sub create_request($$$) {
    my($self, $methodName, $params) = @_;
    my $contents;
    my $writer = XML::Writer->new(OUTPUT => \$contents, ENCODING => 'UTF-8');
    $writer->startTag('methodCall');
    $writer->startTag('methodName');
    $writer->characters($methodName);
    $writer->endTag('methodName');
    $writer->startTag('params');
    for my $param (@$params) {
        $writer->startTag('param');
        $self->value($writer, $param);
        $writer->endTag('param');
    }
    $writer->endTag('params');
    $writer->endTag('methodCall');
    $writer->end();
    return encode('utf8', $contents);
}

sub get_response($$) {
    my($self, $contents) = @_;
    return _get_response($self, { 'url' => $self->url() . '/xmlrpc.cgi',
                                  'contentType' => 'text/xml',
                                  'contents' => encode_utf8($contents) });
}

sub _get_response($$) {
    my($self, $params) = @_;
    my $url = $params->{'url'};
    my $contentType = $params->{'contentType'};
    my $contents = $params->{'contents'};
    if (ref($contents) eq 'ARRAY') {
        require URI;
        my $uri = URI->new('http:');
        $uri->query_form($contents);
        $contents = $uri->query();
    }

    my $req = HTTP::Request->new(POST => $url);
    $req->content_type($contentType);
    $req->content($contents);
    if ($self->{'request_only'}) {
        return $req;
    }
    my $ua = $self->user_agent();

    my($logDir,$logId) = $self->logDirectory();

    if ($logDir) {
        $logId = ++$counter;
        require File::Spec;
        my $fileName = File::Spec->catfile($logDir, "$$.$logId.request.log");
        if (open(my $fh, '>', $fileName)) {
	    for my $header ($req->header_field_names()) {
		for my $value ($req->header($header)) {
		    print $fh "$header: $value\n";
		}
	    }
	    if ($ua->cookie_jar()) {
		print $fh $ua->cookie_jar()->as_string();
	    }
	    print $fh "\n";
            print $fh $contents;
            close($fh);
        }
    }

    my $res = $ua->request($req);
    my $response = $res->is_success() ? $res->content() : undef;
    if ($logDir) {
        my $fileName = File::Spec->catfile($logDir, "$$.$logId.response.log");
        if (open(my $fh, '>', $fileName)) {
	    for my $header ($res->header_field_names()) {
		for my $value ($res->header($header)) {
		    print $fh "$header: $value\n";
		}
	    }
	    print $fh "\n";
	    if ($res->is_success) {
		print $fh $response;
	    }
            close($fh);
        }
    }
    if (!$res->is_success()) {
        my $msg = $res->status_line();
        my $code = $res->code();
        if ($code == 401) {
           $self->error('Authorization error, perhaps invalid user name and/or password', $code);
        } elsif ($code == 404) {
           $self->error('Bugzilla server not found, perhaps invalid URL.', $code);
        } else {
           $self->error("Unknown error: $msg", $code);
        }
    }

    return $response;
}

sub parse_response($$) {
    my($self, $contents) = @_;
    my $parser = BZ::Client::XMLRPC::Parser->new();
    return $parser->parse($contents);
}

sub request($%) {
    my $self = shift;
    my %args = @_;
    my $methodName = $args{'methodName'};
    $self->error('Missing argument: methodName') unless defined($methodName);
    my $params = $args{'params'};
    $self->error('Missing argument: params') unless defined($params);
    $self->error('Invalid argument: params (Expected array)') unless ref($params) eq 'ARRAY';
    my $contents = $self->create_request($methodName, $params);
    $self->log('debug', "BZ::Client::XMLRPC::request: Sending method $methodName to " . $self->url());
    my $response = $self->get_response($contents);
    $self->log('debug', "BZ::Client::XMLRPC::request: Got result for method $methodName");
    return $self->parse_response($response);
}

sub log($$$) {
    my($self, $level, $msg) = @_;
    my $logger = $self->logger();
    if ($logger) {
        &$logger($level, $msg);
    }
}

sub logger($;$) {
    my($self) = shift;
    if (@_) {
        $self->{'logger'} = shift;
    } else {
        return $self->{'logger'};
    }
}

sub logDirectory($;$) {
    my($self) = shift;
    if (@_) {
        $self->{'logDirectory'} = shift;
    } else {
        return $self->{'logDirectory'};
    }
}

package BZ::Client::XMLRPC::int;

sub new($$) {
    my($class, $value) = @_;
    return bless(\$value, (ref($class) || $class));
}

package BZ::Client::XMLRPC::boolean;

sub new($$) {
    my($class, $value) = @_;
    return bless(\$value, (ref($class) || $class));
}

use constant TRUE => BZ::Client::XMLRPC::boolean->new(1);
use constant FALSE => BZ::Client::XMLRPC::boolean->new(0);

package BZ::Client::XMLRPC::double;

sub new($$) {
    my($class, $value) = @_;
    return bless(\$value, (ref($class) || $class));
}

1;

=pod

=head1 NAME

  BZ::Client::XMLRPC - Performs XML-RPC calls on behalf of the client.

=head1 SYNOPSIS

  my $xmlrpc = BZ::Client::XMLRPC->new('url' => $url);
  my $result = $xmlrpc->request('methodName' => $methodName, 'params' => $params);

An instance of BZ::Client::XMLRPC is able to perform XML-RPC calls against the
given URL. A request is performed by passing the method name and the method
parameters to the method L</request>. The request result is returned.

=head1 CLASS METHODS

This section lists the possible class methods.

=head2 new

  my $xmlrpc = BZ::Client::XMLRPC->new('url' => $url);

Creates a new instance with the given URL.

=head1 INSTANCE METHODS

This section lists the possible instance methods.

=head2 url

  my $url = $xmlrpc->url();
  $xmlrpc->url($url);

Returns or sets the XML-RPC servers URL.

=head2 request

  my $result = $xmlrpc->request('methodName' => $methodName, 'params' => $params);

Calls the XML-RPC servers method C<$methodCall>, passing the parameters given by
C<$params>, an array of parameters. Parameters may be hash refs, array refs, or
atomic values. Array refs and hash refs may recursively contain array or hash
refs as values. An instance of L<BZ::Client::Exception> is thrown in case of
errors.

=head1 SEE ALSO

  L<BZ::Client>, L<BZ::Client::Exception>