/usr/local/CPAN/Finance-Bank-Natwest/Finance/Bank/Natwest/Connection.pm


package Finance::Bank::Natwest::Connection;
use strict;
use vars qw( $VERSION );
use Carp;
use LWP::UserAgent;

$VERSION = '0.04';

require Finance::Bank::Natwest;

use constant POSS_PIN => { first => 0, second => 1, third => 2, fourth => 3 };
use constant POSS_PASS =>
    { first => 0, second => 1, third => 2, fourth => 3, fifth => 4,
      sixth => 5, seventh => 6, eighth => 7, ninth => 8, tenth => 9,
      eleventh => 10, twelfth => 11, thirteenth => 12, fourteenth => 13,
      fifteenth => 14, sixteenth => 15, seventeenth => 16,
      eighteenth => 17, nineteenth => 18, twentieth => 19
    };


sub new{
    my ($class, %opts) = @_;

    my $self = bless {}, $class;

    $self->{url_base} = $opts{url_base} || Finance::Bank::Natwest->url_base;

    $self->_set_credentials( %opts );
    $self->_new_ua( %opts );
    
    return $self;
}

sub _new_ua{
    my ($self, %opts) = @_;

    my %proxy;

    if (exists $opts{proxy}) {
        $proxy{env_proxy} = 0;
        $proxy{proxy} = $opts{proxy} if 
            $opts{proxy} ne 'no' and $opts{proxy} ne 'env';
        $proxy{env_proxy} = 1 if $opts{proxy} eq 'env';
    } else {
        $proxy{env_proxy} = 1;
    }

    $self->{ua} = LWP::UserAgent->new(
        env_proxy => $proxy{env_proxy},
        keep_alive => 1,
        timeout => 30,
        cookie_jar => {},
        requests_redirectable => [ 'GET', 'HEAD', 'POST' ],
        agent => "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
    );

    $self->{ua}->proxy('https', $proxy{proxy}) if exists $proxy{proxy};
}

sub _set_credentials{
    my ($self, %opts) = @_;

    croak "Must provide either a premade credentials object or ".
          "a class name together with options, stopped" if
        !exists $opts{credentials};

    if (ref($opts{credentials})) {
        croak "Can't accept credential options if supplying a premade ".
              "credentials object, stopped" if
            exists $opts{credentials_options};

        croak "Not a valid credentials object, stopped" unless
            $self->_isa_credentials($opts{credentials});

        $self->{credentials} = $opts{credentials};
    } else {
        croak "Must provide credential options unless suppying a premade ".
              "credentials object, stopped" if
            !exists $opts{credentials_options};

        $self->{credentials} =
            $self->_new_credentials(
                $opts{credentials}, $opts{credentials_options}
            );
    };
}

sub _new_credentials{
    my ($self, $class, $options) = @_;

    croak "Invalid class name, stopped" if
        $class !~ /^(?:\w|::)+$/;
    
    my $full_class = "Finance::Bank::Natwest::CredentialsProvider::$class";
    
    eval "local \$SIG{'__DIE__'}; 
                    local \$SIG{'__WARN__'}; 
                    require $full_class;
                  ";
    croak "Not a valid credentials class, stopped"
        if $@;

    croak "Not a valid credentials class, stopped"
        unless $self->_isa_credentials($full_class);

    {
        local $Carp::CarpLevel = $Carp::CarpLevel + 1;
        return $full_class->new(%{$options});
    }
}

sub _isa_credentials{
    my ($self, $credentials) = @_;

    my @required_subs = qw( new get_start get_stop get_identity get_pinpass );
  
    foreach my $sub (@required_subs) {
        return unless defined eval {
            local $SIG{'__DIE__'};
            local $SIG{'__WARN__'};
            $credentials->can($sub);
        };
    }

    return 1;
}

