# $Id: Comms.pm 153 2009-01-29 22:43:55Z Cosimo $

package HTTP::DAV::Comms;

use HTTP::DAV::Utils;
use HTTP::DAV::Response;
use LWP;
use URI;

$VERSION = sprintf( "%d.%02d", q$Revision: 0.21 $ =~ /(\d+)\.(\d+)/ );

use strict;
use vars qw($VERSION $DEBUG);

####
# Construct a new object and initialize it
sub new {
    my $class = shift;
    my $self = bless {}, ref($class) || $class;

    #print Data::Dumper->Dump( [$self] , [ '$self' ] );
    $self->_init(@_);
    return $self;
}

# Requires a reusable HTTP Agent.
# and some default headers, like, the user agent
sub _init {
    my ( $self, @p ) = @_;
    my ( $headers, $useragent )
        = HTTP::DAV::Utils::rearrange( [ 'HEADERS', 'USERAGENT' ], @p );

    # This is cached in this object here so that each http request
    # doesn't have to invoke a new useragent.
    $self->init_user_agent($useragent);

    $self->set_headers($headers);
}

sub init_user_agent {
    my ( $self, $useragent ) = @_;
    if ( defined $useragent ) {
        $self->{_user_agent} = $useragent;
    }
    else {
        $self->{_user_agent} = HTTP::DAV::UserAgent->new;
        $self->set_agent("DAV.pm/v$HTTP::DAV::VERSION");
    }
}

####
# GET/SET

# Sets a User-Agent as specified by user or as the default
sub set_agent {
    my ( $self, $agent ) = @_;
    $self->{_user_agent}->agent($agent);
}

sub set_header {
    my ( $self, $var, $val ) = @_;
    $self->set_headers() unless defined $self->{_headers};
    $self->{_headers}->header( $var, $val );
}

sub get_user_agent { $_[0]->{_user_agent}; }
sub get_headers    { $_[0]->{_headers}; }

sub set_headers {
    my ( $self, $headers ) = @_;

    if ( defined $headers && ref($headers) eq "HTTP::Headers" ) {
        $headers = HTTP::DAV::Headers->clone($headers);
    }
    else {
        $headers = HTTP::DAV::Headers->new;
    }

    $self->{_headers} = $headers;
}

sub _set_last_request  { $_[0]->{_last_request}  = $_[1]; }
sub _set_last_response { $_[0]->{_last_response} = $_[1]; }

# Save the Server: header line into this object instance
# We will want to use it later to workaround server bugs.
# For instance mod_dav has a bug in the Destination: header
# whereby it incorrectly throws "Bad Gateway" errors.
# The only way we can munge around this is if the copy() routine
# has some idea of the server it is talking to.
# So this routine stores the "Server: Apache..." line into a host:port hash (i.e. localhost:443).
# so $comms->_set_server_type( "host.org:443", "Apache/1.3.22 (Unix) DAV/1.0.2 ")
# yields
#     %_server_type = {
#        "host.org:443" => "Apache/1.3.22 (Unix) DAV/1.0.2 SSL"
#        "host.org:80" =>  "Apache/1.3.22 (Unix) DAV/1.0.2 "
#        };
# Note that this is an instance hash NOT a class hash.
# So each comms object will be learning independently.
sub _set_server_type { $_[0]->{_server_type}{ $_[1] } = $_[2]; }

# $server = $comms->get_server_type( "host.org:443" )
sub get_server_type { $_[0]->{_server_type}{ $_[1] } }

# Returns an HTTP::Request object
sub get_last_request { $_[0]->{_last_request}; }

# Returns an HTTP::DAV::Response object
sub get_last_response { $_[0]->{_last_response}; }

