Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
595 lines (476 sloc) 14 KB
(function(exp, $) {
var
config = {},
default_lifetime = 3600,
options = {
"debug": false
},
api_redirect,
Api_default_storage,
api_storage,
internalStates = [];
/*
* ------ SECTION: Utilities
*/
/*
* Returns a random string used for state
*/
var uuid = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
/**
* A log wrapper, that only logs if logging is turned on in the config
* @param {string} msg Log message
*/
var log = function(msg) {
if (!options.debug) return;
if (!console) return;
if (!console.log) return;
// console.log("LOG(), Arguments", arguments, msg)
if (arguments.length > 1) {
console.log(arguments);
} else {
console.log(msg);
}
}
/**
* Set the global options.
*/
var setOptions = function(opts) {
if (!opts) return;
for(var k in opts) {
if (opts.hasOwnProperty(k)) {
options[k] = opts[k];
}
}
log("Options is set to ", options);
}
/*
* Takes an URL as input and a params object.
* Each property in the params is added to the url as query string parameters
*/
var encodeURL = function(url, params) {
var res = url;
var k, i = 0;
var firstSeparator = (url.indexOf("?") === -1) ? '?' : '&';
for(k in params) {
document
//res += (i++ === 0 ? firstSeparator : '&') + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
res += (i++ === 0 ? firstSeparator : '&') + encodeURIComponent(k) + '=' + params[k];
}
return res;
}
/*
* Returns epoch, seconds since 1970.
* Used for calculation of expire times.
*/
var epoch = function() {
return Math.round(new Date().getTime()/1000.0);
}
var parseQueryString = function (qs) {
var e,
a = /\+/g, // Regex for replacing addition symbol with a space
r = /([^&;=]+)=?([^&;]*)/g,
d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
q = qs,
urlParams = {};
while (e = r.exec(q))
urlParams[d(e[1])] = d(e[2]);
return urlParams;
}
/*
* ------ / SECTION: Utilities
*/
/*
* Redirects the user to a specific URL
*/
api_redirect = function(url) {
window.location = url;
};
Api_default_storage = function() {
log("Constructor");
};
/**
saveState stores an object with an Identifier.
TODO: Ensure that both localstorage and JSON encoding has fallbacks for ancient browsers.
In the state object, we put the request object, plus these parameters:
* restoreHash
* providerID
* scopes
*/
Api_default_storage.prototype.saveState = function (state, obj) {
localStorage.setItem("state-" + state, JSON.stringify(obj));
}
/**
* getStage() returns the state object, but also removes it.
* @type {Object}
*/
Api_default_storage.prototype.getState = function(state) {
// log("getState (" + state+ ")");
var obj = JSON.parse(localStorage.getItem("state-" + state));
localStorage.removeItem("state-" + state)
return obj;
};
/*
* Checks if a token, has includes a specific scope.
* If token has no scope at all, false is returned.
*/
Api_default_storage.prototype.hasScope = function(token, scope) {
var i;
if (!token.scopes) return false;
for(i = 0; i < token.scopes.length; i++) {
if (token.scopes[i] === scope) return true;
}
return false;
};
/*
* Takes an array of tokens, and removes the ones that
* are expired, and the ones that do not meet a scopes requirement.
*/
Api_default_storage.prototype.filterTokens = function(tokens, scopes) {
var i, j,
result = [],
now = epoch(),
usethis;
if (!scopes) scopes = [];
for(i = 0; i < tokens.length; i++) {
usethis = true;
// Filter out expired tokens. Tokens that is expired in 1 second from now.
if (tokens[i].expires && tokens[i].expires < (now+1)) usethis = false;
// Filter out this token if not all scope requirements are met
for(j = 0; j < scopes.length; j++) {
if (!api_storage.hasScope(tokens[i], scopes[j])) usethis = false;
}
if (usethis) result.push(tokens[i]);
}
return result;
};
/*
* saveTokens() stores a list of tokens for a provider.
Usually the tokens stored are a plain Access token plus:
* expires : time that the token expires
* providerID: the provider of the access token?
* scopes: an array with the scopes (not string)
*/
Api_default_storage.prototype.saveTokens = function(provider, tokens) {
// log("Save Tokens (" + provider+ ")");
localStorage.setItem("tokens-" + provider, JSON.stringify(tokens));
};
Api_default_storage.prototype.getTokens = function(provider) {
// log("Get Tokens (" + provider+ ")");
var tokens = JSON.parse(localStorage.getItem("tokens-" + provider));
if (!tokens) tokens = [];
log("Token received", tokens)
return tokens;
};
Api_default_storage.prototype.wipeTokens = function(provider) {
localStorage.removeItem("tokens-" + provider);
};
/*
* Save a single token for a provider.
* This also cleans up expired tokens for the same provider.
*/
Api_default_storage.prototype.saveToken = function(provider, token) {
var tokens = this.getTokens(provider);
tokens = api_storage.filterTokens(tokens);
tokens.push(token);
this.saveTokens(provider, tokens);
};
/*
* Get a token if exists for a provider with a set of scopes.
* The scopes parameter is OPTIONAL.
*/
Api_default_storage.prototype.getToken = function(provider, scopes) {
var tokens = this.getTokens(provider);
tokens = api_storage.filterTokens(tokens, scopes);
if (tokens.length < 1) return null;
return tokens[0];
};
api_storage = new Api_default_storage();
/**
* Check if the hash contains an access token.
* And if it do, extract the state, compare with
* config, and store the access token for later use.
*
* The url parameter is optional. Used with phonegap and
* childbrowser when the jso context is not receiving the response,
* instead the response is received on a child browser.
*/
exp.jso_checkfortoken = function(providerID, url, callback) {
var
atoken,
h = window.location.hash,
now = epoch(),
state,
co;
log("jso_checkfortoken(" + providerID + ")");
// If a url is provided
if (url) {
// log('Hah, I got the url and it ' + url);
if(url.indexOf('#') === -1) return;
h = url.substring(url.indexOf('#'));
// log('Hah, I got the hash and it is ' + h);
}
/*
* Start with checking if there is a token in the hash
*/
if (h.length < 2) return;
if (h.indexOf("access_token") === -1) return;
h = h.substring(1);
atoken = parseQueryString(h);
if (atoken.state) {
state = api_storage.getState(atoken.state);
} else {
if (!providerID) {throw "Could not get [state] and no default providerid is provided.";}
state = {providerID: providerID};
}
if (!state) throw "Could not retrieve state";
if (!state.providerID) throw "Could not get providerid from state";
if (!config[state.providerID]) throw "Could not retrieve config for this provider.";
co = config[state.providerID];
/**
* If state was not provided, and default provider contains a scope parameter
* we assume this is the one requested...
*/
if (!atoken.state && co.scope) {
state.scopes = co.scope;
log("Setting state: ", state);
}
log("Checking atoken ", atoken, " and co ", co);
/*
* Decide when this token should expire.
* Priority fallback:
* 1. Access token expires_in
* 2. Life time in config (may be false = permanent...)
* 3. Specific permanent scope.
* 4. Default library lifetime:
*/
if (atoken["expires_in"]) {
atoken["expires"] = now + parseInt(atoken["expires_in"], 10);
} else if (co["default_lifetime"] === false) {
// Token is permanent.
} else if (co["default_lifetime"]) {
atoken["expires"] = now + co["default_lifetime"];
} else if (co["permanent_scope"]) {
if (!api_storage.hasScope(atoken, co["permanent_scope"])) {
atoken["expires"] = now + default_lifetime;
}
} else {
atoken["expires"] = now + default_lifetime;
}
/*
* Handle scopes for this token
*/
if (atoken["scope"]) {
atoken["scopes"] = atoken["scope"].split(" ");
} else if (state["scopes"]) {
atoken["scopes"] = state["scopes"];
}
api_storage.saveToken(state.providerID, atoken);
if (state.restoreHash) {
window.location.hash = state.restoreHash;
} else {
window.location.hash = '';
}
log(atoken);
if (internalStates[atoken.state] && typeof internalStates[atoken.state] === 'function') {
// log("InternalState is set, calling it now!");
internalStates[atoken.state]();
delete internalStates[atoken.state];
}
if (typeof callback === 'function') {
callback();
}
// log(atoken);
}
/*
* A config object contains:
*/
var jso_authrequest = function(scopes, callback) {
var
state,
request,
authurl,
co;
providerid = "oanda";
if (!config[providerid]) throw "Could not find configuration for provider " + providerid;
co = config[providerid];
log("About to send an authorization request to [" + providerid + "]. Config:")
log(co);
state = uuid();
request = {
"response_type": "token"
};
request.state = state;
if (callback && typeof callback === 'function') {
internalStates[state] = callback;
}
if (co["redirect_uri"]) {
request["redirect_uri"] = co["redirect_uri"];
}
if (co["client_id"]) {
document.write(scopes);
request["client_id"] = co["client_id"];
}
if (scopes) {
document.write(scopes.join("+"));
request["scope"] = scopes.join("+");
}
authurl = encodeURL(co.authorization, request);
// We'd like to cache the hash for not loosing Application state.
// With the implciit grant flow, the hash will be replaced with the access
// token when we return after authorization.
if (window.location.hash) {
request["restoreHash"] = window.location.hash;
}
request["providerID"] = providerid;
if (scopes) {
request["scopes"] = scopes;
}
log("Saving state [" + state+ "]");
log(JSON.parse(JSON.stringify(request)));
api_storage.saveState(state, request);
api_redirect(authurl);
};
exp.jso_ensureTokens = function (parameters) {
var providerid, scopes, token;
scopes = parameters['oanda'];
//scopes = ["read","trade","marketdata","stream"];
token = api_storage.getToken("oanda", scopes);
log("Ensure token for provider [oanda] ");
log(token);
if (token === null) {
jso_authrequest(scopes);
}
return true;
};
exp.jso_findDefaultEntry = function(c) {
var
k,
i = 0;
if (!c) return;
log("c", c);
for(k in c) {
i++;
if (c[k].isDefault && c[k].isDefault === true) {
return k;
}
}
if (i === 1) return k;
};
exp.jso_configure = function(c, opts) {
config = c;
setOptions(opts);
try {
var def = exp.jso_findDefaultEntry(c);
log("jso_configure() about to check for token for this entry", def);
exp.jso_checkfortoken(def);
} catch(e) {
log("Error when retrieving token from hash: " + e);
window.location.hash = "";
}
}
exp.jso_dump = function() {
var key;
for(key in config) {
log("=====> Processing provider [" + key + "]");
log("=] Config");
log(config[key]);
log("=] Tokens")
log(api_storage.getTokens(key));
}
}
exp.jso_wipe = function() {
var key;
log("jso_wipe()");
for(key in config) {
log("Wipping tokens for " + key);
api_storage.wipeTokens(key);
}
}
exp.jso_getToken = function(providerid, scopes) {
var token = api_storage.getToken(providerid, scopes);
if (!token) return null;
if (!token["access_token"]) return null;
return token["access_token"];
}
// Add configuration for one or more providers.
// jso_configure({
// "oanda": {
// client_id: "abcdefg",
// redirect_uri: "https://alchan-ud.dev.oanda.com/index.html",
// authorization: "https://oanda-cs-dev:1342/v1/oauth2/authorize",
// }
//});
exp.jso_registerRedirectHandler = function(callback) {
api_redirect = callback;
};
exp.jso_registerStorageHandler = function(object) {
api_storage = object;
};
/*
* From now on, we only perform tasks that require jQuery.
* Like adding the $.oajax function.
*/
if (typeof $ === 'undefined') return;
$.oajax = function(settings) {
var
allowia,
scopes,
token,
providerid,
co;
providerid = settings.jso_provider;
allowia = settings.jso_allowia || false;
scopes = settings.jso_scopes;
token = api_storage.getToken(providerid, scopes);
co = config[providerid];
// var successOverridden = settings.success;
// settings.success = function(response) {
// }
var errorOverridden = settings.error || null;
var performAjax = function() {
log("Perform ajax!");
if (!token) throw "Could not perform AJAX call because no valid tokens was found.";
if (co["presenttoken"] && co["presenttoken"] === "qs") {
settings.url += ((h.indexOf("?") === -1) ? '?' : '&') + "access_token=" + encodeURIComponent(token["access_token"]);
if (!settings.data) settings.data = {};
settings.data["access_token"] = token["access_token"];
} else {
if (!settings.headers) settings.headers = {};
settings.headers["Authorization"] = "Bearer " + token["access_token"];
}
return $.ajax(settings);
};
settings.error = function(jqXHR, textStatus, errorThrown) {
log('error(jqXHR, textStatus, errorThrown)');
log(jqXHR);
log(textStatus);
log(errorThrown);
if (jqXHR.status === 401) {
log("Token expired. About to delete this token");
log(token);
api_storage.wipeTokens(providerid);
}
if (errorOverridden && typeof errorOverridden === 'function') {
errorOverridden(jqXHR, textStatus, errorThrown);
}
}
if (!token) {
if (allowia) {
log("Perform authrequest");
jso_authrequest(scopes, function() {
token = api_storage.getToken(providerid, scopes);
performAjax();
});
return;
} else {
throw "Could not perform AJAX call because no valid tokens was found.";
}
}
return performAjax();
};
})(window, window.jQuery);