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
JavaScript
;
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;