####
# Ensure there is a Host: header based on the URL
#
sub do_http_request {
    my ( $self, @p ) = @_;

    my ( $method, $url, $newheaders, $content, $save_to, $callback_func,
        $chunk )
        = HTTP::DAV::Utils::rearrange(
        [   'METHOD', [ 'URL', 'URI' ], 'HEADERS', 'CONTENT',
            'SAVE_TO', 'CALLBACK', 'CHUNK'
        ],
        @p
        );

    # Method management
    if ( !defined $method || $method eq "" || $method !~ /^\w+$/ ) {
        die "Incorrect HTTP Method specified in do_http_request: \"$method\"";
    }
    $method = uc($method);

    # URL management
    my $url_obj;
    $url_obj = ( ref($url) =~ /URI/ ) ? $url : URI->new($url);

    die "Comms: Bad HTTP Url: \"$url_obj\"\n"
        if ( $url_obj->scheme !~ /^http/ );

    # If you see user:pass detail embedded in the URL. Then get it out.
    if ( $url_obj->userinfo ) {
        $self->{_user_agent}
            ->credentials( $url, undef, split( ':', $url_obj->userinfo ) );
    }

    # Header management
    if ( $newheaders && ref($newheaders) !~ /Headers/ ) {
        die "Bad headers object: "
            . Data::Dumper->Dump( [$newheaders], ['$newheaders'] );
    }

    my $headers = HTTP::DAV::Headers->new();
    $headers->add_headers( $self->{_headers} );
    $headers->add_headers($newheaders);

    #$headers->header("Host", $url_obj->host);
    $headers->header( "Host", $url_obj->host_port );

    my $length = ($content) ? length($content) : 0;
    $headers->header( "Content-Length", $length );

    #print "HTTP HEADERS\n" . $self->get_headers->as_string . "\n\n";

    # It would be good if, at this stage, we could prefill the
    # username and password values to prevent the client having
    # to submit 2 requests, submit->401, submit->200
    # This is the same kind of username, password remembering
    # functionality that a browser performs.
    #@userpass = $self->{_user_agent}->get_basic_credentials(undef, $url);

    # Add a Content-type of text/xml if the body has <?xml in it
    if ( $content && $content =~ /<\?xml/i ) {
        $headers->header( "Content-Type", "text/xml" );
    }

    ####
    # Do the HTTP call
    my $req
        = HTTP::Request->new( $method, $url_obj, $headers->to_http_headers,
        $content );

    # It really bugs me, but libwww-perl doesn't honour this call.
    # I'll leave it here anyway for future compatibility.
    $req->protocol("HTTP/1.1");

    my $resp;

    # If a callback is set and it is a ref to a function
    # then pass it through to LWP::UserAgent::request.
    # See man page of LWP for more details of callback.
    # callback is primarily used by DAV::get();
    #
    if ( defined $save_to && $save_to ne "" ) {
        $resp = $self->{_user_agent}->request( $req, $save_to );
    }
    elsif ( ref($callback_func) =~ /CODE/ ) {
        $resp = $self->{_user_agent}->request( $req, $callback_func, $chunk );
    }
    else {
        $resp = $self->{_user_agent}->request($req);
    }

    # Redirect loop {{{
    my $code = $resp->code;
    if (   $code == &HTTP::Status::RC_MOVED_PERMANENTLY
        or $code == &HTTP::Status::RC_MOVED_TEMPORARILY )
    {

        # And then we update the URL based on the Location:-header.
        my ($referral_uri) = $resp->header('Location');
        {

            # Some servers erroneously return a relative URL for redirects,
            # so make it absolute if it not already is.
            local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1;
            my $base = $resp->base;
            $referral_uri
                = $HTTP::URI_CLASS->new( $referral_uri, $base )->abs($base);
        }

        # Check for loop in the redirects
        my $count    = 0;
        my $r        = $resp;
        my $bad_loop = 0;
        while ($r) {
            if ( ++$count > 13
                || $r->request->url->as_string eq $referral_uri->as_string )
            {
                $resp->header( "Client-Warning" => "Redirect loop detected" );

#if ( $HTTP::DAV::DEBUG ) {
#   print "*** CLIENT AND SERVER STUCK IN REDIRECT LOOP OR MOVED PERMENANTLY. $count. BREAKING ***\n";
#   print "***    " . $r->request->url->as_string . "***\n";
#   print "***    " . $referral_uri->as_string . "***\n";
#}
                $bad_loop = 1;
                last;
            }
            $r = $r->previous;
        }
        $resp = $self->do_http_request(
            -method   => $method,
            -url      => $referral_uri,
            -headers  => $newheaders,
            -content  => $content,
            -saveto   => $save_to,
            -callback => $callback_func,
            -chunk    => $chunk,
        ) unless $bad_loop;
    }

    # }}}

    if ( $HTTP::DAV::DEBUG > 1 ) {
        no warnings;

        #open(DEBUG, ">&STDOUT") || die ("Can't open STDERR");;
        open( DEBUG, ">>/tmp/perldav_debug.txt" );
        print DEBUG "\n" . "-" x 70 . "\n";
        print DEBUG localtime() . "\n";
        print DEBUG "$method REQUEST>>\n" . $req->as_string();

        if ( $resp->headers->header('Content-Type') =~ /xml/ ) {
            my $body = $resp->as_string();

            #$body =~ s/>\n*/>\n/g;
            print DEBUG "$method XML RESPONSE>>$body\n";

     #} elsif ( $resp->headers->header('Content-Type') =~ /text.html/ ) {
     #require HTML::TreeBuilder;
     #require HTML::FormatText;
     #my $tree = HTML::TreeBuilder->new->parse($resp->content());
     #my $formatter = HTML::FormatText->new(leftmargin => 0);
     #print DEBUG "$method RESPONSE (HTML)>>\n" . $resp->headers->as_string();
     #print DEBUG $formatter->format($tree);
        }
        else {

            print DEBUG "$method RESPONSE>>\n" . $resp->as_string();
        }
        close DEBUG;
    }

    ####
    # Copy the HTTP:Response into a HTTP::DAV::Response. It specifically
    # knows details about DAV Status Codes and their associated
    # messages.
    my $dav_resp = HTTP::DAV::Response->clone_http_resp($resp);
    $dav_resp->set_message( $resp->code );

    ####
    # Save the req and resp objects as the "last used"
    $self->_set_last_request($req);
    $self->_set_last_response($dav_resp);

    $self->_set_server_type( $url_obj->host_port,
        $dav_resp->headers->header("Server") );

    return $dav_resp;
}

