This repository has been archived on 2025-08-18. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Bungloo/jso.js
2012-10-25 07:56:40 +02:00

613 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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) {
res += (i++ === 0 ? firstSeparator : '&') + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
}
return res;
}
/*
* 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();
/*
* ------ 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);
});
}
/*
* 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;
for(k in params) {
res += (i++ === 0 ? '?' : '&') + encodeURIComponent(k) + '=' + encodeURIComponent(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
*/
/**
* 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(providerid, scopes, callback) {
var
state,
request,
authurl,
co;
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"]) {
request["client_id"] = co["client_id"];
}
if (scopes) {
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 (ensure) {
var providerid, scopes, token;
for(providerid in ensure) {
scopes = undefined;
if (ensure[providerid]) scopes = ensure[providerid];
token = api_storage.getToken(providerid, scopes);
log("Ensure token for provider [" + providerid + "] ");
log(token);
if (token === null) {
jso_authrequest(providerid, 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 = 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"];
}
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"];
}
$.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(providerid, scopes, function() {
token = api_storage.getToken(providerid, scopes);
performAjax();
});
return;
} else {
throw "Could not perform AJAX call because no valid tokens was found.";
}
}
performAjax();
};
})(window, window.jQuery);