(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);