hellojs-xiaotian
Version:
A clientside Javascript library for standardizing requests to OAuth2 web services (and OAuth1 - with a shim)
1,882 lines (1,502 loc) • 69.9 kB
JavaScript
/**
* @hello.js
*
* HelloJS is a client side Javascript SDK for making OAuth2 logins and subsequent REST calls.
*
* @author Andrew Dodson
* @website https://adodson.com/hello.js/
*
* @copyright Andrew Dodson, 2012 - 2015
* @license MIT: You are free to use and modify this code for any use, on the condition that this copyright notice remains.
*/
var hello = function(name) {
return hello.use(name);
};
hello.utils = {
// Extend the first object with the properties and methods of the second
extend: function(r /*, a[, b[, ...]] */) {
// Get the arguments as an array but ommit the initial item
Array.prototype.slice.call(arguments, 1).forEach(function(a) {
if (Array.isArray(r) && Array.isArray(a)) {
Array.prototype.push.apply(r, a);
}
else if (r && (r instanceof Object || typeof r === 'object') && a && (a instanceof Object || typeof a === 'object') && r !== a) {
for (var x in a) {
r[x] = hello.utils.extend(r[x], a[x]);
}
}
else {
if (Array.isArray(a)) {
// Clone it
a = a.slice(0);
}
r = a;
}
});
return r;
}
};
// Core library
hello.utils.extend(hello, {
settings: {
// OAuth2 authentication defaults
redirect_uri: window.location.href.split('#')[0],
response_type: 'token',
display: 'popup',
state: '',
// OAuth1 shim
// The path to the OAuth1 server for signing user requests
// Want to recreate your own? Checkout https://github.com/MrSwitch/node-oauth-shim
oauth_proxy: 'https://auth-server.herokuapp.com/proxy',
// API timeout in milliseconds
timeout: 20000,
// Popup Options
popup: {
resizable: 1,
scrollbars: 1,
width: 500,
height: 550
},
// Default scope
// Many services require atleast a profile scope,
// HelloJS automatially includes the value of provider.scope_map.basic
// If that's not required it can be removed via hello.settings.scope.length = 0;
scope: ['basic'],
// Scope Maps
// This is the default module scope, these are the defaults which each service is mapped too.
// By including them here it prevents the scope from being applied accidentally
scope_map: {
basic: ''
},
// Default service / network
default_service: null,
// Force authentication
// When hello.login is fired.
// (null): ignore current session expiry and continue with login
// (true): ignore current session expiry and continue with login, ask for user to reauthenticate
// (false): if the current session looks good for the request scopes return the current session.
force: null,
// Page URL
// When 'display=page' this property defines where the users page should end up after redirect_uri
// Ths could be problematic if the redirect_uri is indeed the final place,
// Typically this circumvents the problem of the redirect_url being a dumb relay page.
page_uri: window.location.href
},
// Service configuration objects
services: {},
// Use
// Define a new instance of the HelloJS library with a default service
use: function(service) {
// Create self, which inherits from its parent
var self = Object.create(this);
// Inherit the prototype from its parent
self.settings = Object.create(this.settings);
// Define the default service
if (service) {
self.settings.default_service = service;
}
// Create an instance of Events
self.utils.Event.call(self);
return self;
},
// Initialize
// Define the client_ids for the endpoint services
// @param object o, contains a key value pair, service => clientId
// @param object opts, contains a key value pair of options used for defining the authentication defaults
// @param number timeout, timeout in seconds
init: function(services, options) {
var utils = this.utils;
if (!services) {
return this.services;
}
// Define provider credentials
// Reformat the ID field
for (var x in services) {if (services.hasOwnProperty(x)) {
if (typeof (services[x]) !== 'object') {
services[x] = {id: services[x]};
}
}}
// Merge services if there already exists some
utils.extend(this.services, services);
// Update the default settings with this one.
if (options) {
utils.extend(this.settings, options);
// Do this immediatly incase the browser changes the current path.
if ('redirect_uri' in options) {
this.settings.redirect_uri = utils.url(options.redirect_uri).href;
}
}
return this;
},
// Login
// Using the endpoint
// @param network stringify name to connect to
// @param options object (optional) {display mode, is either none|popup(default)|page, scope: email,birthday,publish, .. }
// @param callback function (optional) fired on signin
login: function() {
// Create an object which inherits its parent as the prototype and constructs a new event chain.
var _this = this;
var utils = _this.utils;
var error = utils.error;
var promise = utils.Promise();
// Get parameters
var p = utils.args({network: 's', options: 'o', callback: 'f'}, arguments);
// Local vars
var url;
// Get all the custom options and store to be appended to the querystring
var qs = utils.diffKey(p.options, _this.settings);
// Merge/override options with app defaults
var opts = p.options = utils.merge(_this.settings, p.options || {});
// Merge/override options with app defaults
opts.popup = utils.merge(_this.settings.popup, p.options.popup || {});
// Network
p.network = p.network || _this.settings.default_service;
// Bind callback to both reject and fulfill states
promise.proxy.then(p.callback, p.callback);
// Trigger an event on the global listener
function emit(s, value) {
hello.emit(s, value);
}
promise.proxy.then(emit.bind(this, 'auth.login auth'), emit.bind(this, 'auth.failed auth'));
// Is our service valid?
if (typeof (p.network) !== 'string' || !(p.network in _this.services)) {
// Trigger the default login.
// Ahh we dont have one.
return promise.reject(error('invalid_network', 'The provided network was not recognized'));
}
var provider = _this.services[p.network];
// Create a global listener to capture events triggered out of scope
var callbackId = utils.globalEvent(function(obj) {
// The responseHandler returns a string, lets save this locally
if (obj) {
if (typeof (obj) == 'string') {
obj = JSON.parse(obj);
}
}
else {
obj = error('cancelled', 'The authentication was not completed');
}
// Handle these response using the local
// Trigger on the parent
if (!obj.error) {
// Save on the parent window the new credentials
// This fixes an IE10 bug i think... atleast it does for me.
utils.store(obj.network, obj);
// Fulfill a successful login
promise.fulfill({
network: obj.network,
authResponse: obj
});
}
else {
// Reject a successful login
promise.reject(obj);
}
});
var redirectUri = utils.url(opts.redirect_uri).href;
// May be a space-delimited list of multiple, complementary types
var responseType = provider.oauth.response_type || opts.response_type;
// Fallback to token if the module hasn't defined a grant url
if (/\bcode\b/.test(responseType) && !provider.oauth.grant) {
responseType = responseType.replace(/\bcode\b/, 'token');
}
// Query string parameters, we may pass our own arguments to form the querystring
p.qs = utils.merge(qs, {
client_id: encodeURIComponent(provider.id),
response_type: encodeURIComponent(responseType),
redirect_uri: encodeURIComponent(redirectUri),
state: {
client_id: provider.id,
network: p.network,
display: opts.display,
callback: callbackId,
state: opts.state,
redirect_uri: redirectUri
}
});
// Get current session for merging scopes, and for quick auth response
var session = utils.store(p.network);
// Scopes (authentication permisions)
// Ensure this is a string - IE has a problem moving Arrays between windows
// Append the setup scope
var SCOPE_SPLIT = /[,\s]+/;
// Include default scope settings (cloned).
var scope = _this.settings.scope ? [_this.settings.scope.toString()] : [];
// Extend the providers scope list with the default
var scopeMap = utils.merge(_this.settings.scope_map, provider.scope || {});
// Add user defined scopes...
if (opts.scope) {
scope.push(opts.scope.toString());
}
// Append scopes from a previous session.
// This helps keep app credentials constant,
// Avoiding having to keep tabs on what scopes are authorized
if (session && 'scope' in session && session.scope instanceof String) {
scope.push(session.scope);
}
// Join and Split again
scope = scope.join(',').split(SCOPE_SPLIT);
// Format remove duplicates and empty values
scope = utils.unique(scope).filter(filterEmpty);
// Save the the scopes to the state with the names that they were requested with.
p.qs.state.scope = scope.join(',');
// Map scopes to the providers naming convention
scope = scope.map(function(item) {
// Does this have a mapping?
return (item in scopeMap) ? scopeMap[item] : item;
});
// Stringify and Arrayify so that double mapped scopes are given the chance to be formatted
scope = scope.join(',').split(SCOPE_SPLIT);
// Again...
// Format remove duplicates and empty values
scope = utils.unique(scope).filter(filterEmpty);
// Join with the expected scope delimiter into a string
p.qs.scope = scope.join(provider.scope_delim || ',');
// Is the user already signed in with the appropriate scopes, valid access_token?
if (opts.force === false) {
if (session && 'access_token' in session && session.access_token && 'expires' in session && session.expires > ((new Date()).getTime() / 1e3)) {
// What is different about the scopes in the session vs the scopes in the new login?
var diff = utils.diff((session.scope || '').split(SCOPE_SPLIT), (p.qs.state.scope || '').split(SCOPE_SPLIT));
if (diff.length === 0) {
// OK trigger the callback
promise.fulfill({
unchanged: true,
network: p.network,
authResponse: session
});
// Nothing has changed
return promise;
}
}
}
// Page URL
if (opts.display === 'page' && opts.page_uri) {
// Add a page location, place to endup after session has authenticated
p.qs.state.page_uri = utils.url(opts.page_uri).href;
}
// Bespoke
// Override login querystrings from auth_options
if ('login' in provider && typeof (provider.login) === 'function') {
// Format the paramaters according to the providers formatting function
provider.login(p);
}
// Add OAuth to state
// Where the service is going to take advantage of the oauth_proxy
if (!/\btoken\b/.test(responseType) ||
parseInt(provider.oauth.version, 10) < 2 ||
(opts.display === 'none' && provider.oauth.grant && session && session.refresh_token)) {
// Add the oauth endpoints
p.qs.state.oauth = provider.oauth;
// Add the proxy url
p.qs.state.oauth_proxy = opts.oauth_proxy;
}
// Convert state to a string
p.qs.state = encodeURIComponent(JSON.stringify(p.qs.state));
// URL
if (parseInt(provider.oauth.version, 10) === 1) {
// Turn the request to the OAuth Proxy for 3-legged auth
url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
}
// Refresh token
else if (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token) {
// Add the refresh_token to the request
p.qs.refresh_token = session.refresh_token;
// Define the request path
url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
}
else {
url = utils.qs(provider.oauth.auth, p.qs, encodeFunction);
}
// Broadcast this event as an auth:init
emit('auth.init', p);
// Execute
// Trigger how we want self displayed
if (opts.display === 'none') {
// Sign-in in the background, iframe
utils.iframe(url, redirectUri);
}
// Triggering popup?
else if (opts.display === 'popup') {
var popup = utils.popup(url, redirectUri, opts.popup);
var timer = setInterval(function() {
if (!popup || popup.closed) {
clearInterval(timer);
if (!promise.state) {
var response = error('cancelled', 'Login has been cancelled');
if (!popup) {
response = error('blocked', 'Popup was blocked');
}
response.network = p.network;
promise.reject(response);
}
}
}, 100);
}
else {
window.location = url;
}
return promise.proxy;
function encodeFunction(s) {return s;}
function filterEmpty(s) {return !!s;}
},
// Remove any data associated with a given service
// @param string name of the service
// @param function callback
logout: function() {
var _this = this;
var utils = _this.utils;
var error = utils.error;
// Create a new promise
var promise = utils.Promise();
var p = utils.args({name:'s', options: 'o', callback: 'f'}, arguments);
p.options = p.options || {};
// Add callback to events
promise.proxy.then(p.callback, p.callback);
// Trigger an event on the global listener
function emit(s, value) {
hello.emit(s, value);
}
promise.proxy.then(emit.bind(this, 'auth.logout auth'), emit.bind(this, 'error'));
// Network
p.name = p.name || this.settings.default_service;
p.authResponse = utils.store(p.name);
if (p.name && !(p.name in _this.services)) {
promise.reject(error('invalid_network', 'The network was unrecognized'));
}
else if (p.name && p.authResponse) {
// Define the callback
var callback = function(opts) {
// Remove from the store
utils.store(p.name, null);
// Emit events by default
promise.fulfill(hello.utils.merge({network:p.name}, opts || {}));
};
// Run an async operation to remove the users session
var _opts = {};
if (p.options.force) {
var logout = _this.services[p.name].logout;
if (logout) {
// Convert logout to URL string,
// If no string is returned, then this function will handle the logout async style
if (typeof (logout) === 'function') {
logout = logout(callback, p);
}
// If logout is a string then assume URL and open in iframe.
if (typeof (logout) === 'string') {
utils.iframe(logout);
_opts.force = null;
_opts.message = 'Logout success on providers site was indeterminate';
}
else if (logout === undefined) {
// The callback function will handle the response.
return promise.proxy;
}
}
}
// Remove local credentials
callback(_opts);
}
else {
promise.reject(error('invalid_session', 'There was no session to remove'));
}
return promise.proxy;
},
// Returns all the sessions that are subscribed too
// @param string optional, name of the service to get information about.
getAuthResponse: function(service) {
// If the service doesn't exist
service = service || this.settings.default_service;
if (!service || !(service in this.services)) {
return null;
}
return this.utils.store(service) || null;
},
// Events: placeholder for the events
events: {}
});
// Core utilities
hello.utils.extend(hello.utils, {
// Error
error: function(code, message) {
return {
error: {
code: code,
message: message
}
};
},
// Append the querystring to a url
// @param string url
// @param object parameters
qs: function(url, params, formatFunction) {
if (params) {
// Set default formatting function
formatFunction = formatFunction || encodeURIComponent;
// Override the items in the URL which already exist
for (var x in params) {
var str = '([\\?\\&])' + x + '=[^\\&]*';
var reg = new RegExp(str);
if (url.match(reg)) {
url = url.replace(reg, '$1' + x + '=' + formatFunction(params[x]));
delete params[x];
}
}
}
if (!this.isEmpty(params)) {
return url + (url.indexOf('?') > -1 ? '&' : '?') + this.param(params, formatFunction);
}
return url;
},
// Param
// Explode/encode the parameters of an URL string/object
// @param string s, string to decode
param: function(s, formatFunction) {
var b;
var a = {};
var m;
if (typeof (s) === 'string') {
formatFunction = formatFunction || decodeURIComponent;
m = s.replace(/^[\#\?]/, '').match(/([^=\/\&]+)=([^\&]+)/g);
if (m) {
for (var i = 0; i < m.length; i++) {
b = m[i].match(/([^=]+)=(.*)/);
a[b[1]] = formatFunction(b[2]);
}
}
return a;
}
else {
formatFunction = formatFunction || encodeURIComponent;
var o = s;
a = [];
for (var x in o) {if (o.hasOwnProperty(x)) {
if (o.hasOwnProperty(x)) {
a.push([x, o[x] === '?' ? '?' : formatFunction(o[x])].join('='));
}
}}
return a.join('&');
}
},
// Local storage facade
store: (function() {
var a = ['localStorage', 'sessionStorage'];
var i = -1;
var prefix = 'test';
// Set LocalStorage
var localStorage;
while (a[++i]) {
try {
// In Chrome with cookies blocked, calling localStorage throws an error
localStorage = window[a[i]];
localStorage.setItem(prefix + i, i);
localStorage.removeItem(prefix + i);
break;
}
catch (e) {
localStorage = null;
}
}
if (!localStorage) {
var cache = null;
localStorage = {
getItem: function(prop) {
prop = prop + '=';
var m = document.cookie.split(';');
for (var i = 0; i < m.length; i++) {
var _m = m[i].replace(/(^\s+|\s+$)/, '');
if (_m && _m.indexOf(prop) === 0) {
return _m.substr(prop.length);
}
}
return cache;
},
setItem: function(prop, value) {
cache = value;
document.cookie = prop + '=' + value;
}
};
// Fill the cache up
cache = localStorage.getItem('hello');
}
function get() {
var json = {};
try {
json = JSON.parse(localStorage.getItem('hello')) || {};
}
catch (e) {}
return json;
}
function set(json) {
localStorage.setItem('hello', JSON.stringify(json));
}
// Check if the browser support local storage
return function(name, value, days) {
// Local storage
var json = get();
if (name && value === undefined) {
return json[name] || null;
}
else if (name && value === null) {
try {
delete json[name];
}
catch (e) {
json[name] = null;
}
}
else if (name) {
json[name] = value;
}
else {
return json;
}
set(json);
return json || null;
};
})(),
// Create and Append new DOM elements
// @param node string
// @param attr object literal
// @param dom/string
append: function(node, attr, target) {
var n = typeof (node) === 'string' ? document.createElement(node) : node;
if (typeof (attr) === 'object') {
if ('tagName' in attr) {
target = attr;
}
else {
for (var x in attr) {if (attr.hasOwnProperty(x)) {
if (typeof (attr[x]) === 'object') {
for (var y in attr[x]) {if (attr[x].hasOwnProperty(y)) {
n[x][y] = attr[x][y];
}}
}
else if (x === 'html') {
n.innerHTML = attr[x];
}
// IE doesn't like us setting methods with setAttribute
else if (!/^on/.test(x)) {
n.setAttribute(x, attr[x]);
}
else {
n[x] = attr[x];
}
}}
}
}
if (target === 'body') {
(function self() {
if (document.body) {
document.body.appendChild(n);
}
else {
setTimeout(self, 16);
}
})();
}
else if (typeof (target) === 'object') {
target.appendChild(n);
}
else if (typeof (target) === 'string') {
document.getElementsByTagName(target)[0].appendChild(n);
}
return n;
},
// An easy way to create a hidden iframe
// @param string src
iframe: function(src) {
this.append('iframe', {src: src, style: {position:'absolute', left: '-1000px', bottom: 0, height: '1px', width: '1px'}}, 'body');
},
// Recursive merge two objects into one, second parameter overides the first
// @param a array
merge: function(/* Args: a, b, c, .. n */) {
var args = Array.prototype.slice.call(arguments);
args.unshift({});
return this.extend.apply(null, args);
},
// Makes it easier to assign parameters, where some are optional
// @param o object
// @param a arguments
args: function(o, args) {
var p = {};
var i = 0;
var t = null;
var x = null;
// 'x' is the first key in the list of object parameters
for (x in o) {if (o.hasOwnProperty(x)) {
break;
}}
// Passing in hash object of arguments?
// Where the first argument can't be an object
if ((args.length === 1) && (typeof (args[0]) === 'object') && o[x] != 'o!') {
// Could this object still belong to a property?
// Check the object keys if they match any of the property keys
for (x in args[0]) {if (o.hasOwnProperty(x)) {
// Does this key exist in the property list?
if (x in o) {
// Yes this key does exist so its most likely this function has been invoked with an object parameter
// Return first argument as the hash of all arguments
return args[0];
}
}}
}
// Else loop through and account for the missing ones.
for (x in o) {if (o.hasOwnProperty(x)) {
t = typeof (args[i]);
if ((typeof (o[x]) === 'function' && o[x].test(args[i])) || (typeof (o[x]) === 'string' && (
(o[x].indexOf('s') > -1 && t === 'string') ||
(o[x].indexOf('o') > -1 && t === 'object') ||
(o[x].indexOf('i') > -1 && t === 'number') ||
(o[x].indexOf('a') > -1 && t === 'object') ||
(o[x].indexOf('f') > -1 && t === 'function')
))
) {
p[x] = args[i++];
}
else if (typeof (o[x]) === 'string' && o[x].indexOf('!') > -1) {
return false;
}
}}
return p;
},
// Returns a URL instance
url: function(path) {
// If the path is empty
if (!path) {
return window.location;
}
// Chrome and FireFox support new URL() to extract URL objects
else if (window.URL && URL instanceof Function && URL.length !== 0) {
return new URL(path, window.location);
}
// Ugly shim, it works!
else {
var a = document.createElement('a');
a.href = path;
return a.cloneNode(false);
}
},
diff: function(a, b) {
return b.filter(function(item) {
return a.indexOf(item) === -1;
});
},
// Get the different hash of properties unique to `a`, and not in `b`
diffKey: function(a, b) {
if (a || !b) {
var r = {};
for (var x in a) {
// Does the property not exist?
if (!(x in b)) {
r[x] = a[x];
}
}
return r;
}
return a;
},
// Unique
// Remove duplicate and null values from an array
// @param a array
unique: function(a) {
if (!Array.isArray(a)) { return []; }
return a.filter(function(item, index) {
// Is this the first location of item
return a.indexOf(item) === index;
});
},
isEmpty: function(obj) {
// Scalar
if (!obj)
return true;
// Array
if (Array.isArray(obj)) {
return !obj.length;
}
else if (typeof (obj) === 'object') {
// Object
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
}
return true;
},
//jscs:disable
/*!
** Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
** Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
** Licensed under The MIT License <http://opensource.org/licenses/MIT>
** Source-Code distributed on <http://github.com/rse/thenable>
*/
Promise: (function(){
/* promise states [Promises/A+ 2.1] */
var STATE_PENDING = 0; /* [Promises/A+ 2.1.1] */
var STATE_FULFILLED = 1; /* [Promises/A+ 2.1.2] */
var STATE_REJECTED = 2; /* [Promises/A+ 2.1.3] */
/* promise object constructor */
var api = function (executor) {
/* optionally support non-constructor/plain-function call */
if (!(this instanceof api))
return new api(executor);
/* initialize object */
this.id = "Thenable/1.0.6";
this.state = STATE_PENDING; /* initial state */
this.fulfillValue = undefined; /* initial value */ /* [Promises/A+ 1.3, 2.1.2.2] */
this.rejectReason = undefined; /* initial reason */ /* [Promises/A+ 1.5, 2.1.3.2] */
this.onFulfilled = []; /* initial handlers */
this.onRejected = []; /* initial handlers */
/* provide optional information-hiding proxy */
this.proxy = {
then: this.then.bind(this)
};
/* support optional executor function */
if (typeof executor === "function")
executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
};
/* promise API methods */
api.prototype = {
/* promise resolving methods */
fulfill: function (value) { return deliver(this, STATE_FULFILLED, "fulfillValue", value); },
reject: function (value) { return deliver(this, STATE_REJECTED, "rejectReason", value); },
/* "The then Method" [Promises/A+ 1.1, 1.2, 2.2] */
then: function (onFulfilled, onRejected) {
var curr = this;
var next = new api(); /* [Promises/A+ 2.2.7] */
curr.onFulfilled.push(
resolver(onFulfilled, next, "fulfill")); /* [Promises/A+ 2.2.2/2.2.6] */
curr.onRejected.push(
resolver(onRejected, next, "reject" )); /* [Promises/A+ 2.2.3/2.2.6] */
execute(curr);
return next.proxy; /* [Promises/A+ 2.2.7, 3.3] */
}
};
/* deliver an action */
var deliver = function (curr, state, name, value) {
if (curr.state === STATE_PENDING) {
curr.state = state; /* [Promises/A+ 2.1.2.1, 2.1.3.1] */
curr[name] = value; /* [Promises/A+ 2.1.2.2, 2.1.3.2] */
execute(curr);
}
return curr;
};
/* execute all handlers */
var execute = function (curr) {
if (curr.state === STATE_FULFILLED)
execute_handlers(curr, "onFulfilled", curr.fulfillValue);
else if (curr.state === STATE_REJECTED)
execute_handlers(curr, "onRejected", curr.rejectReason);
};
/* execute particular set of handlers */
var execute_handlers = function (curr, name, value) {
/* global process: true */
/* global setImmediate: true */
/* global setTimeout: true */
/* short-circuit processing */
if (curr[name].length === 0)
return;
/* iterate over all handlers, exactly once */
var handlers = curr[name];
curr[name] = []; /* [Promises/A+ 2.2.2.3, 2.2.3.3] */
var func = function () {
for (var i = 0; i < handlers.length; i++)
handlers[i](value); /* [Promises/A+ 2.2.5] */
};
/* execute procedure asynchronously */ /* [Promises/A+ 2.2.4, 3.1] */
if (typeof process === "object" && typeof process.nextTick === "function")
process.nextTick(func);
else if (typeof setImmediate === "function")
setImmediate(func);
else
setTimeout(func, 0);
};
/* generate a resolver function */
var resolver = function (cb, next, method) {
return function (value) {
if (typeof cb !== "function") /* [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4] */
next[method].call(next, value); /* [Promises/A+ 2.2.7.3, 2.2.7.4] */
else {
var result;
try { result = cb(value); } /* [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2] */
catch (e) {
next.reject(e); /* [Promises/A+ 2.2.7.2] */
return;
}
resolve(next, result); /* [Promises/A+ 2.2.7.1] */
}
};
};
/* "Promise Resolution Procedure" */ /* [Promises/A+ 2.3] */
var resolve = function (promise, x) {
/* sanity check arguments */ /* [Promises/A+ 2.3.1] */
if (promise === x || promise.proxy === x) {
promise.reject(new TypeError("cannot resolve promise with itself"));
return;
}
/* surgically check for a "then" method
(mainly to just call the "getter" of "then" only once) */
var then;
if ((typeof x === "object" && x !== null) || typeof x === "function") {
try { then = x.then; } /* [Promises/A+ 2.3.3.1, 3.5] */
catch (e) {
promise.reject(e); /* [Promises/A+ 2.3.3.2] */
return;
}
}
/* handle own Thenables [Promises/A+ 2.3.2]
and similar "thenables" [Promises/A+ 2.3.3] */
if (typeof then === "function") {
var resolved = false;
try {
/* call retrieved "then" method */ /* [Promises/A+ 2.3.3.3] */
then.call(x,
/* resolvePromise */ /* [Promises/A+ 2.3.3.3.1] */
function (y) {
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
if (y === x) /* [Promises/A+ 3.6] */
promise.reject(new TypeError("circular thenable chain"));
else
resolve(promise, y);
},
/* rejectPromise */ /* [Promises/A+ 2.3.3.3.2] */
function (r) {
if (resolved) return; resolved = true; /* [Promises/A+ 2.3.3.3.3] */
promise.reject(r);
}
);
}
catch (e) {
if (!resolved) /* [Promises/A+ 2.3.3.3.3] */
promise.reject(e); /* [Promises/A+ 2.3.3.3.4] */
}
return;
}
/* handle other values */
promise.fulfill(x); /* [Promises/A+ 2.3.4, 2.3.3.4] */
};
/* export API */
return api;
})(),
//jscs:enable
// Event
// A contructor superclass for adding event menthods, on, off, emit.
Event: function() {
var separator = /[\s\,]+/;
// If this doesn't support getPrototype then we can't get prototype.events of the parent
// So lets get the current instance events, and add those to a parent property
this.parent = {
events: this.events,
findEvents: this.findEvents,
parent: this.parent,
utils: this.utils
};
this.events = {};
// On, subscribe to events
// @param evt string
// @param callback function
this.on = function(evt, callback) {
if (callback && typeof (callback) === 'function') {
var a = evt.split(separator);
for (var i = 0; i < a.length; i++) {
// Has this event already been fired on this instance?
this.events[a[i]] = [callback].concat(this.events[a[i]] || []);
}
}
return this;
};
// Off, unsubscribe to events
// @param evt string
// @param callback function
this.off = function(evt, callback) {
this.findEvents(evt, function(name, index) {
if (!callback || this.events[name][index] === callback) {
this.events[name][index] = null;
}
});
return this;
};
// Emit
// Triggers any subscribed events
this.emit = function(evt /*, data, ... */) {
// Get arguments as an Array, knock off the first one
var args = Array.prototype.slice.call(arguments, 1);
args.push(evt);
// Handler
var handler = function(name, index) {
// Replace the last property with the event name
args[args.length - 1] = (name === '*' ? evt : name);
// Trigger
this.events[name][index].apply(this, args);
};
// Find the callbacks which match the condition and call
var _this = this;
while (_this && _this.findEvents) {
// Find events which match
_this.findEvents(evt + ',*', handler);
_this = _this.parent;
}
return this;
};
//
// Easy functions
this.emitAfter = function() {
var _this = this;
var args = arguments;
setTimeout(function() {
_this.emit.apply(_this, args);
}, 0);
return this;
};
this.findEvents = function(evt, callback) {
var a = evt.split(separator);
for (var name in this.events) {if (this.events.hasOwnProperty(name)) {
if (a.indexOf(name) > -1) {
for (var i = 0; i < this.events[name].length; i++) {
// Does the event handler exist?
if (this.events[name][i]) {
// Emit on the local instance of this
callback.call(this, name, i);
}
}
}
}}
};
return this;
},
// Global Events
// Attach the callback to the window object
// Return its unique reference
globalEvent: function(callback, guid) {
// If the guid has not been supplied then create a new one.
guid = guid || '_hellojs_' + parseInt(Math.random() * 1e12, 10).toString(36);
// Define the callback function
window[guid] = function() {
// Trigger the callback
try {
if (callback.apply(this, arguments)) {
delete window[guid];
}
}
catch (e) {
console.error(e);
}
};
return guid;
},
// Trigger a clientside popup
// This has been augmented to support PhoneGap
popup: function(url, redirectUri, options) {
var documentElement = document.documentElement;
// Multi Screen Popup Positioning (http://stackoverflow.com/a/16861050)
// Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html
// Fixes dual-screen position Most browsers Firefox
if (options.height && options.top === undefined) {
var dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
var height = screen.height || window.innerHeight || documentElement.clientHeight;
options.top = parseInt((height - options.height) / 2, 10) + dualScreenTop;
}
if (options.width && options.left === undefined) {
var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
var width = screen.width || window.innerWidth || documentElement.clientWidth;
options.left = parseInt((width - options.width) / 2, 10) + dualScreenLeft;
}
// Convert options into an array
var optionsArray = [];
Object.keys(options).forEach(function(name) {
var value = options[name];
optionsArray.push(name + (value !== null ? '=' + value : ''));
});
// Call the open() function with the initial path
//
// OAuth redirect, fixes URI fragments from being lost in Safari
// (URI Fragments within 302 Location URI are lost over HTTPS)
// Loading the redirect.html before triggering the OAuth Flow seems to fix it.
//
// Firefox decodes URL fragments when calling location.hash.
// - This is bad if the value contains break points which are escaped
// - Hence the url must be encoded twice as it contains breakpoints.
if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
url = redirectUri + '#oauth_redirect=' + encodeURIComponent(encodeURIComponent(url));
}
var popup = window.open(
url,
'_blank',
optionsArray.join(',')
);
if (popup && popup.focus) {
popup.focus();
}
return popup;
},
// OAuth and API response handler
responseHandler: function(window, parent) {
var _this = this;
var p;
var location = window.location;
// Is this an auth relay message which needs to call the proxy?
p = _this.param(location.search);
// OAuth2 or OAuth1 server response?
if (p && p.state && (p.code || p.oauth_token)) {
try {
var state = JSON.parse(p.state);
// Add this path as the redirect_uri
p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, '');
// Redirect to the host
var path = _this.qs(state.oauth_proxy, p);
location.assign(path);
return;
}
catch (e) {
console.error('Could not decode state parameter', e);
return;
}
}
// Save session, from redirected authentication
// #access_token has come in?
//
// FACEBOOK is returning auth errors within as a query_string... thats a stickler for consistency.
// SoundCloud is the state in the querystring and the token in the hashtag, so we'll mix the two together
p = _this.merge(_this.param(location.search || ''), _this.param(location.hash || ''));
// If p.state
if (p && 'state' in p) {
// Remove any addition information
// E.g. p.state = 'facebook.page';
try {
var a = JSON.parse(p.state);
_this.extend(p, a);
}
catch (e) {
var stateDecoded = decodeURIComponent(p.state);
try {
var b = JSON.parse(stateDecoded);
_this.extend(p, b);
}
catch (e) {
console.error('Could not decode state parameter');
}
}
// Access_token?
if (('access_token' in p && p.access_token) && p.network) {
if (!p.expires_in || parseInt(p.expires_in, 10) === 0) {
// If p.expires_in is unset, set to 0
p.expires_in = 0;
}
p.expires_in = parseInt(p.expires_in, 10);
p.expires = ((new Date()).getTime() / 1e3) + (p.expires_in || (60 * 60 * 24 * 365));
// Lets use the "state" to assign it to one of our networks
authCallback(p, window, parent);
}
// Error=?
// &error_description=?
// &state=?
else if (('error' in p && p.error) && p.network) {
p.error = {
code: p.error,
message: p.error_message || p.error_description
};
// Let the state handler handle it
authCallback(p, window, parent);
}
// API call, or a cancelled login
// Result is serialized JSON string
else if (p.callback && p.callback in parent) {
// Trigger a function in the parent
var res = 'result' in p && p.result ? JSON.parse(p.result) : false;
// Trigger the callback on the parent
callback(parent, p.callback)(res);
closeWindow();
}
// If this page is still open
if (p.page_uri && isValidUrl(p.page_uri)) {
location.assign(p.page_uri);
}
}
// OAuth redirect, fixes URI fragments from being lost in Safari
// (URI Fragments within 302 Location URI are lost over HTTPS)
// Loading the redirect.html before triggering the OAuth Flow seems to fix it.
else if ('oauth_redirect' in p) {
location.assign(decodeURIComponent(p.oauth_redirect));
return;
}
function isValidUrl(url) {
var regexp = /^https?:/;
return regexp.test(url);
}
// Trigger a callback to authenticate
function authCallback(obj, window, parent) {
var cb = obj.callback;
var network = obj.network;
// Trigger the callback on the parent
_this.store(network, obj);
// If this is a page request it has no parent or opener window to handle callbacks
if (('display' in obj) && obj.display === 'page') {
return;
}
// Remove from session object
if (parent && cb && cb in parent) {
try {
delete obj.callback;
}
catch (e) {}
// Update store
_this.store(network, obj);
// Call the globalEvent function on the parent
// It's safer to pass back a string to the parent,
// Rather than an object/array (better for IE8)
var str = JSON.stringify(obj);
try {
callback(parent, cb)(str);
}
catch (e) {
// Error thrown whilst executing parent callback
}
}
closeWindow();
}
function callback(parent, callbackID) {
if (callbackID.indexOf('_hellojs_') !== 0) {
return function() {
throw 'Could not execute callback ' + callbackID;
};
}
return parent[callbackID];
}
function closeWindow() {
if (window.frameElement) {
// Inside an iframe, remove from parent
parent.document.body.removeChild(window.frameElement);
}
else {
// Close this current window
try {
window.close();
}
catch (e) {}
// IOS bug wont let us close a popup if still loading
if (window.addEventListener) {
window.addEventListener('load', function() {
window.close();
});
}
}
}
}
});
// Events
// Extend the hello object with its own event instance
hello.utils.Event.call(hello);
///////////////////////////////////
// Monitoring session state
// Check for session changes
///////////////////////////////////
(function(hello) {
// Monitor for a change in state and fire
var oldSessions = {};
// Hash of expired tokens
var expired = {};
// Listen to other triggers to Auth events, use these to update this
hello.on('auth.login, auth.logout', function(auth) {
if (auth && typeof (auth) === 'object' && auth.network) {
oldSessions[auth.network] = hello.utils.store(auth.network) || {};
}
});
(function self() {
var CURRENT_TIME = ((new Date()).getTime() / 1e3);
var emit = function(eventName) {
hello.emit('auth.' + eventName, {
network: name,
authResponse: session
});
};
// Loop through the services
for (var name in hello.services) {if (hello.services.hasOwnProperty(name)) {
if (!hello.services[name].id) {
// We haven't attached an ID so dont listen.
continue;
}
// Get session
var session = hello.utils.store(name) || {};
var provider = hello.services[name];
var oldSess = oldSessions[name] || {};
// Listen for globalEvents that did not get triggered from the child
if (session && 'callback' in session) {
// To do remove from session object...
var cb = session.callback;
try {
delete session.callback;
}
catch (e) {}
// Update store
// Removing the callback
hello.utils.store(name, session);
// Emit global events
try {
window[cb](session);
}
catch (e) {}
}
// Refresh token
if (session && ('expires' in session) && session.expires < CURRENT_TIME) {
// If auto refresh is possible
// Either the browser supports
var refresh = provider.refresh || session.refresh_token;
// Has the refresh been run recently?
if (refresh && (!(name in expired) || expired[name] < CURRENT_TIME)) {
// Try to resignin
hello.emit('notice', name + ' has expired trying to resignin');
hello.login(name, {display: 'none', force: false});
// Update expired, every 10 minutes
expired[name] = CURRENT_TIME + 600;
}
// Does this provider not support refresh
else if (!refresh && !(name in expired)) {
// Label the event
emit('expired');
expired[name] = true;
}
// If session has expired then we dont want to store its value until it can be established that its been updated
continue;
}
// Has session changed?
else if (oldSess.access_token === session.access_token &&
oldSess.expires === session.expires) {
continue;
}
// Access_token has been removed
else if (!session.access_token && oldSess.access_token) {
emit('logout');
}
// Access_token has been created
else if (session.access_token && !oldSess.access_token) {
emit('login');
}
// Access_token has been updated
else if (session.expires !== oldSess.expires) {
emit('update');
}
// Updated stored session
oldSessions[name] = session;
// Remove the expired flags
if (name in expired) {
delete expired[name];
}
}}
// Check error events
setTimeout(self, 1000);
})();
})(hello);
// EOF CORE lib
//////////////////////////////////
/////////////////////////////////////////
// API
// @param path string
// @param query object (optional)
// @param method string (optional)
// @param data object (optional)
// @param timeout integer (optional)
// @param callback function (optional)
hello.api = function() {
// Shorthand
var _this = this;
var utils = _this.utils;
var error = utils.error;
// Construct a new Promise object
var promise = utils.Promise();
// Arguments
var p = utils.args({path: 's!', query: 'o', method: 's', data: 'o', timeout: 'i', callback: 'f'}, arguments);
// Method
p.method = (p.method || 'get').toLowerCase();
// Headers
p.headers = p.headers || {};
// Query
p.query = p.query || {};
// If get, put all parameters into query
if (p.method === 'get' || p.method === 'delete') {
utils.extend(p.query, p.data);
p.data = {};
}
var data = p.data = p.data || {};
// Completed event callback
promise.then(p.callback, p.callback);
// Remove the network from path, e.g. facebook:/me/friends
// Results in { network : facebook, path : me/friends }
if (!p.path) {
return promise.reject(error('invalid_path', 'Missing the path parameter from the request'));
}
p.path = p.path.replace(/^\/+/, '');
var a = (p.path.split(/[\/\:]/, 2) || [])[0].toLowerCase();
if (a in _this.services) {
p.network = a;
var reg = new RegExp('^' + a + ':?\/?');
p.path = p.path.replace(reg, '');
}
// Network & Provider
// Define the network that this request is made for
p.network = _this.settings.default_service = p.network || _this.settings.default_service;
var o = _this.services[p.network];
// INVALID
// Is there no service by the given network name?
if (!o) {
return promise.reject(error('invalid_network', 'Could not match the service requested: ' + p.network));
}
// PATH
// As long as the path isn't flagged as unavaiable, e.g. path == false
if (!(!(p.method in o) || !(p.path in o[p.method]) || o[p.method][p.path] !== false)) {
return promise.reject(error('invalid_path', 'The provided path is not available on the selected network'));
}
// PROXY
// OAuth1 calls always need a proxy
if (!p.oauth_proxy) {
p.oauth_proxy = _this.settings.oauth_proxy;
}
if (!('proxy' in p)) {
p.proxy = p.oauth_proxy && o.oauth && parseInt(o.oauth.version, 10) === 1;
}
// TIMEOUT
// Adopt timeout from global settings by default
if (!('timeout' in p)) {
p.timeout = _this.settings.timeout;
}
// Format response
// Whether to run the raw response through post processing.
if (!('formatResponse' in p)) {
p.formatResponse = true;
}
// Get the current session
// Append the access_token to the query
p.authResponse = _this.getAuthResponse(p.network);
if (p.authResponse && p.authResponse.access_token) {
p.query.access_token = p.authResponse.access_token;
}
var url = p.path;
var m;
// Store the query as options
// This is used to populate the request object before the data is augmented by the prewrap handlers.
p.options = utils.clone(p.query);
// Clone the data object
// Prevent this script overwriting the data of the incoming object.
// Ensure that everytime we run an iteration the callbacks haven't removed some data
p.data = utils.clone(data);
// URL Mapping
// Is there a map for the given URL?
var actions = o[{'delete': 'del'}[p.method] || p.method] || {};
// Extrapolate the QueryString
// Provide a clean path
// Move the querystring into the data
if (p.method === 'get') {
var query = url.split(/[\?#]/)[1];
if (query) {
utils.extend(p.query, utils.param(query));
// Remove the query part from the URL
url = url.replace(/\?.*?(#|$)/, '$1');
}
}
// Is the hash fragment defined
if ((m = url.match(/#(.+)/, ''))) {
url = url.split('#')[0];
p.path = m[1];
}
else if (url in actions) {
p.path = url;
url = actions[url];
}
else if ('default' in actions) {
url = actions['default'];
}
// Redirect Handler
// This defines for the Form+Iframe+Hash hack where to return the results too.
p.redirect_uri = _this.settings.redirect_uri;
// Define FormatHandler
// The request can be procesed in a multitude of ways
// Here's the options - depending on the browser and endpoint
p.xhr = o.xhr;
p.jsonp = o.jsonp;
p.form = o.form;
// Make request
if (typeof (url) === 'function') {
// Does self have its own callback?
url(p, getPath);
}
else {
// Else the URL is a string
getPath(url);
}
return promise.proxy;
// If url needs a base
// Wrap everything in
function getPath(url) {
// Format the string if it needs it
url = url.replace(/\@\{([a-z\_\-]+)(\|.*?)?\}/gi, function(m, key, defaults) {
var val = defaults ? defaults.replace(/^\|/, '') : '';
if (key in p.query) {
val = p.query[key];
delete p.query[key];
}
else if (p.data && key in p.data) {
val = p.data[key];
delete p.data[key];
}
else if (!defaults) {
promise.reject(error('missing_attribute', 'The attribute ' + key + ' is missing from the request'));
}
return val;
});
// Add base
if (!url.match(/^https?:\/\//)) {
url = o.base + url;
}
// Define the request URL
p.url = url;
// Make the HTTP request with the curated request object
// CALLBACK HANDLER
// @ response object
// @ statusCode integer if available
utils.request(p, function(r, headers) {
// Is this a raw response?
if (!p.formatResponse) {
// Bad request? error statusCode or otherwise contains an error response vis JSONP?
if (typeof headers === 'object' ? (headers.statusCode >= 400) : (typeof r === 'object' && 'error' in r)) {
promise.reject(r);
}
else {
promise.fulfill(r);
}
return;
}
// Should this be an object
if (r === true) {
r = {success:true};
}
else if (!r) {
r = {};
}
// The delete callback needs a better response
if (p.method === 'delete') {
r = (!r || utils.isEmpty(r)) ? {success:true} : r;
}
// FORMAT RESPONSE?
// Does self request have a corresponding formatter
if (o.wrap && ((p.path in o.wrap) || ('default' in o.wrap))) {
var wrap = (p.path in o.wrap ? p.path : 'default');
var time = (new Date()).getTime();
// FORMAT RESPONSE
var b = o.wrap[wrap](r, headers, p);
// Has the response been utterly overwritten?
// Typically self augments the existing object.. but for those rare occassions
if (b) {
r = b;
}
}
// Is there a next_page defined in the response?
if (r && 'paging' in r && r.paging.next) {
// Add the relative path if it is missing from the paging/next path
if (r.paging.next[0] === '?') {
r.paging.next = p.path + r.paging.next;
}
// The relative path has been defined, lets markup the handler in the HashFragment
else {
r.paging.next += '#' + p.path;
}
}
// Dispatch to listeners