From c54f4f2a90c8b836366d2c15688cf9d30034a3fc Mon Sep 17 00:00:00 2001 From: libai <libai@yazhai.co> Date: Wed, 16 Nov 2022 15:44:55 +0800 Subject: [PATCH] 测试 --- mTest/index.html | 3 ++- mTest/lib/hello.js | 5954 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mTest/lib/login.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 6037 insertions(+), 1 deletion(-) create mode 100644 mTest/lib/hello.js diff --git a/mTest/index.html b/mTest/index.html index 4587d5d..ead88f5 100644 --- a/mTest/index.html +++ b/mTest/index.html @@ -38,6 +38,7 @@ <link rel="stylesheet" href="css/login.css?v=2022111602"> <script src="https://accounts.google.com/gsi/client" async defer></script> <script async defer crossorigin="anonymous" src="https://connect.facebook.net/en_US/sdk.js"></script> + <script src="lib/hello.js"></script> <style> .layui-layer-shade { opacity: 0.7 !important; @@ -262,7 +263,7 @@ <ul class="login-more-btn"> <li><img src="images/d2.png" alt=""></li> <li class="facebook_btn"><img src="images/d3.png" alt=""></li> - <li><img src="images/d4.png" alt=""></li> + <li class="twitter_btn"><img src="images/d4.png" alt=""></li> </ul> </div> <div style="display: none;" class="login1"> diff --git a/mTest/lib/hello.js b/mTest/lib/hello.js new file mode 100644 index 0000000..1bd5925 --- /dev/null +++ b/mTest/lib/hello.js @@ -0,0 +1,5954 @@ +/*! 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; +} \ No newline at end of file diff --git a/mTest/lib/login.js b/mTest/lib/login.js index 878c79c..5f8c0ad 100644 --- a/mTest/lib/login.js +++ b/mTest/lib/login.js @@ -494,6 +494,10 @@ function loginPage(){ googleLogin() $(".login-show").show() } + +$('.twitter_btn').on('click', function () { + login_twitter('twitter'); +}) $(".facebook_btn").on("click",function(){ FB.login(function (response) { if (response.status === 'connected') { @@ -631,4 +635,81 @@ function testAPI() { // Testing Graph API after login. See // 'Thanks for logging in, ' + response.name + '!'; // window.location.href = "footseenlogin://" + encodeURIComponent("www.firefly.live?token=" + info.authResponse.accessToken + "&name=" + response.name + "&id=" + response.id); }); +} + +window.twttr = (function (d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0], + t = window.twttr || {}; + if (d.getElementById(id)) return t; + js = d.createElement(s); + js.id = id; + js.src = "https://platform.twitter.com/widgets.js"; + fjs.parentNode.insertBefore(js, fjs); + + t._e = []; + t.ready = function (f) { + t._e.push(f); + }; + + return t; +}(document, "script", "twitter-wjs")); + +hello.init( +{ 'twitter': 'cEGECNgXN3ZN00r3Zb82vwh55' }, //App_key +{ + oauth_proxy: 'https://auth-server.herokuapp.com/proxy', + redirect_uri: 'https://www.footseen.xyz/mTest/index.html' +}); +//推特登錄 +function login_twitter(network) { //登录方法,并将twitter 作为参数传入 + // Twitter instance + var twitter = hello(network); + + // Login + twitter.login().then(function (r) { + // Get Profile + return twitter.api('/me') + }, log).then(function (p) { + var r2 = JSON.parse(localStorage.getItem('hello')); + //console.log(r2.twitter) + $.ajax({ + url:$ip+'third/login', + data:{ + openid:r2.twitter.user_id, + opentype:5, + authToken:r2.twitter.oauth_token, + authSecret:r2.twitter.oauth_token_secret, + nickname:p.name, + facepath:p.thumbnail, + pageID:localStorage.canvasCode, + }, + success:function(data){ + //console.log('推特登录',data) + if(data.code!=1){ + layer.msg(data.msg) + return; + } + localStorage.token=data.token + localStorage.uid=data.uid + layer.closeAll() + setTimeout(function(){ + $.ajax({ + url:$ip+'activeLog/webActive', + data:{ + activeType:7, + pageID:localStorage.canvasCode, + uid:localStorage.uid + }, + success:function(data){ + window.location.reload() + } + }) + },500) + } + }) + //已获取用户信息,在此处理 + // window.location.href = "footseenlogin://" + encodeURIComponent("www.firefly.live?secret=" + r2.twitter.oauth_token_secret + "&token=" + r2.twitter.oauth_token + "&name=" + p.name + "&profileImageUrl=" + p.thumbnail + "&id=" + r2.twitter.user_id); + // window.location.href = ; + + }, log); } \ No newline at end of file -- libgit2 0.25.0