UNPKG

banker

Version:

Downloads transactions and balances from online banking websites using Zombie.js.

377 lines (309 loc) 12.6 kB
var async = require('async'), BankSession = require('./base'), moment = require('moment'), util = require('util'), utils = require('../utils'); function RegionsSession(config) { var self = this; BankSession.call(this, config); // I had an absolutely terrible time trying to get this bank to work with // JavaScript enabled. Luckily, the website seems to support running // without JavaScript, for the most part. self.browser.features = self.browser.features.replace(/\bscripts\b/, 'no-scripts'); self.loginUrl = 'https://securebank.regions.com/login.aspx'; self.afterLoginUrl = 'https://securebank.regions.com/LoginIntercept.aspx'; self.accountsUrl = 'https://securebank.regions.com/balances/AccountSummary.aspx'; self.logoutUrl = 'https://securebank.regions.com/logout.aspx'; self.waitOptions = { duration : 10000 }; self.waitOptions2 = { duration : 20000 }; } util.inherits(RegionsSession, BankSession); RegionsSession.prototype.info = function() { return { description : 'Regions Bank (https://www.regions.com/)', configItems : { bankName : '"regions"', username : 'Online banking username', password : 'Online banking password', securityQuestions : 'Hash of security questions and answers' }, accountItems : { description : 'Link text and account number on Accounts page (e.g. "LIFEGREEN CHECKING******1234")' } }; }; RegionsSession.prototype.login = function(cb) { var self = this; if (self.loggedIn) { self.log('already logged in'); cb(null); return; } self.log('visiting login page'); self.browser.visit(self.loginUrl, function() { if (!self.pageLoaded('login', cb)) return; self.log('sending username and password'); try { self.browser.fill('#txtOnlineID', self.config.username); self.browser.fill('#txtPassword', self.config.password); } catch (err) { cb(err); return; } self.browser.once('request', function(request) { // The server really wants to see btnSignon.x and btnSignon.y // parameters. request.params['btnSignon.x'] = 20; request.params['btnSignon.y'] = 5; delete request.params['btnSignon']; }); self.browser.pressButton('#btnSignon', function() { if (!self.pageLoaded('afterLogin0', cb)) return; // TODO: Why does Zombie.js not follow this redirect? self.browser.visit(self.afterLoginUrl, function() { var question = self.browser.text('#lblChallengeQuestion'); if (question) { if (!self.pageLoaded('securityQuestion', cb)) return; self.log('got security question: ' + question); if (self.config.securityQuestions) { var answer = self.config.securityQuestions[question]; if (answer) { self.log('answering security question'); self.browser.fill('#txtAnswer', answer); self.browser.pressButton('#btnSubmit', function() { // self.browser.visit(self.afterLoginUrl, function() { self._loggedIn(cb); // }); }); } else { cb(new Error('Missing answer to security question: ' + question)); return; } } else { cb(new Error('Missing config.securityQuestions')); return; } } else { self._loggedIn(cb); } }); }); }); }; RegionsSession.prototype._loggedIn = function(cb) { var self = this; if (!self.pageLoaded('afterLogin', cb)) return; var link = null; try { link = self.browser.link('#abHeader_lnkLogOut a'); } catch (err) { } if (!link || !link.href) { cb(new Error('Cannot find LOG OUT link.')); return; } self.loggedIn = true; self.log('logged in'); cb(null); }; RegionsSession.prototype.getTransactions = function(account, cb) { var self = this; self.log('visiting accounts page'); self.browser.visit(self.accountsUrl, self.waitOptions, function() { if (!self.pageLoaded('accounts', cb)) return; var link = null, links1 = self.browser.querySelectorAll('#dgCheckingAccounts td[align=Left]'), links2 = self.browser.querySelectorAll( '#dgSavingsAccounts td[align=Left]'), links = [].slice.call(links1).concat([].slice.call(links2)); for (var i = 0; i < links.length; i++) { if (self.browser.text(links[i]) == account.description) { link = links[i]; break; } } try { link = link.querySelector('a'); } catch (err) { link = null; } if (!link) { cb(new Error('Cannot find account with description "' + account.description + '".')); return; } self.log('clicking account link (description "' + account.description + '")'); self.browser.click(link); self.browser.wait(self.waitOptions, function() { if (!self.pageLoaded('account', cb)) return; if (account.minDate || account.maxDate) { var minDateSel = '#calFromDate_txtDate', maxDateSel = '#calToDate_txtDate'; if (account.minDate) { self.browser.fill(minDateSel, account.minDate); } if (account.maxDate) { self.browser.fill(maxDateSel, account.maxDate); } var minDate = self.browser.query(minDateSel).value, maxDate = self.browser.query(maxDateSel).value; self.browser.once('request', function(request) { // The server really wants to see imgbtnView.x and // imgbtnView.y parameters. request.params['imgbtnView.x'] = 20; request.params['imgbtnView.y'] = 5; delete request.params['imgbtnView']; }); self.log('getting data from ' + minDate + ' to ' + maxDate); self.browser.click('#imgbtnView'); self.browser.wait(self.waitOptions2, function() { if (!self.pageLoaded('accountExpanded', cb)) return; self._scrapeTransactions(cb); }); } else { self._scrapeTransactions(cb); } }); }); }; RegionsSession.prototype._scrapeTransactions = function(cb) { var self = this; var data = { transactions : [], balances : {} }; var cells = self.browser.querySelectorAll('#pnlAcctSummary td'); for (var i = 0; i < cells.length; i++) { switch (self.browser.text(cells[i])) { case 'Available Balance': data.balances.available = self.getBalance(cells[i + 1]); break; case 'Posted Balance': data.balances.actual = self.getBalance(cells[i + 1]); break; } } // Scrape transaction data directly from the account page instead of using // the export function, because the account page has an additional Type // column which tells if the transaction is a transfer etc., and the export // function doesn't seem to work with JavaScript disabled anyway. self.log('scraping transactions'); var table = self.browser.query('#dgTrxHistory'), headerCells = table.querySelectorAll('tr[class^=header] td'), columnIndices = {}, dataRows = table.querySelectorAll('tr.item, tr.alternatingItem'); for (var i = 0; i < headerCells.length; i++) { columnIndices[self.browser.text(headerCells[i])] = i; } var ok = true; [ 'Date', 'Type', 'Description/Category', 'Debit (-)', 'Credit (+)', 'Posted Balance' ].forEach(function(columnName) { if (ok && typeof columnIndices[columnName] == 'undefined') { cb(new Error( 'Could not find column "' + columnName + '" on account page.')); ok = false; } }); if (!ok) { return; } for (var i = 0; i < dataRows.length; i++) { cells = dataRows[i].querySelectorAll('td'); if (i == 0) { if (dataRows[i].id == 'trNoTrxHistory') { // No activity in the current date range for this account break; } data.balances.fromList = self.getBalance(cells[columnIndices['Posted Balance']]); } var transaction = { date : self.browser.text(cells[columnIndices['Date']]), type : self.browser.text(cells[columnIndices['Type']]), description : self.browser.text(cells[columnIndices['Description/Category']]), amount : ( self.browser.text(cells[columnIndices['Credit (+)']]) || ('-' + self.browser.text(cells[columnIndices['Debit (-)']])) ) }; transaction.date = moment(transaction.date, 'MM/DD/YYYY'); transaction.amount = utils.decimal(transaction.amount); try { var imageCode = cells[columnIndices['Description/Category']] .querySelector('a') .getAttribute('onclick'); if (/CheckImage\.ashx/.test(imageCode)) { transaction.imageLink = imageCode.split("'")[1]; } } catch (err) { } data.transactions.push(transaction); } var imageIndex = 0; async.eachSeries(data.transactions, function(transaction, next) { if (transaction.imageLink) { self.log(util.format( 'getting images for transaction: %s on %s', transaction.description, transaction.date.format('M/D/YYYY'))) transaction.images = {}; self.enableImages(function(request, response) { if (response.statusCode == 200) { var imageData = 'data:' + response.headers['content-type'].split(';')[0] + ';base64,' + response.body.toString('base64'); transaction.images.Image = imageData; } }, function(disableImages) { self.browser.visit(transaction.imageLink, function() { if (!self.pageLoaded('image' + (++imageIndex), cb)) return; disableImages(); }); }, function() { delete transaction.imageLink; next(); }); } else { // No images for this transaction // TODO: Some transactions have PDFs attached - example onclick attribute: // javascript:openPDFWindow("06/05/2014","OD ","1","","False", // "https://securebank.regions.com/balances/NSF.ashx");return false; next(); } }, function(err) { self._processData(data, cb); }); }; RegionsSession.prototype._processData = function(data, cb) { var self = this; data.transactions.forEach(function(transaction) { transaction.sourceId = self.makeTransactionId(transaction); }); cb(null, data); }; RegionsSession.prototype.logout = function(cb) { var self = this; if (!cb) cb = function() { }; if (!self.loggedIn) { self.log('may not be logged in'); } self.log('visiting logout page'); self.browser.errors = []; self.browser.visit(self.logoutUrl, function() { if (!self.pageLoaded('logout', cb)) return; var err = self.browser.error; if (!err) { self.loggedIn = false; self.log('logged out'); err = null; } cb(err); }); }; module.exports = RegionsSession;