sub credentials {
    my ( $self, @p ) = @_;
    my ( $user, $pass, $url, $realm )
        = HTTP::DAV::Utils::rearrange( [ 'USER', 'PASS', 'URL', 'REALM' ],
        @p );
    $self->{_user_agent}->credentials( $url, $realm, $user, $pass );
}

###########################################################################
# We make our own specialization of LWP::UserAgent
# called HTTP::DAV::UserAgent.
# The variations allow us to have various levels of protection.
# Where the user hasn't specified what Realm to use we pass the
# userpass combo to all realms of that host
# Also this UserAgent remembers a user on the next request.
# The standard UserAgent doesn't.
{

    package HTTP::DAV::UserAgent;

    use strict;
    use vars qw(@ISA);

    @ISA = qw(LWP::UserAgent);

    #require LWP::UserAgent;

    sub new {
        my $self = LWP::UserAgent::new(@_);
        $self->agent("lwp-request/$HTTP::DAV::VERSION");
        $self;
    }

    sub credentials {
        my ( $self, $netloc, $realm, $user, $pass ) = @_;

        $realm = 'default' unless $realm;

        if ($netloc) {
            $netloc = "http://$netloc" unless $netloc =~ m{^http};
            my $uri = URI->new($netloc);
            $netloc = $uri->host_port;
        }
        else {
            $netloc = 'default';
        }

        {
            no warnings;
            print
                "Setting auth details for $netloc, $realm to '$user', '$pass'\n"
                if $HTTP::DAV::DEBUG > 2;
        }

        my $cred = $self->{basic_authentication}->{$netloc}->{$realm};

        # Replace with new credentials (if any)
        if ( defined $user ) {
            $cred->[0] = $user;
            $cred->[1] = $pass;
        }

        # Return current values

        # User/password pair
        if (wantarray) { return @{$cred} }

        # As string: 'user:password'
        return join( ':', @{$cred} );
    }

    sub get_basic_credentials {
        my ( $self, $realm, $uri ) = @_;

        $uri = HTTP::DAV::Utils::make_uri($uri);
        my $netloc = $uri->host_port;

        my $userpass;
        {
            no warnings;    # SHUTUP with your silly warnings.
            $userpass 
                = $self->{'basic_authentication'}{$netloc}{$realm}
                || $self->{'basic_authentication'}{default}{$realm}
                || $self->{'basic_authentication'}{$netloc}{default}
                || [];

            print "Using user/pass combo: @$userpass. For $realm, $uri\n"
                if $HTTP::DAV::DEBUG > 2;

        }
        return @$userpass;
    }

    # Override to disallow redirects. Also, see RT #19616
    sub redirect_ok {
        return 0;
    }

}

###########################################################################
# We make our own special version of HTTP::Headers
# called HTTP::DAV::Headers. This is because we want to add
# a new method called add_headers
{

    package HTTP::DAV::Headers;

    use strict;
    use vars qw(@ISA);

    @ISA = qw( HTTP::Headers );
    require HTTP::Headers;

    # $dav_headers = HTTP::DAV::Headers->clone( $http_headers );

    sub to_http_headers {
        my ($self) = @_;
        my %clone = %{$self};
        bless {%clone}, "HTTP::Headers";
    }

    sub clone {
        my ( $class, $headers ) = @_;
        my %clone = %{$headers};
        bless {%clone}, ref($class) || $class;
    }

    sub add_headers {
        my ( $self, $headers ) = @_;
        return unless ( defined $headers && ref($headers) =~ /Headers/ );

        #print "About to add headers!!\n";
        #print Data::Dumper->Dump( [$headers] , [ '$headers' ] );
        foreach my $key ( sort keys %$headers ) {
            $self->header( $key, $headers->{$key} );
        }
    }
}

1;
