UNPKG

banker

Version:

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

343 lines (283 loc) 11.7 kB
var BankSession = require('./base'), moment = require('moment'), qif = require('../parsers/qif'), url = require('url'), util = require('util'); function AvadianSession(config) { var self = this; BankSession.call(this, config); self.loginUrl = 'https://www.avadiancu.com/'; self.waitOptions = { duration : 10000 }; self.patchJSDOM(); } util.inherits(AvadianSession, BankSession); AvadianSession.prototype.patchJSDOM = function() { var self = this; // Avadian loads jQuery 1.4.2, which does a querySelectorAll('.TEST') call // early in the DOM load process: // // https://github.com/jquery/jquery/blob/1.4.2/jquery.js#L3519 // // This occasionally causes a segfault. Since jQuery is just looking for a // non-empty result here, return a dummy value. // // There's a similar issue here, with getElementsByClassName: // // https://github.com/jquery/jquery/blob/1.4.2/jquery.js#L3552 function patch(window) { // This works for querySelectorAll but not getElementsByClassName // TODO: Why?? // var jsdom = require('../../node_modules/zombie/node_modules/jsdom'), // html = jsdom.dom.level3.html; // var proto = html.HTMLElement.prototype; var proto = window.document.createElement('div').__proto__; if (proto && proto.querySelectorAll && proto.getElementsByClassName) { var patched = false; if (!proto.querySelectorAllOld) { proto.querySelectorAllOld = proto.querySelectorAll; proto.querySelectorAll = function(selector) { // Note - Just patching over the method seems to be enough // to prevent the crash - we don't even have to intercept // the problematic call. However, we've already come this // far, so might as well go ahead and do it. if (this.toString() == '[ DIV ]' && selector == '.TEST') { return "please don't crash"; } else { return proto.querySelectorAllOld.apply(this, arguments); } }; patched = true; } if (!proto.getElementsByClassNameOld) { proto.getElementsByClassNameOld = proto.getElementsByClassName; proto.getElementsByClassName = function(className) { if (this.toString() == '[ DIV ]' && className == 'e') { return "please don't crash"; } else { return proto.getElementsByClassNameOld.apply(this, arguments); } }; patched = true; } if (patched) { self.log('patched jsdom'); } self.browser.removeListener('opened', patch); } else { self.log('warning - not patching jsdom'); } } self.browser.on('opened', patch); }; AvadianSession.prototype.info = function() { return { description : 'Avadian Credit Union (https://www.avadiancu.com/)', configItems : { bankName : '"avadian"', username : 'Online banking username', password : 'Online banking password' }, accountItems : { description : 'Account text on Download History page (without balance)', name : 'Link text on Account Summary page' } }; }; AvadianSession.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('#HBUSERNAME', self.config.username); self.browser.fill('#PASSWORD' , self.config.password); self.browser.click('#MCWSUBMIT'); } catch (err) { return cb(err); } self.browser.wait(self.waitOptions, function() { if (!self.pageLoaded('afterLogin', cb)) return; var link = self.browser.link('Log Out'); if (!link || !link.href) { cb(new Error(self.browser.alertMessage || 'Cannot find Log Out link.')); return; } var arr = self.browser.evaluate('m1mn1'); if (arr[0] != 'Account Summary') { cb(new Error('Failed to get Account Summary URL.')); return; } self.accountsUrl = url.resolve(self.browser.location.href, arr[1]); arr = self.browser.evaluate('m1mn1_10'); if (arr[0] != 'Download History') { cb(new Error('Failed to get Download History URL.')); return; } self.exportUrl = url.resolve(self.browser.location.href, arr[1]); self.loggedIn = true; self.log('logged in'); self.logoutUrl = url.resolve(self.browser.location.href, link.href); cb(null); }); }); }; AvadianSession.prototype.getTransactions = function(account, cb) { var self = this; var data = { transactions : [], balances : {} }; self.log('visiting accounts page'); self.browser.visit(self.accountsUrl, function() { if (!self.pageLoaded('accounts', cb)) return; self.log('getting balances for account "' + account.name + '"'); var table = self.browser.query('#shares'), headerCells = table.querySelectorAll('th'), columnIndices = {}, dataRows = table.querySelectorAll('tr.lightRow, tr.darkRow'); for (var i = 0; i < headerCells.length; i++) { columnIndices[self.browser.text(headerCells[i])] = i; } var ok = true; [ 'Deposits', 'Available', 'Balance' ].forEach(function(columnName) { if (ok && typeof columnIndices[columnName] == 'undefined') { cb(new Error( 'Could not find column "' + columnName + '" on accounts page.')); ok = false; } }); if (!ok) { return; } ok = false; for (var i = 0; i < dataRows.length; i++) { var cells = dataRows[i].querySelectorAll('td'); if (self.browser.text(cells[columnIndices['Deposits']]) == account.name) { ok = true; data.balances.available = self.getBalance(cells[columnIndices['Available']]); data.balances.actual = self.getBalance(cells[columnIndices['Balance']]); } } if (!ok) { cb(new Error( 'Could not find account with name "' + account.name + '" on accounts page.')); return; } self.log('visiting export page'); self.browser.visit(self.exportUrl, function() { if (!self.pageLoaded('export', cb)) return; var options = self.browser.querySelectorAll('#SLID option'), option = null; for (var i = 0; i < options.length; i++) { var text = self.browser.text(options[i]), desc = account.description; if (text.substring(0, desc.length) == desc) { option = options[i]; break; } } if (!option) { cb(new Error( 'Cannot find export option for account "' + account.description + '".')); return; } self.browser.selectOption(option); if (!account.minDate) { // default is 1-day export, which is probably not what we want account.minDate = moment().subtract(30, 'days').format('MM/DD/YY'); } var minDateSel = '#DATE1', maxDateSel = '#DATE2'; 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.log('getting data from ' + minDate + ' to ' + maxDate); self.browser.choose('input[name=QIFOFXCHOICE][value=QIF]'); self.log('getting QIF'); self.browser.pressButton('#MCWSUBMIT', 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); } }); }); }); }); }; AvadianSession.prototype._processData = function(data, cb) { var self = this; var transactions = []; data.transactions.forEach(function(tx) { if (tx.memo == '' && tx.description == 'Withdrawal Point of Sale ') { // Sometimes incomplete transactions get included in the QIF // download, and they are updated with the full data later. Throw // them out so they're not double-counted on the next update. // (Really, the text value above was in the memo field, but the QIF // parser moves it to the description field). } else { // This transaction looks OK. tx.sourceId = self.makeTransactionId(tx); transactions.push(tx); } }); data.transactions = transactions; cb(null, data); }; AvadianSession.prototype.logout = function(cb) { var self = this; if (!cb) cb = function() { }; if (!self.loggedIn) { self.log('may not be logged in'); } if (!self.logoutUrl) { cb(new Error('Logout URL is not known.')); return; } self.log('visiting logout page'); self.browser.errors = []; // self.browser.silent = false; // self.browser.debug = true; self.browser.visit(self.logoutUrl, self.waitOptions, function() { // A whole bunch of stuff happens here, and we get here before all the // redirects finish. To debug, uncomment the two statements above and // run with DEBUG=zombie. setTimeout(function() { // This always seems to fail due to the following error: // Called run() after dispose(). Error: Called run() after dispose(). // at sandbox.run (.../jsdom/node_modules/contextify/lib/contextify.js:21:19) // at DOMWindow.window._evaluate (.../zombie/lib/zombie/window.js:187:25) // if (!self.pageLoaded('logout', cb)) return; var err = self.browser.error; if (!err || err.message == 'XPathResult is not defined' || err.message == 'Called run() after dispose().') { self.loggedIn = false; self.log('logged out'); err = null; } cb(err); }, 5000); }); }; module.exports = AvadianSession;