UNPKG

node-clippercard

Version:

Unofficial Node.js library to retrieve and parse profile and activity data from the Bay Area's Clipper Card system

300 lines (243 loc) 10.1 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _request = require('request'); var _request2 = _interopRequireDefault(_request); var _cheerio = require('cheerio'); var _cheerio2 = _interopRequireDefault(_cheerio); var _package = require('../package.json'); var _package2 = _interopRequireDefault(_package); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /* * The ClipperAccount class serves as the wrapper for the account itself and is * the entry point for logging in to the Clipper Card. It has profile and * payment data bound to it, and provides access to the cards and their * statements. */ var ClipperAccount = function () { /** * Constructor to create a new ClipperAccount * * @param {object} options - Contains required options for creating the class */ function ClipperAccount(options) { _classCallCheck(this, ClipperAccount); this.MAX_LOGIN_ATTEMPTS = 3; this.uris = { login: 'https://www.clippercard.com/ClipperCard/loginFrame.jsf', dashboard: 'https://www.clippercard.com/ClipperCard/dashboard.jsf', sessionExpired: 'https://www.clippercard.com/ClipperCard/sessionExpired.jsf' }; this.userAgent = _package2.default.name + '@' + _package2.default.version; this.cookies = undefined; this.email = undefined; this.password = undefined; this.profile = undefined; this.cards = undefined; this._failedLoginCount = 0; if ('object' === (typeof options === 'undefined' ? 'undefined' : _typeof(options))) { // check we have a valid email address if (options.email && 'string' === typeof options.email) { this.email = options.email; } else { throw new Error('Email address must be provided in the class options'); } // check we have a valid password if (options.password && 'string' === typeof options.password) { this.password = options.password; } else { throw new Error('The password must be provided in the class options'); } if (options.userAgent) { this.userAgent = options.userAgent; } } } /** * Loads the login form and extracts the form elements, then submits the * form. By the time the callback is invoked, the cookie jar is in the right * state to be logged in. Once a cookie jar exists, this method will lazy * bypass itself unless forceRefresh=true. * * @param {function} callback - invoked as callback(error); * @param {boolean} forceRefresh - set true to refresh cookies [false] */ _createClass(ClipperAccount, [{ key: 'login', value: function login(callback) { var forceRefresh = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; if ('function' !== typeof callback) { callback = function callback() {}; } if (false === forceRefresh && 'object' === _typeof(this.cookies)) { return callback(null); } this._failedLoginAttempts = 0; this._login(callback); } /* Processes the actual login request * * @param {function} callback - invoked as callback(error); */ }, { key: '_login', value: function _login(callback) { var self = this; this.cookies = _request2.default.jar(); (0, _request2.default)({ method: 'GET', uri: this.uris.login, gzip: true, jar: this.cookies }, function (error, response, body) { if (error) { return callback(error); } var $ = _cheerio2.default.load(body); var form = $('form').first().serializeArray(); var formId = $('form').first().attr('id'); // parse the form elements var params = {}; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = form[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var i = _step.value; if (undefined !== i.name && 'string' === typeof i.name) { params[i.name] = i.value; } } // manually add in the username and password } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } params[formId + ':username'] = self.email; params[formId + ':password'] = self.password; // and some other parameters that look like they're needed params['javax.faces.source'] = formId + ':submitLogin'; params['javax.faces.partial.event'] = 'click'; params['javax.faces.partial.execute'] = formId + ':submitLogin ' + formId + ':username' + formId + ':password'; params['javax.faces.partial.render'] = formId + ':err'; params['javax.faces.behavior.event'] = 'action'; params['javax.faces.partial.ajax'] = 'true'; (0, _request2.default)({ method: 'POST', uri: self.uris.login, gzip: true, jar: self.cookies, form: params }, function (error, response, body) { if (error) { return callback(error); } if ('string' === typeof response.headers.location) { if (self.uris.dashboard === response.headers.location) { self._failedLoginCount = 0; return callback(null); } else if (self.uris.sessionExpired === resposne.headers.location) { if (++self._failedLoginCount >= self.MAX_LOGIN_ATTEMPTS) { return callback(new Error('Exceeded maximum number of failed login attempts')); } else { return self._login(callback, forceRefresh); } } else { return callback(new Error('Redirected to an unexpected location: ' + response.headers.location)); } } else { return callback(new Error('Logging in did not redirect as expected: ' + JSON.stringify(response.headers))); } }); }); } }, { key: 'getProfile', value: function getProfile(callback) { var forceRefresh = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; if ('function' !== typeof callback) { callback = function callback() {}; } if (false === forceRefresh && 'object' === _typeof(this.profile)) { return callback(null, this.profile); } var self = this; this._login(function (error) { if (error) { return callback(error); } self._getProfile(function (error, result) { if (error) { // we'll force a refreshed login this time self._login(function (error) { if (error) { return callback(error); } self._getProfile(callback); }, true); } callback(null, result); }); }); } }, { key: '_getProfile', value: function _getProfile(callback) { var self = this; (0, _request2.default)({ method: 'GET', uri: this.uris.dashboard, gzip: true, jar: this.cookies }, function (error, response, body) { if (error) { return callback(error); } var $ = _cheerio2.default.load(body); self.profile = {}; try { self.profile.name = $('div.fieldName:contains("Name on Account:")').next('div.fieldData').text(); self.profile.email = $('div.fieldName:contains("Email:")').next('div.fieldData').text(); self.profile.address = $('div.fieldName:contains("Address:")').next('div.fieldData').text(); self.profile.phone = $('div.fieldName:contains("Phone:")').next('div.fieldData').text(); } catch (e) { if (e instanceof TypeError) { // swallow any TypeError exceptions that are thrown console.log(e); } else { return callback(e); } } self.profile.payment = {}; try { self.profile.payment.primary = $('div.fieldName:contains("Primary:")').next('div.fieldData').text(); self.profile.payment.expiry = $('div.fieldData:contains("Exp.")').text().match(/[0-9]{2}\/[0-9]{2}/)[0]; } catch (e) { if (e instanceof TypeError) { // swallow any TypeError exceptions that are thrown console.log(e); } else { return callback(e); } } callback(null, self.profile); }); } }]); return ClipperAccount; }(); exports.default = ClipperAccount;