File Coverage

blib/lib/Finance/Bank/IE.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Finance::Bank::IE - shared functions for the Finance::Bank::IE module tree
4              
5             =head1 DESCRIPTION
6              
7             This module implements shared functions for Finance::Bank::IE::*
8              
9             =over
10              
11             =cut
12              
13             package Finance::Bank::IE;
14              
15 3     3   19 use File::Path;
  3         4  
  3         225  
16 3     3   4744 use WWW::Mechanize;
  0            
  0            
17             use HTTP::Status qw(:constants);
18             use Carp qw( confess );
19              
20             use strict;
21             use warnings;
22              
23             our $VERSION = "0.30";
24              
25             # Class state. Each of these is keyed by the hash value of $self to
26             # provide individual class-level variables. Ideally I'd just hack the
27             # namespace of the subclass, though.
28             my %agent;
29             my %cached_config;
30              
31             =item * $self->reset()
32              
33             Take necessary steps to reset the object to a pristine state, such as deleting cached configuration, etc.
34              
35             =cut
36              
37             sub reset {
38             my $self = shift;
39              
40             $agent{$self} = undef;
41             $cached_config{$self} = undef;;
42              
43             return 1;
44             }
45              
46             =item * $self->_agent
47              
48             Return the WWW::Mechanize object currently in use, or create one if
49             no such object exists.
50              
51             =cut
52              
53             sub _agent {
54             my $self = shift;
55              
56             if ( !$agent{$self} ) {
57             $agent{$self} = WWW::Mechanize->new( env_proxy => 1,
58             autocheck => 0,
59             keep_alive => 10 );
60             if ( !$agent{$self}) {
61             confess( "can't create agent" );
62             }
63             $agent{$self}->quiet( 0 );
64             }
65              
66             $agent{$self};
67             }
68              
69             =item * $self->cached_config( [config] )
70              
71             Get or set the cached config
72              
73             =cut
74             sub cached_config {
75             my ( $self, $config ) = @_;
76             if ( defined( $config )) {
77             $cached_config{$self} = $config;
78             }
79              
80             return $cached_config{$self};
81             }
82              
83             =item * $class = $self->_get_class()
84              
85             Return the bottom level class of $self
86              
87             =cut
88             sub _get_class {
89             my $self = shift;
90             my $class = ref( $self );
91              
92             if ( !$class ) {
93             $class = $self;
94             }
95              
96             # clean it up
97             my $basename = ( split /::/, $class )[-1];
98             $basename =~ s/\.[^.]*$//;
99              
100             $basename;
101             }
102              
103             =item * $scrubbed = $self->_scrub_page( $content )
104              
105             Scrub the supplied content for PII.
106              
107             =cut
108             sub _scrub_page {
109             my ( $self, $content ) = @_;
110              
111             return $content;
112             }
113              
114             =item * $self->_save_page()
115              
116             Save the current page if $ENV{SAVEPAGES} is set. The pages are
117             anonymised before saving so that they can be used as test pages
118             without fear of divulging any information.
119              
120             =cut
121              
122             sub _save_page {
123             my $self = shift;
124             my @params = @_;
125             return unless $ENV{SAVEPAGES};
126              
127             # get a filename from the agent
128             my $res = $self->_agent()->response();
129             my $filename = $res->request->uri();
130             $filename =~ s@^.*/@@;
131             if ( !$filename ) {
132             $filename = "index.html";
133             }
134              
135             if ( $self->_identify_page() ne 'UNKNOWN' ) {
136             $filename = $self->_identify_page();
137             }
138              
139             # embed the code if it's a failed page
140             if ( !$res->is_success()) {
141             $filename = $res->code() . "-$filename";
142             }
143              
144             if ( @params ) {
145             $filename .= '?' . join( '&', @params );
146             }
147              
148             # Keep windows happy
149             $filename =~ s/\?/_/g;
150              
151             my $path = 'data/savedpages/' . $self->_get_class();
152             mkpath( [ $path ], 0, 0700 );
153             $filename = "$path/$filename";
154              
155             $self->_dprintf( "writing data to $filename\n" );
156              
157             # we'd like to anonymize this content before saving it.
158             my $content = $self->_agent()->content();
159             g
160             $content = $self->_scrub_page( $content );
161              
162             my $error = 0;
163             if ( open( my $FILEHANDLE, ">", $filename )) {
164             binmode $FILEHANDLE, ':utf8';
165             print $FILEHANDLE $content;
166             close( $FILEHANDLE );
167             } else {
168             $error = $!;
169             warn "Failed to create $filename: $!";
170             }
171              
172             return $error;
173             }
174              
175             =item * $self->_streq( $a, $b )
176              
177             Return $a eq $b; if either is undef, substitutes an empty string.
178              
179             =cut
180              
181             sub _streq {
182             my ( $self, $a, $b ) = @_;
183              
184             $a = "" if !defined( $a );
185             $b = "" if !defined( $b );
186              
187             return $a eq $b;
188             }
189              
190             =item * $self->_rebuild_tag( $html_tokeparser_decomposed_tag )
191              
192             Return C as a composed HTML tag.
193              
194             =cut
195              
196             sub _rebuild_tag {
197             my $self = shift;
198             my $data = shift;
199             my $tag = $data->[1];
200             if ( $data->[0] eq 'E' ) {
201             $tag = "/$tag";
202             }
203             my $attr_values = $data->[2];
204             my $attr_order = $data->[3];
205             my @rebuild = "<$tag";
206             for my $attr ( @{$attr_order} ) {
207             next unless exists($attr_values->{$attr});
208             my $attrstring = "$attr=";
209             my $attrvalue = $attr_values->{$attr};
210             $attrvalue =~ s@\\@\\\\@g;
211             $attrvalue =~ s@"@\\"@g;
212             $attrstring .= "\"$attrvalue\"";
213             push @rebuild, $attrstring;
214             }
215             return join( ' ', @rebuild ) . ">";
216             }
217              
218             =item * $self->_dprintf( ... )
219              
220             Print to STDERR using printf formatting if $ENV{DEBUG} is set.
221              
222             =cut
223              
224             sub _dprintf {
225             my $self = shift;
226             binmode( STDERR, ':utf8' );
227              
228             if ( $ENV{DEBUG}) {
229             printf STDERR "[%s] ", ref( $self ) || $self || "DEBUG";
230             printf STDERR @_ if $ENV{DEBUG};
231             }
232             }
233              
234             =item * $self->_pages()
235              
236             Return a hashref of URLs & sentinel text to allow each page to be requested or identified. Format of each entry is a hash, 'url' => 'http://...', 'sentinel' => 'sentinel text'. Sentinel text will be used as-is in a regular expression match on the page content.
237              
238             =cut
239             sub _pages {
240             my $self = shift;
241             confess "_pages() not implemented for " . (ref($self) || $self);
242             }
243              
244             =item * $self->_identify_page()
245              
246             Identify the current page among C<$self->_pages()> hashref. Returns C if no match is found.
247              
248             =cut
249             sub _identify_page {
250             my $self = shift;
251             my $content = shift || $self->_agent()->content();
252             my $page;
253              
254             my $pages = $self->_pages();
255              
256             for my $pagekey ( keys %{$pages} ) {
257             my $sentinel = $pages->{$pagekey}->{sentinel};
258             if ( !$sentinel ) {
259             confess( "sentinel unset for $pagekey" );
260             }
261             if ( $content =~ /$sentinel/s ) {
262             $page = $pagekey;
263             last;
264             }
265             }
266              
267             if ( !defined( $page )) {
268             $page = "UNKNOWN";
269             }
270              
271             return $page;
272             }
273              
274             =item * $self->_get( url, [config] )
275              
276             Get the specified URL, dealing with login if necessary along the way.
277              
278             =cut
279              
280             sub _get {
281             my $self = shift;
282             my $url = shift;
283             my $confref = shift;
284              
285             confess "No URL specified" unless $url;
286              
287             my $pages = $self->_pages();
288              
289             if ( $confref ) {
290             $self->cached_config( $confref );
291             }
292              
293             my $res;
294             if ( $self->_agent()->find_link( url => $url )) {
295             $self->_dprintf( " following $url\n" );
296             $res = $self->_agent()->follow_link( url => $url );
297             } else {
298             $self->_dprintf( " getting $url\n" );
299             $res = $self->_agent()->get( $url );
300             }
301              
302             # if we get the login page then treat it as a 401
303             my $loop = 0;
304             my $expired = 0;
305             NEXTPAGE:
306             if ( $res->is_success ) {
307             my $page = $self->_identify_page();
308             if ( $page eq 'UNKNOWN' ) {
309             $self->_dprintf( " Looking for URL '$url', got unknown page\n" );
310             $page = "";
311             } else {
312             $self->_dprintf( " Looking for URL '$url', got '$page'\n" );
313             }
314              
315             $self->_save_page();
316             confess "unrecognised page" unless $page;
317              
318             if ( $page eq 'termsandconds' ) {
319             die "Terms & Conditions page detected. Please log in manually to accept.";
320             }
321              
322             if ( $page eq 'sitedown' ) {
323             $res->code( HTTP_INTERNAL_SERVER_ERROR );
324             die "Site appears to be offline for maintenance.";
325             }
326              
327             if ( $page eq 'expired' or $page eq 'accessDenied' ) {
328             if ( !$expired ) {
329             $self->_dprintf( " session expired, logging in again\n" );
330             $res = $self->_agent()->get($self->_pages()->{login}->{url});
331             $expired = 1;
332             goto NEXTPAGE;
333             } else {
334             $self->_dprintf( " session expiry looping, bailing to avoid lockout\n" );
335             $res->code( HTTP_INTERNAL_SERVER_ERROR )
336             }
337             } elsif ( $page eq 'login' ) {
338             if ( $loop != 0 ) {
339             $self->_dprintf( " login appears to have looped, bailing to avoid lockout\n" );
340             $res->code( HTTP_UNAUTHORIZED );
341             } else {
342             # do the login
343             $self->_dprintf( " login step 1\n" );
344             $res = $self->_submit_first_login_page( $confref );
345             $loop = 1;
346             goto NEXTPAGE;
347             }
348             } elsif ( $page eq 'login2' ) {
349             $self->_dprintf( " login 2 of 2\n" );
350             if ( $loop ==2 ) {
351             $self->_dprintf( " login appears to be stuck on page 2, bailing\n" );
352             $res->code( HTTP_UNAUTHORIZED );
353             } else {
354             $res = $self->_submit_second_login_page( $confref );
355             $loop = 2;
356             goto NEXTPAGE;
357             }
358             } else {
359             # just assume we're not yet on the page we're looking for
360             if ( $self->_pages()->{$page}->{url} ne $url ) {
361             $self->_dprintf( " now chasing URL '$url'\n" );
362             $self->_save_page();
363             $res = $self->_agent()->get( $url );
364             }
365             }
366             }
367              
368             $self->_save_page();
369              
370             if ( $res->is_success ) {
371             return $self->_agent()->content();
372             } else {
373             $self->_dprintf( " page fetch failed with " . $res->code() . "\n" );
374             return undef;
375             }
376             }
377              
378             =item * $self->_as_qif( $account_details_array_ref[, $type ] )
379              
380             Render C<$account_details_array_ref> as QIF. I
381             limited. Optional C<$type> is the type of account.
382              
383             C<$account_details_array_ref> should be an arrayref of hashrefs, each
384             containing the date, the payee, and the amount. Negative amounts
385             indicate debits.
386              
387             =cut
388              
389             sub as_qif {
390             my $self = shift;
391             my $details_aref = shift;
392             my $type = shift || "Bank";
393              
394             my $qif = "!Type:Bank\n";
395              
396             for my $details ( @{$details_aref} ) {
397             $qif .= sprintf("D%s
398             P%s
399             T%0.02f
400             ^
401             ", $details->{date}, $details->{payee}, $details->{amount});
402             }
403              
404             return $qif;
405             }
406              
407             # starting the genericisation of this
408             sub check_balance {
409             my $self = shift;
410             my $confref = shift;
411             $confref ||= $self->cached_config();
412              
413             my $res = $self->_get( $self->_pages()->{accounts}->{url}, $confref );
414             return unless $res;
415              
416             return $self->_parse_account_balance_page();
417             }
418              
419             =back
420              
421             =cut
422              
423             1;