/*! hellojs v1.19.5 - (c) 2012-2021 Andrew Dodson - MIT https://adodson.com/hello.js/LICENSE */ // ES5 Object.create if (!Object.create) { // Shim, Object create // A shim for Object.create(), it adds a prototype to a new object Object.create = (function() { function F() {} return function(o) { if (arguments.length != 1) { throw new Error('Object.create implementation only accepts one parameter.'); } F.prototype = o; return new F(); }; })(); } // ES5 Object.keys if (!Object.keys) { Object.keys = function(o, k, r) { r = []; for (k in o) { if (r.hasOwnProperty.call(o, k)) r.push(k); } return r; }; } /* eslint-disable no-extend-native */ // ES5 [].indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(s) { for (var j = 0; j < this.length; j++) { if (this[j] === s) { return j; } } return -1; }; } // ES5 [].forEach if (!Array.prototype.forEach) { Array.prototype.forEach = function(fun/*, thisArg*/) { if (this === void 0 || this === null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (typeof fun !== 'function') { throw new TypeError(); } var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { if (i in t) { fun.call(thisArg, t[i], i, t); } } return this; }; } // ES5 [].filter if (!Array.prototype.filter) { Array.prototype.filter = function(fun, thisArg) { var a = []; this.forEach(function(val, i, t) { if (fun.call(thisArg || void 0, val, i, t)) { a.push(val); } }); return a; }; } // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.io/#x15.4.4.19 if (!Array.prototype.map) { Array.prototype.map = function(fun, thisArg) { var a = []; this.forEach(function(val, i, t) { a.push(fun.call(thisArg || void 0, val, i, t)); }); return a; }; } // ES5 isArray if (!Array.isArray) { // Function Array.isArray Array.isArray = function(o) { return Object.prototype.toString.call(o) === '[object Array]'; }; } // Test for location.assign if (typeof window === 'object' && typeof window.location === 'object' && !window.location.assign) { window.location.assign = function(url) { window.location = url; }; } // Test for Function.bind if (!Function.prototype.bind) { // MDN // Polyfill IE8, does not support native Function.bind Function.prototype.bind = function(b) { if (typeof this !== 'function') { throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } function C() {} var a = [].slice; var f = a.call(arguments, 1); var _this = this; var D = function() { return _this.apply(this instanceof C ? this : b || window, f.concat(a.call(arguments))); }; C.prototype = this.prototype; D.prototype = new C(); return D; }; } /* eslint-enable no-extend-native */ /** * @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) { // Prevent prototype pollution if (x === '__proto__' || x === 'constructor') { continue; } 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; }, /* eslint-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; })(), /* eslint-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); if (isValidUrl(path)) { 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) { var url = decodeURIComponent(p.oauth_redirect); if (isValidUrl(url)) { location.assign(url); } return; } function isValidUrl(url) { var regexp = /^https?:/; return regexp.test(url) // If `HELLOJS_REDIRECT_URL` is defined in the window context, validate that the URL matches it. && ( !Object.prototype.hasOwnProperty.call(window, 'HELLOJS_REDIRECT_URL') || url.match(window.HELLOJS_REDIRECT_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 // Emit events which pertain to the formatted response if (!r || 'error' in r) { promise.reject(r); } else { promise.fulfill(r); } }); } }; // API utilities hello.utils.extend(hello.utils, { // Make an HTTP request request: function(p, callback) { var _this = this; var error = _this.error; // This has to go through a POST request if (!_this.isEmpty(p.data) && !('FileList' in window) && _this.hasBinary(p.data)) { // Disable XHR and JSONP p.xhr = false; p.jsonp = false; } // Check if the browser and service support CORS var cors = this.request_cors(function() { // If it does then run this... return ((p.xhr === undefined) || (p.xhr && (typeof (p.xhr) !== 'function' || p.xhr(p, p.query)))); }); if (cors) { formatUrl(p, function(url) { var x = _this.xhr(p.method, url, p.headers, p.data, callback); x.onprogress = p.onprogress || null; // Windows Phone does not support xhr.upload, see #74 // Feature detect if (x.upload && p.onuploadprogress) { x.upload.onprogress = p.onuploadprogress; } }); return; } // Clone the query object // Each request modifies the query object and needs to be tared after each one. var _query = p.query; p.query = _this.clone(p.query); // Assign a new callbackID p.callbackID = _this.globalEvent(); // JSONP if (p.jsonp !== false) { // Clone the query object p.query.callback = p.callbackID; // If the JSONP is a function then run it if (typeof (p.jsonp) === 'function') { p.jsonp(p, p.query); } // Lets use JSONP if the method is 'get' if (p.method === 'get') { formatUrl(p, function(url) { _this.jsonp(url, callback, p.callbackID, p.timeout); }); return; } else { // It's not compatible reset query p.query = _query; } } // Otherwise we're on to the old school, iframe hacks and JSONP if (p.form !== false) { // Add some additional query parameters to the URL // We're pretty stuffed if the endpoint doesn't like these p.query.redirect_uri = p.redirect_uri; p.query.state = JSON.stringify({callback: p.callbackID}); var opts; if (typeof (p.form) === 'function') { // Format the request opts = p.form(p, p.query); } if (p.method === 'post' && opts !== false) { formatUrl(p, function(url) { _this.post(url, p.data, opts, callback, p.callbackID, p.timeout); }); return; } } // None of the methods were successful throw an error callback(error('invalid_request', 'There was no mechanism for handling this request')); return; // Format URL // Constructs the request URL, optionally wraps the URL through a call to a proxy server // Returns the formatted URL function formatUrl(p, callback) { // Are we signing the request? var sign; // OAuth1 // Remove the token from the query before signing if (p.authResponse && p.authResponse.oauth && parseInt(p.authResponse.oauth.version, 10) === 1) { // OAUTH SIGNING PROXY sign = p.query.access_token; // Remove the access_token delete p.query.access_token; // Enfore use of Proxy p.proxy = true; } // POST body to querystring if (p.data && (p.method === 'get' || p.method === 'delete')) { // Attach the p.data to the querystring. _this.extend(p.query, p.data); p.data = null; } // Construct the path var path = _this.qs(p.url, p.query); // Proxy the request through a server // Used for signing OAuth1 // And circumventing services without Access-Control Headers if (p.proxy) { // Use the proxy as a path path = _this.qs(p.oauth_proxy, { path: path, access_token: sign || '', // This will prompt the request to be signed as though it is OAuth1 then: p.proxy_response_type || (p.method.toLowerCase() === 'get' ? 'redirect' : 'proxy'), method: p.method.toLowerCase(), suppress_response_codes: p.suppress_response_codes || true }); } callback(path); } }, // Test whether the browser supports the CORS response request_cors: function(callback) { return 'withCredentials' in new XMLHttpRequest() && callback(); }, // Return the type of DOM object domInstance: function(type, data) { var test = 'HTML' + (type || '').replace( /^[a-z]/, function(m) { return m.toUpperCase(); } ) + 'Element'; if (!data) { return false; } if (window[test]) { return data instanceof window[test]; } else if (window.Element) { return data instanceof window.Element && (!type || (data.tagName && data.tagName.toLowerCase() === type)); } else { return (!(data instanceof Object || data instanceof Array || data instanceof String || data instanceof Number) && data.tagName && data.tagName.toLowerCase() === type); } }, // Create a clone of an object clone: function(obj) { // Does not clone DOM elements, nor Binary data, e.g. Blobs, Filelists if (obj === null || typeof (obj) !== 'object' || obj instanceof Date || 'nodeName' in obj || this.isBinary(obj) || (typeof FormData === 'function' && obj instanceof FormData)) { return obj; } if (Array.isArray(obj)) { // Clone each item in the array return obj.map(this.clone.bind(this)); } // But does clone everything else. var clone = {}; for (var x in obj) { clone[x] = this.clone(obj[x]); } return clone; }, // XHR: uses CORS to make requests xhr: function(method, url, headers, data, callback) { var r = new XMLHttpRequest(); var error = this.error; // Binary? var binary = false; if (method === 'blob') { binary = method; method = 'GET'; } method = method.toUpperCase(); // Xhr.responseType 'json' is not supported in any of the vendors yet. r.onload = function(e) { var json = r.response; try { json = JSON.parse(r.responseText); } catch (_e) { if (r.status === 401) { json = error('access_denied', r.statusText); } } var headers = headersToJSON(r.getAllResponseHeaders()); headers.statusCode = r.status; callback(json || (method === 'GET' ? error('empty_response', 'Could not get resource') : {}), headers); }; r.onerror = function(e) { var json = r.responseText; try { json = JSON.parse(r.responseText); } catch (_e) {} callback(json || error('access_denied', 'Could not get resource')); }; var x; // Should we add the query to the URL? if (method === 'GET' || method === 'DELETE') { data = null; } else if (data && typeof (data) !== 'string' && !(data instanceof FormData) && !(data instanceof File) && !(data instanceof Blob)) { // Loop through and add formData var f = new FormData(); for (x in data) if (data.hasOwnProperty(x)) { if (data[x] instanceof HTMLInputElement) { if ('files' in data[x] && data[x].files.length > 0) { f.append(x, data[x].files[0]); } } else if (data[x] instanceof Blob) { f.append(x, data[x], data.name); } else { f.append(x, data[x]); } } data = f; } // Open the path, async r.open(method, url, true); if (binary) { if ('responseType' in r) { r.responseType = binary; } else { r.overrideMimeType('text/plain; charset=x-user-defined'); } } // Set any bespoke headers if (headers) { for (x in headers) { r.setRequestHeader(x, headers[x]); } } r.send(data); return r; // Headers are returned as a string function headersToJSON(s) { var r = {}; var reg = /([a-z\-]+):\s?(.*);?/gi; var m; while ((m = reg.exec(s))) { r[m[1]] = m[2]; } return r; } }, // JSONP // Injects a script tag into the DOM to be executed and appends a callback function to the window object // @param string/function pathFunc either a string of the URL or a callback function pathFunc(querystringhash, continueFunc); // @param function callback a function to call on completion; jsonp: function(url, callback, callbackID, timeout) { var _this = this; var error = _this.error; // Change the name of the callback var bool = 0; var head = document.getElementsByTagName('head')[0]; var operaFix; var result = error('server_error', 'server_error'); var cb = function() { if (!(bool++)) { window.setTimeout(function() { callback(result); head.removeChild(script); }, 0); } }; // Add callback to the window object callbackID = _this.globalEvent(function(json) { result = json; return true; // Mark callback as done }, callbackID); // The URL is a function for some cases and as such // Determine its value with a callback containing the new parameters of this function. url = url.replace(new RegExp('=\\?(&|$)'), '=' + callbackID + '$1'); // Build script tag var script = _this.append('script', { id: callbackID, name: callbackID, src: url, async: true, onload: cb, onerror: cb, onreadystatechange: function() { if (/loaded|complete/i.test(this.readyState)) { cb(); } } }); // Opera fix error // Problem: If an error occurs with script loading Opera fails to trigger the script.onerror handler we specified // // Fix: // By setting the request to synchronous we can trigger the error handler when all else fails. // This action will be ignored if we've already called the callback handler "cb" with a successful onload event if (window.navigator.userAgent.toLowerCase().indexOf('opera') > -1) { operaFix = _this.append('script', { text: 'document.getElementById(\'' + callbackID + '\').onerror();' }); script.async = false; } // Add timeout if (timeout) { window.setTimeout(function() { result = error('timeout', 'timeout'); cb(); }, timeout); } // TODO: add fix for IE, // However: unable recreate the bug of firing off the onreadystatechange before the script content has been executed and the value of "result" has been defined. // Inject script tag into the head element head.appendChild(script); // Append Opera Fix to run after our script if (operaFix) { head.appendChild(operaFix); } }, // Post // Send information to a remote location using the post mechanism // @param string uri path // @param object data, key value data to send // @param function callback, function to execute in response post: function(url, data, options, callback, callbackID, timeout) { var _this = this; var error = _this.error; var doc = document; // This hack needs a form var form = null; var reenableAfterSubmit = []; var newform; var i = 0; var x = null; var bool = 0; var cb = function(r) { if (!(bool++)) { callback(r); } }; // What is the name of the callback to contain // We'll also use this to name the iframe _this.globalEvent(cb, callbackID); // Build the iframe window var win; try { // IE7 hack, only lets us define the name here, not later. win = doc.createElement('<iframe name="' + callbackID + '">'); } catch (e) { win = doc.createElement('iframe'); } win.name = callbackID; win.id = callbackID; win.style.display = 'none'; // Override callback mechanism. Triggger a response onload/onerror if (options && options.callbackonload) { // Onload is being fired twice win.onload = function() { cb({ response: 'posted', message: 'Content was posted' }); }; } if (timeout) { setTimeout(function() { cb(error('timeout', 'The post operation timed out')); }, timeout); } doc.body.appendChild(win); // If we are just posting a single item if (_this.domInstance('form', data)) { // Get the parent form form = data.form; // Loop through and disable all of its siblings for (i = 0; i < form.elements.length; i++) { if (form.elements[i] !== data) { form.elements[i].setAttribute('disabled', true); } } // Move the focus to the form data = form; } // Posting a form if (_this.domInstance('form', data)) { // This is a form element form = data; // Does this form need to be a multipart form? for (i = 0; i < form.elements.length; i++) { if (!form.elements[i].disabled && form.elements[i].type === 'file') { form.encoding = form.enctype = 'multipart/form-data'; form.elements[i].setAttribute('name', 'file'); } } } else { // Its not a form element, // Therefore it must be a JSON object of Key=>Value or Key=>Element // If anyone of those values are a input type=file we shall shall insert its siblings into the form for which it belongs. for (x in data) if (data.hasOwnProperty(x)) { // Is this an input Element? if (_this.domInstance('input', data[x]) && data[x].type === 'file') { form = data[x].form; form.encoding = form.enctype = 'multipart/form-data'; } } // Do If there is no defined form element, lets create one. if (!form) { // Build form form = doc.createElement('form'); doc.body.appendChild(form); newform = form; } var input; // Add elements to the form if they dont exist for (x in data) if (data.hasOwnProperty(x)) { // Is this an element? var el = (_this.domInstance('input', data[x]) || _this.domInstance('textArea', data[x]) || _this.domInstance('select', data[x])); // Is this not an input element, or one that exists outside the form. if (!el || data[x].form !== form) { // Does an element have the same name? var inputs = form.elements[x]; if (input) { // Remove it. if (!(inputs instanceof NodeList)) { inputs = [inputs]; } for (i = 0; i < inputs.length; i++) { inputs[i].parentNode.removeChild(inputs[i]); } } // Create an input element input = doc.createElement('input'); input.setAttribute('type', 'hidden'); input.setAttribute('name', x); // Does it have a value attribute? if (el) { input.value = data[x].value; } else if (_this.domInstance(null, data[x])) { input.value = data[x].innerHTML || data[x].innerText; } else { input.value = data[x]; } form.appendChild(input); } // It is an element, which exists within the form, but the name is wrong else if (el && data[x].name !== x) { data[x].setAttribute('name', x); data[x].name = x; } } // Disable elements from within the form if they weren't specified for (i = 0; i < form.elements.length; i++) { input = form.elements[i]; // Does the same name and value exist in the parent if (!(input.name in data) && input.getAttribute('disabled') !== true) { // Disable input.setAttribute('disabled', true); // Add re-enable to callback reenableAfterSubmit.push(input); } } } // Set the target of the form form.setAttribute('method', 'POST'); form.setAttribute('target', callbackID); form.target = callbackID; // Update the form URL form.setAttribute('action', url); // Submit the form // Some reason this needs to be offset from the current window execution setTimeout(function() { form.submit(); setTimeout(function() { try { // Remove the iframe from the page. //win.parentNode.removeChild(win); // Remove the form if (newform) { newform.parentNode.removeChild(newform); } } catch (e) { try { console.error('HelloJS: could not remove iframe'); } catch (ee) {} } // Reenable the disabled form for (var i = 0; i < reenableAfterSubmit.length; i++) { if (reenableAfterSubmit[i]) { reenableAfterSubmit[i].setAttribute('disabled', false); reenableAfterSubmit[i].disabled = false; } } }, 0); }, 100); }, // Some of the providers require that only multipart is used with non-binary forms. // This function checks whether the form contains binary data hasBinary: function(data) { for (var x in data) if (data.hasOwnProperty(x)) { if (this.isBinary(data[x])) { return true; } } return false; }, // Determines if a variable Either Is or like a FormInput has the value of a Blob isBinary: function(data) { return data instanceof Object && ( (this.domInstance('input', data) && data.type === 'file') || ('FileList' in window && data instanceof window.FileList) || ('File' in window && data instanceof window.File) || ('Blob' in window && data instanceof window.Blob)); }, // Convert Data-URI to Blob string toBlob: function(dataURI) { var reg = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i; var m = dataURI.match(reg); if (!m) { return dataURI; } var binary = atob(dataURI.replace(reg, '')); var array = []; for (var i = 0; i < binary.length; i++) { array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], {type: m[1]}); } }); // EXTRA: Convert FormElement to JSON for POSTing // Wrappers to add additional functionality to existing functions (function(hello) { // Copy original function var api = hello.api; var utils = hello.utils; utils.extend(utils, { // DataToJSON // This takes a FormElement|NodeList|InputElement|MixedObjects and convers the data object to JSON. dataToJSON: function(p) { var _this = this; var w = window; var data = p.data; // Is data a form object if (_this.domInstance('form', data)) { data = _this.nodeListToJSON(data.elements); } else if ('NodeList' in w && data instanceof NodeList) { data = _this.nodeListToJSON(data); } else if (_this.domInstance('input', data)) { data = _this.nodeListToJSON([data]); } // Is data a blob, File, FileList? if (('File' in w && data instanceof w.File) || ('Blob' in w && data instanceof w.Blob) || ('FileList' in w && data instanceof w.FileList)) { data = {file: data}; } // Loop through data if it's not form data it must now be a JSON object if (!('FormData' in w && data instanceof w.FormData)) { for (var x in data) if (data.hasOwnProperty(x)) { if ('FileList' in w && data[x] instanceof w.FileList) { if (data[x].length === 1) { data[x] = data[x][0]; } } else if (_this.domInstance('input', data[x]) && data[x].type === 'file') { continue; } else if (_this.domInstance('input', data[x]) || _this.domInstance('select', data[x]) || _this.domInstance('textArea', data[x])) { data[x] = data[x].value; } else if (_this.domInstance(null, data[x])) { data[x] = data[x].innerHTML || data[x].innerText; } } } p.data = data; return data; }, // NodeListToJSON // Given a list of elements extrapolate their values and return as a json object nodeListToJSON: function(nodelist) { var json = {}; // Create a data string for (var i = 0; i < nodelist.length; i++) { var input = nodelist[i]; // If the name of the input is empty or diabled, dont add it. if (input.disabled || !input.name) { continue; } // Is this a file, does the browser not support 'files' and 'FormData'? if (input.type === 'file') { json[input.name] = input; } else { json[input.name] = input.value || input.innerHTML; } } return json; } }); // Replace it hello.api = function() { // Get arguments var p = utils.args({path: 's!', method: 's', data: 'o', timeout: 'i', callback: 'f'}, arguments); // Change for into a data object if (p.data) { utils.dataToJSON(p); } return api.call(this, p); }; })(hello); ///////////////////////////////////// // // Save any access token that is in the current page URL // Handle any response solicited through iframe hash tag following an API request // ///////////////////////////////////// hello.utils.responseHandler(window, window.opener || window.parent); // Script to support ChromeApps // This overides the hello.utils.popup method to support chrome.identity.launchWebAuthFlow // See https://developer.chrome.com/apps/app_identity#non // Is this a chrome app? if (typeof chrome === 'object' && typeof chrome.identity === 'object' && chrome.identity.launchWebAuthFlow) { (function() { // Swap the popup method hello.utils.popup = function(url) { return _open(url, true); }; // Swap the hidden iframe method hello.utils.iframe = function(url) { _open(url, false); }; // Swap the request_cors method hello.utils.request_cors = function(callback) { callback(); // Always run as CORS return true; }; // Swap the storage method var _cache = {}; chrome.storage.local.get('hello', function(r) { // Update the cache _cache = r.hello || {}; }); hello.utils.store = function(name, value) { // Get all if (arguments.length === 0) { return _cache; } // Get if (arguments.length === 1) { return _cache[name] || null; } // Set if (value) { _cache[name] = value; chrome.storage.local.set({hello: _cache}); return value; } // Delete if (value === null) { delete _cache[name]; chrome.storage.local.set({hello: _cache}); return null; } }; // Open function function _open(url, interactive) { // Launch var ref = { closed: false }; // Launch the webAuthFlow chrome.identity.launchWebAuthFlow({ url: url, interactive: interactive }, function(responseUrl) { // Did the user cancel this prematurely if (responseUrl === undefined) { ref.closed = true; return; } // Split appart the URL var a = hello.utils.url(responseUrl); // The location can be augmented in to a location object like so... // We dont have window operations on the popup so lets create some var _popup = { location: { // Change the location of the popup assign: function(url) { // If there is a secondary reassign // In the case of OAuth1 // Trigger this in non-interactive mode. _open(url, false); }, search: a.search, hash: a.hash, href: a.href }, close: function() {} }; // Then this URL contains information which HelloJS must process // URL string // Window - any action such as window relocation goes here // Opener - the parent window which opened this, aka this script hello.utils.responseHandler(_popup, window); }); // Return the reference return ref; } })(); } // Phonegap override for hello.phonegap.js (function() { // Is this a phonegap implementation? if (!(/^file:\/{3}[^\/]/.test(window.location.href) && window.cordova)) { // Cordova is not included. return; } // Augment the hidden iframe method hello.utils.iframe = function(url, redirectUri) { hello.utils.popup(url, redirectUri, {hidden: 'yes'}); }; // Augment the popup var utilPopup = hello.utils.popup; // Replace popup hello.utils.popup = function(url, redirectUri, options) { // Run the standard var popup = utilPopup.call(this, url, redirectUri, options); // Create a function for reopening the popup, and assigning events to the new popup object // PhoneGap support // Add an event listener to listen to the change in the popup windows URL // This must appear before popup.focus(); try { if (popup && popup.addEventListener) { // Get the origin of the redirect URI var a = hello.utils.url(redirectUri); var redirectUriOrigin = a.origin || (a.protocol + '//' + a.hostname); // Listen to changes in the InAppBrowser window popup.addEventListener('loadstart', function(e) { var url = e.url; // Is this the path, as given by the redirectUri? // Check the new URL agains the redirectUriOrigin. // According to #63 a user could click 'cancel' in some dialog boxes .... // The popup redirects to another page with the same origin, yet we still wish it to close. if (url.indexOf(redirectUriOrigin) !== 0) { return; } // Split appart the URL var a = hello.utils.url(url); // We dont have window operations on the popup so lets create some // The location can be augmented in to a location object like so... var _popup = { location: { // Change the location of the popup assign: function(location) { // Unfourtunatly an app is may not change the location of a InAppBrowser window. // So to shim this, just open a new one. popup.executeScript({code: 'window.location.href = "' + location + ';"'}); }, search: a.search, hash: a.hash, href: a.href }, close: function() { if (popup.close) { popup.close(); try { popup.closed = true; } catch (_e) {} } } }; // Then this URL contains information which HelloJS must process // URL string // Window - any action such as window relocation goes here // Opener - the parent window which opened this, aka this script hello.utils.responseHandler(_popup, window); }); } } catch (e) {} return popup; }; })(); (function(hello) { // OAuth1 var OAuth1Settings = { version: '1.0', auth: 'https://www.dropbox.com/1/oauth/authorize', request: 'https://api.dropbox.com/1/oauth/request_token', token: 'https://api.dropbox.com/1/oauth/access_token' }; // OAuth2 Settings var OAuth2Settings = { version: 2, auth: 'https://www.dropbox.com/1/oauth2/authorize', grant: 'https://api.dropbox.com/1/oauth2/token' }; // Initiate the Dropbox module hello.init({ dropbox: { name: 'Dropbox', oauth: OAuth2Settings, login: function(p) { // OAuth2 non-standard adjustments p.qs.scope = ''; // Should this be run as OAuth1? // If the redirect_uri is is HTTP (non-secure) then its required to revert to the OAuth1 endpoints var redirect = decodeURIComponent(p.qs.redirect_uri); if (redirect.indexOf('http:') === 0 && redirect.indexOf('http://localhost/') !== 0) { // Override the dropbox OAuth settings. hello.services.dropbox.oauth = OAuth1Settings; } else { // Override the dropbox OAuth settings. hello.services.dropbox.oauth = OAuth2Settings; } // The dropbox login window is a different size p.options.popup.width = 1000; p.options.popup.height = 1000; }, /* Dropbox does not allow insecure HTTP URI's in the redirect_uri field ...otherwise I'd love to use OAuth2 Follow request https://forums.dropbox.com/topic.php?id=106505 p.qs.response_type = 'code'; oauth: { version: 2, auth: 'https://www.dropbox.com/1/oauth2/authorize', grant: 'https://api.dropbox.com/1/oauth2/token' } */ // API Base URL base: 'https://api.dropbox.com/1/', // Bespoke setting: this is states whether to use the custom environment of Dropbox or to use their own environment // Because it's notoriously difficult for Dropbox too provide access from other webservices, this defaults to Sandbox root: 'sandbox', // Map GET requests get: { me: 'account/info', // Https://www.dropbox.com/developers/core/docs#metadata 'me/files': req('metadata/auto/@{parent|}'), 'me/folder': req('metadata/auto/@{id}'), 'me/folders': req('metadata/auto/'), 'default': function(p, callback) { if (p.path.match('https://api-content.dropbox.com/1/files/')) { // This is a file, return binary data p.method = 'blob'; } callback(p.path); } }, post: { 'me/files': function(p, callback) { var path = p.data.parent; var fileName = p.data.name; p.data = { file: p.data.file }; // Does this have a data-uri to upload as a file? if (typeof (p.data.file) === 'string') { p.data.file = hello.utils.toBlob(p.data.file); } callback('https://api-content.dropbox.com/1/files_put/auto/' + path + '/' + fileName); }, 'me/folders': function(p, callback) { var name = p.data.name; p.data = {}; callback('fileops/create_folder?root=@{root|sandbox}&' + hello.utils.param({ path: name })); } }, // Map DELETE requests del: { 'me/files': 'fileops/delete?root=@{root|sandbox}&path=@{id}', 'me/folder': 'fileops/delete?root=@{root|sandbox}&path=@{id}' }, wrap: { me: function(o) { formatError(o); if (!o.uid) { return o; } o.name = o.display_name; var m = o.name.split(' '); o.first_name = m.shift(); o.last_name = m.join(' '); o.id = o.uid; delete o.uid; delete o.display_name; return o; }, 'default': function(o, headers, req) { formatError(o); if (o.is_dir && o.contents) { o.data = o.contents; delete o.contents; o.data.forEach(function(item) { item.root = o.root; formatFile(item, headers, req); }); } formatFile(o, headers, req); if (o.is_deleted) { o.success = true; } return o; } }, // Doesn't return the CORS headers xhr: function(p) { // The proxy supports allow-cross-origin-resource // Alas that's the only thing we're using. if (p.data && p.data.file) { var file = p.data.file; if (file) { if (file.files) { p.data = file.files[0]; } else { p.data = file; } } } if (p.method === 'delete') { p.method = 'post'; } return true; }, form: function(p, qs) { delete qs.state; delete qs.redirect_uri; } } }); function formatError(o) { if (o && 'error' in o) { o.error = { code: 'server_error', message: o.error.message || o.error }; } } function formatFile(o, headers, req) { if (typeof o !== 'object' || (typeof Blob !== 'undefined' && o instanceof Blob) || (typeof ArrayBuffer !== 'undefined' && o instanceof ArrayBuffer)) { // This is a file, let it through unformatted return; } if ('error' in o) { return; } var path = (o.root !== 'app_folder' ? o.root : '') + o.path.replace(/\&/g, '%26'); path = path.replace(/^\//, ''); if (o.thumb_exists) { o.thumbnail = req.oauth_proxy + '?path=' + encodeURIComponent('https://api-content.dropbox.com/1/thumbnails/auto/' + path + '?format=jpeg&size=m') + '&access_token=' + req.options.access_token; } o.type = (o.is_dir ? 'folder' : o.mime_type); o.name = o.path.replace(/.*\//g, ''); if (o.is_dir) { o.files = path.replace(/^\//, ''); } else { o.downloadLink = hello.settings.oauth_proxy + '?path=' + encodeURIComponent('https://api-content.dropbox.com/1/files/auto/' + path) + '&access_token=' + req.options.access_token; o.file = 'https://api-content.dropbox.com/1/files/auto/' + path; } if (!o.id) { o.id = o.path.replace(/^\//, ''); } // O.media = 'https://api-content.dropbox.com/1/files/' + path; } function req(str) { return function(p, cb) { delete p.query.limit; cb(str); }; } })(hello); (function(hello) { // For APIs, once a version is no longer usable, any calls made to it will be defaulted to the next oldest usable version. // So we explicitly state it. var version = 'v2.9'; hello.init({ facebook: { name: 'Facebook', // SEE https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow oauth: { version: 2, auth: 'https://www.facebook.com/' + version + '/dialog/oauth/', grant: 'https://graph.facebook.com/oauth/access_token' }, // Authorization scopes scope: { basic: 'public_profile', email: 'email', share: 'user_posts', birthday: 'user_birthday', events: 'user_events', photos: 'user_photos', videos: 'user_videos', friends: 'user_friends', files: 'user_photos,user_videos', publish_files: 'user_photos,user_videos,publish_actions', publish: 'publish_actions', // Deprecated in v2.0 // Create_event: 'create_event', offline_access: '' }, // Refresh the access_token refresh: false, login: function(p) { // Reauthenticate // https://developers.facebook.com/docs/facebook-login/reauthentication if (p.options.force) { p.qs.auth_type = 'reauthenticate'; } // Set the display value p.qs.display = p.options.display || 'popup'; }, logout: function(callback, options) { // Assign callback to a global handler var callbackID = hello.utils.globalEvent(callback); var redirect = encodeURIComponent(hello.settings.redirect_uri + '?' + hello.utils.param({callback: callbackID, result: JSON.stringify({force: true}), state: '{}'})); var token = (options.authResponse || {}).access_token; hello.utils.iframe('https://www.facebook.com/logout.php?next=' + redirect + '&access_token=' + token); // Possible responses: // String URL - hello.logout should handle the logout // Undefined - this function will handle the callback // True - throw a success, this callback isn't handling the callback // False - throw a error if (!token) { // If there isn't a token, the above wont return a response, so lets trigger a response return false; } }, // API Base URL base: 'https://graph.facebook.com/' + version + '/', // Map GET requests get: { me: 'me?fields=email,first_name,last_name,name,timezone,verified', 'me/friends': 'me/friends', 'me/following': 'me/friends', 'me/followers': 'me/friends', 'me/share': 'me/feed', 'me/like': 'me/likes', 'me/files': 'me/albums', 'me/albums': 'me/albums?fields=cover_photo,name', 'me/album': '@{id}/photos?fields=picture', 'me/photos': 'me/photos', 'me/photo': '@{id}', 'friend/albums': '@{id}/albums', 'friend/photos': '@{id}/photos' // Pagination // Https://developers.facebook.com/docs/reference/api/pagination/ }, // Map POST requests post: { 'me/share': 'me/feed', 'me/photo': '@{id}' // Https://developers.facebook.com/docs/graph-api/reference/v2.2/object/likes/ }, wrap: { me: formatUser, 'me/friends': formatFriends, 'me/following': formatFriends, 'me/followers': formatFriends, 'me/albums': format, 'me/photos': format, 'me/files': format, 'default': format }, // Special requirements for handling XHR xhr: function(p, qs) { if (p.method === 'get' || p.method === 'post') { qs.suppress_response_codes = true; } // Is this a post with a data-uri? if (p.method === 'post' && p.data && typeof (p.data.file) === 'string') { // Convert the Data-URI to a Blob p.data.file = hello.utils.toBlob(p.data.file); } return true; }, // Special requirements for handling JSONP fallback jsonp: function(p, qs) { var m = p.method; if (m !== 'get' && !hello.utils.hasBinary(p.data)) { p.data.method = m; p.method = 'get'; } else if (p.method === 'delete') { qs.method = 'delete'; p.method = 'post'; } }, // Special requirements for iframe form hack form: function(p) { return { // Fire the callback onload callbackonload: true }; } } }); var base = 'https://graph.facebook.com/'; function formatUser(o) { if (o.id) { o.thumbnail = o.picture = 'https://graph.facebook.com/' + o.id + '/picture'; } return o; } function formatFriends(o) { if ('data' in o) { o.data.forEach(formatUser); } return o; } function format(o, headers, req) { if (typeof o === 'boolean') { o = {success: o}; } if (o && 'data' in o) { var token = req.query.access_token; if (!(o.data instanceof Array)) { var data = o.data; delete o.data; o.data = [data]; } o.data.forEach(function(d) { if (d.picture) { d.thumbnail = d.picture; } d.pictures = (d.images || []) .sort(function(a, b) { return a.width - b.width; }); if (d.cover_photo && d.cover_photo.id) { d.thumbnail = base + d.cover_photo.id + '/picture?access_token=' + token; } if (d.type === 'album') { d.files = d.photos = base + d.id + '/photos'; } if (d.can_upload) { d.upload_location = base + d.id + '/photos'; } }); } return o; } })(hello); (function(hello) { hello.init({ flickr: { name: 'Flickr', // Ensure that you define an oauth_proxy oauth: { version: '1.0a', auth: 'https://www.flickr.com/services/oauth/authorize?perms=read', request: 'https://www.flickr.com/services/oauth/request_token', token: 'https://www.flickr.com/services/oauth/access_token' }, // API base URL base: 'https://api.flickr.com/services/rest', // Map GET resquests get: { me: sign('flickr.people.getInfo'), 'me/friends': sign('flickr.contacts.getList', {per_page: '@{limit|50}'}), 'me/following': sign('flickr.contacts.getList', {per_page: '@{limit|50}'}), 'me/followers': sign('flickr.contacts.getList', {per_page: '@{limit|50}'}), 'me/albums': sign('flickr.photosets.getList', {per_page: '@{limit|50}'}), 'me/album': sign('flickr.photosets.getPhotos', {photoset_id: '@{id}'}), 'me/photos': sign('flickr.people.getPhotos', {per_page: '@{limit|50}'}) }, wrap: { me: function(o) { formatError(o); o = checkResponse(o, 'person'); if (o.id) { if (o.realname) { o.name = o.realname._content; var m = o.name.split(' '); o.first_name = m.shift(); o.last_name = m.join(' '); } o.thumbnail = getBuddyIcon(o, 'l'); o.picture = getBuddyIcon(o, 'l'); } return o; }, 'me/friends': formatFriends, 'me/followers': formatFriends, 'me/following': formatFriends, 'me/albums': function(o) { formatError(o); o = checkResponse(o, 'photosets'); paging(o); if (o.photoset) { o.data = o.photoset; o.data.forEach(function(item) { item.name = item.title._content; item.photos = 'https://api.flickr.com/services/rest' + getApiUrl('flickr.photosets.getPhotos', {photoset_id: item.id}, true); }); delete o.photoset; } return o; }, 'me/photos': function(o) { formatError(o); return formatPhotos(o); }, 'default': function(o) { formatError(o); return formatPhotos(o); } }, xhr: false, jsonp: function(p, qs) { if (p.method == 'get') { delete qs.callback; qs.jsoncallback = p.callbackID; } } } }); function getApiUrl(method, extraParams, skipNetwork) { var url = ((skipNetwork) ? '' : 'flickr:') + '?method=' + method + '&api_key=' + hello.services.flickr.id + '&format=json'; for (var param in extraParams) { if (extraParams.hasOwnProperty(param)) { url += '&' + param + '=' + extraParams[param]; } } return url; } // This is not exactly neat but avoid to call // The method 'flickr.test.login' for each api call function withUser(cb) { var auth = hello.getAuthResponse('flickr'); cb(auth && auth.user_nsid ? auth.user_nsid : null); } function sign(url, params) { if (!params) { params = {}; } return function(p, callback) { withUser(function(userId) { params.user_id = userId; callback(getApiUrl(url, params, true)); }); }; } function getBuddyIcon(profile, size) { var url = 'https://www.flickr.com/images/buddyicon.gif'; if (profile.nsid && profile.iconserver && profile.iconfarm) { url = 'https://farm' + profile.iconfarm + '.staticflickr.com/' + profile.iconserver + '/' + 'buddyicons/' + profile.nsid + ((size) ? '_' + size : '') + '.jpg'; } return url; } // See: https://www.flickr.com/services/api/misc.urls.html function createPhotoUrl(id, farm, server, secret, size) { size = (size) ? '_' + size : ''; return 'https://farm' + farm + '.staticflickr.com/' + server + '/' + id + '_' + secret + size + '.jpg'; } function formatUser(o) { } function formatError(o) { if (o && o.stat && o.stat.toLowerCase() != 'ok') { o.error = { code: 'invalid_request', message: o.message }; } } function formatPhotos(o) { if (o.photoset || o.photos) { var set = ('photoset' in o) ? 'photoset' : 'photos'; o = checkResponse(o, set); paging(o); o.data = o.photo; delete o.photo; for (var i = 0; i < o.data.length; i++) { var photo = o.data[i]; photo.name = photo.title; photo.picture = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, ''); photo.pictures = createPictures(photo.id, photo.farm, photo.server, photo.secret); photo.source = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'b'); photo.thumbnail = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'm'); } } return o; } // See: https://www.flickr.com/services/api/misc.urls.html function createPictures(id, farm, server, secret) { var NO_LIMIT = 2048; var sizes = [ {id: 't', max: 100}, {id: 'm', max: 240}, {id: 'n', max: 320}, {id: '', max: 500}, {id: 'z', max: 640}, {id: 'c', max: 800}, {id: 'b', max: 1024}, {id: 'h', max: 1600}, {id: 'k', max: 2048}, {id: 'o', max: NO_LIMIT} ]; return sizes.map(function(size) { return { source: createPhotoUrl(id, farm, server, secret, size.id), // Note: this is a guess that's almost certain to be wrong (unless square source) width: size.max, height: size.max }; }); } function checkResponse(o, key) { if (key in o) { o = o[key]; } else if (!('error' in o)) { o.error = { code: 'invalid_request', message: o.message || 'Failed to get data from Flickr' }; } return o; } function formatFriends(o) { formatError(o); if (o.contacts) { o = checkResponse(o, 'contacts'); paging(o); o.data = o.contact; delete o.contact; for (var i = 0; i < o.data.length; i++) { var item = o.data[i]; item.id = item.nsid; item.name = item.realname || item.username; item.thumbnail = getBuddyIcon(item, 'm'); } } return o; } function paging(res) { if (res.page && res.pages && res.page !== res.pages) { res.paging = { next: '?page=' + (++res.page) }; } } })(hello); (function(hello) { hello.init({ foursquare: { name: 'Foursquare', oauth: { // See: https://developer.foursquare.com/overview/auth version: 2, auth: 'https://foursquare.com/oauth2/authenticate', grant: 'https://foursquare.com/oauth2/access_token' }, // Refresh the access_token once expired refresh: true, base: 'https://api.foursquare.com/v2/', get: { me: 'users/self', 'me/friends': 'users/self/friends', 'me/followers': 'users/self/friends', 'me/following': 'users/self/friends' }, wrap: { me: function(o) { formatError(o); if (o && o.response) { o = o.response.user; formatUser(o); } return o; }, 'default': function(o) { formatError(o); // Format friends if (o && 'response' in o && 'friends' in o.response && 'items' in o.response.friends) { o.data = o.response.friends.items; o.data.forEach(formatUser); delete o.response; } return o; } }, xhr: formatRequest, jsonp: formatRequest } }); function formatError(o) { if (o.meta && (o.meta.code === 400 || o.meta.code === 401)) { o.error = { code: 'access_denied', message: o.meta.errorDetail }; } } function formatUser(o) { if (o && o.id) { o.thumbnail = o.photo.prefix + '100x100' + o.photo.suffix; o.name = o.firstName + ' ' + o.lastName; o.first_name = o.firstName; o.last_name = o.lastName; if (o.contact) { if (o.contact.email) { o.email = o.contact.email; } } } } function formatRequest(p, qs) { var token = qs.access_token; delete qs.access_token; qs.oauth_token = token; qs.v = 20121125; return true; } })(hello); (function(hello) { hello.init({ github: { name: 'GitHub', oauth: { version: 2, auth: 'https://github.com/login/oauth/authorize', grant: 'https://github.com/login/oauth/access_token', response_type: 'code' }, scope: { email: 'user:email' }, base: 'https://api.github.com/', get: { me: 'user', 'me/friends': 'user/following?per_page=@{limit|100}', 'me/following': 'user/following?per_page=@{limit|100}', 'me/followers': 'user/followers?per_page=@{limit|100}', 'me/like': 'user/starred?per_page=@{limit|100}' }, wrap: { me: function(o, headers) { formatError(o, headers); formatUser(o); return o; }, 'default': function(o, headers, req) { formatError(o, headers); if (Array.isArray(o)) { o = {data: o}; } if (o.data) { paging(o, headers, req); o.data.forEach(formatUser); } return o; } }, xhr: function(p) { if (p.method !== 'get' && p.data) { // Serialize payload as JSON p.headers = p.headers || {}; p.headers['Content-Type'] = 'application/json'; if (typeof (p.data) === 'object') { p.data = JSON.stringify(p.data); } } return true; } } }); function formatError(o, headers) { var code = headers ? headers.statusCode : (o && 'meta' in o && 'status' in o.meta && o.meta.status); if ((code === 401 || code === 403)) { o.error = { code: 'access_denied', message: o.message || (o.data ? o.data.message : 'Could not get response') }; delete o.message; } } function formatUser(o) { if (o.id) { o.thumbnail = o.picture = o.avatar_url; o.name = o.login; } } function paging(res, headers, req) { if (res.data && res.data.length && headers && headers.Link) { var next = headers.Link.match(/<(.*?)>;\s*rel=\"next\"/); if (next) { res.paging = { next: next[1] }; } } } })(hello); (function(hello) { var contactsUrl = 'https://www.google.com/m8/feeds/contacts/default/full?v=3.0&alt=json&max-results=@{limit|1000}&start-index=@{start|1}'; hello.init({ google: { name: 'Google Sign-In', // See: http://code.google.com/apis/accounts/docs/OAuth2UserAgent.html oauth: { version: 2, auth: 'https://accounts.google.com/o/oauth2/v2/auth', grant: 'https://www.googleapis.com/oauth2/v4/token' }, // Authorization scopes scope: { basic: 'openid profile', email: 'email', birthday: '', events: '', photos: 'https://picasaweb.google.com/data/', videos: 'http://gdata.youtube.com', files: 'https://www.googleapis.com/auth/drive.readonly', publish: '', publish_files: 'https://www.googleapis.com/auth/drive', share: '', create_event: '', offline_access: '' }, scope_delim: ' ', login: function(p) { if (p.qs.response_type === 'code') { // Let's set this to an offline access to return a refresh_token p.qs.access_type = 'offline'; } else if (p.qs.response_type.indexOf('id_token') > -1) { p.qs.nonce = parseInt(Math.random() * 1e12, 10).toString(36); } // Reauthenticate // https://developers.google.com/identity/protocols/ if (p.options.force) { p.qs.prompt = 'consent'; } }, // API base URI base: 'https://www.googleapis.com/', // Map GET requests get: { me: 'oauth2/v3/userinfo?alt=json', // Deprecated Sept 1, 2014 //'me': 'oauth2/v1/userinfo?alt=json', // See: https://developers.google.com/+/api/latest/people/list 'me/following': contactsUrl, 'me/followers': contactsUrl, 'me/contacts': contactsUrl, 'me/albums': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&max-results=@{limit|100}&start-index=@{start|1}', 'me/album': function(p, callback) { var key = p.query.id; delete p.query.id; callback(key.replace('/entry/', '/feed/')); }, 'me/photos': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&kind=photo&max-results=@{limit|100}&start-index=@{start|1}', // See: https://developers.google.com/drive/v2/reference/files/list 'me/file': 'drive/v2/files/@{id}', 'me/files': 'drive/v2/files?q=%22@{parent|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}', // See: https://developers.google.com/drive/v2/reference/files/list 'me/folders': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+mimeType+=+%22application/vnd.google-apps.folder%22+and+trashed=false&maxResults=@{limit|100}', // See: https://developers.google.com/drive/v2/reference/files/list 'me/folder': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}' }, // Map POST requests post: { // Google Drive 'me/files': uploadDrive, 'me/folders': function(p, callback) { p.data = { title: p.data.name, parents: [{id: p.data.parent || 'root'}], mimeType: 'application/vnd.google-apps.folder' }; callback('drive/v2/files'); } }, // Map PUT requests put: { 'me/files': uploadDrive }, // Map DELETE requests del: { 'me/files': 'drive/v2/files/@{id}', 'me/folder': 'drive/v2/files/@{id}' }, // Map PATCH requests patch: { 'me/file': 'drive/v2/files/@{id}' }, wrap: { me: function(o) { if (o.sub) { o.id = o.sub; } if (o.id) { o.last_name = o.family_name || (o.name ? o.name.familyName : null); o.first_name = o.given_name || (o.name ? o.name.givenName : null); if (o.emails && o.emails.length) { o.email = o.emails[0].value; } formatPerson(o); } return o; }, 'me/friends': function(o) { if (o.items) { paging(o); o.data = o.items; o.data.forEach(formatPerson); delete o.items; } return o; }, 'me/contacts': formatFriends, 'me/followers': formatFriends, 'me/following': formatFriends, 'me/share': formatFeed, 'me/feed': formatFeed, 'me/albums': gEntry, 'me/photos': formatPhotos, 'default': gEntry }, xhr: function(p) { if (p.method === 'post' || p.method === 'put') { toJSON(p); } else if (p.method === 'patch') { hello.utils.extend(p.query, p.data); p.data = null; } return true; }, // Don't even try submitting via form. // This means no POST operations in <=IE9 form: false } }); function toInt(s) { return parseInt(s, 10); } function formatFeed(o) { paging(o); o.data = o.items; delete o.items; return o; } // Format: ensure each record contains a name, id etc. function formatItem(o) { if (o.error) { return; } if (!o.name) { o.name = o.title || o.message; } if (!o.picture) { o.picture = o.thumbnailLink; } if (!o.thumbnail) { o.thumbnail = o.thumbnailLink; } if (o.mimeType === 'application/vnd.google-apps.folder') { o.type = 'folder'; o.files = 'https://www.googleapis.com/drive/v2/files?q=%22' + o.id + '%22+in+parents'; } return o; } function formatImage(image) { return { source: image.url, width: image.width, height: image.height }; } function formatPhotos(o) { if ('feed' in o) { o.data = 'entry' in o.feed ? o.feed.entry.map(formatEntry) : []; delete o.feed; } return o; } // Google has a horrible JSON API function gEntry(o) { paging(o); if ('feed' in o && 'entry' in o.feed) { o.data = o.feed.entry.map(formatEntry); delete o.feed; } // Old style: Picasa, etc. else if ('entry' in o) { return formatEntry(o.entry); } // New style: Google Drive else if ('items' in o) { o.data = o.items.map(formatItem); delete o.items; } else { formatItem(o); } return o; } function formatPerson(o) { o.name = o.displayName || o.name; o.picture = o.picture || (o.image ? o.image.url : null); o.thumbnail = o.picture; } function formatFriends(o, headers, req) { paging(o); var r = []; if ('feed' in o && 'entry' in o.feed) { var token = req.query.access_token; for (var i = 0; i < o.feed.entry.length; i++) { var a = o.feed.entry[i]; a.id = a.id.$t; a.name = a.title.$t; delete a.title; if (a.gd$email) { a.email = (a.gd$email && a.gd$email.length > 0) ? a.gd$email[0].address : null; a.emails = a.gd$email; delete a.gd$email; } if (a.updated) { a.updated = a.updated.$t; } if (a.link) { var pic = (a.link.length > 0) ? a.link[0].href : null; if (pic && a.link[0].gd$etag) { pic += (pic.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + token; a.picture = pic; a.thumbnail = pic; } delete a.link; } if (a.category) { delete a.category; } } o.data = o.feed.entry; delete o.feed; } return o; } function formatEntry(a) { var group = a.media$group; var photo = group.media$content.length ? group.media$content[0] : {}; var mediaContent = group.media$content || []; var mediaThumbnail = group.media$thumbnail || []; var pictures = mediaContent .concat(mediaThumbnail) .map(formatImage) .sort(function(a, b) { return a.width - b.width; }); var i = 0; var _a; var p = { id: a.id.$t, name: a.title.$t, description: a.summary.$t, updated_time: a.updated.$t, created_time: a.published.$t, picture: photo ? photo.url : null, pictures: pictures, images: [], thumbnail: photo ? photo.url : null, width: photo.width, height: photo.height }; // Get feed/children if ('link' in a) { for (i = 0; i < a.link.length; i++) { var d = a.link[i]; if (d.rel.match(/\#feed$/)) { p.upload_location = p.files = p.photos = d.href; break; } } } // Get images of different scales if ('category' in a && a.category.length) { _a = a.category; for (i = 0; i < _a.length; i++) { if (_a[i].scheme && _a[i].scheme.match(/\#kind$/)) { p.type = _a[i].term.replace(/^.*?\#/, ''); } } } // Get images of different scales if ('media$thumbnail' in group && group.media$thumbnail.length) { _a = group.media$thumbnail; p.thumbnail = _a[0].url; p.images = _a.map(formatImage); } _a = group.media$content; if (_a && _a.length) { p.images.push(formatImage(_a[0])); } return p; } function paging(res) { // Contacts V2 if ('feed' in res && res.feed.openSearch$itemsPerPage) { var limit = toInt(res.feed.openSearch$itemsPerPage.$t); var start = toInt(res.feed.openSearch$startIndex.$t); var total = toInt(res.feed.openSearch$totalResults.$t); if ((start + limit) < total) { res.paging = { next: '?start=' + (start + limit) }; } } else if ('nextPageToken' in res) { res.paging = { next: '?pageToken=' + res.nextPageToken }; } } // Construct a multipart message function Multipart() { // Internal body var body = []; var boundary = (Math.random() * 1e10).toString(32); var counter = 0; var lineBreak = '\r\n'; var delim = lineBreak + '--' + boundary; var ready = function() {}; var dataUri = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i; // Add file function addFile(item) { var fr = new FileReader(); fr.onload = function(e) { addContent(btoa(e.target.result), item.type + lineBreak + 'Content-Transfer-Encoding: base64'); }; fr.readAsBinaryString(item); } // Add content function addContent(content, type) { body.push(lineBreak + 'Content-Type: ' + type + lineBreak + lineBreak + content); counter--; ready(); } // Add new things to the object this.append = function(content, type) { // Does the content have an array if (typeof (content) === 'string' || !('length' in Object(content))) { // Converti to multiples content = [content]; } for (var i = 0; i < content.length; i++) { counter++; var item = content[i]; // Is this a file? // Files can be either Blobs or File types if ( (typeof (File) !== 'undefined' && item instanceof File) || (typeof (Blob) !== 'undefined' && item instanceof Blob) ) { // Read the file in addFile(item); } // Data-URI? // Data:[<mime type>][;charset=<charset>][;base64],<encoded data> // /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i else if (typeof (item) === 'string' && item.match(dataUri)) { var m = item.match(dataUri); addContent(item.replace(dataUri, ''), m[1] + lineBreak + 'Content-Transfer-Encoding: base64'); } // Regular string else { addContent(item, type); } } }; this.onready = function(fn) { ready = function() { if (counter === 0) { // Trigger ready body.unshift(''); body.push('--'); fn(body.join(delim), boundary); body = []; } }; ready(); }; } // Upload to Drive // If this is PUT then only augment the file uploaded // PUT https://developers.google.com/drive/v2/reference/files/update // POST https://developers.google.com/drive/manage-uploads function uploadDrive(p, callback) { var data = {}; // Test for DOM element if (p.data && (typeof (HTMLInputElement) !== 'undefined' && p.data instanceof HTMLInputElement) ) { p.data = {file: p.data}; } if (!p.data.name && Object(Object(p.data.file).files).length && p.method === 'post') { p.data.name = p.data.file.files[0].name; } if (p.method === 'post') { p.data = { title: p.data.name, parents: [{id: p.data.parent || 'root'}], file: p.data.file }; } else { // Make a reference data = p.data; p.data = {}; // Add the parts to change as required if (data.parent) { p.data.parents = [{id: p.data.parent || 'root'}]; } if (data.file) { p.data.file = data.file; } if (data.name) { p.data.title = data.name; } } // Extract the file, if it exists from the data object // If the File is an INPUT element lets just concern ourselves with the NodeList var file; if ('file' in p.data) { file = p.data.file; delete p.data.file; if (typeof (file) === 'object' && 'files' in file) { // Assign the NodeList file = file.files; } if (!file || !file.length) { callback({ error: { code: 'request_invalid', message: 'There were no files attached with this request to upload' } }); return; } } // Set type p.data.mimeType = Object(file[0]).type || 'application/octet-stream'; // Construct a multipart message var parts = new Multipart(); parts.append(JSON.stringify(p.data), 'application/json'); // Read the file into a base64 string... yep a hassle, i know // FormData doesn't let us assign our own Multipart headers and HTTP Content-Type // Alas GoogleApi need these in a particular format if (file) { parts.append(file); } parts.onready(function(body, boundary) { p.headers['content-type'] = 'multipart/related; boundary="' + boundary + '"'; p.data = body; callback('upload/drive/v2/files' + (data.id ? '/' + data.id : '') + '?uploadType=multipart'); }); } function toJSON(p) { if (typeof (p.data) === 'object') { // Convert the POST into a javascript object try { p.data = JSON.stringify(p.data); p.headers['content-type'] = 'application/json'; } catch (e) {} } } })(hello); (function(hello) { hello.init({ instagram: { name: 'Instagram', oauth: { // See: http://instagram.com/developer/authentication/ version: 2, auth: 'https://instagram.com/oauth/authorize/', grant: 'https://api.instagram.com/oauth/access_token' }, // Refresh the access_token once expired refresh: true, scope: { basic: 'basic', photos: '', friends: 'relationships', publish: 'likes comments', email: '', share: '', publish_files: '', files: '', videos: '', offline_access: '' }, scope_delim: ' ', base: 'https://api.instagram.com/v1/', get: { me: 'users/self', 'me/feed': 'users/self/feed?count=@{limit|100}', 'me/photos': 'users/self/media/recent?min_id=0&count=@{limit|100}', 'me/friends': 'users/self/follows?count=@{limit|100}', 'me/following': 'users/self/follows?count=@{limit|100}', 'me/followers': 'users/self/followed-by?count=@{limit|100}', 'friend/photos': 'users/@{id}/media/recent?min_id=0&count=@{limit|100}' }, post: { 'me/like': function(p, callback) { var id = p.data.id; p.data = {}; callback('media/' + id + '/likes'); } }, del: { 'me/like': 'media/@{id}/likes' }, wrap: { me: function(o) { formatError(o); if ('data' in o) { o.id = o.data.id; o.thumbnail = o.data.profile_picture; o.name = o.data.full_name || o.data.username; } return o; }, 'me/friends': formatFriends, 'me/following': formatFriends, 'me/followers': formatFriends, 'me/photos': function(o) { formatError(o); paging(o); if ('data' in o) { o.data = o.data.filter(function(d) { return d.type === 'image'; }); o.data.forEach(function(d) { d.name = d.caption ? d.caption.text : null; d.thumbnail = d.images.thumbnail.url; d.picture = d.images.standard_resolution.url; d.pictures = Object.keys(d.images) .map(function(key) { var image = d.images[key]; return formatImage(image); }) .sort(function(a, b) { return a.width - b.width; }); }); } return o; }, 'default': function(o) { o = formatError(o); paging(o); return o; } }, // Instagram does not return any CORS Headers // So besides JSONP we're stuck with proxy xhr: function(p, qs) { var method = p.method; var proxy = method !== 'get'; if (proxy) { if ((method === 'post' || method === 'put') && p.query.access_token) { p.data.access_token = p.query.access_token; delete p.query.access_token; } // No access control headers // Use the proxy instead p.proxy = proxy; } return proxy; }, // No form form: false } }); function formatImage(image) { return { source: image.url, width: image.width, height: image.height }; } function formatError(o) { if (typeof o === 'string') { return { error: { code: 'invalid_request', message: o } }; } if (o && 'meta' in o && 'error_type' in o.meta) { o.error = { code: o.meta.error_type, message: o.meta.error_message }; } return o; } function formatFriends(o) { paging(o); if (o && 'data' in o) { o.data.forEach(formatFriend); } return o; } function formatFriend(o) { if (o.id) { o.thumbnail = o.profile_picture; o.name = o.full_name || o.username; } } // See: http://instagram.com/developer/endpoints/ function paging(res) { if ('pagination' in res) { res.paging = { next: res.pagination.next_url }; delete res.pagination; } } })(hello); (function(hello) { hello.init({ joinme: { name: 'join.me', oauth: { version: 2, auth: 'https://secure.join.me/api/public/v1/auth/oauth2', grant: 'https://secure.join.me/api/public/v1/auth/oauth2' }, refresh: false, scope: { basic: 'user_info', user: 'user_info', scheduler: 'scheduler', start: 'start_meeting', email: '', friends: '', share: '', publish: '', photos: '', publish_files: '', files: '', videos: '', offline_access: '' }, scope_delim: ' ', login: function(p) { p.options.popup.width = 400; p.options.popup.height = 700; }, base: 'https://api.join.me/v1/', get: { me: 'user', meetings: 'meetings', 'meetings/info': 'meetings/@{id}' }, post: { 'meetings/start/adhoc': function(p, callback) { callback('meetings/start'); }, 'meetings/start/scheduled': function(p, callback) { var meetingId = p.data.meetingId; p.data = {}; callback('meetings/' + meetingId + '/start'); }, 'meetings/schedule': function(p, callback) { callback('meetings'); } }, patch: { 'meetings/update': function(p, callback) { callback('meetings/' + p.data.meetingId); } }, del: { 'meetings/delete': 'meetings/@{id}' }, wrap: { me: function(o, headers) { formatError(o, headers); if (!o.email) { return o; } o.name = o.fullName; o.first_name = o.name.split(' ')[0]; o.last_name = o.name.split(' ')[1]; o.id = o.email; return o; }, 'default': function(o, headers) { formatError(o, headers); return o; } }, xhr: formatRequest } }); function formatError(o, headers) { var errorCode; var message; var details; if (o && ('Message' in o)) { message = o.Message; delete o.Message; if ('ErrorCode' in o) { errorCode = o.ErrorCode; delete o.ErrorCode; } else { errorCode = getErrorCode(headers); } o.error = { code: errorCode, message: message, details: o }; } return o; } function formatRequest(p, qs) { // Move the access token from the request body to the request header var token = qs.access_token; delete qs.access_token; p.headers.Authorization = 'Bearer ' + token; // Format non-get requests to indicate json body if (p.method !== 'get' && p.data) { p.headers['Content-Type'] = 'application/json'; if (typeof (p.data) === 'object') { p.data = JSON.stringify(p.data); } } if (p.method === 'put') { p.method = 'patch'; } return true; } function getErrorCode(headers) { switch (headers.statusCode) { case 400: return 'invalid_request'; case 403: return 'stale_token'; case 401: return 'invalid_token'; case 500: return 'server_error'; default: return 'server_error'; } } }(hello)); (function(hello) { hello.init({ linkedin: { oauth: { version: 2, response_type: 'code', auth: 'https://www.linkedin.com/uas/oauth2/authorization', grant: 'https://www.linkedin.com/uas/oauth2/accessToken' }, // Refresh the access_token once expired refresh: true, scope: { basic: 'r_basicprofile', email: 'r_emailaddress', files: '', friends: '', photos: '', publish: 'w_share', publish_files: 'w_share', share: '', videos: '', offline_access: '' }, scope_delim: ' ', base: 'https://api.linkedin.com/v1/', get: { me: 'people/~:(picture-url,first-name,last-name,id,formatted-name,email-address)', // See: http://developer.linkedin.com/documents/get-network-updates-and-statistics-api 'me/share': 'people/~/network/updates?count=@{limit|250}' }, post: { // See: https://developer.linkedin.com/documents/api-requests-json 'me/share': function(p, callback) { var data = { visibility: { code: 'anyone' } }; if (p.data.id) { data.attribution = { share: { id: p.data.id } }; } else { data.comment = p.data.message; if (p.data.picture && p.data.link) { data.content = { 'submitted-url': p.data.link, 'submitted-image-url': p.data.picture }; } } p.data = JSON.stringify(data); callback('people/~/shares?format=json'); }, 'me/like': like }, del: { 'me/like': like }, wrap: { me: function(o) { formatError(o); formatUser(o); return o; }, 'me/friends': formatFriends, 'me/following': formatFriends, 'me/followers': formatFriends, 'me/share': function(o) { formatError(o); paging(o); if (o.values) { o.data = o.values.map(formatUser); o.data.forEach(function(item) { item.message = item.headline; }); delete o.values; } return o; }, 'default': function(o, headers) { formatError(o); empty(o, headers); paging(o); } }, jsonp: function(p, qs) { formatQuery(qs); if (p.method === 'get') { qs.format = 'jsonp'; qs['error-callback'] = p.callbackID; } }, xhr: function(p, qs) { if (p.method !== 'get') { formatQuery(qs); p.headers['Content-Type'] = 'application/json'; // Note: x-li-format ensures error responses are not returned in XML p.headers['x-li-format'] = 'json'; p.proxy = true; return true; } return false; } } }); function formatError(o) { if (o && 'errorCode' in o) { o.error = { code: o.status, message: o.message }; } } function formatUser(o) { if (o.error) { return; } o.first_name = o.firstName; o.last_name = o.lastName; o.name = o.formattedName || (o.first_name + ' ' + o.last_name); o.thumbnail = o.pictureUrl; o.email = o.emailAddress; return o; } function formatFriends(o) { formatError(o); paging(o); if (o.values) { o.data = o.values.map(formatUser); delete o.values; } return o; } function paging(res) { if ('_count' in res && '_start' in res && (res._count + res._start) < res._total) { res.paging = { next: '?start=' + (res._start + res._count) + '&count=' + res._count }; } } function empty(o, headers) { if (JSON.stringify(o) === '{}' && headers.statusCode === 200) { o.success = true; } } function formatQuery(qs) { // LinkedIn signs requests with the parameter 'oauth2_access_token' // ... yeah another one who thinks they should be different! if (qs.access_token) { qs.oauth2_access_token = qs.access_token; delete qs.access_token; } } function like(p, callback) { p.headers['x-li-format'] = 'json'; var id = p.data.id; p.data = (p.method !== 'delete').toString(); p.method = 'put'; callback('people/~/network/updates/key=' + id + '/is-liked'); } })(hello); // See: https://developers.soundcloud.com/docs/api/reference (function(hello) { hello.init({ soundcloud: { name: 'SoundCloud', oauth: { version: 2, auth: 'https://soundcloud.com/connect', grant: 'https://soundcloud.com/oauth2/token' }, // Request path translated base: 'https://api.soundcloud.com/', get: { me: 'me.json', // Http://developers.soundcloud.com/docs/api/reference#me 'me/friends': 'me/followings.json', 'me/followers': 'me/followers.json', 'me/following': 'me/followings.json', // See: http://developers.soundcloud.com/docs/api/reference#activities 'default': function(p, callback) { // Include '.json at the end of each request' callback(p.path + '.json'); } }, // Response handlers wrap: { me: function(o) { formatUser(o); return o; }, 'default': function(o) { if (Array.isArray(o)) { o = { data: o.map(formatUser) }; } paging(o); return o; } }, xhr: formatRequest, jsonp: formatRequest } }); function formatRequest(p, qs) { // Alter the querystring var token = qs.access_token; delete qs.access_token; qs.oauth_token = token; qs['_status_code_map[302]'] = 200; return true; } function formatUser(o) { if (o.id) { o.picture = o.avatar_url; o.thumbnail = o.avatar_url; o.name = o.username || o.full_name; } return o; } // See: http://developers.soundcloud.com/docs/api/reference#activities function paging(res) { if ('next_href' in res) { res.paging = { next: res.next_href }; } } })(hello); // See: https://developer.spotify.com/web-api/ (function(hello) { hello.init({ spotify: { name: 'Spotify', oauth: { version: 2, auth: 'https://accounts.spotify.com/authorize', grant: 'https://accounts.spotify.com/api/token' }, // See: https://developer.spotify.com/web-api/using-scopes/ scope_delim: ' ', scope: { basic: '', photos: '', friends: 'user-follow-read', publish: 'user-library-read', email: 'user-read-email', share: '', publish_files: '', files: '', videos: '', offline_access: '' }, // Request path translated base: 'https://api.spotify.com', // See: https://developer.spotify.com/web-api/endpoint-reference/ get: { me: '/v1/me', 'me/following': '/v1/me/following?type=artist', // Only 'artist' is supported // Because tracks, albums and playlist exist on spotify, the tracks are considered // the resource for the 'me/likes' endpoint 'me/like': '/v1/me/tracks' }, // Response handlers wrap: { me: formatUser, 'me/following': formatFollowees, 'me/like': formatTracks }, xhr: formatRequest, jsonp: false } }); // Move the access token from the request body to the request header function formatRequest(p, qs) { var token = qs.access_token; delete qs.access_token; p.headers.Authorization = 'Bearer ' + token; return true; } function formatUser(o) { if (o.id) { o.name = o.display_name; o.thumbnail = o.images.length ? o.images[0].url : null; o.picture = o.thumbnail; } return o; } function formatFollowees(o) { paging(o); if (o && 'artists' in o) { o.data = o.artists.items.forEach(formatUser); } return o; } function formatTracks(o) { paging(o); o.data = o.items; return o; } function paging(res) { if (res && 'next' in res) { res.paging = { next: res.next }; delete res.next; } } })(hello); (function(hello) { var base = 'https://api.twitter.com/'; hello.init({ twitter: { // Ensure that you define an oauth_proxy oauth: { version: '1.0a', auth: base + 'oauth/authenticate', request: base + 'oauth/request_token', token: base + 'oauth/access_token' }, login: function(p) { // Reauthenticate // https://dev.twitter.com/oauth/reference/get/oauth/authenticate var prefix = '?force_login=true'; this.oauth.auth = this.oauth.auth.replace(prefix, '') + (p.options.force ? prefix : ''); }, base: base + '1.1/', get: { me: 'account/verify_credentials.json', 'me/friends': 'friends/list.json?count=@{limit|200}', 'me/following': 'friends/list.json?count=@{limit|200}', 'me/followers': 'followers/list.json?count=@{limit|200}', // Https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline 'me/share': 'statuses/user_timeline.json?count=@{limit|200}', // Https://dev.twitter.com/rest/reference/get/favorites/list 'me/like': 'favorites/list.json?count=@{limit|200}' }, post: { 'me/share': function(p, callback) { var data = p.data; p.data = null; var status = []; // Change message to status if (data.message) { status.push(data.message); delete data.message; } // If link is given if (data.link) { status.push(data.link); delete data.link; } if (data.picture) { status.push(data.picture); delete data.picture; } // Compound all the components if (status.length) { data.status = status.join(' '); } // Tweet media if (data.file) { data['media[]'] = data.file; delete data.file; p.data = data; callback('statuses/update_with_media.json'); } // Retweet? else if ('id' in data) { callback('statuses/retweet/' + data.id + '.json'); } // Tweet else { // Assign the post body to the query parameters hello.utils.extend(p.query, data); callback('statuses/update.json?include_entities=1'); } }, // See: https://dev.twitter.com/rest/reference/post/favorites/create 'me/like': function(p, callback) { var id = p.data.id; p.data = null; callback('favorites/create.json?id=' + id); } }, del: { // See: https://dev.twitter.com/rest/reference/post/favorites/destroy 'me/like': function(p, callback) { p.method = 'post'; var id = p.data.id; p.data = null; callback('favorites/destroy.json?id=' + id); } }, wrap: { me: function(res) { formatError(res); formatUser(res); return res; }, 'me/friends': formatFriends, 'me/followers': formatFriends, 'me/following': formatFriends, 'me/share': function(res) { formatError(res); paging(res); if (!res.error && 'length' in res) { return {data: res}; } return res; }, 'default': function(res) { res = arrayToDataResponse(res); paging(res); return res; } }, xhr: function(p) { // Rely on the proxy for non-GET requests. return (p.method !== 'get'); } } }); function formatUser(o) { if (o.id) { if (o.name) { var m = o.name.split(' '); o.first_name = m.shift(); o.last_name = m.join(' '); } // See: https://dev.twitter.com/overview/general/user-profile-images-and-banners o.thumbnail = o.profile_image_url_https || o.profile_image_url; } return o; } function formatFriends(o) { formatError(o); paging(o); if (o.users) { o.data = o.users.map(formatUser); delete o.users; } return o; } function formatError(o) { if (o.errors) { var e = o.errors[0]; o.error = { code: 'request_failed', message: e.message }; } } // Take a cursor and add it to the path function paging(res) { // Does the response include a 'next_cursor_string' if ('next_cursor_str' in res) { // See: https://dev.twitter.com/docs/misc/cursoring res.paging = { next: '?cursor=' + res.next_cursor_str }; } } function arrayToDataResponse(res) { return Array.isArray(res) ? {data: res} : res; } /** // The documentation says to define user in the request // Although its not actually required. var user_id; function withUserId(callback){ if(user_id){ callback(user_id); } else{ hello.api('twitter:/me', function(o){ user_id = o.id; callback(o.id); }); } } function sign(url){ return function(p, callback){ withUserId(function(user_id){ callback(url+'?user_id='+user_id); }); }; } */ })(hello); // Vkontakte (vk.com) (function(hello) { hello.init({ vk: { name: 'Vk', // See https://vk.com/dev/oauth_dialog oauth: { version: 2, auth: 'https://oauth.vk.com/authorize', grant: 'https://oauth.vk.com/access_token' }, // Authorization scopes // See https://vk.com/dev/permissions scope: { email: 'email', friends: 'friends', photos: 'photos', videos: 'video', share: 'share', offline_access: 'offline' }, // Refresh the access_token refresh: true, login: function(p) { p.qs.display = window.navigator && window.navigator.userAgent && /ipad|phone|phone|android/.test(window.navigator.userAgent.toLowerCase()) ? 'mobile' : 'popup'; }, // API Base URL base: 'https://api.vk.com/method/', // Map GET requests get: { me: function(p, callback) { p.query.fields = 'id,first_name,last_name,photo_max'; callback('users.get'); } }, wrap: { me: function(res, headers, req) { formatError(res); return formatUser(res, req); } }, // No XHR xhr: false, // All requests should be JSONP as of missing CORS headers in https://api.vk.com/method/* jsonp: true, // No form form: false } }); function formatUser(o, req) { if (o !== null && 'response' in o && o.response !== null && o.response.length) { o = o.response[0]; o.id = o.uid; o.thumbnail = o.picture = o.photo_max; o.name = o.first_name + ' ' + o.last_name; if (req.authResponse && req.authResponse.email !== null) o.email = req.authResponse.email; } return o; } function formatError(o) { if (o.error) { var e = o.error; o.error = { code: e.error_code, message: e.error_msg }; } } })(hello); (function(hello) { hello.init({ windows: { name: 'Windows live', // REF: http://msdn.microsoft.com/en-us/library/hh243641.aspx oauth: { version: 2, auth: 'https://login.live.com/oauth20_authorize.srf', grant: 'https://login.live.com/oauth20_token.srf' }, // Refresh the access_token once expired refresh: true, logout: function() { return 'http://login.live.com/oauth20_logout.srf?ts=' + (new Date()).getTime(); }, // Authorization scopes scope: { basic: 'wl.signin,wl.basic', email: 'wl.emails', birthday: 'wl.birthday', events: 'wl.calendars', photos: 'wl.photos', videos: 'wl.photos', friends: 'wl.contacts_emails', files: 'wl.skydrive', publish: 'wl.share', publish_files: 'wl.skydrive_update', share: 'wl.share', create_event: 'wl.calendars_update,wl.events_create', offline_access: 'wl.offline_access' }, // API base URL base: 'https://apis.live.net/v5.0/', // Map GET requests get: { // Friends me: 'me', 'me/friends': 'me/friends', 'me/following': 'me/contacts', 'me/followers': 'me/friends', 'me/contacts': 'me/contacts', 'me/albums': 'me/albums', // Include the data[id] in the path 'me/album': '@{id}/files', 'me/photo': '@{id}', // Files 'me/files': '@{parent|me/skydrive}/files', 'me/folders': '@{id|me/skydrive}/files', 'me/folder': '@{id|me/skydrive}/files' }, // Map POST requests post: { 'me/albums': 'me/albums', 'me/album': '@{id}/files/', 'me/folders': '@{id|me/skydrive/}', 'me/files': '@{parent|me/skydrive}/files' }, // Map DELETE requests del: { // Include the data[id] in the path 'me/album': '@{id}', 'me/photo': '@{id}', 'me/folder': '@{id}', 'me/files': '@{id}' }, wrap: { me: formatUser, 'me/friends': formatFriends, 'me/contacts': formatFriends, 'me/followers': formatFriends, 'me/following': formatFriends, 'me/albums': formatAlbums, 'me/photos': formatDefault, 'default': formatDefault }, xhr: function(p) { if (p.method !== 'get' && p.method !== 'delete' && !hello.utils.hasBinary(p.data)) { // Does this have a data-uri to upload as a file? if (typeof (p.data.file) === 'string') { p.data.file = hello.utils.toBlob(p.data.file); } else { p.data = JSON.stringify(p.data); p.headers = { 'Content-Type': 'application/json' }; } } return true; }, jsonp: function(p) { if (p.method !== 'get' && !hello.utils.hasBinary(p.data)) { p.data.method = p.method; p.method = 'get'; } } } }); function formatDefault(o) { if ('data' in o) { o.data.forEach(function(d) { if (d.picture) { d.thumbnail = d.picture; } if (d.images) { d.pictures = d.images .map(formatImage) .sort(function(a, b) { return a.width - b.width; }); } }); } return o; } function formatImage(image) { return { width: image.width, height: image.height, source: image.source }; } function formatAlbums(o) { if ('data' in o) { o.data.forEach(function(d) { d.photos = d.files = 'https://apis.live.net/v5.0/' + d.id + '/photos'; }); } return o; } function formatUser(o, headers, req) { if (o.id) { var token = req.query.access_token; if (o.emails) { o.email = o.emails.preferred; } // If this is not an non-network friend if (o.is_friend !== false) { // Use the id of the user_id if available var id = (o.user_id || o.id); o.thumbnail = o.picture = 'https://apis.live.net/v5.0/' + id + '/picture?access_token=' + token; } } return o; } function formatFriends(o, headers, req) { if ('data' in o) { o.data.forEach(function(d) { formatUser(d, headers, req); }); } return o; } })(hello); (function(hello) { hello.init({ yahoo: { // Ensure that you define an oauth_proxy oauth: { version: '1.0a', auth: 'https://api.login.yahoo.com/oauth/v2/request_auth', request: 'https://api.login.yahoo.com/oauth/v2/get_request_token', token: 'https://api.login.yahoo.com/oauth/v2/get_token' }, // Login handler login: function(p) { // Change the default popup window to be at least 560 // Yahoo does dynamically change it on the fly for the signin screen (only, what if your already signed in) p.options.popup.width = 560; // Yahoo throws an parameter error if for whatever reason the state.scope contains a comma, so lets remove scope try {delete p.qs.state.scope;} catch (e) {} }, base: 'https://social.yahooapis.com/v1/', get: { me: yql('select * from social.profile(0) where guid=me'), 'me/friends': yql('select * from social.contacts(0) where guid=me'), 'me/following': yql('select * from social.contacts(0) where guid=me') }, wrap: { me: formatUser, // Can't get IDs // It might be better to loop through the social.relationship table with has unique IDs of users. 'me/friends': formatFriends, 'me/following': formatFriends, 'default': paging } } }); /* // Auto-refresh fix: bug in Yahoo can't get this to work with node-oauth-shim login : function(o){ // Is the user already logged in var auth = hello('yahoo').getAuthResponse(); // Is this a refresh token? if(o.options.display==='none'&&auth&&auth.access_token&&auth.refresh_token){ // Add the old token and the refresh token, including path to the query // See http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html o.qs.access_token = auth.access_token; o.qs.refresh_token = auth.refresh_token; o.qs.token_url = 'https://api.login.yahoo.com/oauth/v2/get_token'; } }, */ function formatError(o) { if (o && 'meta' in o && 'error_type' in o.meta) { o.error = { code: o.meta.error_type, message: o.meta.error_message }; } } function formatUser(o) { formatError(o); if (o.query && o.query.results && o.query.results.profile) { o = o.query.results.profile; o.id = o.guid; o.last_name = o.familyName; o.first_name = o.givenName || o.nickname; var a = []; if (o.first_name) { a.push(o.first_name); } if (o.last_name) { a.push(o.last_name); } o.name = a.join(' '); o.email = (o.emails && o.emails[0]) ? o.emails[0].handle : null; o.thumbnail = o.image ? o.image.imageUrl : null; } return o; } function formatFriends(o, headers, request) { formatError(o); paging(o, headers, request); var contact; var field; if (o.query && o.query.results && o.query.results.contact) { o.data = o.query.results.contact; delete o.query; if (!Array.isArray(o.data)) { o.data = [o.data]; } o.data.forEach(formatFriend); } return o; } function formatFriend(contact) { contact.id = null; // #362: Reports of responses returning a single item, rather than an Array of items. // Format the contact.fields to be an array. if (contact.fields && !(contact.fields instanceof Array)) { contact.fields = [contact.fields]; } (contact.fields || []).forEach(function(field) { if (field.type === 'email') { contact.email = field.value; } if (field.type === 'name') { contact.first_name = field.value.givenName; contact.last_name = field.value.familyName; contact.name = field.value.givenName + ' ' + field.value.familyName; } if (field.type === 'yahooid') { contact.id = field.value; } }); } function paging(res, headers, request) { // See: http://developer.yahoo.com/yql/guide/paging.html#local_limits if (res.query && res.query.count && request.options) { res.paging = { next: '?start=' + (res.query.count + (+request.options.start || 1)) }; } return res; } function yql(q) { return 'https://query.yahooapis.com/v1/yql?q=' + (q + ' limit @{limit|100} offset @{start|0}').replace(/\s/g, '%20') + '&format=json'; } })(hello); // Register as anonymous AMD module if (typeof define === 'function' && define.amd) { define(function() { return hello; }); } // CommonJS module for browserify if (typeof module === 'object' && module.exports) { module.exports = hello; }