line
stmt
bran
cond
sub
pod
time
code
1
package Finance::Bank::DE::DeutscheBank;
2
1
1
9933
use strict;
1
2
1
51
3
1
1
21
use warnings;
1
2
1
40
4
1
1
5
use Carp;
1
8
1
88
5
1
1
6
use base 'Class::Accessor';
1
2
1
2632
6
7
1
1
4495
use WWW::Mechanize;
1
493090
1
45
8
1
1
9673
use HTML::LinkExtractor;
1
2793
1
35
9
1
1
1420
use HTML::TreeBuilder;
1
69276
1
17
10
1
1
1649
use Text::CSV_XS;
1
11868
1
81
11
12
13
1
1
12
use vars qw[ $VERSION ];
1
4
1
71
14
15
$VERSION = '0.06';
16
17
BEGIN {
18
1
1
16
Finance::Bank::DE::DeutscheBank->mk_accessors(qw( agent ));
19
};
20
21
1
1
107
use constant BASEURL => 'https://meine.deutsche-bank.de';
1
2
1
77
22
1
1
6
use constant LOGIN => BASEURL . '/trxm/db/';
1
1
1
61
23
1
1
5
use constant FUNCTIONS => "(Übersicht)|(Ihr Konto)|(Ihr Depot)|(Service / Optionen)|(Umsatzanzeige)|(Inlands-Überweisung)|(Daueraufträge)|(Lastschrift)|(Kunden-Logout)|(Überweisungsvorlagen)|(Ihre Finanzübersicht als PDF-Datei speichern)|(Ihre Finanzübersicht als CSV-Datei speichern)|(Customer-Logout)|(Overview)|(Your account)|(Your sec. account)|(Service / Options)|(Transactions)|(Domestic transfer order)|(Standing orders)|(Direct debit)|(Transfer order templates)|(Customer-Logout)|(Save as PDF File)|(Save as CSV File)|(Save your financial overview as PDF file)|(Save your financial overview as CSV file)";
1
3
1
3356
24
25
26
sub new
27
{
28
0
0
1
my ($class,%args) = @_;
29
30
0
0
croak "Filiale/Branch number must be specified"
31
unless $args{Branch};
32
33
0
0
croak "Konto/Account number must be specified"
34
unless $args{Account};
35
36
0
0
croak "Unterkonto/SubAccount number must be specified"
37
unless $args{SubAccount};
38
39
0
0
croak "PIN/Password must be specified"
40
unless $args{PIN};
41
42
0
0
0
my $logger = $args{status} || sub {};
0
43
44
0
my $self = {
45
agent => undef,
46
account => $args{Account},
47
password => $args{PIN},
48
branch => $args{Branch},
49
subaccount => $args{SubAccount},
50
logger => $logger,
51
navigation => undef,
52
};
53
0
bless $self, $class;
54
55
0
$self->log("New $class created");
56
0
$self;
57
};
58
59
60
sub log
61
{
62
0
0
0
$_[0]->{logger}->(@_);
63
};
64
65
66
sub log_httpresult
67
{
68
0
0
0
$_[0]->log("HTTP Code",$_[0]->agent->status,$_[0]->agent->res->as_string)
69
};
70
71
72
sub new_session
73
{
74
# Reset our user agent
75
0
0
0
my ($self) = @_;
76
0
my $url;
77
78
0
0
$self->close_session()
79
if ($self->agent);
80
81
0
my $result = $self->get_login_page(LOGIN);
82
83
0
0
if ( $result != 200 )
84
{
85
0
$self->log("Status"," Banking is unavailable");
86
0
die "Banking is unavailable";
87
}
88
89
0
0
if ( $result == 200 )
90
{
91
0
0
if ($self->maintenance)
92
{
93
0
$self->log("Status","Banking is unavailable due to maintenance");
94
0
die "Banking unavailable due to maintenance";
95
};
96
97
0
my $agent = $self->agent();
98
0
my $function = 'ACCOUNTBALANCE';
99
0
$self->log("Logging into function $function");
100
101
# gvo=DisplayFinancialOverview&loginTab=iTAN&javascriptEnabled=true&branch=XXX&account=XXXXXX&subaccount=XX&pin=XXXXX&quickLink=DisplayFinancialOverview
102
0
$agent->current_form->value('gvo','DisplayFinancialOverview');
103
0
$agent->current_form->value('loginTab','iTAN');
104
0
$agent->current_form->value('javascriptEnabled','false');
105
0
$agent->current_form->value('quickLink','DisplayFinancialOverview');
106
0
$agent->current_form->value('account',$self->{account});
107
0
$agent->current_form->value('branch',$self->{branch});
108
0
$agent->current_form->value('subaccount',$self->{subaccount});
109
0
$agent->current_form->value('pin',$self->{password});
110
0
$agent->add_header('Accept-Charset' => 'utf-8');
111
0
$agent->add_header('Accept-Encoding' => '');
112
113
0
local $^W=0;
114
0
$result = $agent->submit();
115
116
0
0
if ( $self->access_denied )
117
{
118
0
$self->log("Not possible to authenticate at bank server ( wrong account/pin combination ? )");
119
0
return 0;
120
}
121
122
# extract links to account functions
123
0
my $LinkExtractor = new HTML::LinkExtractor();
124
125
0
$LinkExtractor->strip( 1 );
126
0
$LinkExtractor->parse(\$agent->content());
127
128
# needed here because of empty links ( href attribute )
129
0
local $^W=1;
130
131
# now we have the links in the format
132
# {
133
# '_TEXT' => 'Overview',
134
# 'target' => '_top',
135
# 'href' => '/mod/WebObjects/dbpbc.woa/618/wo/HpRl1hqezkxfYRosJRjTg0/4.11.1.5.3.3.5.3',
136
# 'tag' => 'a',
137
# 'class' => 'NaviDirektLink'
138
# },
139
# { ...
140
# }
141
#
142
143
144
# but I would like to have them as
145
# {
146
# '_TEXT' => 'Overview',
147
# 'href' => '/mod/WebObjects/dbpbc.woa/618/wo/HpRl1hqezkxfYRosJRjTg0/4.11.1.5.3.3.5.3',
148
# },
149
# { ...
150
# }
151
# and only for functions ( not all links and images ... )
152
153
0
my @tmp = ();
154
0
foreach my $elem ( @{$LinkExtractor->links} )
0
155
{
156
0
0
0
if (( defined( $elem->{ '_TEXT' } ) && ( $elem->{ '_TEXT' } ne '' ) && ( $elem->{ '_TEXT' } =~ "m/". FUNCTIONS ."/" )) )
0
157
{
158
0
foreach $_ ( keys %$elem )
159
{
160
0
0
if ( $_ !~ m/(_TEXT)|(href)/ )
161
{
162
0
delete $elem->{ $_ };
163
}
164
}
165
166
0
push @tmp, \%$elem;
167
}
168
}
169
170
# save these links so that we can remember them
171
0
$self->{navigation} = \@tmp;
172
0
$self->log_httpresult();
173
0
$result = $agent->status;
174
};
175
0
$result;
176
};
177
178
179
sub get_login_page
180
{
181
0
0
0
my ($self,$url) = @_;
182
0
$self->log("Connecting to $url");
183
0
$self->agent(WWW::Mechanize->new(agent => "Mozilla/4.78 (Linux 2.4.19-4GB i686; U) Opera 6.03 [en]", cookie_jar => {} ));
184
185
0
my $agent = $self->agent();
186
0
$agent->add_header('Accept-Language' => 'en-us,en;q=0.5');
187
0
$agent->get(LOGIN);
188
0
$self->log_httpresult();
189
0
$agent->status;
190
};
191
192
193
sub error_page
194
{
195
# Check if an error page is shown (a page with much red on it)
196
0
0
0
my ($self) = @_;
197
0
my $content = $self->agent->content;
198
0
$content =~ s!Bitte beachten Sie, da\ß die "Hochstell-Taste" \(Capslock\) aktiv ist.
!!;
199
0
$content =~ s!Please note capslock-key is active.
!!;
200
0
0
0
$content =~ / /sm || $content =~ /
201
};
202
203
204
sub maintenance
205
{
206
0
0
0
my ($self) = @_;
207
208
# would be nice if someone could mail me the actual english and german messages which are displayed in case of maintenance ...
209
0
0
$self->error_page or
210
$self->agent->content =~ /derzeit steht das Internet Banking aufgrund von Wartungsarbeiten leider nicht zur Verfügung.\s* \s*In Kürze wird das Internet Banking wieder wie gewohnt erreichbar sein./gsm;
211
};
212
213
214
sub access_denied
215
{
216
0
0
0
my ($self) = @_;
217
0
my $content = $self->agent->content;
218
219
0
0
0
$self->error_page or
0
0
0
0
0
0
220
( $content =~ /Die eingegebene Kontonummer ist unvollständig oder falsch\..*\(2051\)/gsm
221
or $content =~ /Die eingegebene PIN ist falsch\. Bitte geben Sie die richtige PIN ein\.\s*\(10011\)/gsm
222
or $content =~ /Die von Ihnen eingegebene Kontonummer ist ungültig und entspricht keiner Deutsche Bank-Kontonummer.\s*\(3040\)/gsm
223
or $content =~ /Leider konnte Ihre Anmeldung nicht erfolgreich durchgeführt werden/
224
or $content =~ /Unfortunately, you were not able to register successfully./
225
or $content =~ /Bitte überprüfen Sie Ihre Anmeldedaten oder versuchen Sie es zu einem späteren Zeitpunkt noch einmal./
226
or $content =~ /Please check your registration data or try again later./
227
or $content =~ /Bitte geben Sie ein gültiges Datum ein/
228
);
229
};
230
231
232
sub session_timed_out
233
{
234
0
0
0
my ($self) = @_;
235
0
$self->agent->content =~ /Die Sitzungsdaten sind ungültig, bitte führen Sie einen erneuten Login durch.\s+\(27000\)/;
236
};
237
238
239
sub functions
240
{
241
0
0
0
my ($self,$function) = @_;
242
0
my $link = ();
243
244
0
0
if ( $function =~ "m/". FUNCTIONS ."/" )
245
{
246
0
foreach $_ ( @{$self->{ navigation }} )
0
247
{
248
0
0
if ( $_->{ '_TEXT' } eq $function )
249
{
250
0
$link = $_->{ 'href' };
251
}
252
}
253
0
return $link;
254
}
255
else
256
{
257
0
return 0;
258
}
259
}
260
261
262
sub select_function
263
{
264
0
0
1
my ($self,$function) = @_;
265
0
0
carp "Unknown account function '$function'"
266
unless $self->functions($function);
267
268
0
0
$self->new_session unless $self->agent;
269
0
$self->agent->get( $self->functions( "$function" ) );
270
271
0
0
if ( $self->session_timed_out )
272
{
273
0
$self->log("Session timed out");
274
0
$self->agent(undef);
275
0
$self->new_session();
276
0
$self->agent->get( $self->functions( $function ) );
277
};
278
0
$self->log_httpresult();
279
0
$self->agent->status;
280
};
281
282
283
sub close_session
284
{
285
0
0
1
my ($self) = @_;
286
0
my $result;
287
0
0
if (not $self->access_denied)
288
{
289
0
$self->log("Closing session");
290
0
local $^W=0;
291
0
$self->select_function('Customer-Logout');
292
0
local $^W=1;
293
0
$result = $self->agent->res->as_string =~ /https:\/\/meine.deutsche-bank.de\/trxm\/db\/.*;link=trxm_en_logout-pbcde-to-txm_login.*/;
294
}
295
else
296
{
297
0
$result = 'Never logged in';
298
};
299
0
$self->agent(undef);
300
0
$result;
301
};
302
303
304
sub login
305
{
306
0
0
1
my ($self) = @_;
307
308
0
0
if ( $self->new_session() )
309
{
310
0
return 1;
311
}
312
else
313
{
314
0
return 0;
315
}
316
317
};
318
319
320
sub parse_account_overview
321
{
322
0
0
0
my ($self) = @_;
323
0
my $agent = $self->agent();
324
0
my %saldo = ();
325
326
0
my $tree = HTML::TreeBuilder->new();
327
0
$tree->parse( $agent->content() );
328
329
0
foreach my $table ( $tree->look_down('_tag', 'table') )
330
{
331
0
foreach my $row ( $table->look_down('_tag', 'tr') )
332
{
333
0
foreach my $child ( $row->look_down('_tag', 'td') )
334
{
335
0
0
0
if (( defined $child->attr('class')) && ( $child->attr('class') eq 'balance'))
336
{
337
0
my $tmp = $child->as_trimmed_text;
338
339
0
0
if ( $child->attr('class') eq 'balance')
340
{
341
0
0
if ( $tmp =~ m/\.[0-9][0-9]$/ )
342
{
343
0
$tmp =~ s/\./#/;
344
0
$tmp =~ s/,/./g;
345
0
$tmp =~ s/#/,/;
346
}
347
0
$saldo{ 'Saldo' } = $tmp;
348
}
349
}
350
351
0
foreach my $morechildren ( $child->look_down('_tag', 'acronym') )
352
{
353
0
0
0
if (( defined $morechildren->attr('title')) && ( $morechildren->attr('title') eq 'Euro'))
354
{
355
0
$saldo{ 'Währung' } = $morechildren->as_trimmed_text;
356
}
357
}
358
}
359
}
360
}
361
0
return %saldo
362
}
363
364
365
sub saldo
366
{
367
0
0
0
my ($self) = @_;
368
0
my $agent = $self->agent;
369
0
0
if ($agent)
370
{
371
0
local $^W=0;
372
0
$self->select_function('Overview');
373
0
local $^W=1;
374
0
return $self->parse_account_overview();
375
}
376
else
377
{
378
0
return undef;
379
}
380
};
381
382
383
sub MapData
384
{
385
0
0
0
my ( @data ) = @_;
386
0
for my $row ( @data )
387
{
388
0
foreach $_ ( keys %$row )
389
{
390
0
0
0
if (( $_ eq 'Haben' )||( $_ eq 'Soll' ))
0
0
0
391
{
392
0
$row->{ $_ } =~ s/\./#/;
393
0
$row->{ $_ } =~ s/,/./g;
394
0
$row->{ $_ } =~ s/#/,/;
395
}
396
elsif (( $_ eq 'Buchungstag' )||( $_ eq 'Wert' ))
397
{
398
0
my @tmp = split( /\//, $row->{ $_ } );
399
0
$row->{ $_ } = join( '.', $tmp[1], $tmp[0], $tmp[2] );
400
}
401
elsif ( $_ eq 'Waehrung' )
402
{
403
0
my $tmp = $row->{ 'Waehrung' };
404
0
delete $row->{ 'Waehrung' };
405
0
$row->{ 'Währung' } = $tmp;
406
}
407
}
408
}
409
0
return @data;
410
}
411
412
413
sub account_statement
414
{
415
0
0
1
my ($self, %parameter) = @_;
416
417
0
my $count = 0;
418
0
my @header = ();
419
0
my @AccountStatement = ();
420
0
my $AccountRow = ();
421
0
my @date;
422
0
my $agent = $self->agent;
423
0
0
if ($agent)
424
{
425
0
local $^W=0;
426
0
$self->select_function('Overview');
427
428
0
my %account = $self->parse_account_overview();
429
0
$agent->follow_link( 'text' => 'Transactions' );
430
0
local $^W=1;
431
432
# should I get account statement for user defined period ?
433
0
0
if ( defined $parameter{ 'period' } )
0
434
{
435
0
my ( $day, $month, $year ) = split( '\.', $parameter{ 'StartDate' } );
436
437
0
$day = sprintf("%02d", $day );
438
0
$month = sprintf("%02d", $month );
439
0
$year = sprintf("%04d", $year );
440
441
0
0
croak "Year must have 4 digits in StartDate"
442
unless ( length $year == 4 );
443
444
0
$agent->current_form->value( 'period','dynamicRange');
445
0
$agent->current_form->value( 'periodStartDay', $day );
446
0
$agent->current_form->value( 'periodStartMonth', $month );
447
0
$agent->current_form->value( 'periodStartYear', $year );
448
449
0
( $day, $month, $year ) = split( '\.', $parameter{ 'EndDate' } );
450
0
$day = sprintf("%02d", $day );
451
0
$month = sprintf("%02d", $month );
452
0
$year = sprintf("%04d", $year );
453
454
0
0
0
croak "Year must have 4 digits in EndDate"
455
unless (( length $year == 4 )&&( $year > 1900 ));
456
457
0
$agent->current_form->value( 'period','dynamicRange');
458
0
$agent->current_form->value( 'periodEndDay', $day );
459
0
$agent->current_form->value( 'periodEndMonth', $month );
460
0
$agent->current_form->value( 'periodEndYear', $year );
461
}
462
elsif ( defined $parameter{ 'last' } )
463
{
464
0
my $last = ();
465
0
$agent->current_form->value('period','fixedRange');
466
0
0
if ( $parameter{ 'last' } <= 10 )
0
0
0
0
0
467
{
468
0
$last = 10;
469
}
470
elsif ( $parameter{ 'last' } <= 20 )
471
{
472
0
$last = 20;
473
}
474
elsif ( $parameter{ 'last' } <= 30 )
475
{
476
0
$last = 30;
477
}
478
elsif ( $parameter{ 'last' } <= 60 )
479
{
480
0
$last = 60;
481
}
482
elsif ( $parameter{ 'last' } <= 90 )
483
{
484
0
$last = 90;
485
}
486
elsif ( $parameter{ 'last' } <= 120 )
487
{
488
0
$last = 120;
489
}
490
else # > 120
491
{
492
0
$last = 180;
493
}
494
0
$agent->select('periodDays', $last);
495
}
496
else #expect that per default last login date is set ...
497
{
498
;
499
}
500
501
0
local $^W=0;
502
# 'refresh view' is used to trigger update of account balance
503
0
my $result = $agent->submit();
504
505
# download CSV formated data of account balances
506
0
$result = $agent->follow_link( text_regex => qr/Save your account turnover as.*CSV.*file/ );
507
0
local $^W=1;
508
509
#successfully downloaded account balance data in csv format
510
0
0
if ( $result->is_success )
511
{
512
0
my @balance = split( '\n', $result->content );
513
0
my $csv = Text::CSV_XS->new( { 'sep_char' => ';' });
514
515
0
my $StartLineDetected = 0;
516
0
for ( my $loop = 0; $loop < scalar @balance; $loop++ )
517
{
518
0
my $line = $balance[ $loop ];
519
0
chomp( $line );
520
521
0
$line =~ s/^Booking date;/Buchungstag;/;
522
0
$line =~ s/;Value date;/;Wert;/;
523
0
$line =~ s/;Transactions Payment details;/;Verwendungszweck;/;
524
0
$line =~ s/;Debit;/;Soll;/;
525
0
$line =~ s/;Credit;/;Haben;/;
526
0
$line =~ s/;Currency/;Waehrung/;
527
0
$line =~ s/^Account balance;/Kontostand;/;
528
529
0
0
0
if ( $StartLineDetected == 1 )
0
0
530
{
531
0
my $status = $csv->parse( $line );
532
0
@header = $csv->fields();
533
0
$AccountRow = 0;
534
0
@AccountStatement = ();
535
0
$StartLineDetected = 2;
536
}
537
elsif ( $StartLineDetected == 2 )
538
{
539
0
0
if ( $line !~ /^Kontostand;/ )
540
{
541
0
my $status = $csv->parse( $line );
542
0
my @columns = $csv->fields();
543
544
0
for (my $loop = 0; $loop < scalar @columns; $loop++ )
545
{
546
0
$AccountStatement[ $AccountRow ]{ $header[ $loop ] } = $columns[ $loop ];
547
}
548
0
$AccountRow++;
549
}
550
}
551
elsif ( ( $line =~ /Vorgemerkte und noch nicht gebuchte Umsätze sind nicht Bestandteil dieser Aufstellung/ )||
552
( $line =~ /Transactions pending are not included in this report/ ) )
553
{
554
0
$StartLineDetected = 1;
555
}
556
}
557
0
return MapData( @AccountStatement );
558
}
559
else
560
{
561
0
return undef;
562
}
563
}
564
else
565
{
566
0
return undef;
567
}
568
}
569
570
571
1;
572
__END__