UNPKG

banker

Version:

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

377 lines (319 loc) 14.6 kB
var async = require('async'), BankSession = require('./base'), fs = require('fs'), path = require('path'), qif = require('../parsers/qif'), util = require('util'); // Zombie.js / jsdom don't populate the document.forms and document[formName] // properties. The Fiserv JavaScript code uses these properties pretty // extensively. var populateDocumentForms = fs.readFileSync( path.join( __dirname, '..', 'browser', 'populate-document-forms.js' )).toString(); function FiservOldSession(config) { var self = this; BankSession.call(self, config); var urlBase = 'https://www.netit.financial-net.com/' + self.config.bankName + '/cgi-bin/ebs'; self.url1 = urlBase + '?OLB_CMD-SMN-126'; self.url2 = 'https://www.ea.netit.financial-net.com/passmark/signin.do?&detect=5'; self.mainUrl = urlBase + '?OLB_CMD-SHB-233&CMD=OLB_CMD-SHB-041'; self.logoutUrl = urlBase + '?OLB_CMD-SMN-127'; // Selectors for the button to load more transactions and to export in QIF format self.moreButtonSel = ['form[name=more]' , 'input[name=OLB_CMD-EXP-115]'].join(' '); self.exportButtonSel = ['form[name=form1]', 'input[name=OLB_CMD-EXP-116]'].join(' '); } util.inherits(FiservOldSession, BankSession); FiservOldSession.prototype.info = function() { return { description : 'Credit unions handled by the Fiserv software (https://www.netit.financial-net.com/* and possibly others)', configItems : { driver : '"fiserv-old"', bankName : 'Bank name (appears in URL as https://www.netit.financial-net.com/BANKNAME/cgi-bin/...)', username : 'Online banking username', password : 'Online banking password', securityQuestions : 'Hash of security questions and answers', passmarkName : 'The "alt" attribute of your passmark image (optional; will be validated if given)' }, accountItems : { code : 'The account code (*1234=56) listed in the Account column' } }; }; FiservOldSession.prototype.login = function(cb) { var self = this; if (self.loggedIn) { self.log('already logged in'); cb(null); return; } self.log('visiting url 1'); self.browser.visit(self.url1, function() { if (!self.pageLoaded('url1', cb)) return; var field = self.browser.field('LOGNID'); if (!field) { cb(new Error('Could not find login ID field.')); return; } self.log('sending username'); self.browser.fill('LOGNID', self.config.username); self.browser.pressButton('input[type=submit]', function() { if (!self.pageLoaded('afterLogin', cb)) return; self.log('visiting url 2'); self.browser.visit(self.url2, function() { if (!self.pageLoaded('url2', cb)) return; var question = self.browser.text('.mod-hdr'); if (question) { 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('answer1', answer); self.browser.pressButton('input[type=submit]', function() { if (!self.pageLoaded('afterQuestion', cb)) return; self._enterPassword(cb); }); } else { cb(new Error('Missing answer to security question: ' + question)); return; } } else { cb(new Error('Missing config.securityQuestions')); return; } } else { self._enterPassword(cb); } }); }); }); }; FiservOldSession.prototype._enterPassword = function(cb) { var self = this; var field = self.browser.field('PARAM_PASSWORD'); if (!field) { cb(new Error('Could not find password field.')); return; } if (self.config.passmarkName) { self.log('checking passmark image'); var passmark = self.browser.query('img.passmarkImage'); if (!passmark || passmark.alt !== self.config.passmarkName) { cb(new Error('Invalid passmark name: expected "' + self.config.passmarkName + '" but found "' + (passmark ? passmark.alt : '') + '"')); return; } } self.log('sending password'); self.browser.fill('PARAM_PASSWORD', self.config.password); self.browser.alertMessage = null; self.browser.pressButton('input[type=submit]', function() { if (!self.pageLoaded('afterPassword', cb)) return; self.log('visiting main page'); self.browser.visit(self.mainUrl, function() { if (!self.pageLoaded('main', cb)) return; var link = null; try { link = self.browser.link('EXIT'); } catch (err) { } if (!link || !link.href) { cb(new Error(self.browser.alertMessage || 'Cannot find EXIT link.')); return; } self.loggedIn = true; self.log('logged in'); cb(null); }); }); }; FiservOldSession.prototype.getTransactions = function(account, cb) { var self = this; self.log('visiting main page'); self.browser.visit(self.mainUrl, function() { if (!self.pageLoaded('mainForAccount', cb)) return; self.browser.evaluate(populateDocumentForms); var link = null, links = self.browser.querySelectorAll('td.SUMTABL a'); for (var i = 0; i < links.length; i++) { if (self.browser.text(links[i]) == account.code) { link = links[i]; break; } } if (!link) { cb(new Error('Cannot find account with code "' + account.code + '".')); return; } self.log('clicking account link (code "' + account.code + '")'); self.browser.evaluate(link.href.replace(/^javascript:/, '')); self.browser.wait(function() { if (!self.pageLoaded('account', cb)) return; var status = self.browser.text('form[name=form1] font.body6'); if (status == 'Error') { cb(new Error( self.browser.text('form[name=form1] td.CENTER') || 'Unknown error on account page.')); return; } var link = null; try { link = self.browser.link('Export'); } catch (err) { } if (!link) { cb(new Error('Cannot find Export link on account page.')); return; } var data = { checkImages : {}, balances : {} }; var headers = self.browser.querySelectorAll('th.SUMTABR'), cells = self.browser.querySelectorAll('td.SUMTABR'); for (var i = 0; i < headers.length; i++) { if (self.browser.text(headers[i]) == 'Balance') { data.balances.fromList = self.getBalance(cells[i]); break; } } var sel = 'table[summary="Provides additional details about this account."] td'; cells = self.browser.querySelectorAll(sel); for (var i = 0; i < cells.length; i++) { switch (self.browser.text(cells[i])) { case 'Actual Balance': data.balances.actual = self.getBalance(cells[i + 1]); break; case 'Avail Balance': data.balances.available = self.getBalance(cells[i + 1]); break; } } var checkLinks = self.browser.querySelectorAll('a[target=FrontCheckImage]'); async.eachSeries(checkLinks, function(checkLink, next) { var checkNum = self.browser.text(checkLink); if (data.checkImages[checkNum]) { self.log('already got images for check #' + checkNum); next(null); } else { self.log('getting images for check #' + checkNum); data.checkImages[checkNum] = {}; self.enableImages(function(request, response) { if (response.statusCode == 200) { var imageData = 'data:' + response.headers['content-type'].split(';')[0] + ';base64,' + response.body.toString('base64'); if (/[?&]F_B=F(&|$)/.test(response.url)) { data.checkImages[checkNum]['Check front'] = imageData; } else if (/[?&]F_B=B(&|$)/.test(response.url)) { data.checkImages[checkNum]['Check back'] = imageData; } else { self.log('unrecognized check image: ' + response.url); } } }, function(disableImages) { self.browser.click(checkLink, function() { if (!self.pageLoaded('check' + checkNum, cb)) return; // the check page opens in a new window self.browser.close('FrontCheckImage'); disableImages(); }); }, function() { next(); }); } }, function(err) { self.log('clicking Export link'); self.browser.click(link, function() { if (!self.pageLoaded('accountExport', cb)) return; var moreIndex = 1; function loadMoreOrGetQIF(i) { if (i > 0) { self.log('getting more transactions'); self.browser.pressButton(self.moreButtonSel, function() { if (!self.pageLoaded('more' + (moreIndex++), cb)) return; loadMoreOrGetQIF(i - 1); }); } else { self.browser.evaluate(populateDocumentForms); self.browser.evaluate('document.form1.allbox.checked = true; CheckAll();'); self.log('getting QIF'); var button = null; try { button = self.browser.querySelector(self.exportButtonSel); } catch (err) { cb(new Error( 'Cannot find Export button.')); return; } self.browser.pressButton(button, function() { if (!self.pageLoaded('qif', cb)) return; qif.parse(self.browser.response.body, function(err, transactions) { if (err) { cb(err); } else { data.transactions = transactions; self._processData(data, cb); } }); }); } } // Loading more transactions does not currently work, because // the markup for the relevant form is so bad that Zombie.js // can't parse it properly. It's structured as follows: // // table > form > tr > th > input // // Zombie.js parses this as an empty form element with the // input elements outside of the form element. To get around // this, we'd probably have to find all input elements under // the td or table that contains the form[name=more] element, // and do the POST manually. //loadMoreOrGetQIF(+(account.loadMoreTransactions || 0)); loadMoreOrGetQIF(0); }); }); }); }); }; FiservOldSession.prototype._processData = function(data, cb) { var self = this; data.transactions.forEach(function(transaction) { if (/id:|ref:|t(x|rans(action)?) *id/i.test(transaction.memo)) { transaction.sourceId = transaction.memo; delete transaction.memo; } else { transaction.sourceId = self.makeTransactionId(transaction); } var match = transaction.description.match(/^SHARE DRAFT #\s*([0-9]+)\s*$/); if (match && data.checkImages[match[1]]) { transaction.images = data.checkImages[match[1]]; delete data.checkImages[match[1]]; } }); var keys = Object.keys(data.checkImages); if (keys.length) { self.log('check images with unmatched transactions: ' + keys.join(', ')); } delete data.checkImages; cb(null, data); }; FiservOldSession.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 || err.message == "Cannot read property 'PARAM_PASSWORD' of undefined") { self.loggedIn = false; self.log('logged out'); err = null; } cb(err); }); }; module.exports = FiservOldSession;