UNPKG

banker

Version:

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

378 lines (318 loc) 14.2 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 FiservNewSession(config) { var self = this; BankSession.call(self, config); var urlBase = 'https://www.financial-net.com/' + self.config.bankName; self.loginUrl = urlBase + '/security/OlbCmdSmn000'; self.mainUrl = urlBase + '/banking/OlbCmdShb041'; // These pages take a while to fully load due to all the ASP.NET JavaScript // stuff going on after the HTML loads. self.waitOptions = { duration : 10000 }; } util.inherits(FiservNewSession, BankSession); FiservNewSession.prototype.info = function() { return { description : 'Credit unions handled by the Fiserv software (https://www.financial-net.com/* and possibly others)', configItems : { driver : '"fiserv-new"', bankName : 'Bank name (appears in URL as https://www.financial-net.com/BANKNAME/...)', 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' } }; }; FiservNewSession.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; var usernameSel = '#body_body_txtUserName', field = self.browser.field(usernameSel); if (!field) { cb(new Error('Could not find login ID field.')); return; } self.log('sending username'); self.browser.fill(usernameSel, self.config.username); self.browser.once('request', function(request) { // Thanks, Ballmer request.params['ctl00$ctl00$body$body$btnSubmit'] = 'Logon'; }); self.browser.query('#form1').submit(); self.browser.wait(self.waitOptions, function() { if (!self.pageLoaded('afterLogin', cb)) return; var question = self.browser.text('#body_body_lblQuestion'); 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('#body_body_txtAnswer', answer); self.browser.once('request', function(request) { // Thanks, Ballmer request.params['ctl00$ctl00$body$body$btnOK'] = 'OK'; }); self.browser.query('#form1').submit(); self.browser.wait(self.waitOptions, 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); } }); }); }; FiservNewSession.prototype._enterPassword = function(cb) { var self = this; var passwordSel = '#body_body_txtPassword', field = self.browser.field(passwordSel); if (!field) { cb(new Error('Could not find password field.')); return; } if (self.config.securityPhrase) { self.log('checking security phrase'); var phrase = self.browser.text('#body_body_lblPhrase'); if (!phrase || phrase !== self.config.securityPhrase) { cb(new Error('Invalid security phrase: expected "' + self.config.securityPhrase + '" but found "' + phrase + '"')); return; } } self.log('sending password'); self.browser.fill(passwordSel, self.config.password); self.browser.once('request', function(request) { // Thanks, Ballmer request.params['ctl00$ctl00$body$body$btnSubmit'] = 'OK'; }); self.browser.query('#form1').submit(); self.browser.wait(self.waitOptions, 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); }); }); }; FiservNewSession.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.click(link); self.browser.wait(self.waitOptions, function() { if (!self.pageLoaded('account', cb)) 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('table.sortable th.SUMTABR'), cells = self.browser.querySelectorAll('table.sortable 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 = 'div.window-body td.LEFT, div.window-body td.RIGHT'; cells = self.browser.querySelectorAll(sel); for (var i = 0; i < cells.length; i++) { switch (self.browser.text(cells[i])) { case 'Actual Balance': try { data.balances.actual = self.getBalance(cells[i + 1]); } catch (err) { // sometimes this is *****, which is not a valid BigNumber self.log('failed to set actual balance: ' + err.message); } break; case 'Available Balance': try { data.balances.available = self.getBalance(cells[i + 1]); } catch (err) { // sometimes this is *****, which is not a valid BigNumber self.log('failed to set available balance: ' + err.message); } break; } } var checkLinks = self.browser.querySelectorAll('a[target=check]'); 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 (/sessionParameterName=ImageF/.test(response.url)) { data.checkImages[checkNum]['Check front'] = imageData; } else if (/sessionParameterName=ImageB/.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('check'); 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.click('input[name=moreButton]'); self.browser.wait(self.waitOptions, function() { if (!self.pageLoaded('more' + (moreIndex++), cb)) return; loadMoreOrGetQIF(i - 1); }); } else { self.browser.evaluate(populateDocumentForms); self.browser.evaluate('document.forms[0].allbox.checked = true; CheckAll();'); self.log('getting QIF'); var button = null; try { button = self.browser.querySelector('input[name=qifButton]'); } catch (err) { cb(new Error( 'Cannot find Export button.')); return; } self.browser.click(button); self.browser.wait(self.waitOptions, 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); } }); }); } } loadMoreOrGetQIF(+(account.loadMoreTransactions || 0)); }); }); }); }); }; FiservNewSession.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); }; FiservNewSession.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.visit(self.logoutUrl, function() { if (!self.pageLoaded('logout', cb)) return; cb(null); }); }; module.exports = FiservNewSession;