sub login{
    my ($self) = @_;

    my $page;

    $self->{login_ok} = 0;
    $self->{in_login} = 1;
    delete $self->{rb_id};

    $self->{credentials}->get_start();

    my $identity = $self->{credentials}->get_identity();

    ($self->{rb_id}, $page) = $self->post( 'logon.asp',
        {   DBIDa => $identity->{dob}, DBIDb => $identity->{uid},
            radType => '', scriptingon => 'yup' } );

    croak "Error during login process. " . 
          "The website is temporarily unavailable, stopped" if
        $page =~ m|Service Temporarily Unvailable|i;

    croak "Error during login process, stopped" if
        $page =~ m|<div class=ErrorMsg>.*?</div>|i;

    croak "Error during login process. " .
          "Current page cannot be recognised, stopped" unless
        $page =~ m#
                                        Please \s enter \s the \s
                                        ([a-z]{5,6}), \s ([a-z]{5,6}) \s and \s ([a-z]{5,6}) \s
                                        digits \s from \s your \s (?:Security \s Number|PIN):
                                    #ix;

    croak "Error during login process. " .
          "Unrecognised pin request ($1, $2, $3), stopped" unless
        exists POSS_PIN->{$1} && 
        exists POSS_PIN->{$2} && 
        exists POSS_PIN->{$3};

    my $pin_digits = [ POSS_PIN->{$1}, POSS_PIN->{$2}, POSS_PIN->{$3} ];

    croak "Error during login process. " .
          "Current page cannot be recognised, stopped" unless
        $page =~ m|
                                        Please \s enter \s the \s
                                        ([a-z]{5,11}), \s ([a-z]{5,11}) \s and \s ([a-z]{5,11}) \s
                                        characters \s from \s your \s Password:
                                    |ix;
    
    croak "Error during login process. " .
          "Unrecognised password request ($1, $2, $3), stopped" unless
        exists POSS_PASS->{$1} && 
        exists POSS_PASS->{$2} && 
        exists POSS_PASS->{$3};
    
    my $pass_chars = [ POSS_PASS->{$1}, POSS_PASS->{$2}, POSS_PASS->{$3} ];

    my $pinpass = $self->{credentials}->get_pinpass( $pin_digits, $pass_chars );
    $self->{credentials}->get_stop();

    $page = $self->post('Logon-PinPass.asp', 
        {   pin1 => $pinpass->{pin}[0], 
            pin2 => $pinpass->{pin}[1],
            pin3 => $pinpass->{pin}[2],
            pass1 => $pinpass->{password}[0],
            pass2 => $pinpass->{password}[1],
            pass3 => $pinpass->{password}[2],
            buttonComplete => 'Submitted',
            buttonFinish => 'Finish' } );

    $page = $self->post('LogonMessage.asp', { buttonOK => 'Next' }) if
        $page =~ m|LogonMessage\.asp|i;

    croak "Error during login process, stopped" if
        $page =~ m|<div class=ErrorMsg>.*?</div>|i;

    $self->{login_ok} = 1;
    delete $self->{in_login};
}

sub post{
    my $self = shift;

    $self->login(@_)
        if !$self->{login_ok} and !exists $self->{in_login};

    my $resp = $self->_post(@_);

    if ($self->_check_expired($resp)) {
        $self->_login(@_);
    
        $resp = $self->_post(@_);
        croak "Error talking to nwolb. " .
              "Session has timed out even though only just logged in, stopped"
            if $self->_check_expired($resp);
    }

    return unless defined wantarray;

    if (wantarray) {
        return (($resp->base->path_segments)[2], $resp->content);
    } else {
        return $resp->content;
    }
}

sub _check_expired{
    my ($self, $resp) = @_;

    return lc(($resp->base->path_segments)[-1]) eq 'exit.asp';
}

sub _post{
    my $self = shift;
    my $url = shift;
    my $full_url;

    if (exists $self->{rb_id}) {
        $full_url = $self->{url_base} . $self->{rb_id} . '/' . $url;
    } else {
        $full_url = $self->{url_base} . $url;
    }

    my $resp = $self->{ua}->post($full_url, @_);

    croak "Error talking to nwolb: " . $resp->message . ", stopped"
        if !$resp->is_success;

    croak "Unknown error talking to nwolb, stopped"
        if !exists $self->{in_login} and 
            lc($resp->base->as_string) ne lc($full_url);

    return $resp;
}

1;