PageSourceSearch

https://www.medicalmassage-edu.com/js/hello.all.js?v=2023-12

js medicalmassage-edu.com collected 2026-09-26 16:26:30 UTC 138,796 bytes, 5,826 lines download raw bytes

1/*! hellojs v1.14.1 | (c) 2012-2017 Andrew Dodson | MIT https://adodson.com/hello.js/LICENSE */
2// ES5 Object.create
3if (!Object.create) {
4
5	// Shim, Object create
6	// A shim for Object.create(), it adds a prototype to a new object
7	Object.create = (function() {
8
9		function F() {}
10
11		return function(o) {
12
13			if (arguments.length != 1) {
14				throw new Error('Object.create implementation only accepts one parameter.');
15			}
16
17			F.prototype = o;
18			return new F();
19		};
20
21	})();
22
23}
24
25// ES5 Object.keys
26if (!Object.keys) {
27	Object.keys = function(o, k, r) {
28		r = [];
29		for (k in o) {
30			if (r.hasOwnProperty.call(o, k))
31				r.push(k);
32		}
33
34		return r;
35	};
36}
37
38// ES5 [].indexOf
39if (!Array.prototype.indexOf) {
40	Array.prototype.indexOf = function(s) {
41
42		for (var j = 0; j < this.length; j++) {
43			if (this[j] === s) {
44				return j;
45			}
46		}
47
48		return -1;
49	};
50}
51
52// ES5 [].forEach
53if (!Array.prototype.forEach) {
54	Array.prototype.forEach = function(fun/*, thisArg*/) {
55
56		if (this === void 0 || this === null) {
57			throw new TypeError();
58		}
59
60		var t = Object(this);
61		var len = t.length >>> 0;
62		if (typeof fun !== 'function') {
63			throw new TypeError();
64		}
65
66		var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
67		for (var i = 0; i < len; i++) {
68			if (i in t) {
69				fun.call(thisArg, t[i], i, t);
70			}
71		}
72
73		return this;
74	};
75}
76
77// ES5 [].filter
78if (!Array.prototype.filter) {
79	Array.prototype.filter = function(fun, thisArg) {
80
81		var a = [];
82		this.forEach(function(val, i, t) {
83			if (fun.call(thisArg || void 0, val, i, t)) {
84				a.push(val);
85			}
86		});
87
88		return a;
89	};
90}
91
92// Production steps of ECMA-262, Edition 5, 15.4.4.19
93// Reference: http://es5.github.io/#x15.4.4.19
94if (!Array.prototype.map) {
95
96	Array.prototype.map = function(fun, thisArg) {
97
98		var a = [];
99		this.forEach(function(val, i, t) {
100			a.push(fun.call(thisArg || void 0, val, i, t));
101		});
102
103		return a;
104	};
105}
106
107// ES5 isArray
108if (!Array.isArray) {
109
110	// Function Array.isArray
111	Array.isArray = function(o) {
112		return Object.prototype.toString.call(o) === '[object Array]';
113	};
114
115}
116
117// Test for location.assign
118if (typeof window === 'object' && typeof window.location === 'object' && !window.location.assign) {
119
120	window.location.assign = function(url) {
121		window.location = url;
122	};
123
124}
125
126// Test for Function.bind
127if (!Function.prototype.bind) {
128
129	// MDN
130	// Polyfill IE8, does not support native Function.bind
131	Function.prototype.bind = function(b) {
132
133		if (typeof this !== 'function') {
134			throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
135		}
136
137		function C() {}
138
139		var a = [].slice;
140		var f = a.call(arguments, 1);
141		var _this = this;
142		var D = function() {
143			return _this.apply(this instanceof C ? this : b || window, f.concat(a.call(arguments)));
144		};
145
146		C.prototype = this.prototype;
147		D.prototype = new C();
148
149		return D;
150	};
151
152}
153
154/**
155 * @hello.js
156 *
157 * HelloJS is a client side Javascript SDK for making OAuth2 logins and subsequent REST calls.
158 *
159 * @author Andrew Dodson
160 * @website https://adodson.com/hello.js/
161 *
162 * @copyright Andrew Dodson, 2012 - 2015
163 * @license MIT: You are free to use and modify this code for any use, on the condition that this copyright notice remains.
164 */
165
166var hello = function(name) {
167	return hello.use(name);
168};
169
170hello.utils = {
171
172	// Extend the first object with the properties and methods of the second
173	extend: function(r /*, a[, b[, ...]] */) {
174
175		// Get the arguments as an array but ommit the initial item
176		Array.prototype.slice.call(arguments, 1).forEach(function(a) {
177			if (Array.isArray(r) && Array.isArray(a)) {
178				Array.prototype.push.apply(r, a);
179			}
180			else if (r && (r instanceof Object || typeof r === 'object') && a && (a instanceof Object || typeof a === 'object') && r !== a) {
181				for (var x in a) {
182					r[x] = hello.utils.extend(r[x], a[x]);
183				}
184			}
185			else {
186
187				if (Array.isArray(a)) {
188					// Clone it
189					a = a.slice(0);
190				}
191
192				r = a;
193			}
194		});
195
196		return r;
197	}
198};
199
200// Core library
201hello.utils.extend(hello, {
202
203	settings: {
204
205		// OAuth2 authentication defaults
206		redirect_uri: window.location.href.split('#')[0],
207		response_type: 'token',
208		display: 'popup',
209		state: '',
210
211		// OAuth1 shim
212		// The path to the OAuth1 server for signing user requests
213		// Want to recreate your own? Checkout https://github.com/MrSwitch/node-oauth-shim
214		oauth_proxy: 'https://auth-server.herokuapp.com/proxy',
215
216		// API timeout in milliseconds
217		timeout: 20000,
218
219		// Popup Options
220		popup: {
221			resizable: 1,
222			scrollbars: 1,
223			width: 500,
224			height: 550
225		},
226
227		// Default scope
228		// Many services require atleast a profile scope,
229		// HelloJS automatially includes the value of provider.scope_map.basic
230		// If that's not required it can be removed via hello.settings.scope.length = 0;
231		scope: ['basic'],
232
233		// Scope Maps
234		// This is the default module scope, these are the defaults which each service is mapped too.
235		// By including them here it prevents the scope from being applied accidentally
236		scope_map: {
237			basic: ''
238		},
239
240		// Default service / network
241		default_service: null,
242
243		// Force authentication
244		// When hello.login is fired.
245		// (null): ignore current session expiry and continue with login
246		// (true): ignore current session expiry and continue with login, ask for user to reauthenticate
247		// (false): if the current session looks good for the request scopes return the current session.
248		force: null,
249
250		// Page URL
251		// When 'display=page' this property defines where the users page should end up after redirect_uri
252		// Ths could be problematic if the redirect_uri is indeed the final place,
253		// Typically this circumvents the problem of the redirect_url being a dumb relay page.
254		page_uri: window.location.href
255	},
256
257	// Service configuration objects
258	services: {},
259
260	// Use
261	// Define a new instance of the HelloJS library with a default service
262	use: function(service) {
263
264		// Create self, which inherits from its parent
265		var self = Object.create(this);
266
267		// Inherit the prototype from its parent
268		self.settings = Object.create(this.settings);
269
270		// Define the default service
271		if (service) {
272			self.settings.default_service = service;
273		}
274
275		// Create an instance of Events
276		self.utils.Event.call(self);
277
278		return self;
279	},
280
281	// Initialize
282	// Define the client_ids for the endpoint services
283	// @param object o, contains a key value pair, service => clientId
284	// @param object opts, contains a key value pair of options used for defining the authentication defaults
285	// @param number timeout, timeout in seconds
286	init: function(services, options) {
287
288		var utils = this.utils;
289
290		if (!services) {
291			return this.services;
292		}
293
294		// Define provider credentials
295		// Reformat the ID field
296		for (var x in services) {if (services.hasOwnProperty(x)) {
297			if (typeof (services[x]) !== 'object') {
298				services[x] = {id: services[x]};
299			}
300		}}
301
302		// Merge services if there already exists some
303		utils.extend(this.services, services);
304
305		// Update the default settings with this one.
306		if (options) {
307			utils.extend(this.settings, options);
308
309			// Do this immediatly incase the browser changes the current path.
310			if ('redirect_uri' in options) {
311				this.settings.redirect_uri = utils.url(options.redirect_uri).href;
312			}
313		}
314
315		return this;
316	},
317
vendor: 7,304 bytes, lines 318-548
318	// Login
319	// Using the endpoint
320	// @param network stringify       name to connect to
321	// @param options object    (optional)  {display mode, is either none|popup(default)|page, scope: email,birthday,publish, .. }
322	// @param callback  function  (optional)  fired on signin
323	login: function() {
324
325		// Create an object which inherits its parent as the prototype and constructs a new event chain.
326		var _this = this;
327		var utils = _this.utils;
328		var error = utils.error;
329		var promise = utils.Promise();
330
331		// Get parameters
332		var p = utils.args({network: 's', options: 'o', callback: 'f'}, arguments);
333
334		// Local vars
335		var url;
336
337		// Get all the custom options and store to be appended to the querystring
338		var qs = utils.diffKey(p.options, _this.settings);
339
340		// Merge/override options with app defaults
341		var opts = p.options = utils.merge(_this.settings, p.options || {});
342
343		// Merge/override options with app defaults
344		opts.popup = utils.merge(_this.settings.popup, p.options.popup || {});
345
346		// Network
347		p.network = p.network || _this.settings.default_service;
348
349		// Bind callback to both reject and fulfill states
350		promise.proxy.then(p.callback, p.callback);
351
352		// Trigger an event on the global listener
353		function emit(s, value) {
354			hello.emit(s, value);
355		}
356
357		promise.proxy.then(emit.bind(this, 'auth.login auth'), emit.bind(this, 'auth.failed auth'));
358
359		// Is our service valid?
360		if (typeof (p.network) !== 'string' || !(p.network in _this.services)) {
361			// Trigger the default login.
362			// Ahh we dont have one.
363			return promise.reject(error('invalid_network', 'The provided network was not recognized'));
364		}
365
366		var provider = _this.services[p.network];
367
368		// Create a global listener to capture events triggered out of scope
369		var callbackId = utils.globalEvent(function(str) {
370
371			// The responseHandler returns a string, lets save this locally
372			var obj;
373
374			if (str) {
375				obj = JSON.parse(str);
376			}
377			else {
378				obj = error('cancelled', 'The authentication was not completed');
379			}
380
381			// Handle these response using the local
382			// Trigger on the parent
383			if (!obj.error) {
384
385				// Save on the parent window the new credentials
386				// This fixes an IE10 bug i think... atleast it does for me.
387				utils.store(obj.network, obj);
388
389				// Fulfill a successful login
390				promise.fulfill({
391					network: obj.network,
392					authResponse: obj
393				});
394			}
395			else {
396				// Reject a successful login
397				promise.reject(obj);
398			}
399		});
400
401		var redirectUri = utils.url(opts.redirect_uri).href;
402
403		// May be a space-delimited list of multiple, complementary types
404		var responseType = provider.oauth.response_type || opts.response_type;
405
406		// Fallback to token if the module hasn't defined a grant url
407		if (/\bcode\b/.test(responseType) && !provider.oauth.grant) {
408			responseType = responseType.replace(/\bcode\b/, 'token');
409		}
410
411		// Query string parameters, we may pass our own arguments to form the querystring
412		p.qs = utils.merge(qs, {
413			client_id: encodeURIComponent(provider.id),
414			response_type: encodeURIComponent(responseType),
415			redirect_uri: encodeURIComponent(redirectUri),
416			state: {
417				client_id: provider.id,
418				network: p.network,
419				display: opts.display,
420				callback: callbackId,
421				state: opts.state,
422				redirect_uri: redirectUri
423			}
424		});
425
426		// Get current session for merging scopes, and for quick auth response
427		var session = utils.store(p.network);
428
429		// Scopes (authentication permisions)
430		// Ensure this is a string - IE has a problem moving Arrays between windows
431		// Append the setup scope
432		var SCOPE_SPLIT = /[,\s]+/;
433
434		// Include default scope settings (cloned).
435		var scope = _this.settings.scope ? [_this.settings.scope.toString()] : [];
436
437		// Extend the providers scope list with the default
438		var scopeMap = utils.merge(_this.settings.scope_map, provider.scope || {});
439
440		// Add user defined scopes...
441		if (opts.scope) {
442			scope.push(opts.scope.toString());
443		}
444
445		// Append scopes from a previous session.
446		// This helps keep app credentials constant,
447		// Avoiding having to keep tabs on what scopes are authorized
448		if (session && 'scope' in session && session.scope instanceof String) {
449			scope.push(session.scope);
450		}
451
452		// Join and Split again
453		scope = scope.join(',').split(SCOPE_SPLIT);
454
455		// Format remove duplicates and empty values
456		scope = utils.unique(scope).filter(filterEmpty);
457
458		// Save the the scopes to the state with the names that they were requested with.
459		p.qs.state.scope = scope.join(',');
460
461		// Map scopes to the providers naming convention
462		scope = scope.map(function(item) {
463			// Does this have a mapping?
464			return (item in scopeMap) ? scopeMap[item] : item;
465		});
466
467		// Stringify and Arrayify so that double mapped scopes are given the chance to be formatted
468		scope = scope.join(',').split(SCOPE_SPLIT);
469
470		// Again...
471		// Format remove duplicates and empty values
472		scope = utils.unique(scope).filter(filterEmpty);
473
474		// Join with the expected scope delimiter into a string
475		p.qs.scope = scope.join(provider.scope_delim || ',');
476
477		// Is the user already signed in with the appropriate scopes, valid access_token?
478		if (opts.force === false) {
479
480			if (session && 'access_token' in session && session.access_token && 'expires' in session && session.expires > ((new Date()).getTime() / 1e3)) {
481				// What is different about the scopes in the session vs the scopes in the new login?
482				var diff = utils.diff((session.scope || '').split(SCOPE_SPLIT), (p.qs.state.scope || '').split(SCOPE_SPLIT));
483				if (diff.length === 0) {
484
485					// OK trigger the callback
486					promise.fulfill({
487						unchanged: true,
488						network: p.network,
489						authResponse: session
490					});
491
492					// Nothing has changed
493					return promise;
494				}
495			}
496		}
497
498		// Page URL
499		if (opts.display === 'page' && opts.page_uri) {
500			// Add a page location, place to endup after session has authenticated
501			p.qs.state.page_uri = utils.url(opts.page_uri).href;
502		}
503
504		// Bespoke
505		// Override login querystrings from auth_options
506		if ('login' in provider && typeof (provider.login) === 'function') {
507			// Format the paramaters according to the providers formatting function
508			provider.login(p);
509		}
510
511		// Add OAuth to state
512		// Where the service is going to take advantage of the oauth_proxy
513		if (!/\btoken\b/.test(responseType) ||
514		parseInt(provider.oauth.version, 10) < 2 ||
515		(opts.display === 'none' && provider.oauth.grant && session && session.refresh_token)) {
516
517			// Add the oauth endpoints
518			p.qs.state.oauth = provider.oauth;
519
520			// Add the proxy url
521			p.qs.state.oauth_proxy = opts.oauth_proxy;
522
523		}
524
525		// Convert state to a string
526		p.qs.state = encodeURIComponent(JSON.stringify(p.qs.state));
527
528		// URL
529		if (parseInt(provider.oauth.version, 10) === 1) {
530
531			// Turn the request to the OAuth Proxy for 3-legged auth
532			url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
533		}
534
535		// Refresh token
536		else if (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token) {
537
538			// Add the refresh_token to the request
539			p.qs.refresh_token = session.refresh_token;
540
541			// Define the request path
542			url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction);
543		}
544		else {
545			url = utils.qs(provider.oauth.auth, p.qs, encodeFunction);
546		}
547
548		// Broadcast this event as an auth:init
549		emit('auth.init', p);
550
551		// Execute
552		// Trigger how we want self displayed
553		if (opts.display === 'none') {
554			// Sign-in in the background, iframe
555			utils.iframe(url, redirectUri);
556		}
557
558		// Triggering popup?
559		else if (opts.display === 'popup') {
560
561			var popup = utils.popup(url, redirectUri, opts.popup);
562
563			var timer = setInterval(function() {
564				if (!popup || popup.closed) {
565					clearInterval(timer);
566					if (!promise.state) {
567
568						var response = error('cancelled', 'Login has been cancelled');
569
570						if (!popup) {
571							response = error('blocked', 'Popup was blocked');
572						}
573
574						response.network = p.network;
575
576						promise.reject(response);
577					}
578				}
579			}, 100);
580		}
581
582		else {
583			window.location = url;
584		}
585
586		return promise.proxy;
587
588		function encodeFunction(s) {return s;}
589
590		function filterEmpty(s) {return !!s;}
591	},
592
593	// Remove any data associated with a given service
594	// @param string name of the service
595	// @param function callback
596	logout: function() {
597
598		var _this = this;
599		var utils = _this.utils;
600		var error = utils.error;
601
602		// Create a new promise
603		var promise = utils.Promise();
604
605		var p = utils.args({name:'s', options: 'o', callback: 'f'}, arguments);
606
607		p.options = p.options || {};
608
609		// Add callback to events
610		promise.proxy.then(p.callback, p.callback);
611
612		// Trigger an event on the global listener
613		function emit(s, value) {
614			hello.emit(s, value);
615		}
616
617		promise.proxy.then(emit.bind(this, 'auth.logout auth'), emit.bind(this, 'error'));
618
619		// Network
620		p.name = p.name || this.settings.default_service;
621		p.authResponse = utils.store(p.name);
622
623		if (p.name && !(p.name in _this.services)) {
624
625			promise.reject(error('invalid_network', 'The network was unrecognized'));
626
627		}
628		else if (p.name && p.authResponse) {
629
630			// Define the callback
631			var callback = function(opts) {
632
633				// Remove from the store
634				utils.store(p.name, null);
635
636				// Emit events by default
637				promise.fulfill(hello.utils.merge({network:p.name}, opts || {}));
638			};
639
640			// Run an async operation to remove the users session
641			var _opts = {};
642			if (p.options.force) {
643				var logout = _this.services[p.name].logout;
644				if (logout) {
645					// Convert logout to URL string,
646					// If no string is returned, then this function will handle the logout async style
647					if (typeof (logout) === 'function') {
648						logout = logout(callback, p);
649					}
650
651					// If logout is a string then assume URL and open in iframe.
652					if (typeof (logout) === 'string') {
653						utils.iframe(logout);
654						_opts.force = null;
655						_opts.message = 'Logout success on providers site was indeterminate';
656					}
657					else if (logout === undefined) {
658						// The callback function will handle the response.
659						return promise.proxy;
660					}
661				}
662			}
663
664			// Remove local credentials
665			callback(_opts);
666		}
667		else {
668			promise.reject(error('invalid_session', 'There was no session to remove'));
669		}
670
671		return promise.proxy;
672	},
673
674	// Returns all the sessions that are subscribed too
675	// @param string optional, name of the service to get information about.
676	getAuthResponse: function(service) {
677
678		// If the service doesn't exist
679		service = service || this.settings.default_service;
680
681		if (!service || !(service in this.services)) {
682			return null;
683		}
684
685		return this.utils.store(service) || null;
686	},
687
688	// Events: placeholder for the events
689	events: {}
690});
691
692// Core utilities
693hello.utils.extend(hello.utils, {
694
695	// Error
696	error: function(code, message) {
697		return {
698			error: {
699				code: code,
700				message: message
701			}
702		};
703	},
704
705	// Append the querystring to a url
706	// @param string url
707	// @param object parameters
708	qs: function(url, params, formatFunction) {
709
710		if (params) {
711
712			// Set default formatting function
713			formatFunction = formatFunction || encodeURIComponent;
714
715			// Override the items in the URL which already exist
716			for (var x in params) {
717				var str = '([\\?\\&])' + x + '=[^\\&]*';
718				var reg = new RegExp(str);
719				if (url.match(reg)) {
720					url = url.replace(reg, '$1' + x + '=' + formatFunction(params[x]));
721					delete params[x];
722				}
723			}
724		}
725
726		if (!this.isEmpty(params)) {
727			return url + (url.indexOf('?') > -1 ? '&' : '?') + this.param(params, formatFunction);
728		}
729
730		return url;
731	},
732
733	// Param
734	// Explode/encode the parameters of an URL string/object
735	// @param string s, string to decode
736	param: function(s, formatFunction) {
737		var b;
738		var a = {};
739		var m;
740
741		if (typeof (s) === 'string') {
742
743			formatFunction = formatFunction || decodeURIComponent;
744
745			m = s.replace(/^[\#\?]/, '').match(/([^=\/\&]+)=([^\&]+)/g);
746			if (m) {
747				for (var i = 0; i < m.length; i++) {
748					b = m[i].match(/([^=]+)=(.*)/);
749					a[b[1]] = formatFunction(b[2]);
750				}
751			}
752
753			return a;
754		}
755		else {
756
757			formatFunction = formatFunction || encodeURIComponent;
758
759			var o = s;
760
761			a = [];
762
763			for (var x in o) {if (o.hasOwnProperty(x)) {
764				if (o.hasOwnProperty(x)) {
765					a.push([x, o[x] === '?' ? '?' : formatFunction(o[x])].join('='));
766				}
767			}}
768
769			return a.join('&');
770		}
771	},
772
vendor: 4,891 bytes, lines 773-985
773	// Local storage facade
774	store: (function() {
775
776		var a = ['localStorage', 'sessionStorage'];
777		var i = -1;
778		var prefix = 'test';
779
780		// Set LocalStorage
781		var localStorage;
782
783		while (a[++i]) {
784			try {
785				// In Chrome with cookies blocked, calling localStorage throws an error
786				localStorage = window[a[i]];
787				localStorage.setItem(prefix + i, i);
788				localStorage.removeItem(prefix + i);
789				break;
790			}
791			catch (e) {
792				localStorage = null;
793			}
794		}
795
796		if (!localStorage) {
797
798			var cache = null;
799
800			localStorage = {
801				getItem: function(prop) {
802					prop = prop + '=';
803					var m = document.cookie.split(';');
804					for (var i = 0; i < m.length; i++) {
805						var _m = m[i].replace(/(^\s+|\s+$)/, '');
806						if (_m && _m.indexOf(prop) === 0) {
807							return _m.substr(prop.length);
808						}
809					}
810
811					return cache;
812				},
813
814				setItem: function(prop, value) {
815					cache = value;
816					document.cookie = prop + '=' + value;
817				}
818			};
819
820			// Fill the cache up
821			cache = localStorage.getItem('hello');
822		}
823
824		function get() {
825			var json = {};
826			try {
827				json = JSON.parse(localStorage.getItem('hello')) || {};
828			}
829			catch (e) {}
830
831			return json;
832		}
833
834		function set(json) {
835			localStorage.setItem('hello', JSON.stringify(json));
836		}
837
838		// Check if the browser support local storage
839		return function(name, value, days) {
840
841			// Local storage
842			var json = get();
843
844			if (name && value === undefined) {
845				return json[name] || null;
846			}
847			else if (name && value === null) {
848				try {
849					delete json[name];
850				}
851				catch (e) {
852					json[name] = null;
853				}
854			}
855			else if (name) {
856				json[name] = value;
857			}
858			else {
859				return json;
860			}
861
862			set(json);
863
864			return json || null;
865		};
866
867	})(),
868
869	// Create and Append new DOM elements
870	// @param node string
871	// @param attr object literal
872	// @param dom/string
873	append: function(node, attr, target) {
874
875		var n = typeof (node) === 'string' ? document.createElement(node) : node;
876
877		if (typeof (attr) === 'object') {
878			if ('tagName' in attr) {
879				target = attr;
880			}
881			else {
882				for (var x in attr) {if (attr.hasOwnProperty(x)) {
883					if (typeof (attr[x]) === 'object') {
884						for (var y in attr[x]) {if (attr[x].hasOwnProperty(y)) {
885							n[x][y] = attr[x][y];
886						}}
887					}
888					else if (x === 'html') {
889						n.innerHTML = attr[x];
890					}
891
892					// IE doesn't like us setting methods with setAttribute
893					else if (!/^on/.test(x)) {
894						n.setAttribute(x, attr[x]);
895					}
896					else {
897						n[x] = attr[x];
898					}
899				}}
900			}
901		}
902
903		if (target === 'body') {
904			(function self() {
905				if (document.body) {
906					document.body.appendChild(n);
907				}
908				else {
909					setTimeout(self, 16);
910				}
911			})();
912		}
913		else if (typeof (target) === 'object') {
914			target.appendChild(n);
915		}
916		else if (typeof (target) === 'string') {
917			document.getElementsByTagName(target)[0].appendChild(n);
918		}
919
920		return n;
921	},
922
923	// An easy way to create a hidden iframe
924	// @param string src
925	iframe: function(src) {
926		this.append('iframe', {src: src, style: {position:'absolute', left: '-1000px', bottom: 0, height: '1px', width: '1px'}}, 'body');
927	},
928
929	// Recursive merge two objects into one, second parameter overides the first
930	// @param a array
931	merge: function(/* Args: a, b, c, .. n */) {
932		var args = Array.prototype.slice.call(arguments);
933		args.unshift({});
934		return this.extend.apply(null, args);
935	},
936
937	// Makes it easier to assign parameters, where some are optional
938	// @param o object
939	// @param a arguments
940	args: function(o, args) {
941
942		var p = {};
943		var i = 0;
944		var t = null;
945		var x = null;
946
947		// 'x' is the first key in the list of object parameters
948		for (x in o) {if (o.hasOwnProperty(x)) {
949			break;
950		}}
951
952		// Passing in hash object of arguments?
953		// Where the first argument can't be an object
954		if ((args.length === 1) && (typeof (args[0]) === 'object') && o[x] != 'o!') {
955
956			// Could this object still belong to a property?
957			// Check the object keys if they match any of the property keys
958			for (x in args[0]) {if (o.hasOwnProperty(x)) {
959				// Does this key exist in the property list?
960				if (x in o) {
961					// Yes this key does exist so its most likely this function has been invoked with an object parameter
962					// Return first argument as the hash of all arguments
963					return args[0];
964				}
965			}}
966		}
967
968		// Else loop through and account for the missing ones.
969		for (x in o) {if (o.hasOwnProperty(x)) {
970
971			t = typeof (args[i]);
972
973			if ((typeof (o[x]) === 'function' && o[x].test(args[i])) || (typeof (o[x]) === 'string' && (
974			(o[x].indexOf('s') > -1 && t === 'string') ||
975			(o[x].indexOf('o') > -1 && t === 'object') ||
976			(o[x].indexOf('i') > -1 && t === 'number') ||
977			(o[x].indexOf('a') > -1 && t === 'object') ||
978			(o[x].indexOf('f') > -1 && t === 'function')
979			))
980			) {
981				p[x] = args[i++];
982			}
983
984			else if (typeof (o[x]) === 'string' && o[x].indexOf('!') > -1) {
985				return false;
986			}
987		}}
988
989		return p;
990	},
991
992	// Returns a URL instance
993	url: function(path) {
994
995		// If the path is empty
996		if (!path) {
997			return window.location;
998		}
999
1000		// Chrome and FireFox support new URL() to extract URL objects
1001		else if (window.URL && URL instanceof Function && URL.length !== 0) {
1002			return new URL(path, window.location);
1003		}
1004
1005		// Ugly shim, it works!
1006		else {
1007			var a = document.createElement('a');
1008			a.href = path;
1009			return a.cloneNode(false);
1010		}
1011	},
1012
1013	diff: function(a, b) {
1014		return b.filter(function(item) {
1015			return a.indexOf(item) === -1;
1016		});
1017	},
1018
1019	// Get the different hash of properties unique to `a`, and not in `b`
1020	diffKey: function(a, b) {
1021		if (a || !b) {
1022			var r = {};
1023			for (var x in a) {
1024				// Does the property not exist?
1025				if (!(x in b)) {
1026					r[x] = a[x];
1027				}
1028			}
1029
1030			return r;
1031		}
1032
1033		return a;
1034	},
1035
1036	// Unique
1037	// Remove duplicate and null values from an array
1038	// @param a array
1039	unique: function(a) {
1040		if (!Array.isArray(a)) { return []; }
1041
1042		return a.filter(function(item, index) {
1043			// Is this the first location of item
1044			return a.indexOf(item) === index;
1045		});
1046	},
1047
1048	isEmpty: function(obj) {
1049
1050		// Scalar
1051		if (!obj)
1052			return true;
1053
1054		// Array
1055		if (Array.isArray(obj)) {
1056			return !obj.length;
1057		}
1058		else if (typeof (obj) === 'object') {
1059			// Object
1060			for (var key in obj) {
1061				if (obj.hasOwnProperty(key)) {
1062					return false;
1063				}
1064			}
1065		}
1066
1067		return true;
1068	},
1069
1070	//jscs:disable
1071
1072	/*!
1073	 **  Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
1074	 **  Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
1075	 **  Licensed under The MIT License <http://opensource.org/licenses/MIT>
1076	 **  Source-Code distributed on <http://github.com/rse/thenable>
1077	 */
1078	Promise: (function(){
1079		/*  promise states [Promises/A+ 2.1]  */
1080		var STATE_PENDING   = 0;                                         /*  [Promises/A+ 2.1.1]  */
1081		var STATE_FULFILLED = 1;                                         /*  [Promises/A+ 2.1.2]  */
1082		var STATE_REJECTED  = 2;                                         /*  [Promises/A+ 2.1.3]  */
1083
1084		/*  promise object constructor  */
1085		var api = function (executor) {
1086			/*  optionally support non-constructor/plain-function call  */
1087			if (!(this instanceof api))
1088				return new api(executor);
1089
1090			/*  initialize object  */
1091			this.id           = "Thenable/1.0.6";
1092			this.state        = STATE_PENDING; /*  initial state  */
1093			this.fulfillValue = undefined;     /*  initial value  */     /*  [Promises/A+ 1.3, 2.1.2.2]  */
1094			this.rejectReason = undefined;     /*  initial reason */     /*  [Promises/A+ 1.5, 2.1.3.2]  */
1095			this.onFulfilled  = [];            /*  initial handlers  */
1096			this.onRejected   = [];            /*  initial handlers  */
1097
1098			/*  provide optional information-hiding proxy  */
1099			this.proxy = {
1100				then: this.then.bind(this)
1101			};
1102
1103			/*  support optional executor function  */
1104			if (typeof executor === "function")
1105				executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
1106		};
1107
1108		/*  promise API methods  */
1109		api.prototype = {
1110			/*  promise resolving methods  */
1111			fulfill: function (value) { return deliver(this, STATE_FULFILLED, "fulfillValue", value); },
1112			reject:  function (value) { return deliver(this, STATE_REJECTED,  "rejectReason", value); },
1113
1114			/*  "The then Method" [Promises/A+ 1.1, 1.2, 2.2]  */
1115			then: function (onFulfilled, onRejected) {
1116				var curr = this;
1117				var next = new api();                                    /*  [Promises/A+ 2.2.7]  */
1118				curr.onFulfilled.push(
1119					resolver(onFulfilled, next, "fulfill"));             /*  [Promises/A+ 2.2.2/2.2.6]  */
1120				curr.onRejected.push(
1121					resolver(onRejected,  next, "reject" ));             /*  [Promises/A+ 2.2.3/2.2.6]  */
1122				execute(curr);
1123				return next.proxy;                                       /*  [Promises/A+ 2.2.7, 3.3]  */
1124			}
1125		};
1126
1127		/*  deliver an action  */
1128		var deliver = function (curr, state, name, value) {
1129			if (curr.state === STATE_PENDING) {
1130				curr.state = state;                                      /*  [Promises/A+ 2.1.2.1, 2.1.3.1]  */
1131				curr[name] = value;                                      /*  [Promises/A+ 2.1.2.2, 2.1.3.2]  */
1132				execute(curr);
1133			}
1134			return curr;
1135		};
1136
1137		/*  execute all handlers  */
1138		var execute = function (curr) {
1139			if (curr.state === STATE_FULFILLED)
1140				execute_handlers(curr, "onFulfilled", curr.fulfillValue);
1141			else if (curr.state === STATE_REJECTED)
1142				execute_handlers(curr, "onRejected",  curr.rejectReason);
1143		};
1144
1145		/*  execute particular set of handlers  */
1146		var execute_handlers = function (curr, name, value) {
1147			/* global process: true */
1148			/* global setImmediate: true */
1149			/* global setTimeout: true */
1150
1151			/*  short-circuit processing  */
1152			if (curr[name].length === 0)
1153				return;
1154
1155			/*  iterate over all handlers, exactly once  */
1156			var handlers = curr[name];
1157			curr[name] = [];                                             /*  [Promises/A+ 2.2.2.3, 2.2.3.3]  */
1158			var func = function () {
1159				for (var i = 0; i < handlers.length; i++)
1160					handlers[i](value);                                  /*  [Promises/A+ 2.2.5]  */
1161			};
1162
1163			/*  execute procedure asynchronously  */                     /*  [Promises/A+ 2.2.4, 3.1]  */
1164			if (typeof process === "object" && typeof process.nextTick === "function")
1165				process.nextTick(func);
1166			else if (typeof setImmediate === "function")
1167				setImmediate(func);
1168			else
1169				setTimeout(func, 0);
1170		};
1171
1172		/*  generate a resolver function  */
1173		var resolver = function (cb, next, method) {
1174			return function (value) {
1175				if (typeof cb !== "function")                            /*  [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4]  */
1176					next[method].call(next, value);                      /*  [Promises/A+ 2.2.7.3, 2.2.7.4]  */
1177				else {
1178					var result;
1179					try { result = cb(value); }                          /*  [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2]  */
1180					catch (e) {
1181						next.reject(e);                                  /*  [Promises/A+ 2.2.7.2]  */
1182						return;
1183					}
1184					resolve(next, result);                               /*  [Promises/A+ 2.2.7.1]  */
1185				}
1186			};
1187		};
1188
1189		/*  "Promise Resolution Procedure"  */                           /*  [Promises/A+ 2.3]  */
1190		var resolve = function (promise, x) {
1191			/*  sanity check arguments  */                               /*  [Promises/A+ 2.3.1]  */
1192			if (promise === x || promise.proxy === x) {
1193				promise.reject(new TypeError("cannot resolve promise with itself"));
1194				return;
1195			}
1196
1197			/*  surgically check for a "then" method
1198				(mainly to just call the "getter" of "then" only once)  */
1199			var then;
1200			if ((typeof x === "object" && x !== null) || typeof x === "function") {
1201				try { then = x.then; }                                   /*  [Promises/A+ 2.3.3.1, 3.5]  */
1202				catch (e) {
1203					promise.reject(e);                                   /*  [Promises/A+ 2.3.3.2]  */
1204					return;
1205				}
1206			}
1207
1208			/*  handle own Thenables    [Promises/A+ 2.3.2]
1209				and similar "thenables" [Promises/A+ 2.3.3]  */
1210			if (typeof then === "function") {
1211				var resolved = false;
1212				try {
1213					/*  call retrieved "then" method */                  /*  [Promises/A+ 2.3.3.3]  */
1214					then.call(x,
1215						/*  resolvePromise  */                           /*  [Promises/A+ 2.3.3.3.1]  */
1216						function (y) {
1217							if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
1218							if (y === x)                                 /*  [Promises/A+ 3.6]  */
1219								promise.reject(new TypeError("circular thenable chain"));
1220							else
1221								resolve(promise, y);
1222						},
1223
1224						/*  rejectPromise  */                            /*  [Promises/A+ 2.3.3.3.2]  */
1225						function (r) {
1226							if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
1227							promise.reject(r);
1228						}
1229					);
1230				}
1231				catch (e) {
1232					if (!resolved)                                       /*  [Promises/A+ 2.3.3.3.3]  */
1233						promise.reject(e);                               /*  [Promises/A+ 2.3.3.3.4]  */
1234				}
1235				return;
1236			}
1237
1238			/*  handle other values  */
1239			promise.fulfill(x);                                          /*  [Promises/A+ 2.3.4, 2.3.3.4]  */
1240		};
1241
1242		/*  export API  */
1243		return api;
1244	})(),
1245
1246	//jscs:enable
1247
1248	// Event
1249	// A contructor superclass for adding event menthods, on, off, emit.
1250	Event: function() {
1251
1252		var separator = /[\s\,]+/;
1253
1254		// If this doesn't support getPrototype then we can't get prototype.events of the parent
1255		// So lets get the current instance events, and add those to a parent property
1256		this.parent = {
1257			events: this.events,
1258			findEvents: this.findEvents,
1259			parent: this.parent,
1260			utils: this.utils
1261		};
1262
1263		this.events = {};
1264
1265		// On, subscribe to events
1266		// @param evt   string
1267		// @param callback  function
1268		this.on = function(evt, callback) {
1269
1270			if (callback && typeof (callback) === 'function') {
1271				var a = evt.split(separator);
1272				for (var i = 0; i < a.length; i++) {
1273
1274					// Has this event already been fired on this instance?
1275					this.events[a[i]] = [callback].concat(this.events[a[i]] || []);
1276				}
1277			}
1278
1279			return this;
1280		};
1281
1282		// Off, unsubscribe to events
1283		// @param evt   string
1284		// @param callback  function
1285		this.off = function(evt, callback) {
1286
1287			this.findEvents(evt, function(name, index) {
1288				if (!callback || this.events[name][index] === callback) {
1289					this.events[name][index] = null;
1290				}
1291			});
1292
1293			return this;
1294		};
1295
1296		// Emit
1297		// Triggers any subscribed events
1298		this.emit = function(evt /*, data, ... */) {
1299
1300			// Get arguments as an Array, knock off the first one
1301			var args = Array.prototype.slice.call(arguments, 1);
1302			args.push(evt);
1303
1304			// Handler
1305			var handler = function(name, index) {
1306
1307				// Replace the last property with the event name
1308				args[args.length - 1] = (name === '*' ? evt : name);
1309
1310				// Trigger
1311				this.events[name][index].apply(this, args);
1312			};
1313
1314			// Find the callbacks which match the condition and call
1315			var _this = this;
1316			while (_this && _this.findEvents) {
1317
1318				// Find events which match
1319				_this.findEvents(evt + ',*', handler);
1320				_this = _this.parent;
1321			}
1322
1323			return this;
1324		};
1325
1326		//
1327		// Easy functions
1328		this.emitAfter = function() {
1329			var _this = this;
1330			var args = arguments;
1331			setTimeout(function() {
1332				_this.emit.apply(_this, args);
1333			}, 0);
1334
1335			return this;
1336		};
1337
1338		this.findEvents = function(evt, callback) {
1339
1340			var a = evt.split(separator);
1341
1342			for (var name in this.events) {if (this.events.hasOwnProperty(name)) {
1343
1344				if (a.indexOf(name) > -1) {
1345
1346					for (var i = 0; i < this.events[name].length; i++) {
1347
1348						// Does the event handler exist?
1349						if (this.events[name][i]) {
1350							// Emit on the local instance of this
1351							callback.call(this, name, i);
1352						}
1353					}
1354				}
1355			}}
1356		};
1357
1358		return this;
1359	},
1360
1361	// Global Events
1362	// Attach the callback to the window object
1363	// Return its unique reference
1364	globalEvent: function(callback, guid) {
1365		// If the guid has not been supplied then create a new one.
1366		guid = guid || '_hellojs_' + parseInt(Math.random() * 1e12, 10).toString(36);
1367
1368		// Define the callback function
1369		window[guid] = function() {
1370			// Trigger the callback
1371			try {
1372				if (callback.apply(this, arguments)) {
1373					delete window[guid];
1374				}
1375			}
1376			catch (e) {
1377				console.error(e);
1378			}
1379		};
1380
1381		return guid;
1382	},
1383
1384	// Trigger a clientside popup
1385	// This has been augmented to support PhoneGap
1386	popup: function(url, redirectUri, options) {
1387
1388		var documentElement = document.documentElement;
1389
1390		// Multi Screen Popup Positioning (http://stackoverflow.com/a/16861050)
1391		// Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html
1392		// Fixes dual-screen position                         Most browsers      Firefox
1393
1394		if (options.height) {
1395			var dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
1396			var height = screen.height || window.innerHeight || documentElement.clientHeight;
1397			options.top = parseInt((height - options.height) / 2, 10) + dualScreenTop;
1398		}
1399
1400		if (options.width) {
1401			var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
1402			var width = screen.width || window.innerWidth || documentElement.clientWidth;
1403			options.left = parseInt((width - options.width) / 2, 10) + dualScreenLeft;
1404		}
1405
1406		// Convert options into an array
1407		var optionsArray = [];
vendor: 19,515 bytes, lines 1408-2181
1408		Object.keys(options).forEach(function(name) {
1409			var value = options[name];
1410			optionsArray.push(name + (value !== null ? '=' + value : ''));
1411		});
1412
1413		// Call the open() function with the initial path
1414		//
1415		// OAuth redirect, fixes URI fragments from being lost in Safari
1416		// (URI Fragments within 302 Location URI are lost over HTTPS)
1417		// Loading the redirect.html before triggering the OAuth Flow seems to fix it.
1418		//
1419		// Firefox  decodes URL fragments when calling location.hash.
1420		//  - This is bad if the value contains break points which are escaped
1421		//  - Hence the url must be encoded twice as it contains breakpoints.
1422		if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
1423			url = redirectUri + '#oauth_redirect=' + encodeURIComponent(encodeURIComponent(url));
1424		}
1425
1426		var popup = window.open(
1427			url,
1428			'_blank',
1429			optionsArray.join(',')
1430		);
1431
1432		if (popup && popup.focus) {
1433			popup.focus();
1434		}
1435
1436		return popup;
1437	},
1438
1439	// OAuth and API response handler
1440	responseHandler: function(window, parent) {
1441
1442		var _this = this;
1443		var p;
1444		var location = window.location;
1445
1446		// Is this an auth relay message which needs to call the proxy?
1447		p = _this.param(location.search);
1448
1449		// OAuth2 or OAuth1 server response?
1450		if (p && p.state && (p.code || p.oauth_token)) {
1451
1452			var state = JSON.parse(p.state);
1453
1454			// Add this path as the redirect_uri
1455			p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, '');
1456
1457			// Redirect to the host
1458			var path = state.oauth_proxy + '?' + _this.param(p);
1459
1460			location.assign(path);
1461
1462			return;
1463		}
1464
1465		// Save session, from redirected authentication
1466		// #access_token has come in?
1467		//
1468		// FACEBOOK is returning auth errors within as a query_string... thats a stickler for consistency.
1469		// SoundCloud is the state in the querystring and the token in the hashtag, so we'll mix the two together
1470
1471		p = _this.merge(_this.param(location.search || ''), _this.param(location.hash || ''));
1472
1473		// If p.state
1474		if (p && 'state' in p) {
1475
1476			// Remove any addition information
1477			// E.g. p.state = 'facebook.page';
1478			try {
1479				var a = JSON.parse(p.state);
1480				_this.extend(p, a);
1481			}
1482			catch (e) {
1483				console.error('Could not decode state parameter');
1484			}
1485
1486			// Access_token?
1487			if (('access_token' in p && p.access_token) && p.network) {
1488
1489				if (!p.expires_in || parseInt(p.expires_in, 10) === 0) {
1490					// If p.expires_in is unset, set to 0
1491					p.expires_in = 0;
1492				}
1493
1494				p.expires_in = parseInt(p.expires_in, 10);
1495				p.expires = ((new Date()).getTime() / 1e3) + (p.expires_in || (60 * 60 * 24 * 365));
1496
1497				// Lets use the "state" to assign it to one of our networks
1498				authCallback(p, window, parent);
1499			}
1500
1501			// Error=?
1502			// &error_description=?
1503			// &state=?
1504			else if (('error' in p && p.error) && p.network) {
1505
1506				p.error = {
1507					code: p.error,
1508					message: p.error_message || p.error_description
1509				};
1510
1511				// Let the state handler handle it
1512				authCallback(p, window, parent);
1513			}
1514
1515			// API call, or a cancelled login
1516			// Result is serialized JSON string
1517			else if (p.callback && p.callback in parent) {
1518
1519				// Trigger a function in the parent
1520				var res = 'result' in p && p.result ? JSON.parse(p.result) : false;
1521
1522				// Trigger the callback on the parent
1523				callback(parent, p.callback)(res);
1524				closeWindow();
1525			}
1526
1527			// If this page is still open
1528			if (p.page_uri) {
1529				location.assign(p.page_uri);
1530			}
1531		}
1532
1533		// OAuth redirect, fixes URI fragments from being lost in Safari
1534		// (URI Fragments within 302 Location URI are lost over HTTPS)
1535		// Loading the redirect.html before triggering the OAuth Flow seems to fix it.
1536		else if ('oauth_redirect' in p) {
1537
1538			location.assign(decodeURIComponent(p.oauth_redirect));
1539			return;
1540		}
1541
1542		// Trigger a callback to authenticate
1543		function authCallback(obj, window, parent) {
1544
1545			var cb = obj.callback;
1546			var network = obj.network;
1547
1548			// Trigger the callback on the parent
1549			_this.store(network, obj);
1550
1551			// If this is a page request it has no parent or opener window to handle callbacks
1552			if (('display' in obj) && obj.display === 'page') {
1553				return;
1554			}
1555
1556			// Remove from session object
1557			if (parent && cb && cb in parent) {
1558
1559				try {
1560					delete obj.callback;
1561				}
1562				catch (e) {}
1563
1564				// Update store
1565				_this.store(network, obj);
1566
1567				// Call the globalEvent function on the parent
1568				// It's safer to pass back a string to the parent,
1569				// Rather than an object/array (better for IE8)
1570				var str = JSON.stringify(obj);
1571
1572				try {
1573					callback(parent, cb)(str);
1574				}
1575				catch (e) {
1576					// Error thrown whilst executing parent callback
1577				}
1578			}
1579
1580			closeWindow();
1581		}
1582
1583		function callback(parent, callbackID) {
1584			if (callbackID.indexOf('_hellojs_') !== 0) {
1585				return function() {
1586					throw 'Could not execute callback ' + callbackID;
1587				};
1588			}
1589
1590			return parent[callbackID];
1591		}
1592
1593		function closeWindow() {
1594
1595			if (window.frameElement) {
1596				// Inside an iframe, remove from parent
1597				parent.document.body.removeChild(window.frameElement);
1598			}
1599			else {
1600				// Close this current window
1601				try {
1602					window.close();
1603				}
1604				catch (e) {}
1605
1606				// IOS bug wont let us close a popup if still loading
1607				if (window.addEventListener) {
1608					window.addEventListener('load', function() {
1609						window.close();
1610					});
1611				}
1612			}
1613
1614		}
1615	}
1616});
1617
1618// Events
1619// Extend the hello object with its own event instance
1620hello.utils.Event.call(hello);
1621
1622///////////////////////////////////
1623// Monitoring session state
1624// Check for session changes
1625///////////////////////////////////
1626
1627(function(hello) {
1628
1629	// Monitor for a change in state and fire
1630	var oldSessions = {};
1631
1632	// Hash of expired tokens
1633	var expired = {};
1634
1635	// Listen to other triggers to Auth events, use these to update this
1636	hello.on('auth.login, auth.logout', function(auth) {
1637		if (auth && typeof (auth) === 'object' && auth.network) {
1638			oldSessions[auth.network] = hello.utils.store(auth.network) || {};
1639		}
1640	});
1641
1642	(function self() {
1643
1644		var CURRENT_TIME = ((new Date()).getTime() / 1e3);
1645		var emit = function(eventName) {
1646			hello.emit('auth.' + eventName, {
1647				network: name,
1648				authResponse: session
1649			});
1650		};
1651
1652		// Loop through the services
1653		for (var name in hello.services) {if (hello.services.hasOwnProperty(name)) {
1654
1655			if (!hello.services[name].id) {
1656				// We haven't attached an ID so dont listen.
1657				continue;
1658			}
1659
1660			// Get session
1661			var session = hello.utils.store(name) || {};
1662			var provider = hello.services[name];
1663			var oldSess = oldSessions[name] || {};
1664
1665			// Listen for globalEvents that did not get triggered from the child
1666			if (session && 'callback' in session) {
1667
1668				// To do remove from session object...
1669				var cb = session.callback;
1670				try {
1671					delete session.callback;
1672				}
1673				catch (e) {}
1674
1675				// Update store
1676				// Removing the callback
1677				hello.utils.store(name, session);
1678
1679				// Emit global events
1680				try {
1681					window[cb](session);
1682				}
1683				catch (e) {}
1684			}
1685
1686			// Refresh token
1687			if (session && ('expires' in session) && session.expires < CURRENT_TIME) {
1688
1689				// If auto refresh is possible
1690				// Either the browser supports
1691				var refresh = provider.refresh || session.refresh_token;
1692
1693				// Has the refresh been run recently?
1694				if (refresh && (!(name in expired) || expired[name] < CURRENT_TIME)) {
1695					// Try to resignin
1696					hello.emit('notice', name + ' has expired trying to resignin');
1697					hello.login(name, {display: 'none', force: false});
1698
1699					// Update expired, every 10 minutes
1700					expired[name] = CURRENT_TIME + 600;
1701				}
1702
1703				// Does this provider not support refresh
1704				else if (!refresh && !(name in expired)) {
1705					// Label the event
1706					emit('expired');
1707					expired[name] = true;
1708				}
1709
1710				// If session has expired then we dont want to store its value until it can be established that its been updated
1711				continue;
1712			}
1713
1714			// Has session changed?
1715			else if (oldSess.access_token === session.access_token &&
1716			oldSess.expires === session.expires) {
1717				continue;
1718			}
1719
1720			// Access_token has been removed
1721			else if (!session.access_token && oldSess.access_token) {
1722				emit('logout');
1723			}
1724
1725			// Access_token has been created
1726			else if (session.access_token && !oldSess.access_token) {
1727				emit('login');
1728			}
1729
1730			// Access_token has been updated
1731			else if (session.expires !== oldSess.expires) {
1732				emit('update');
1733			}
1734
1735			// Updated stored session
1736			oldSessions[name] = session;
1737
1738			// Remove the expired flags
1739			if (name in expired) {
1740				delete expired[name];
1741			}
1742		}}
1743
1744		// Check error events
1745		setTimeout(self, 1000);
1746	})();
1747
1748})(hello);
1749
1750// EOF CORE lib
1751//////////////////////////////////
1752
1753/////////////////////////////////////////
1754// API
1755// @param path    string
1756// @param query   object (optional)
1757// @param method  string (optional)
1758// @param data    object (optional)
1759// @param timeout integer (optional)
1760// @param callback  function (optional)
1761
1762hello.api = function() {
1763
1764	// Shorthand
1765	var _this = this;
1766	var utils = _this.utils;
1767	var error = utils.error;
1768
1769	// Construct a new Promise object
1770	var promise = utils.Promise();
1771
1772	// Arguments
1773	var p = utils.args({path: 's!', query: 'o', method: 's', data: 'o', timeout: 'i', callback: 'f'}, arguments);
1774
1775	// Method
1776	p.method = (p.method || 'get').toLowerCase();
1777
1778	// Headers
1779	p.headers = p.headers || {};
1780
1781	// Query
1782	p.query = p.query || {};
1783
1784	// If get, put all parameters into query
1785	if (p.method === 'get' || p.method === 'delete') {
1786		utils.extend(p.query, p.data);
1787		p.data = {};
1788	}
1789
1790	var data = p.data = p.data || {};
1791
1792	// Completed event callback
1793	promise.then(p.callback, p.callback);
1794
1795	// Remove the network from path, e.g. facebook:/me/friends
1796	// Results in { network : facebook, path : me/friends }
1797	if (!p.path) {
1798		return promise.reject(error('invalid_path', 'Missing the path parameter from the request'));
1799	}
1800
1801	p.path = p.path.replace(/^\/+/, '');
1802	var a = (p.path.split(/[\/\:]/, 2) || [])[0].toLowerCase();
1803
1804	if (a in _this.services) {
1805		p.network = a;
1806		var reg = new RegExp('^' + a + ':?\/?');
1807		p.path = p.path.replace(reg, '');
1808	}
1809
1810	// Network & Provider
1811	// Define the network that this request is made for
1812	p.network = _this.settings.default_service = p.network || _this.settings.default_service;
1813	var o = _this.services[p.network];
1814
1815	// INVALID
1816	// Is there no service by the given network name?
1817	if (!o) {
1818		return promise.reject(error('invalid_network', 'Could not match the service requested: ' + p.network));
1819	}
1820
1821	// PATH
1822	// As long as the path isn't flagged as unavaiable, e.g. path == false
1823
1824	if (!(!(p.method in o) || !(p.path in o[p.method]) || o[p.method][p.path] !== false)) {
1825		return promise.reject(error('invalid_path', 'The provided path is not available on the selected network'));
1826	}
1827
1828	// PROXY
1829	// OAuth1 calls always need a proxy
1830
1831	if (!p.oauth_proxy) {
1832		p.oauth_proxy = _this.settings.oauth_proxy;
1833	}
1834
1835	if (!('proxy' in p)) {
1836		p.proxy = p.oauth_proxy && o.oauth && parseInt(o.oauth.version, 10) === 1;
1837	}
1838
1839	// TIMEOUT
1840	// Adopt timeout from global settings by default
1841
1842	if (!('timeout' in p)) {
1843		p.timeout = _this.settings.timeout;
1844	}
1845
1846	// Format response
1847	// Whether to run the raw response through post processing.
1848	if (!('formatResponse' in p)) {
1849		p.formatResponse = true;
1850	}
1851
1852	// Get the current session
1853	// Append the access_token to the query
1854	p.authResponse = _this.getAuthResponse(p.network);
1855	if (p.authResponse && p.authResponse.access_token) {
1856		p.query.access_token = p.authResponse.access_token;
1857	}
1858
1859	var url = p.path;
1860	var m;
1861
1862	// Store the query as options
1863	// This is used to populate the request object before the data is augmented by the prewrap handlers.
1864	p.options = utils.clone(p.query);
1865
1866	// Clone the data object
1867	// Prevent this script overwriting the data of the incoming object.
1868	// Ensure that everytime we run an iteration the callbacks haven't removed some data
1869	p.data = utils.clone(data);
1870
1871	// URL Mapping
1872	// Is there a map for the given URL?
1873	var actions = o[{'delete': 'del'}[p.method] || p.method] || {};
1874
1875	// Extrapolate the QueryString
1876	// Provide a clean path
1877	// Move the querystring into the data
1878	if (p.method === 'get') {
1879
1880		var query = url.split(/[\?#]/)[1];
1881		if (query) {
1882			utils.extend(p.query, utils.param(query));
1883
1884			// Remove the query part from the URL
1885			url = url.replace(/\?.*?(#|$)/, '$1');
1886		}
1887	}
1888
1889	// Is the hash fragment defined
1890	if ((m = url.match(/#(.+)/, ''))) {
1891		url = url.split('#')[0];
1892		p.path = m[1];
1893	}
1894	else if (url in actions) {
1895		p.path = url;
1896		url = actions[url];
1897	}
1898	else if ('default' in actions) {
1899		url = actions['default'];
1900	}
1901
1902	// Redirect Handler
1903	// This defines for the Form+Iframe+Hash hack where to return the results too.
1904	p.redirect_uri = _this.settings.redirect_uri;
1905
1906	// Define FormatHandler
1907	// The request can be procesed in a multitude of ways
1908	// Here's the options - depending on the browser and endpoint
1909	p.xhr = o.xhr;
1910	p.jsonp = o.jsonp;
1911	p.form = o.form;
1912
1913	// Make request
1914	if (typeof (url) === 'function') {
1915		// Does self have its own callback?
1916		url(p, getPath);
1917	}
1918	else {
1919		// Else the URL is a string
1920		getPath(url);
1921	}
1922
1923	return promise.proxy;
1924
1925	// If url needs a base
1926	// Wrap everything in
1927	function getPath(url) {
1928
1929		// Format the string if it needs it
1930		url = url.replace(/\@\{([a-z\_\-]+)(\|.*?)?\}/gi, function(m, key, defaults) {
1931			var val = defaults ? defaults.replace(/^\|/, '') : '';
1932			if (key in p.query) {
1933				val = p.query[key];
1934				delete p.query[key];
1935			}
1936			else if (p.data && key in p.data) {
1937				val = p.data[key];
1938				delete p.data[key];
1939			}
1940			else if (!defaults) {
1941				promise.reject(error('missing_attribute', 'The attribute ' + key + ' is missing from the request'));
1942			}
1943
1944			return val;
1945		});
1946
1947		// Add base
1948		if (!url.match(/^https?:\/\//)) {
1949			url = o.base + url;
1950		}
1951
1952		// Define the request URL
1953		p.url = url;
1954
1955		// Make the HTTP request with the curated request object
1956		// CALLBACK HANDLER
1957		// @ response object
1958		// @ statusCode integer if available
1959		utils.request(p, function(r, headers) {
1960
1961			// Is this a raw response?
1962			if (!p.formatResponse) {
1963				// Bad request? error statusCode or otherwise contains an error response vis JSONP?
1964				if (typeof headers === 'object' ? (headers.statusCode >= 400) : (typeof r === 'object' && 'error' in r)) {
1965					promise.reject(r);
1966				}
1967				else {
1968					promise.fulfill(r);
1969				}
1970
1971				return;
1972			}
1973
1974			// Should this be an object
1975			if (r === true) {
1976				r = {success:true};
1977			}
1978			else if (!r) {
1979				r = {};
1980			}
1981
1982			// The delete callback needs a better response
1983			if (p.method === 'delete') {
1984				r = (!r || utils.isEmpty(r)) ? {success:true} : r;
1985			}
1986
1987			// FORMAT RESPONSE?
1988			// Does self request have a corresponding formatter
1989			if (o.wrap && ((p.path in o.wrap) || ('default' in o.wrap))) {
1990				var wrap = (p.path in o.wrap ? p.path : 'default');
1991				var time = (new Date()).getTime();
1992
1993				// FORMAT RESPONSE
1994				var b = o.wrap[wrap](r, headers, p);
1995
1996				// Has the response been utterly overwritten?
1997				// Typically self augments the existing object.. but for those rare occassions
1998				if (b) {
1999					r = b;
2000				}
2001			}
2002
2003			// Is there a next_page defined in the response?
2004			if (r && 'paging' in r && r.paging.next) {
2005
2006				// Add the relative path if it is missing from the paging/next path
2007				if (r.paging.next[0] === '?') {
2008					r.paging.next = p.path + r.paging.next;
2009				}
2010
2011				// The relative path has been defined, lets markup the handler in the HashFragment
2012				else {
2013					r.paging.next += '#' + p.path;
2014				}
2015			}
2016
2017			// Dispatch to listeners
2018			// Emit events which pertain to the formatted response
2019			if (!r || 'error' in r) {
2020				promise.reject(r);
2021			}
2022			else {
2023				promise.fulfill(r);
2024			}
2025		});
2026	}
2027};
2028
2029// API utilities
2030hello.utils.extend(hello.utils, {
2031
2032	// Make an HTTP request
2033	request: function(p, callback) {
2034
2035		var _this = this;
2036		var error = _this.error;
2037
2038		// This has to go through a POST request
2039		if (!_this.isEmpty(p.data) && !('FileList' in window) && _this.hasBinary(p.data)) {
2040
2041			// Disable XHR and JSONP
2042			p.xhr = false;
2043			p.jsonp = false;
2044		}
2045
2046		// Check if the browser and service support CORS
2047		var cors = this.request_cors(function() {
2048			// If it does then run this...
2049			return ((p.xhr === undefined) || (p.xhr && (typeof (p.xhr) !== 'function' || p.xhr(p, p.query))));
2050		});
2051
2052		if (cors) {
2053
2054			formatUrl(p, function(url) {
2055
2056				var x = _this.xhr(p.method, url, p.headers, p.data, callback);
2057				x.onprogress = p.onprogress || null;
2058
2059				// Windows Phone does not support xhr.upload, see #74
2060				// Feature detect
2061				if (x.upload && p.onuploadprogress) {
2062					x.upload.onprogress = p.onuploadprogress;
2063				}
2064
2065			});
2066
2067			return;
2068		}
2069
2070		// Clone the query object
2071		// Each request modifies the query object and needs to be tared after each one.
2072		var _query = p.query;
2073
2074		p.query = _this.clone(p.query);
2075
2076		// Assign a new callbackID
2077		p.callbackID = _this.globalEvent();
2078
2079		// JSONP
2080		if (p.jsonp !== false) {
2081
2082			// Clone the query object
2083			p.query.callback = p.callbackID;
2084
2085			// If the JSONP is a function then run it
2086			if (typeof (p.jsonp) === 'function') {
2087				p.jsonp(p, p.query);
2088			}
2089
2090			// Lets use JSONP if the method is 'get'
2091			if (p.method === 'get') {
2092
2093				formatUrl(p, function(url) {
2094					_this.jsonp(url, callback, p.callbackID, p.timeout);
2095				});
2096
2097				return;
2098			}
2099			else {
2100				// It's not compatible reset query
2101				p.query = _query;
2102			}
2103
2104		}
2105
2106		// Otherwise we're on to the old school, iframe hacks and JSONP
2107		if (p.form !== false) {
2108
2109			// Add some additional query parameters to the URL
2110			// We're pretty stuffed if the endpoint doesn't like these
2111			p.query.redirect_uri = p.redirect_uri;
2112			p.query.state = JSON.stringify({callback:p.callbackID});
2113
2114			var opts;
2115
2116			if (typeof (p.form) === 'function') {
2117
2118				// Format the request
2119				opts = p.form(p, p.query);
2120			}
2121
2122			if (p.method === 'post' && opts !== false) {
2123
2124				formatUrl(p, function(url) {
2125					_this.post(url, p.data, opts, callback, p.callbackID, p.timeout);
2126				});
2127
2128				return;
2129			}
2130		}
2131
2132		// None of the methods were successful throw an error
2133		callback(error('invalid_request', 'There was no mechanism for handling this request'));
2134
2135		return;
2136
2137		// Format URL
2138		// Constructs the request URL, optionally wraps the URL through a call to a proxy server
2139		// Returns the formatted URL
2140		function formatUrl(p, callback) {
2141
2142			// Are we signing the request?
2143			var sign;
2144
2145			// OAuth1
2146			// Remove the token from the query before signing
2147			if (p.authResponse && p.authResponse.oauth && parseInt(p.authResponse.oauth.version, 10) === 1) {
2148
2149				// OAUTH SIGNING PROXY
2150				sign = p.query.access_token;
2151
2152				// Remove the access_token
2153				delete p.query.access_token;
2154
2155				// Enfore use of Proxy
2156				p.proxy = true;
2157			}
2158
2159			// POST body to querystring
2160			if (p.data && (p.method === 'get' || p.method === 'delete')) {
2161				// Attach the p.data to the querystring.
2162				_this.extend(p.query, p.data);
2163				p.data = null;
2164			}
2165
2166			// Construct the path
2167			var path = _this.qs(p.url, p.query);
2168
2169			// Proxy the request through a server
2170			// Used for signing OAuth1
2171			// And circumventing services without Access-Control Headers
2172			if (p.proxy) {
2173				// Use the proxy as a path
2174				path = _this.qs(p.oauth_proxy, {
2175					path: path,
2176					access_token: sign || '',
2177
2178					// This will prompt the request to be signed as though it is OAuth1
2179					then: p.proxy_response_type || (p.method.toLowerCase() === 'get' ? 'redirect' : 'proxy'),
2180					method: p.method.toLowerCase(),
2181					suppress_response_codes:
vendor: 12,259 bytes, lines 2181-2662
2181 true
2182				});
2183			}
2184
2185			callback(path);
2186		}
2187	},
2188
2189	// Test whether the browser supports the CORS response
2190	request_cors: function(callback) {
2191		return 'withCredentials' in new XMLHttpRequest() && callback();
2192	},
2193
2194	// Return the type of DOM object
2195	domInstance: function(type, data) {
2196		var test = 'HTML' + (type || '').replace(
2197			/^[a-z]/,
2198			function(m) {
2199				return m.toUpperCase();
2200			}
2201
2202		) + 'Element';
2203
2204		if (!data) {
2205			return false;
2206		}
2207
2208		if (window[test]) {
2209			return data instanceof window[test];
2210		}
2211		else if (window.Element) {
2212			return data instanceof window.Element && (!type || (data.tagName && data.tagName.toLowerCase() === type));
2213		}
2214		else {
2215			return (!(data instanceof Object || data instanceof Array || data instanceof String || data instanceof Number) && data.tagName && data.tagName.toLowerCase() === type);
2216		}
2217	},
2218
2219	// Create a clone of an object
2220	clone: function(obj) {
2221		// Does not clone DOM elements, nor Binary data, e.g. Blobs, Filelists
2222		if (obj === null || typeof (obj) !== 'object' || obj instanceof Date || 'nodeName' in obj || this.isBinary(obj) || (typeof FormData === 'function' && obj instanceof FormData)) {
2223			return obj;
2224		}
2225
2226		if (Array.isArray(obj)) {
2227			// Clone each item in the array
2228			return obj.map(this.clone.bind(this));
2229		}
2230
2231		// But does clone everything else.
2232		var clone = {};
2233		for (var x in obj) {
2234			clone[x] = this.clone(obj[x]);
2235		}
2236
2237		return clone;
2238	},
2239
2240	// XHR: uses CORS to make requests
2241	xhr: function(method, url, headers, data, callback) {
2242
2243		var r = new XMLHttpRequest();
2244		var error = this.error;
2245
2246		// Binary?
2247		var binary = false;
2248		if (method === 'blob') {
2249			binary = method;
2250			method = 'GET';
2251		}
2252
2253		method = method.toUpperCase();
2254
2255		// Xhr.responseType 'json' is not supported in any of the vendors yet.
2256		r.onload = function(e) {
2257			var json = r.response;
2258			try {
2259				json = JSON.parse(r.responseText);
2260			}
2261			catch (_e) {
2262				if (r.status === 401) {
2263					json = error('access_denied', r.statusText);
2264				}
2265			}
2266
2267			var headers = headersToJSON(r.getAllResponseHeaders());
2268			headers.statusCode = r.status;
2269
2270			callback(json || (method === 'GET' ? error('empty_response', 'Could not get resource') : {}), headers);
2271		};
2272
2273		r.onerror = function(e) {
2274			var json = r.responseText;
2275			try {
2276				json = JSON.parse(r.responseText);
2277			}
2278			catch (_e) {}
2279
2280			callback(json || error('access_denied', 'Could not get resource'));
2281		};
2282
2283		var x;
2284
2285		// Should we add the query to the URL?
2286		if (method === 'GET' || method === 'DELETE') {
2287			data = null;
2288		}
2289		else if (data && typeof (data) !== 'string' && !(data instanceof FormData) && !(data instanceof File) && !(data instanceof Blob)) {
2290			// Loop through and add formData
2291			var f = new FormData();
2292			for (x in data) if (data.hasOwnProperty(x)) {
2293				if (data[x] instanceof HTMLInputElement) {
2294					if ('files' in data[x] && data[x].files.length > 0) {
2295						f.append(x, data[x].files[0]);
2296					}
2297				}
2298				else if (data[x] instanceof Blob) {
2299					f.append(x, data[x], data.name);
2300				}
2301				else {
2302					f.append(x, data[x]);
2303				}
2304			}
2305
2306			data = f;
2307		}
2308
2309		// Open the path, async
2310		r.open(method, url, true);
2311
2312		if (binary) {
2313			if ('responseType' in r) {
2314				r.responseType = binary;
2315			}
2316			else {
2317				r.overrideMimeType('text/plain; charset=x-user-defined');
2318			}
2319		}
2320
2321		// Set any bespoke headers
2322		if (headers) {
2323			for (x in headers) {
2324				r.setRequestHeader(x, headers[x]);
2325			}
2326		}
2327
2328		r.send(data);
2329
2330		return r;
2331
2332		// Headers are returned as a string
2333		function headersToJSON(s) {
2334			var r = {};
2335			var reg = /([a-z\-]+):\s?(.*);?/gi;
2336			var m;
2337			while ((m = reg.exec(s))) {
2338				r[m[1]] = m[2];
2339			}
2340
2341			return r;
2342		}
2343	},
2344
2345	// JSONP
2346	// Injects a script tag into the DOM to be executed and appends a callback function to the window object
2347	// @param string/function pathFunc either a string of the URL or a callback function pathFunc(querystringhash, continueFunc);
2348	// @param function callback a function to call on completion;
2349	jsonp: function(url, callback, callbackID, timeout) {
2350
2351		var _this = this;
2352		var error = _this.error;
2353
2354		// Change the name of the callback
2355		var bool = 0;
2356		var head = document.getElementsByTagName('head')[0];
2357		var operaFix;
2358		var result = error('server_error', 'server_error');
2359		var cb = function() {
2360			if (!(bool++)) {
2361				window.setTimeout(function() {
2362					callback(result);
2363					head.removeChild(script);
2364				}, 0);
2365			}
2366
2367		};
2368
2369		// Add callback to the window object
2370		callbackID = _this.globalEvent(function(json) {
2371			result = json;
2372			return true;
2373
2374			// Mark callback as done
2375		}, callbackID);
2376
2377		// The URL is a function for some cases and as such
2378		// Determine its value with a callback containing the new parameters of this function.
2379		url = url.replace(new RegExp('=\\?(&|$)'), '=' + callbackID + '$1');
2380
2381		// Build script tag
2382		var script = _this.append('script', {
2383			id: callbackID,
2384			name: callbackID,
2385			src: url,
2386			async: true,
2387			onload: cb,
2388			onerror: cb,
2389			onreadystatechange: function() {
2390				if (/loaded|complete/i.test(this.readyState)) {
2391					cb();
2392				}
2393			}
2394		});
2395
2396		// Opera fix error
2397		// Problem: If an error occurs with script loading Opera fails to trigger the script.onerror handler we specified
2398		//
2399		// Fix:
2400		// By setting the request to synchronous we can trigger the error handler when all else fails.
2401		// This action will be ignored if we've already called the callback handler "cb" with a successful onload event
2402		if (window.navigator.userAgent.toLowerCase().indexOf('opera') > -1) {
2403			operaFix = _this.append('script', {
2404				text: 'document.getElementById(\'' + callbackID + '\').onerror();'
2405			});
2406			script.async = false;
2407		}
2408
2409		// Add timeout
2410		if (timeout) {
2411			window.setTimeout(function() {
2412				result = error('timeout', 'timeout');
2413				cb();
2414			}, timeout);
2415		}
2416
2417		// TODO: add fix for IE,
2418		// 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.
2419		// Inject script tag into the head element
2420		head.appendChild(script);
2421
2422		// Append Opera Fix to run after our script
2423		if (operaFix) {
2424			head.appendChild(operaFix);
2425		}
2426	},
2427
2428	// Post
2429	// Send information to a remote location using the post mechanism
2430	// @param string uri path
2431	// @param object data, key value data to send
2432	// @param function callback, function to execute in response
2433	post: function(url, data, options, callback, callbackID, timeout) {
2434
2435		var _this = this;
2436		var error = _this.error;
2437		var doc = document;
2438
2439		// This hack needs a form
2440		var form = null;
2441		var reenableAfterSubmit = [];
2442		var newform;
2443		var i = 0;
2444		var x = null;
2445		var bool = 0;
2446		var cb = function(r) {
2447			if (!(bool++)) {
2448				callback(r);
2449			}
2450		};
2451
2452		// What is the name of the callback to contain
2453		// We'll also use this to name the iframe
2454		_this.globalEvent(cb, callbackID);
2455
2456		// Build the iframe window
2457		var win;
2458		try {
2459			// IE7 hack, only lets us define the name here, not later.
2460			win = doc.createElement('<iframe name="' + callbackID + '">');
2461		}
2462		catch (e) {
2463			win = doc.createElement('iframe');
2464		}
2465
2466		win.name = callbackID;
2467		win.id = callbackID;
2468		win.style.display = 'none';
2469
2470		// Override callback mechanism. Triggger a response onload/onerror
2471		if (options && options.callbackonload) {
2472			// Onload is being fired twice
2473			win.onload = function() {
2474				cb({
2475					response: 'posted',
2476					message: 'Content was posted'
2477				});
2478			};
2479		}
2480
2481		if (timeout) {
2482			setTimeout(function() {
2483				cb(error('timeout', 'The post operation timed out'));
2484			}, timeout);
2485		}
2486
2487		doc.body.appendChild(win);
2488
2489		// If we are just posting a single item
2490		if (_this.domInstance('form', data)) {
2491			// Get the parent form
2492			form = data.form;
2493
2494			// Loop through and disable all of its siblings
2495			for (i = 0; i < form.elements.length; i++) {
2496				if (form.elements[i] !== data) {
2497					form.elements[i].setAttribute('disabled', true);
2498				}
2499			}
2500
2501			// Move the focus to the form
2502			data = form;
2503		}
2504
2505		// Posting a form
2506		if (_this.domInstance('form', data)) {
2507			// This is a form element
2508			form = data;
2509
2510			// Does this form need to be a multipart form?
2511			for (i = 0; i < form.elements.length; i++) {
2512				if (!form.elements[i].disabled && form.elements[i].type === 'file') {
2513					form.encoding = form.enctype = 'multipart/form-data';
2514					form.elements[i].setAttribute('name', 'file');
2515				}
2516			}
2517		}
2518		else {
2519			// Its not a form element,
2520			// Therefore it must be a JSON object of Key=>Value or Key=>Element
2521			// If anyone of those values are a input type=file we shall shall insert its siblings into the form for which it belongs.
2522			for (x in data) if (data.hasOwnProperty(x)) {
2523				// Is this an input Element?
2524				if (_this.domInstance('input', data[x]) && data[x].type === 'file') {
2525					form = data[x].form;
2526					form.encoding = form.enctype = 'multipart/form-data';
2527				}
2528			}
2529
2530			// Do If there is no defined form element, lets create one.
2531			if (!form) {
2532				// Build form
2533				form = doc.createElement('form');
2534				doc.body.appendChild(form);
2535				newform = form;
2536			}
2537
2538			var input;
2539
2540			// Add elements to the form if they dont exist
2541			for (x in data) if (data.hasOwnProperty(x)) {
2542
2543				// Is this an element?
2544				var el = (_this.domInstance('input', data[x]) || _this.domInstance('textArea', data[x]) || _this.domInstance('select', data[x]));
2545
2546				// Is this not an input element, or one that exists outside the form.
2547				if (!el || data[x].form !== form) {
2548
2549					// Does an element have the same name?
2550					var inputs = form.elements[x];
2551					if (input) {
2552						// Remove it.
2553						if (!(inputs instanceof NodeList)) {
2554							inputs = [inputs];
2555						}
2556
2557						for (i = 0; i < inputs.length; i++) {
2558							inputs[i].parentNode.removeChild(inputs[i]);
2559						}
2560
2561					}
2562
2563					// Create an input element
2564					input = doc.createElement('input');
2565					input.setAttribute('type', 'hidden');
2566					input.setAttribute('name', x);
2567
2568					// Does it have a value attribute?
2569					if (el) {
2570						input.value = data[x].value;
2571					}
2572					else if (_this.domInstance(null, data[x])) {
2573						input.value = data[x].innerHTML || data[x].innerText;
2574					}
2575					else {
2576						input.value = data[x];
2577					}
2578
2579					form.appendChild(input);
2580				}
2581
2582				// It is an element, which exists within the form, but the name is wrong
2583				else if (el && data[x].name !== x) {
2584					data[x].setAttribute('name', x);
2585					data[x].name = x;
2586				}
2587			}
2588
2589			// Disable elements from within the form if they weren't specified
2590			for (i = 0; i < form.elements.length; i++) {
2591
2592				input = form.elements[i];
2593
2594				// Does the same name and value exist in the parent
2595				if (!(input.name in data) && input.getAttribute('disabled') !== true) {
2596					// Disable
2597					input.setAttribute('disabled', true);
2598
2599					// Add re-enable to callback
2600					reenableAfterSubmit.push(input);
2601				}
2602			}
2603		}
2604
2605		// Set the target of the form
2606		form.setAttribute('method', 'POST');
2607		form.setAttribute('target', callbackID);
2608		form.target = callbackID;
2609
2610		// Update the form URL
2611		form.setAttribute('action', url);
2612
2613		// Submit the form
2614		// Some reason this needs to be offset from the current window execution
2615		setTimeout(function() {
2616			form.submit();
2617
2618			setTimeout(function() {
2619				try {
2620					// Remove the iframe from the page.
2621					//win.parentNode.removeChild(win);
2622					// Remove the form
2623					if (newform) {
2624						newform.parentNode.removeChild(newform);
2625					}
2626				}
2627				catch (e) {
2628					try {
2629						console.error('HelloJS: could not remove iframe');
2630					}
2631					catch (ee) {}
2632				}
2633
2634				// Reenable the disabled form
2635				for (var i = 0; i < reenableAfterSubmit.length; i++) {
2636					if (reenableAfterSubmit[i]) {
2637						reenableAfterSubmit[i].setAttribute('disabled', false);
2638						reenableAfterSubmit[i].disabled = false;
2639					}
2640				}
2641			}, 0);
2642		}, 100);
2643	},
2644
2645	// Some of the providers require that only multipart is used with non-binary forms.
2646	// This function checks whether the form contains binary data
2647	hasBinary: function(data) {
2648		for (var x in data) if (data.hasOwnProperty(x)) {
2649			if (this.isBinary(data[x])) {
2650				return true;
2651			}
2652		}
2653
2654		return false;
2655	},
2656
2657	// Determines if a variable Either Is or like a FormInput has the value of a Blob
2658
2659	isBinary: function(data) {
2660
2661		return data instanceof Object && (
2662		
vendor: 36,029 bytes, lines 2662-4199
2662(this.domInstance('input', data) && data.type === 'file') ||
2663		('FileList' in window && data instanceof window.FileList) ||
2664		('File' in window && data instanceof window.File) ||
2665		('Blob' in window && data instanceof window.Blob));
2666
2667	},
2668
2669	// Convert Data-URI to Blob string
2670	toBlob: function(dataURI) {
2671		var reg = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i;
2672		var m = dataURI.match(reg);
2673		if (!m) {
2674			return dataURI;
2675		}
2676
2677		var binary = atob(dataURI.replace(reg, ''));
2678		var array = [];
2679		for (var i = 0; i < binary.length; i++) {
2680			array.push(binary.charCodeAt(i));
2681		}
2682
2683		return new Blob([new Uint8Array(array)], {type: m[1]});
2684	}
2685
2686});
2687
2688// EXTRA: Convert FormElement to JSON for POSTing
2689// Wrappers to add additional functionality to existing functions
2690(function(hello) {
2691
2692	// Copy original function
2693	var api = hello.api;
2694	var utils = hello.utils;
2695
2696	utils.extend(utils, {
2697
2698		// DataToJSON
2699		// This takes a FormElement|NodeList|InputElement|MixedObjects and convers the data object to JSON.
2700		dataToJSON: function(p) {
2701
2702			var _this = this;
2703			var w = window;
2704			var data = p.data;
2705
2706			// Is data a form object
2707			if (_this.domInstance('form', data)) {
2708				data = _this.nodeListToJSON(data.elements);
2709			}
2710			else if ('NodeList' in w && data instanceof NodeList) {
2711				data = _this.nodeListToJSON(data);
2712			}
2713			else if (_this.domInstance('input', data)) {
2714				data = _this.nodeListToJSON([data]);
2715			}
2716
2717			// Is data a blob, File, FileList?
2718			if (('File' in w && data instanceof w.File) ||
2719				('Blob' in w && data instanceof w.Blob) ||
2720				('FileList' in w && data instanceof w.FileList)) {
2721				data = {file: data};
2722			}
2723
2724			// Loop through data if it's not form data it must now be a JSON object
2725			if (!('FormData' in w && data instanceof w.FormData)) {
2726
2727				for (var x in data) if (data.hasOwnProperty(x)) {
2728
2729					if ('FileList' in w && data[x] instanceof w.FileList) {
2730						if (data[x].length === 1) {
2731							data[x] = data[x][0];
2732						}
2733					}
2734					else if (_this.domInstance('input', data[x]) && data[x].type === 'file') {
2735						continue;
2736					}
2737					else if (_this.domInstance('input', data[x]) ||
2738						_this.domInstance('select', data[x]) ||
2739						_this.domInstance('textArea', data[x])) {
2740						data[x] = data[x].value;
2741					}
2742					else if (_this.domInstance(null, data[x])) {
2743						data[x] = data[x].innerHTML || data[x].innerText;
2744					}
2745				}
2746			}
2747
2748			p.data = data;
2749			return data;
2750		},
2751
2752		// NodeListToJSON
2753		// Given a list of elements extrapolate their values and return as a json object
2754		nodeListToJSON: function(nodelist) {
2755
2756			var json = {};
2757
2758			// Create a data string
2759			for (var i = 0; i < nodelist.length; i++) {
2760
2761				var input = nodelist[i];
2762
2763				// If the name of the input is empty or diabled, dont add it.
2764				if (input.disabled || !input.name) {
2765					continue;
2766				}
2767
2768				// Is this a file, does the browser not support 'files' and 'FormData'?
2769				if (input.type === 'file') {
2770					json[input.name] = input;
2771				}
2772				else {
2773					json[input.name] = input.value || input.innerHTML;
2774				}
2775			}
2776
2777			return json;
2778		}
2779	});
2780
2781	// Replace it
2782	hello.api = function() {
2783
2784		// Get arguments
2785		var p = utils.args({path: 's!', method: 's', data:'o', timeout: 'i', callback: 'f'}, arguments);
2786
2787		// Change for into a data object
2788		if (p.data) {
2789			utils.dataToJSON(p);
2790		}
2791
2792		return api.call(this, p);
2793	};
2794
2795})(hello);
2796
2797/////////////////////////////////////
2798//
2799// Save any access token that is in the current page URL
2800// Handle any response solicited through iframe hash tag following an API request
2801//
2802/////////////////////////////////////
2803
2804hello.utils.responseHandler(window, window.opener || window.parent);
2805
2806// Script to support ChromeApps
2807// This overides the hello.utils.popup method to support chrome.identity.launchWebAuthFlow
2808// See https://developer.chrome.com/apps/app_identity#non
2809
2810// Is this a chrome app?
2811
2812if (typeof chrome === 'object' && typeof chrome.identity === 'object' && chrome.identity.launchWebAuthFlow) {
2813
2814	(function() {
2815
2816		// Swap the popup method
2817		hello.utils.popup = function(url) {
2818
2819			return _open(url, true);
2820
2821		};
2822
2823		// Swap the hidden iframe method
2824		hello.utils.iframe = function(url) {
2825
2826			_open(url, false);
2827
2828		};
2829
2830		// Swap the request_cors method
2831		hello.utils.request_cors = function(callback) {
2832
2833			callback();
2834
2835			// Always run as CORS
2836
2837			return true;
2838		};
2839
2840		// Swap the storage method
2841		var _cache = {};
2842		chrome.storage.local.get('hello', function(r) {
2843			// Update the cache
2844			_cache = r.hello || {};
2845		});
2846
2847		hello.utils.store = function(name, value) {
2848
2849			// Get all
2850			if (arguments.length === 0) {
2851				return _cache;
2852			}
2853
2854			// Get
2855			if (arguments.length === 1) {
2856				return _cache[name] || null;
2857			}
2858
2859			// Set
2860			if (value) {
2861				_cache[name] = value;
2862				chrome.storage.local.set({hello: _cache});
2863				return value;
2864			}
2865
2866			// Delete
2867			if (value === null) {
2868				delete _cache[name];
2869				chrome.storage.local.set({hello: _cache});
2870				return null;
2871			}
2872		};
2873
2874		// Open function
2875		function _open(url, interactive) {
2876
2877			// Launch
2878			var ref = {
2879				closed: false
2880			};
2881
2882			// Launch the webAuthFlow
2883			chrome.identity.launchWebAuthFlow({
2884				url: url,
2885				interactive: interactive
2886			}, function(responseUrl) {
2887
2888				// Did the user cancel this prematurely
2889				if (responseUrl === undefined) {
2890					ref.closed = true;
2891					return;
2892				}
2893
2894				// Split appart the URL
2895				var a = hello.utils.url(responseUrl);
2896
2897				// The location can be augmented in to a location object like so...
2898				// We dont have window operations on the popup so lets create some
2899				var _popup = {
2900					location: {
2901
2902						// Change the location of the popup
2903						assign: function(url) {
2904
2905							// If there is a secondary reassign
2906							// In the case of OAuth1
2907							// Trigger this in non-interactive mode.
2908							_open(url, false);
2909						},
2910
2911						search: a.search,
2912						hash: a.hash,
2913						href: a.href
2914					},
2915					close: function() {}
2916				};
2917
2918				// Then this URL contains information which HelloJS must process
2919				// URL string
2920				// Window - any action such as window relocation goes here
2921				// Opener - the parent window which opened this, aka this script
2922
2923				hello.utils.responseHandler(_popup, window);
2924			});
2925
2926			// Return the reference
2927			return ref;
2928		}
2929
2930	})();
2931}
2932
2933// Phonegap override for hello.phonegap.js
2934(function() {
2935
2936	// Is this a phonegap implementation?
2937	if (!(/^file:\/{3}[^\/]/.test(window.location.href) && window.cordova)) {
2938		// Cordova is not included.
2939		return;
2940	}
2941
2942	// Augment the hidden iframe method
2943	hello.utils.iframe = function(url, redirectUri) {
2944		hello.utils.popup(url, redirectUri, {hidden: 'yes'});
2945	};
2946
2947	// Augment the popup
2948	var utilPopup = hello.utils.popup;
2949
2950	// Replace popup
2951	hello.utils.popup = function(url, redirectUri, options) {
2952
2953		// Run the standard
2954		var popup = utilPopup.call(this, url, redirectUri, options);
2955
2956		// Create a function for reopening the popup, and assigning events to the new popup object
2957		// PhoneGap support
2958		// Add an event listener to listen to the change in the popup windows URL
2959		// This must appear before popup.focus();
2960		try {
2961			if (popup && popup.addEventListener) {
2962
2963				// Get the origin of the redirect URI
2964
2965				var a = hello.utils.url(redirectUri);
2966				var redirectUriOrigin = a.origin || (a.protocol + '//' + a.hostname);
2967
2968				// Listen to changes in the InAppBrowser window
2969
2970				popup.addEventListener('loadstart', function(e) {
2971
2972					var url = e.url;
2973
2974					// Is this the path, as given by the redirectUri?
2975					// Check the new URL agains the redirectUriOrigin.
2976					// According to #63 a user could click 'cancel' in some dialog boxes ....
2977					// The popup redirects to another page with the same origin, yet we still wish it to close.
2978
2979					if (url.indexOf(redirectUriOrigin) !== 0) {
2980						return;
2981					}
2982
2983					// Split appart the URL
2984					var a = hello.utils.url(url);
2985
2986					// We dont have window operations on the popup so lets create some
2987					// The location can be augmented in to a location object like so...
2988
2989					var _popup = {
2990						location: {
2991							// Change the location of the popup
2992							assign: function(location) {
2993
2994								// Unfourtunatly an app is may not change the location of a InAppBrowser window.
2995								// So to shim this, just open a new one.
2996								popup.executeScript({code: 'window.location.href = "' + location + ';"'});
2997							},
2998
2999							search: a.search,
3000							hash: a.hash,
3001							href: a.href
3002						},
3003						close: function() {
3004							if (popup.close) {
3005								popup.close();
3006								try {
3007									popup.closed = true;
3008								}
3009								catch (_e) {}
3010							}
3011						}
3012					};
3013
3014					// Then this URL contains information which HelloJS must process
3015					// URL string
3016					// Window - any action such as window relocation goes here
3017					// Opener - the parent window which opened this, aka this script
3018
3019					hello.utils.responseHandler(_popup, window);
3020
3021				});
3022			}
3023		}
3024		catch (e) {}
3025
3026		return popup;
3027	};
3028
3029})();
3030
3031(function(hello) {
3032
3033	// OAuth1
3034	var OAuth1Settings = {
3035		version: '1.0',
3036		auth: 'https://www.dropbox.com/1/oauth/authorize',
3037		request: 'https://api.dropbox.com/1/oauth/request_token',
3038		token: 'https://api.dropbox.com/1/oauth/access_token'
3039	};
3040
3041	// OAuth2 Settings
3042	var OAuth2Settings = {
3043		version: 2,
3044		auth: 'https://www.dropbox.com/1/oauth2/authorize',
3045		grant: 'https://api.dropbox.com/1/oauth2/token'
3046	};
3047
3048	// Initiate the Dropbox module
3049	hello.init({
3050
3051		dropbox: {
3052
3053			name: 'Dropbox',
3054
3055			oauth: OAuth2Settings,
3056
3057			login: function(p) {
3058				// OAuth2 non-standard adjustments
3059				p.qs.scope = '';
3060
3061				// Should this be run as OAuth1?
3062				// If the redirect_uri is is HTTP (non-secure) then its required to revert to the OAuth1 endpoints
3063				var redirect = decodeURIComponent(p.qs.redirect_uri);
3064				if (redirect.indexOf('http:') === 0 && redirect.indexOf('http://localhost/') !== 0) {
3065
3066					// Override the dropbox OAuth settings.
3067					hello.services.dropbox.oauth = OAuth1Settings;
3068				}
3069				else {
3070					// Override the dropbox OAuth settings.
3071					hello.services.dropbox.oauth = OAuth2Settings;
3072				}
3073
3074				// The dropbox login window is a different size
3075				p.options.popup.width = 1000;
3076				p.options.popup.height = 1000;
3077			},
3078
3079			/*
3080				Dropbox does not allow insecure HTTP URI's in the redirect_uri field
3081				...otherwise I'd love to use OAuth2
3082
3083				Follow request https://forums.dropbox.com/topic.php?id=106505
3084
3085				p.qs.response_type = 'code';
3086				oauth: {
3087					version: 2,
3088					auth: 'https://www.dropbox.com/1/oauth2/authorize',
3089					grant: 'https://api.dropbox.com/1/oauth2/token'
3090				}
3091			*/
3092
3093			// API Base URL
3094			base: 'https://api.dropbox.com/1/',
3095
3096			// Bespoke setting: this is states whether to use the custom environment of Dropbox or to use their own environment
3097			// Because it's notoriously difficult for Dropbox too provide access from other webservices, this defaults to Sandbox
3098			root: 'sandbox',
3099
3100			// Map GET requests
3101			get: {
3102				me: 'account/info',
3103
3104				// Https://www.dropbox.com/developers/core/docs#metadata
3105				'me/files': req('metadata/auto/@{parent|}'),
3106				'me/folder': req('metadata/auto/@{id}'),
3107				'me/folders': req('metadata/auto/'),
3108
3109				'default': function(p, callback) {
3110					if (p.path.match('https://api-content.dropbox.com/1/files/')) {
3111						// This is a file, return binary data
3112						p.method = 'blob';
3113					}
3114
3115					callback(p.path);
3116				}
3117			},
3118
3119			post: {
3120				'me/files': function(p, callback) {
3121
3122					var path = p.data.parent;
3123					var fileName = p.data.name;
3124
3125					p.data = {
3126						file: p.data.file
3127					};
3128
3129					// Does this have a data-uri to upload as a file?
3130					if (typeof (p.data.file) === 'string') {
3131						p.data.file = hello.utils.toBlob(p.data.file);
3132					}
3133
3134					callback('https://api-content.dropbox.com/1/files_put/auto/' + path + '/' + fileName);
3135				},
3136
3137				'me/folders': function(p, callback) {
3138
3139					var name = p.data.name;
3140					p.data = {};
3141
3142					callback('fileops/create_folder?root=@{root|sandbox}&' + hello.utils.param({
3143						path: name
3144					}));
3145				}
3146			},
3147
3148			// Map DELETE requests
3149			del: {
3150				'me/files': 'fileops/delete?root=@{root|sandbox}&path=@{id}',
3151				'me/folder': 'fileops/delete?root=@{root|sandbox}&path=@{id}'
3152			},
3153
3154			wrap: {
3155				me: function(o) {
3156					formatError(o);
3157					if (!o.uid) {
3158						return o;
3159					}
3160
3161					o.name = o.display_name;
3162					var m = o.name.split(' ');
3163					o.first_name = m.shift();
3164					o.last_name = m.join(' ');
3165					o.id = o.uid;
3166					delete o.uid;
3167					delete o.display_name;
3168					return o;
3169				},
3170
3171				'default': function(o, headers, req) {
3172					formatError(o);
3173					if (o.is_dir && o.contents) {
3174						o.data = o.contents;
3175						delete o.contents;
3176
3177						o.data.forEach(function(item) {
3178							item.root = o.root;
3179							formatFile(item, headers, req);
3180						});
3181					}
3182
3183					formatFile(o, headers, req);
3184
3185					if (o.is_deleted) {
3186						o.success = true;
3187					}
3188
3189					return o;
3190				}
3191			},
3192
3193			// Doesn't return the CORS headers
3194			xhr: function(p) {
3195
3196				// The proxy supports allow-cross-origin-resource
3197				// Alas that's the only thing we're using.
3198				if (p.data && p.data.file) {
3199					var file = p.data.file;
3200					if (file) {
3201						if (file.files) {
3202							p.data = file.files[0];
3203						}
3204						else {
3205							p.data = file;
3206						}
3207					}
3208				}
3209
3210				if (p.method === 'delete') {
3211					p.method = 'post';
3212				}
3213
3214				return true;
3215			},
3216
3217			form: function(p, qs) {
3218				delete qs.state;
3219				delete qs.redirect_uri;
3220			}
3221		}
3222	});
3223
3224	function formatError(o) {
3225		if (o && 'error' in o) {
3226			o.error = {
3227				code: 'server_error',
3228				message: o.error.message || o.error
3229			};
3230		}
3231	}
3232
3233	function formatFile(o, headers, req) {
3234
3235		if (typeof o !== 'object' ||
3236			(typeof Blob !== 'undefined' && o instanceof Blob) ||
3237			(typeof ArrayBuffer !== 'undefined' && o instanceof ArrayBuffer)) {
3238			// This is a file, let it through unformatted
3239			return;
3240		}
3241
3242		if ('error' in o) {
3243			return;
3244		}
3245
3246		var path = (o.root !== 'app_folder' ? o.root : '') + o.path.replace(/\&/g, '%26');
3247		path = path.replace(/^\//, '');
3248		if (o.thumb_exists) {
3249			o.thumbnail = req.oauth_proxy + '?path=' +
3250			encodeURIComponent('https://api-content.dropbox.com/1/thumbnails/auto/' + path + '?format=jpeg&size=m') + '&access_token=' + req.options.access_token;
3251		}
3252
3253		o.type = (o.is_dir ? 'folder' : o.mime_type);
3254		o.name = o.path.replace(/.*\//g, '');
3255		if (o.is_dir) {
3256			o.files = path.replace(/^\//, '');
3257		}
3258		else {
3259			o.downloadLink = hello.settings.oauth_proxy + '?path=' +
3260			encodeURIComponent('https://api-content.dropbox.com/1/files/auto/' + path) + '&access_token=' + req.options.access_token;
3261			o.file = 'https://api-content.dropbox.com/1/files/auto/' + path;
3262		}
3263
3264		if (!o.id) {
3265			o.id = o.path.replace(/^\//, '');
3266		}
3267
3268		// O.media = 'https://api-content.dropbox.com/1/files/' + path;
3269	}
3270
3271	function req(str) {
3272		return function(p, cb) {
3273			delete p.query.limit;
3274			cb(str);
3275		};
3276	}
3277
3278})(hello);
3279
3280(function(hello) {
3281
3282	hello.init({
3283
3284		facebook: {
3285
3286			name: 'Facebook',
3287
3288			// SEE https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.1
3289			oauth: {
3290				version: 2,
3291				auth: 'https://www.facebook.com/dialog/oauth/',
3292				grant: 'https://graph.facebook.com/oauth/access_token'
3293			},
3294
3295			// Authorization scopes
3296			scope: {
3297				basic: 'public_profile',
3298				email: 'email',
3299				share: 'user_posts',
3300				birthday: 'user_birthday',
3301				events: 'user_events',
3302				photos: 'user_photos',
3303				videos: 'user_videos',
3304				friends: 'user_friends',
3305				files: 'user_photos,user_videos',
3306				publish_files: 'user_photos,user_videos,publish_actions',
3307				publish: 'publish_actions',
3308
3309				// Deprecated in v2.0
3310				// Create_event	: 'create_event',
3311
3312				offline_access: ''
3313			},
3314
3315			// Refresh the access_token
3316			refresh: false,
3317
3318			login: function(p) {
3319
3320				// Reauthenticate
3321				// https://developers.facebook.com/docs/facebook-login/reauthentication
3322				if (p.options.force) {
3323					p.qs.auth_type = 'reauthenticate';
3324				}
3325
3326				// Set the display value
3327				p.qs.display = p.options.display || 'popup';
3328			},
3329
3330			logout: function(callback, options) {
3331				// Assign callback to a global handler
3332				var callbackID = hello.utils.globalEvent(callback);
3333				var redirect = encodeURIComponent(hello.settings.redirect_uri + '?' + hello.utils.param({callback:callbackID, result: JSON.stringify({force:true}), state: '{}'}));
3334				var token = (options.authResponse || {}).access_token;
3335				hello.utils.iframe('https://www.facebook.com/logout.php?next=' + redirect + '&access_token=' + token);
3336
3337				// Possible responses:
3338				// String URL	- hello.logout should handle the logout
3339				// Undefined	- this function will handle the callback
3340				// True - throw a success, this callback isn't handling the callback
3341				// False - throw a error
3342				if (!token) {
3343					// If there isn't a token, the above wont return a response, so lets trigger a response
3344					return false;
3345				}
3346			},
3347
3348			// API Base URL
3349			base: 'https://graph.facebook.com/v2.7/',
3350
3351			// Map GET requests
3352			get: {
3353				me: 'me?fields=email,first_name,last_name,name,timezone,verified',
3354				'me/friends': 'me/friends',
3355				'me/following': 'me/friends',
3356				'me/followers': 'me/friends',
3357				'me/share': 'me/feed',
3358				'me/like': 'me/likes',
3359				'me/files': 'me/albums',
3360				'me/albums': 'me/albums?fields=cover_photo,name',
3361				'me/album': '@{id}/photos?fields=picture',
3362				'me/photos': 'me/photos',
3363				'me/photo': '@{id}',
3364				'friend/albums': '@{id}/albums',
3365				'friend/photos': '@{id}/photos'
3366
3367				// Pagination
3368				// Https://developers.facebook.com/docs/reference/api/pagination/
3369			},
3370
3371			// Map POST requests
3372			post: {
3373				'me/share': 'me/feed',
3374				'me/photo': '@{id}'
3375
3376				// Https://developers.facebook.com/docs/graph-api/reference/v2.2/object/likes/
3377			},
3378
3379			wrap: {
3380				me: formatUser,
3381				'me/friends': formatFriends,
3382				'me/following': formatFriends,
3383				'me/followers': formatFriends,
3384				'me/albums': format,
3385				'me/photos': format,
3386				'me/files': format,
3387				'default': format
3388			},
3389
3390			// Special requirements for handling XHR
3391			xhr: function(p, qs) {
3392				if (p.method === 'get' || p.method === 'post') {
3393					qs.suppress_response_codes = true;
3394				}
3395
3396				// Is this a post with a data-uri?
3397				if (p.method === 'post' && p.data && typeof (p.data.file) === 'string') {
3398					// Convert the Data-URI to a Blob
3399					p.data.file = hello.utils.toBlob(p.data.file);
3400				}
3401
3402				return true;
3403			},
3404
3405			// Special requirements for handling JSONP fallback
3406			jsonp: function(p, qs) {
3407				var m = p.method;
3408				if (m !== 'get' && !hello.utils.hasBinary(p.data)) {
3409					p.data.method = m;
3410					p.method = 'get';
3411				}
3412				else if (p.method === 'delete') {
3413					qs.method = 'delete';
3414					p.method = 'post';
3415				}
3416			},
3417
3418			// Special requirements for iframe form hack
3419			form: function(p) {
3420				return {
3421					// Fire the callback onload
3422					callbackonload: true
3423				};
3424			}
3425		}
3426	});
3427
3428	var base = 'https://graph.facebook.com/';
3429
3430	function formatUser(o) {
3431		if (o.id) {
3432			o.thumbnail = o.picture = 'https://graph.facebook.com/' + o.id + '/picture';
3433		}
3434
3435		return o;
3436	}
3437
3438	function formatFriends(o) {
3439		if ('data' in o) {
3440			o.data.forEach(formatUser);
3441		}
3442
3443		return o;
3444	}
3445
3446	function format(o, headers, req) {
3447		if (typeof o === 'boolean') {
3448			o = {success: o};
3449		}
3450
3451		if (o && 'data' in o) {
3452			var token = req.query.access_token;
3453
3454			if (!(o.data instanceof Array)) {
3455				var data = o.data;
3456				delete o.data;
3457				o.data = [data];
3458			}
3459
3460			o.data.forEach(function(d) {
3461
3462				if (d.picture) {
3463					d.thumbnail = d.picture;
3464				}
3465
3466				d.pictures = (d.images || [])
3467					.sort(function(a, b) {
3468						return a.width - b.width;
3469					});
3470
3471				if (d.cover_photo && d.cover_photo.id) {
3472					d.thumbnail = base + d.cover_photo.id + '/picture?access_token=' + token;
3473				}
3474
3475				if (d.type === 'album') {
3476					d.files = d.photos = base + d.id + '/photos';
3477				}
3478
3479				if (d.can_upload) {
3480					d.upload_location = base + d.id + '/photos';
3481				}
3482			});
3483		}
3484
3485		return o;
3486	}
3487
3488})(hello);
3489
3490(function(hello) {
3491
3492	hello.init({
3493
3494		flickr: {
3495
3496			name: 'Flickr',
3497
3498			// Ensure that you define an oauth_proxy
3499			oauth: {
3500				version: '1.0a',
3501				auth: 'https://www.flickr.com/services/oauth/authorize?perms=read',
3502				request: 'https://www.flickr.com/services/oauth/request_token',
3503				token: 'https://www.flickr.com/services/oauth/access_token'
3504			},
3505
3506			// API base URL
3507			base: 'https://api.flickr.com/services/rest',
3508
3509			// Map GET resquests
3510			get: {
3511				me: sign('flickr.people.getInfo'),
3512				'me/friends': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
3513				'me/following': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
3514				'me/followers': sign('flickr.contacts.getList', {per_page:'@{limit|50}'}),
3515				'me/albums': sign('flickr.photosets.getList', {per_page:'@{limit|50}'}),
3516				'me/album': sign('flickr.photosets.getPhotos', {photoset_id: '@{id}'}),
3517				'me/photos': sign('flickr.people.getPhotos', {per_page:'@{limit|50}'})
3518			},
3519
3520			wrap: {
3521				me: function(o) {
3522					formatError(o);
3523					o = checkResponse(o, 'person');
3524					if (o.id) {
3525						if (o.realname) {
3526							o.name = o.realname._content;
3527							var m = o.name.split(' ');
3528							o.first_name = m.shift();
3529							o.last_name = m.join(' ');
3530						}
3531
3532						o.thumbnail = getBuddyIcon(o, 'l');
3533						o.picture = getBuddyIcon(o, 'l');
3534					}
3535
3536					return o;
3537				},
3538
3539				'me/friends': formatFriends,
3540				'me/followers': formatFriends,
3541				'me/following': formatFriends,
3542				'me/albums': function(o) {
3543					formatError(o);
3544					o = checkResponse(o, 'photosets');
3545					paging(o);
3546					if (o.photoset) {
3547						o.data = o.photoset;
3548						o.data.forEach(function(item) {
3549							item.name = item.title._content;
3550							item.photos = 'https://api.flickr.com/services/rest' + getApiUrl('flickr.photosets.getPhotos', {photoset_id: item.id}, true);
3551						});
3552
3553						delete o.photoset;
3554					}
3555
3556					return o;
3557				},
3558
3559				'me/photos': function(o) {
3560					formatError(o);
3561					return formatPhotos(o);
3562				},
3563
3564				'default': function(o) {
3565					formatError(o);
3566					return formatPhotos(o);
3567				}
3568			},
3569
3570			xhr: false,
3571
3572			jsonp: function(p, qs) {
3573				if (p.method == 'get') {
3574					delete qs.callback;
3575					qs.jsoncallback = p.callbackID;
3576				}
3577			}
3578		}
3579	});
3580
3581	function getApiUrl(method, extraParams, skipNetwork) {
3582		var url = ((skipNetwork) ? '' : 'flickr:') +
3583			'?method=' + method +
3584			'&api_key=' + hello.services.flickr.id +
3585			'&format=json';
3586		for (var param in extraParams) {
3587			if (extraParams.hasOwnProperty(param)) {
3588				url += '&' + param + '=' + extraParams[param];
3589			}
3590		}
3591
3592		return url;
3593	}
3594
3595	// This is not exactly neat but avoid to call
3596	// The method 'flickr.test.login' for each api call
3597
3598	function withUser(cb) {
3599		var auth = hello.getAuthResponse('flickr');
3600		cb(auth && auth.user_nsid ? auth.user_nsid : null);
3601	}
3602
3603	function sign(url, params) {
3604		if (!params) {
3605			params = {};
3606		}
3607
3608		return function(p, callback) {
3609			withUser(function(userId) {
3610				params.user_id = userId;
3611				callback(getApiUrl(url, params, true));
3612			});
3613		};
3614	}
3615
3616	function getBuddyIcon(profile, size) {
3617		var url = 'https://www.flickr.com/images/buddyicon.gif';
3618		if (profile.nsid && profile.iconserver && profile.iconfarm) {
3619			url = 'https://farm' + profile.iconfarm + '.staticflickr.com/' +
3620				profile.iconserver + '/' +
3621				'buddyicons/' + profile.nsid +
3622				((size) ? '_' + size : '') + '.jpg';
3623		}
3624
3625		return url;
3626	}
3627
3628	// See: https://www.flickr.com/services/api/misc.urls.html
3629	function createPhotoUrl(id, farm, server, secret, size) {
3630		size = (size) ? '_' + size : '';
3631		return 'https://farm' + farm + '.staticflickr.com/' + server + '/' + id + '_' + secret + size + '.jpg';
3632	}
3633
3634	function formatUser(o) {
3635	}
3636
3637	function formatError(o) {
3638		if (o && o.stat && o.stat.toLowerCase() != 'ok') {
3639			o.error = {
3640				code: 'invalid_request',
3641				message: o.message
3642			};
3643		}
3644	}
3645
3646	function formatPhotos(o) {
3647		if (o.photoset || o.photos) {
3648			var set = ('photoset' in o) ? 'photoset' : 'photos';
3649			o = checkResponse(o, set);
3650			paging(o);
3651			o.data = o.photo;
3652			delete o.photo;
3653			for (var i = 0; i < o.data.length; i++) {
3654				var photo = o.data[i];
3655				photo.name = photo.title;
3656				photo.picture = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, '');
3657				photo.pictures = createPictures(photo.id, photo.farm, photo.server, photo.secret);
3658				photo.source = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'b');
3659				photo.thumbnail = createPhotoUrl(photo.id, photo.farm, photo.server, photo.secret, 'm');
3660			}
3661		}
3662
3663		return o;
3664	}
3665
3666	// See: https://www.flickr.com/services/api/misc.urls.html
3667	function createPictures(id, farm, server, secret) {
3668
3669		var NO_LIMIT = 2048;
3670		var sizes = [
3671			{id: 't', max: 100},
3672			{id: 'm', max: 240},
3673			{id: 'n', max: 320},
3674			{id: '', max: 500},
3675			{id: 'z', max: 640},
3676			{id: 'c', max: 800},
3677			{id: 'b', max: 1024},
3678			{id: 'h', max: 1600},
3679			{id: 'k', max: 2048},
3680			{id: 'o', max: NO_LIMIT}
3681		];
3682
3683		return sizes.map(function(size) {
3684			return {
3685				source: createPhotoUrl(id, farm, server, secret, size.id),
3686
3687				// Note: this is a guess that's almost certain to be wrong (unless square source)
3688				width: size.max,
3689				height: size.max
3690			};
3691		});
3692	}
3693
3694	function checkResponse(o, key) {
3695
3696		if (key in o) {
3697			o = o[key];
3698		}
3699		else if (!('error' in o)) {
3700			o.error = {
3701				code: 'invalid_request',
3702				message: o.message || 'Failed to get data from Flickr'
3703			};
3704		}
3705
3706		return o;
3707	}
3708
3709	function formatFriends(o) {
3710		formatError(o);
3711		if (o.contacts) {
3712			o = checkResponse(o, 'contacts');
3713			paging(o);
3714			o.data = o.contact;
3715			delete o.contact;
3716			for (var i = 0; i < o.data.length; i++) {
3717				var item = o.data[i];
3718				item.id = item.nsid;
3719				item.name = item.realname || item.username;
3720				item.thumbnail = getBuddyIcon(item, 'm');
3721			}
3722		}
3723
3724		return o;
3725	}
3726
3727	function paging(res) {
3728		if (res.page && res.pages && res.page !== res.pages) {
3729			res.paging = {
3730				next: '?page=' + (++res.page)
3731			};
3732		}
3733	}
3734
3735})(hello);
3736
3737(function(hello) {
3738
3739	hello.init({
3740
3741		foursquare: {
3742
3743			name: 'Foursquare',
3744
3745			oauth: {
3746				// See: https://developer.foursquare.com/overview/auth
3747				version: 2,
3748				auth: 'https://foursquare.com/oauth2/authenticate',
3749				grant: 'https://foursquare.com/oauth2/access_token'
3750			},
3751
3752			// Refresh the access_token once expired
3753			refresh: true,
3754
3755			base: 'https://api.foursquare.com/v2/',
3756
3757			get: {
3758				me: 'users/self',
3759				'me/friends': 'users/self/friends',
3760				'me/followers': 'users/self/friends',
3761				'me/following': 'users/self/friends'
3762			},
3763
3764			wrap: {
3765				me: function(o) {
3766					formatError(o);
3767					if (o && o.response) {
3768						o = o.response.user;
3769						formatUser(o);
3770					}
3771
3772					return o;
3773				},
3774
3775				'default': function(o) {
3776					formatError(o);
3777
3778					// Format friends
3779					if (o && 'response' in o && 'friends' in o.response && 'items' in o.response.friends) {
3780						o.data = o.response.friends.items;
3781						o.data.forEach(formatUser);
3782						delete o.response;
3783					}
3784
3785					return o;
3786				}
3787			},
3788
3789			xhr: formatRequest,
3790			jsonp: formatRequest
3791		}
3792	});
3793
3794	function formatError(o) {
3795		if (o.meta && (o.meta.code === 400 || o.meta.code === 401)) {
3796			o.error = {
3797				code: 'access_denied',
3798				message: o.meta.errorDetail
3799			};
3800		}
3801	}
3802
3803	function formatUser(o) {
3804		if (o && o.id) {
3805			o.thumbnail = o.photo.prefix + '100x100' + o.photo.suffix;
3806			o.name = o.firstName + ' ' + o.lastName;
3807			o.first_name = o.firstName;
3808			o.last_name = o.lastName;
3809			if (o.contact) {
3810				if (o.contact.email) {
3811					o.email = o.contact.email;
3812				}
3813			}
3814		}
3815	}
3816
3817	function formatRequest(p, qs) {
3818		var token = qs.access_token;
3819		delete qs.access_token;
3820		qs.oauth_token = token;
3821		qs.v = 20121125;
3822		return true;
3823	}
3824
3825})(hello);
3826
3827(function(hello) {
3828
3829	hello.init({
3830
3831		github: {
3832
3833			name: 'GitHub',
3834
3835			oauth: {
3836				version: 2,
3837				auth: 'https://github.com/login/oauth/authorize',
3838				grant: 'https://github.com/login/oauth/access_token',
3839				response_type: 'code'
3840			},
3841
3842			scope: {
3843				email: 'user:email'
3844			},
3845
3846			base: 'https://api.github.com/',
3847
3848			get: {
3849				me: 'user',
3850				'me/friends': 'user/following?per_page=@{limit|100}',
3851				'me/following': 'user/following?per_page=@{limit|100}',
3852				'me/followers': 'user/followers?per_page=@{limit|100}',
3853				'me/like': 'user/starred?per_page=@{limit|100}'
3854			},
3855
3856			wrap: {
3857				me: function(o, headers) {
3858
3859					formatError(o, headers);
3860					formatUser(o);
3861
3862					return o;
3863				},
3864
3865				'default': function(o, headers, req) {
3866
3867					formatError(o, headers);
3868
3869					if (Array.isArray(o)) {
3870						o = {data:o};
3871					}
3872
3873					if (o.data) {
3874						paging(o, headers, req);
3875						o.data.forEach(formatUser);
3876					}
3877
3878					return o;
3879				}
3880			},
3881
3882			xhr: function(p) {
3883
3884				if (p.method !== 'get' && p.data) {
3885
3886					// Serialize payload as JSON
3887					p.headers = p.headers || {};
3888					p.headers['Content-Type'] = 'application/json';
3889					if (typeof (p.data) === 'object') {
3890						p.data = JSON.stringify(p.data);
3891					}
3892				}
3893
3894				return true;
3895			}
3896		}
3897	});
3898
3899	function formatError(o, headers) {
3900		var code = headers ? headers.statusCode : (o && 'meta' in o && 'status' in o.meta && o.meta.status);
3901		if ((code === 401 || code === 403)) {
3902			o.error = {
3903				code: 'access_denied',
3904				message: o.message || (o.data ? o.data.message : 'Could not get response')
3905			};
3906			delete o.message;
3907		}
3908	}
3909
3910	function formatUser(o) {
3911		if (o.id) {
3912			o.thumbnail = o.picture = o.avatar_url;
3913			o.name = o.login;
3914		}
3915	}
3916
3917	function paging(res, headers, req) {
3918		if (res.data && res.data.length && headers && headers.Link) {
3919			var next = headers.Link.match(/<(.*?)>;\s*rel=\"next\"/);
3920			if (next) {
3921				res.paging = {
3922					next: next[1]
3923				};
3924			}
3925		}
3926	}
3927
3928})(hello);
3929
3930(function(hello) {
3931
3932	var contactsUrl = 'https://www.google.com/m8/feeds/contacts/default/full?v=3.0&alt=json&max-results=@{limit|1000}&start-index=@{start|1}';
3933
3934	hello.init({
3935
3936		google: {
3937
3938			name: 'Google Plus',
3939
3940			// See: http://code.google.com/apis/accounts/docs/OAuth2UserAgent.html
3941			oauth: {
3942				version: 2,
3943				auth: 'https://accounts.google.com/o/oauth2/auth',
3944				grant: 'https://accounts.google.com/o/oauth2/token'
3945			},
3946
3947			// Authorization scopes
3948			scope: {
3949				basic: 'https://www.googleapis.com/auth/plus.me profile',
3950				email: 'email',
3951				birthday: '',
3952				events: '',
3953				photos: 'https://picasaweb.google.com/data/',
3954				videos: 'http://gdata.youtube.com',
3955				friends: 'https://www.google.com/m8/feeds, https://www.googleapis.com/auth/plus.login',
3956				files: 'https://www.googleapis.com/auth/drive.readonly',
3957				publish: '',
3958				publish_files: 'https://www.googleapis.com/auth/drive',
3959				share: '',
3960				create_event: '',
3961				offline_access: ''
3962			},
3963
3964			scope_delim: ' ',
3965
3966			login: function(p) {
3967
3968				if (p.qs.response_type === 'code') {
3969
3970					// Let's set this to an offline access to return a refresh_token
3971					p.qs.access_type = 'offline';
3972				}
3973
3974				// Reauthenticate
3975				// https://developers.google.com/identity/protocols/
3976				if (p.options.force) {
3977					p.qs.approval_prompt = 'force';
3978				}
3979			},
3980
3981			// API base URI
3982			base: 'https://www.googleapis.com/',
3983
3984			// Map GET requests
3985			get: {
3986				me: 'plus/v1/people/me',
3987
3988				// Deprecated Sept 1, 2014
3989				//'me': 'oauth2/v1/userinfo?alt=json',
3990
3991				// See: https://developers.google.com/+/api/latest/people/list
3992				'me/friends': 'plus/v1/people/me/people/visible?maxResults=@{limit|100}',
3993				'me/following': contactsUrl,
3994				'me/followers': contactsUrl,
3995				'me/contacts': contactsUrl,
3996				'me/share': 'plus/v1/people/me/activities/public?maxResults=@{limit|100}',
3997				'me/feed': 'plus/v1/people/me/activities/public?maxResults=@{limit|100}',
3998				'me/albums': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&max-results=@{limit|100}&start-index=@{start|1}',
3999				'me/album': function(p, callback) {
4000					var key = p.query.id;
4001					delete p.query.id;
4002					callback(key.replace('/entry/', '/feed/'));
4003				},
4004
4005				'me/photos': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&kind=photo&max-results=@{limit|100}&start-index=@{start|1}',
4006
4007				// See: https://developers.google.com/drive/v2/reference/files/list
4008				'me/file': 'drive/v2/files/@{id}',
4009				'me/files': 'drive/v2/files?q=%22@{parent|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}',
4010
4011				// See: https://developers.google.com/drive/v2/reference/files/list
4012				'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}',
4013
4014				// See: https://developers.google.com/drive/v2/reference/files/list
4015				'me/folder': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}'
4016			},
4017
4018			// Map POST requests
4019			post: {
4020
4021				// Google Drive
4022				'me/files': uploadDrive,
4023				'me/folders': function(p, callback) {
4024					p.data = {
4025						title: p.data.name,
4026						parents: [{id: p.data.parent || 'root'}],
4027						mimeType: 'application/vnd.google-apps.folder'
4028					};
4029					callback('drive/v2/files');
4030				}
4031			},
4032
4033			// Map PUT requests
4034			put: {
4035				'me/files': uploadDrive
4036			},
4037
4038			// Map DELETE requests
4039			del: {
4040				'me/files': 'drive/v2/files/@{id}',
4041				'me/folder': 'drive/v2/files/@{id}'
4042			},
4043
4044			// Map PATCH requests
4045			patch: {
4046				'me/file': 'drive/v2/files/@{id}'
4047			},
4048
4049			wrap: {
4050				me: function(o) {
4051					if (o.id) {
4052						o.last_name = o.family_name || (o.name ? o.name.familyName : null);
4053						o.first_name = o.given_name || (o.name ? o.name.givenName : null);
4054
4055						if (o.emails && o.emails.length) {
4056							o.email = o.emails[0].value;
4057						}
4058
4059						formatPerson(o);
4060					}
4061
4062					return o;
4063				},
4064
4065				'me/friends': function(o) {
4066					if (o.items) {
4067						paging(o);
4068						o.data = o.items;
4069						o.data.forEach(formatPerson);
4070						delete o.items;
4071					}
4072
4073					return o;
4074				},
4075
4076				'me/contacts': formatFriends,
4077				'me/followers': formatFriends,
4078				'me/following': formatFriends,
4079				'me/share': formatFeed,
4080				'me/feed': formatFeed,
4081				'me/albums': gEntry,
4082				'me/photos': formatPhotos,
4083				'default': gEntry
4084			},
4085
4086			xhr: function(p) {
4087
4088				if (p.method === 'post' || p.method === 'put') {
4089					toJSON(p);
4090				}
4091				else if (p.method === 'patch') {
4092					hello.utils.extend(p.query, p.data);
4093					p.data = null;
4094				}
4095
4096				return true;
4097			},
4098
4099			// Don't even try submitting via form.
4100			// This means no POST operations in <=IE9
4101			form: false
4102		}
4103	});
4104
4105	function toInt(s) {
4106		return parseInt(s, 10);
4107	}
4108
4109	function formatFeed(o) {
4110		paging(o);
4111		o.data = o.items;
4112		delete o.items;
4113		return o;
4114	}
4115
4116	// Format: ensure each record contains a name, id etc.
4117	function formatItem(o) {
4118		if (o.error) {
4119			return;
4120		}
4121
4122		if (!o.name) {
4123			o.name = o.title || o.message;
4124		}
4125
4126		if (!o.picture) {
4127			o.picture = o.thumbnailLink;
4128		}
4129
4130		if (!o.thumbnail) {
4131			o.thumbnail = o.thumbnailLink;
4132		}
4133
4134		if (o.mimeType === 'application/vnd.google-apps.folder') {
4135			o.type = 'folder';
4136			o.files = 'https://www.googleapis.com/drive/v2/files?q=%22' + o.id + '%22+in+parents';
4137		}
4138
4139		return o;
4140	}
4141
4142	function formatImage(image) {
4143		return {
4144			source: image.url,
4145			width: image.width,
4146			height: image.height
4147		};
4148	}
4149
4150	function formatPhotos(o) {
4151		o.data = o.feed.entry.map(formatEntry);
4152		delete o.feed;
4153	}
4154
4155	// Google has a horrible JSON API
4156	function gEntry(o) {
4157		paging(o);
4158
4159		if ('feed' in o && 'entry' in o.feed) {
4160			o.data = o.feed.entry.map(formatEntry);
4161			delete o.feed;
4162		}
4163
4164		// Old style: Picasa, etc.
4165		else if ('entry' in o) {
4166			return formatEntry(o.entry);
4167		}
4168
4169		// New style: Google Drive & Plus
4170		else if ('items' in o) {
4171			o.data = o.items.map(formatItem);
4172			delete o.items;
4173		}
4174		else {
4175			formatItem(o);
4176		}
4177
4178		return o;
4179	}
4180
4181	function formatPerson(o) {
4182		o.name = o.displayName || o.name;
4183		o.picture = o.picture || (o.image ? o.image.url : null);
4184		o.thumbnail = o.picture;
4185	}
4186
4187	function formatFriends(o, headers, req) {
4188		paging(o);
4189		var r = [];
4190		if ('feed' in o && 'entry' in o.feed) {
4191			var token = req.query.access_token;
4192			for (var i = 0; i < o.feed.entry.length; i++) {
4193				var a = o.feed.entry[i];
4194
4195				a.id	= a.id.$t;
4196				a.name	= a.title.$t;
4197				delete a.title;
4198				if (a.gd$email) {
4199					a.email	
vendor: 7,124 bytes, lines 4199-4508
4199= (a.gd$email && a.gd$email.length > 0) ? a.gd$email[0].address : null;
4200					a.emails = a.gd$email;
4201					delete a.gd$email;
4202				}
4203
4204				if (a.updated) {
4205					a.updated = a.updated.$t;
4206				}
4207
4208				if (a.link) {
4209
4210					var pic = (a.link.length > 0) ? a.link[0].href : null;
4211					if (pic && a.link[0].gd$etag) {
4212						pic += (pic.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + token;
4213						a.picture = pic;
4214						a.thumbnail = pic;
4215					}
4216
4217					delete a.link;
4218				}
4219
4220				if (a.category) {
4221					delete a.category;
4222				}
4223			}
4224
4225			o.data = o.feed.entry;
4226			delete o.feed;
4227		}
4228
4229		return o;
4230	}
4231
4232	function formatEntry(a) {
4233
4234		var group = a.media$group;
4235		var photo = group.media$content.length ? group.media$content[0] : {};
4236		var mediaContent = group.media$content || [];
4237		var mediaThumbnail = group.media$thumbnail || [];
4238
4239		var pictures = mediaContent
4240			.concat(mediaThumbnail)
4241			.map(formatImage)
4242			.sort(function(a, b) {
4243				return a.width - b.width;
4244			});
4245
4246		var i = 0;
4247		var _a;
4248		var p = {
4249			id: a.id.$t,
4250			name: a.title.$t,
4251			description: a.summary.$t,
4252			updated_time: a.updated.$t,
4253			created_time: a.published.$t,
4254			picture: photo ? photo.url : null,
4255			pictures: pictures,
4256			images: [],
4257			thumbnail: photo ? photo.url : null,
4258			width: photo.width,
4259			height: photo.height
4260		};
4261
4262		// Get feed/children
4263		if ('link' in a) {
4264			for (i = 0; i < a.link.length; i++) {
4265				var d = a.link[i];
4266				if (d.rel.match(/\#feed$/)) {
4267					p.upload_location = p.files = p.photos = d.href;
4268					break;
4269				}
4270			}
4271		}
4272
4273		// Get images of different scales
4274		if ('category' in a && a.category.length) {
4275			_a = a.category;
4276			for (i = 0; i < _a.length; i++) {
4277				if (_a[i].scheme && _a[i].scheme.match(/\#kind$/)) {
4278					p.type = _a[i].term.replace(/^.*?\#/, '');
4279				}
4280			}
4281		}
4282
4283		// Get images of different scales
4284		if ('media$thumbnail' in group && group.media$thumbnail.length) {
4285			_a = group.media$thumbnail;
4286			p.thumbnail = _a[0].url;
4287			p.images = _a.map(formatImage);
4288		}
4289
4290		_a = group.media$content;
4291
4292		if (_a && _a.length) {
4293			p.images.push(formatImage(_a[0]));
4294		}
4295
4296		return p;
4297	}
4298
4299	function paging(res) {
4300
4301		// Contacts V2
4302		if ('feed' in res && res.feed.openSearch$itemsPerPage) {
4303			var limit = toInt(res.feed.openSearch$itemsPerPage.$t);
4304			var start = toInt(res.feed.openSearch$startIndex.$t);
4305			var total = toInt(res.feed.openSearch$totalResults.$t);
4306
4307			if ((start + limit) < total) {
4308				res.paging = {
4309					next: '?start=' + (start + limit)
4310				};
4311			}
4312		}
4313		else if ('nextPageToken' in res) {
4314			res.paging = {
4315				next: '?pageToken=' + res.nextPageToken
4316			};
4317		}
4318	}
4319
4320	// Construct a multipart message
4321	function Multipart() {
4322
4323		// Internal body
4324		var body = [];
4325		var boundary = (Math.random() * 1e10).toString(32);
4326		var counter = 0;
4327		var lineBreak = '\r\n';
4328		var delim = lineBreak + '--' + boundary;
4329		var ready = function() {};
4330
4331		var dataUri = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i;
4332
4333		// Add file
4334		function addFile(item) {
4335			var fr = new FileReader();
4336			fr.onload = function(e) {
4337				addContent(btoa(e.target.result), item.type + lineBreak + 'Content-Transfer-Encoding: base64');
4338			};
4339
4340			fr.readAsBinaryString(item);
4341		}
4342
4343		// Add content
4344		function addContent(content, type) {
4345			body.push(lineBreak + 'Content-Type: ' + type + lineBreak + lineBreak + content);
4346			counter--;
4347			ready();
4348		}
4349
4350		// Add new things to the object
4351		this.append = function(content, type) {
4352
4353			// Does the content have an array
4354			if (typeof (content) === 'string' || !('length' in Object(content))) {
4355				// Converti to multiples
4356				content = [content];
4357			}
4358
4359			for (var i = 0; i < content.length; i++) {
4360
4361				counter++;
4362
4363				var item = content[i];
4364
4365				// Is this a file?
4366				// Files can be either Blobs or File types
4367				if (
4368					(typeof (File) !== 'undefined' && item instanceof File) ||
4369					(typeof (Blob) !== 'undefined' && item instanceof Blob)
4370				) {
4371					// Read the file in
4372					addFile(item);
4373				}
4374
4375				// Data-URI?
4376				// Data:[<mime type>][;charset=<charset>][;base64],<encoded data>
4377				// /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i
4378				else if (typeof (item) === 'string' && item.match(dataUri)) {
4379					var m = item.match(dataUri);
4380					addContent(item.replace(dataUri, ''), m[1] + lineBreak + 'Content-Transfer-Encoding: base64');
4381				}
4382
4383				// Regular string
4384				else {
4385					addContent(item, type);
4386				}
4387			}
4388		};
4389
4390		this.onready = function(fn) {
4391			ready = function() {
4392				if (counter === 0) {
4393					// Trigger ready
4394					body.unshift('');
4395					body.push('--');
4396					fn(body.join(delim), boundary);
4397					body = [];
4398				}
4399			};
4400
4401			ready();
4402		};
4403	}
4404
4405	// Upload to Drive
4406	// If this is PUT then only augment the file uploaded
4407	// PUT https://developers.google.com/drive/v2/reference/files/update
4408	// POST https://developers.google.com/drive/manage-uploads
4409	function uploadDrive(p, callback) {
4410
4411		var data = {};
4412
4413		// Test for DOM element
4414		if (p.data &&
4415			(typeof (HTMLInputElement) !== 'undefined' && p.data instanceof HTMLInputElement)
4416		) {
4417			p.data = {file: p.data};
4418		}
4419
4420		if (!p.data.name && Object(Object(p.data.file).files).length && p.method === 'post') {
4421			p.data.name = p.data.file.files[0].name;
4422		}
4423
4424		if (p.method === 'post') {
4425			p.data = {
4426				title: p.data.name,
4427				parents: [{id: p.data.parent || 'root'}],
4428				file: p.data.file
4429			};
4430		}
4431		else {
4432
4433			// Make a reference
4434			data = p.data;
4435			p.data = {};
4436
4437			// Add the parts to change as required
4438			if (data.parent) {
4439				p.data.parents = [{id: p.data.parent || 'root'}];
4440			}
4441
4442			if (data.file) {
4443				p.data.file = data.file;
4444			}
4445
4446			if (data.name) {
4447				p.data.title = data.name;
4448			}
4449		}
4450
4451		// Extract the file, if it exists from the data object
4452		// If the File is an INPUT element lets just concern ourselves with the NodeList
4453		var file;
4454		if ('file' in p.data) {
4455			file = p.data.file;
4456			delete p.data.file;
4457
4458			if (typeof (file) === 'object' && 'files' in file) {
4459				// Assign the NodeList
4460				file = file.files;
4461			}
4462
4463			if (!file || !file.length) {
4464				callback({
4465					error: {
4466						code: 'request_invalid',
4467						message: 'There were no files attached with this request to upload'
4468					}
4469				});
4470				return;
4471			}
4472		}
4473
4474		// Set type p.data.mimeType = Object(file[0]).type || 'application/octet-stream';
4475
4476		// Construct a multipart message
4477		var parts = new Multipart();
4478		parts.append(JSON.stringify(p.data), 'application/json');
4479
4480		// Read the file into a  base64 string... yep a hassle, i know
4481		// FormData doesn't let us assign our own Multipart headers and HTTP Content-Type
4482		// Alas GoogleApi need these in a particular format
4483		if (file) {
4484			parts.append(file);
4485		}
4486
4487		parts.onready(function(body, boundary) {
4488
4489			p.headers['content-type'] = 'multipart/related; boundary="' + boundary + '"';
4490			p.data = body;
4491
4492			callback('upload/drive/v2/files' + (data.id ? '/' + data.id : '') + '?uploadType=multipart');
4493		});
4494
4495	}
4496
4497	function toJSON(p) {
4498		if (typeof (p.data) === 'object') {
4499			// Convert the POST into a javascript object
4500			try {
4501				p.data = JSON.stringify(p.data);
4502				p.headers['content-type'] = 'application/json';
4503			}
4504			catch (e) {}
4505		}
4506	}
4507
4508})(hello);
vendor: 27,013 bytes, lines 4509-5826
4509
4510(function(hello) {
4511
4512	hello.init({
4513
4514		instagram: {
4515
4516			name: 'Instagram',
4517
4518			oauth: {
4519				// See: http://instagram.com/developer/authentication/
4520				version: 2,
4521				auth: 'https://instagram.com/oauth/authorize/',
4522				grant: 'https://api.instagram.com/oauth/access_token'
4523			},
4524
4525			// Refresh the access_token once expired
4526			refresh: true,
4527
4528			scope: {
4529				basic: 'basic',
4530				photos: '',
4531				friends: 'relationships',
4532				publish: 'likes comments',
4533				email: '',
4534				share: '',
4535				publish_files: '',
4536				files: '',
4537				videos: '',
4538				offline_access: ''
4539			},
4540
4541			scope_delim: ' ',
4542
4543			base: 'https://api.instagram.com/v1/',
4544
4545			get: {
4546				me: 'users/self',
4547				'me/feed': 'users/self/feed?count=@{limit|100}',
4548				'me/photos': 'users/self/media/recent?min_id=0&count=@{limit|100}',
4549				'me/friends': 'users/self/follows?count=@{limit|100}',
4550				'me/following': 'users/self/follows?count=@{limit|100}',
4551				'me/followers': 'users/self/followed-by?count=@{limit|100}',
4552				'friend/photos': 'users/@{id}/media/recent?min_id=0&count=@{limit|100}'
4553			},
4554
4555			post: {
4556				'me/like': function(p, callback) {
4557					var id = p.data.id;
4558					p.data = {};
4559					callback('media/' + id + '/likes');
4560				}
4561			},
4562
4563			del: {
4564				'me/like': 'media/@{id}/likes'
4565			},
4566
4567			wrap: {
4568				me: function(o) {
4569
4570					formatError(o);
4571
4572					if ('data' in o) {
4573						o.id = o.data.id;
4574						o.thumbnail = o.data.profile_picture;
4575						o.name = o.data.full_name || o.data.username;
4576					}
4577
4578					return o;
4579				},
4580
4581				'me/friends': formatFriends,
4582				'me/following': formatFriends,
4583				'me/followers': formatFriends,
4584				'me/photos': function(o) {
4585
4586					formatError(o);
4587					paging(o);
4588
4589					if ('data' in o) {
4590						o.data = o.data.filter(function(d) {
4591							return d.type === 'image';
4592						});
4593
4594						o.data.forEach(function(d) {
4595							d.name = d.caption ? d.caption.text : null;
4596							d.thumbnail = d.images.thumbnail.url;
4597							d.picture = d.images.standard_resolution.url;
4598							d.pictures = Object.keys(d.images)
4599								.map(function(key) {
4600									var image = d.images[key];
4601									return formatImage(image);
4602								})
4603								.sort(function(a, b) {
4604									return a.width - b.width;
4605								});
4606						});
4607					}
4608
4609					return o;
4610				},
4611
4612				'default': function(o) {
4613					o = formatError(o);
4614					paging(o);
4615					return o;
4616				}
4617			},
4618
4619			// Instagram does not return any CORS Headers
4620			// So besides JSONP we're stuck with proxy
4621			xhr: function(p, qs) {
4622
4623				var method = p.method;
4624				var proxy = method !== 'get';
4625
4626				if (proxy) {
4627
4628					if ((method === 'post' || method === 'put') && p.query.access_token) {
4629						p.data.access_token = p.query.access_token;
4630						delete p.query.access_token;
4631					}
4632
4633					// No access control headers
4634					// Use the proxy instead
4635					p.proxy = proxy;
4636				}
4637
4638				return proxy;
4639			},
4640
4641			// No form
4642			form: false
4643		}
4644	});
4645
4646	function formatImage(image) {
4647		return {
4648			source: image.url,
4649			width: image.width,
4650			height: image.height
4651		};
4652	}
4653
4654	function formatError(o) {
4655		if (typeof o === 'string') {
4656			return {
4657				error: {
4658					code: 'invalid_request',
4659					message: o
4660				}
4661			};
4662		}
4663
4664		if (o && 'meta' in o && 'error_type' in o.meta) {
4665			o.error = {
4666				code: o.meta.error_type,
4667				message: o.meta.error_message
4668			};
4669		}
4670
4671		return o;
4672	}
4673
4674	function formatFriends(o) {
4675		paging(o);
4676		if (o && 'data' in o) {
4677			o.data.forEach(formatFriend);
4678		}
4679
4680		return o;
4681	}
4682
4683	function formatFriend(o) {
4684		if (o.id) {
4685			o.thumbnail = o.profile_picture;
4686			o.name = o.full_name || o.username;
4687		}
4688	}
4689
4690	// See: http://instagram.com/developer/endpoints/
4691	function paging(res) {
4692		if ('pagination' in res) {
4693			res.paging = {
4694				next: res.pagination.next_url
4695			};
4696			delete res.pagination;
4697		}
4698	}
4699
4700})(hello);
4701
4702(function(hello) {
4703
4704	hello.init({
4705
4706		joinme: {
4707
4708			name: 'join.me',
4709
4710			oauth: {
4711				version: 2,
4712				auth: 'https://secure.join.me/api/public/v1/auth/oauth2',
4713				grant: 'https://secure.join.me/api/public/v1/auth/oauth2'
4714			},
4715
4716			refresh: false,
4717
4718			scope: {
4719				basic: 'user_info',
4720				user: 'user_info',
4721				scheduler: 'scheduler',
4722				start: 'start_meeting',
4723				email: '',
4724				friends: '',
4725				share: '',
4726				publish: '',
4727				photos: '',
4728				publish_files: '',
4729				files: '',
4730				videos: '',
4731				offline_access: ''
4732			},
4733
4734			scope_delim: ' ',
4735
4736			login: function(p) {
4737				p.options.popup.width = 400;
4738				p.options.popup.height = 700;
4739			},
4740
4741			base: 'https://api.join.me/v1/',
4742
4743			get: {
4744				me: 'user',
4745				meetings: 'meetings',
4746				'meetings/info': 'meetings/@{id}'
4747			},
4748
4749			post: {
4750				'meetings/start/adhoc': function(p, callback) {
4751					callback('meetings/start');
4752				},
4753
4754				'meetings/start/scheduled': function(p, callback) {
4755					var meetingId = p.data.meetingId;
4756					p.data = {};
4757					callback('meetings/' + meetingId + '/start');
4758				},
4759
4760				'meetings/schedule': function(p, callback) {
4761					callback('meetings');
4762				}
4763			},
4764
4765			patch: {
4766				'meetings/update': function(p, callback) {
4767					callback('meetings/' + p.data.meetingId);
4768				}
4769			},
4770
4771			del: {
4772				'meetings/delete': 'meetings/@{id}'
4773			},
4774
4775			wrap: {
4776				me: function(o, headers) {
4777					formatError(o, headers);
4778
4779					if (!o.email) {
4780						return o;
4781					}
4782
4783					o.name = o.fullName;
4784					o.first_name = o.name.split(' ')[0];
4785					o.last_name = o.name.split(' ')[1];
4786					o.id = o.email;
4787
4788					return o;
4789				},
4790
4791				'default': function(o, headers) {
4792					formatError(o, headers);
4793
4794					return o;
4795				}
4796			},
4797
4798			xhr: formatRequest
4799
4800		}
4801	});
4802
4803	function formatError(o, headers) {
4804		var errorCode;
4805		var message;
4806		var details;
4807
4808		if (o && ('Message' in o)) {
4809			message = o.Message;
4810			delete o.Message;
4811
4812			if ('ErrorCode' in o) {
4813				errorCode = o.ErrorCode;
4814				delete o.ErrorCode;
4815			}
4816			else {
4817				errorCode = getErrorCode(headers);
4818			}
4819
4820			o.error = {
4821				code: errorCode,
4822				message: message,
4823				details: o
4824			};
4825		}
4826
4827		return o;
4828	}
4829
4830	function formatRequest(p, qs) {
4831		// Move the access token from the request body to the request header
4832		var token = qs.access_token;
4833		delete qs.access_token;
4834		p.headers.Authorization = 'Bearer ' + token;
4835
4836		// Format non-get requests to indicate json body
4837		if (p.method !== 'get' && p.data) {
4838			p.headers['Content-Type'] = 'application/json';
4839			if (typeof (p.data) === 'object') {
4840				p.data = JSON.stringify(p.data);
4841			}
4842		}
4843
4844		if (p.method === 'put') {
4845			p.method = 'patch';
4846		}
4847
4848		return true;
4849	}
4850
4851	function getErrorCode(headers) {
4852		switch (headers.statusCode) {
4853			case 400:
4854				return 'invalid_request';
4855			case 403:
4856				return 'stale_token';
4857			case 401:
4858				return 'invalid_token';
4859			case 500:
4860				return 'server_error';
4861			default:
4862				return 'server_error';
4863		}
4864	}
4865
4866}(hello));
4867
4868(function(hello) {
4869
4870	hello.init({
4871
4872		linkedin: {
4873
4874			oauth: {
4875				version: 2,
4876				response_type: 'code',
4877				auth: 'https://www.linkedin.com/uas/oauth2/authorization',
4878				grant: 'https://www.linkedin.com/uas/oauth2/accessToken'
4879			},
4880
4881			// Refresh the access_token once expired
4882			refresh: true,
4883
4884			scope: {
4885				basic: 'r_basicprofile',
4886				email: 'r_emailaddress',
4887				files: '',
4888				friends: '',
4889				photos: '',
4890				publish: 'w_share',
4891				publish_files: 'w_share',
4892				share: '',
4893				videos: '',
4894				offline_access: ''
4895			},
4896			scope_delim: ' ',
4897
4898			base: 'https://api.linkedin.com/v1/',
4899
4900			get: {
4901				me: 'people/~:(picture-url,first-name,last-name,id,formatted-name,email-address)',
4902
4903				// See: http://developer.linkedin.com/documents/get-network-updates-and-statistics-api
4904				'me/share': 'people/~/network/updates?count=@{limit|250}'
4905			},
4906
4907			post: {
4908
4909				// See: https://developer.linkedin.com/documents/api-requests-json
4910				'me/share': function(p, callback) {
4911					var data = {
4912						visibility: {
4913							code: 'anyone'
4914						}
4915					};
4916
4917					if (p.data.id) {
4918
4919						data.attribution = {
4920							share: {
4921								id: p.data.id
4922							}
4923						};
4924
4925					}
4926					else {
4927						data.comment = p.data.message;
4928						if (p.data.picture && p.data.link) {
4929							data.content = {
4930								'submitted-url': p.data.link,
4931								'submitted-image-url': p.data.picture
4932							};
4933						}
4934					}
4935
4936					p.data = JSON.stringify(data);
4937
4938					callback('people/~/shares?format=json');
4939				},
4940
4941				'me/like': like
4942			},
4943
4944			del:{
4945				'me/like': like
4946			},
4947
4948			wrap: {
4949				me: function(o) {
4950					formatError(o);
4951					formatUser(o);
4952					return o;
4953				},
4954
4955				'me/friends': formatFriends,
4956				'me/following': formatFriends,
4957				'me/followers': formatFriends,
4958				'me/share': function(o) {
4959					formatError(o);
4960					paging(o);
4961					if (o.values) {
4962						o.data = o.values.map(formatUser);
4963						o.data.forEach(function(item) {
4964							item.message = item.headline;
4965						});
4966
4967						delete o.values;
4968					}
4969
4970					return o;
4971				},
4972
4973				'default': function(o, headers) {
4974					formatError(o);
4975					empty(o, headers);
4976					paging(o);
4977				}
4978			},
4979
4980			jsonp: function(p, qs) {
4981				formatQuery(qs);
4982				if (p.method === 'get') {
4983					qs.format = 'jsonp';
4984					qs['error-callback'] = p.callbackID;
4985				}
4986			},
4987
4988			xhr: function(p, qs) {
4989				if (p.method !== 'get') {
4990					formatQuery(qs);
4991					p.headers['Content-Type'] = 'application/json';
4992
4993					// Note: x-li-format ensures error responses are not returned in XML
4994					p.headers['x-li-format'] = 'json';
4995					p.proxy = true;
4996					return true;
4997				}
4998
4999				return false;
5000			}
5001		}
5002	});
5003
5004	function formatError(o) {
5005		if (o && 'errorCode' in o) {
5006			o.error = {
5007				code: o.status,
5008				message: o.message
5009			};
5010		}
5011	}
5012
5013	function formatUser(o) {
5014		if (o.error) {
5015			return;
5016		}
5017
5018		o.first_name = o.firstName;
5019		o.last_name = o.lastName;
5020		o.name = o.formattedName || (o.first_name + ' ' + o.last_name);
5021		o.thumbnail = o.pictureUrl;
5022		o.email = o.emailAddress;
5023		return o;
5024	}
5025
5026	function formatFriends(o) {
5027		formatError(o);
5028		paging(o);
5029		if (o.values) {
5030			o.data = o.values.map(formatUser);
5031			delete o.values;
5032		}
5033
5034		return o;
5035	}
5036
5037	function paging(res) {
5038		if ('_count' in res && '_start' in res && (res._count + res._start) < res._total) {
5039			res.paging = {
5040				next: '?start=' + (res._start + res._count) + '&count=' + res._count
5041			};
5042		}
5043	}
5044
5045	function empty(o, headers) {
5046		if (JSON.stringify(o) === '{}' && headers.statusCode === 200) {
5047			o.success = true;
5048		}
5049	}
5050
5051	function formatQuery(qs) {
5052		// LinkedIn signs requests with the parameter 'oauth2_access_token'
5053		// ... yeah another one who thinks they should be different!
5054		if (qs.access_token) {
5055			qs.oauth2_access_token = qs.access_token;
5056			delete qs.access_token;
5057		}
5058	}
5059
5060	function like(p, callback) {
5061		p.headers['x-li-format'] = 'json';
5062		var id = p.data.id;
5063		p.data = (p.method !== 'delete').toString();
5064		p.method = 'put';
5065		callback('people/~/network/updates/key=' + id + '/is-liked');
5066	}
5067
5068})(hello);
5069
5070// See: https://developers.soundcloud.com/docs/api/reference
5071(function(hello) {
5072
5073	hello.init({
5074
5075		soundcloud: {
5076			name: 'SoundCloud',
5077
5078			oauth: {
5079				version: 2,
5080				auth: 'https://soundcloud.com/connect',
5081				grant: 'https://soundcloud.com/oauth2/token'
5082			},
5083
5084			// Request path translated
5085			base: 'https://api.soundcloud.com/',
5086			get: {
5087				me: 'me.json',
5088
5089				// Http://developers.soundcloud.com/docs/api/reference#me
5090				'me/friends': 'me/followings.json',
5091				'me/followers': 'me/followers.json',
5092				'me/following': 'me/followings.json',
5093
5094				// See: http://developers.soundcloud.com/docs/api/reference#activities
5095				'default': function(p, callback) {
5096
5097					// Include '.json at the end of each request'
5098					callback(p.path + '.json');
5099				}
5100			},
5101
5102			// Response handlers
5103			wrap: {
5104				me: function(o) {
5105					formatUser(o);
5106					return o;
5107				},
5108
5109				'default': function(o) {
5110					if (Array.isArray(o)) {
5111						o = {
5112							data: o.map(formatUser)
5113						};
5114					}
5115
5116					paging(o);
5117					return o;
5118				}
5119			},
5120
5121			xhr: formatRequest,
5122			jsonp: formatRequest
5123		}
5124	});
5125
5126	function formatRequest(p, qs) {
5127		// Alter the querystring
5128		var token = qs.access_token;
5129		delete qs.access_token;
5130		qs.oauth_token = token;
5131		qs['_status_code_map[302]'] = 200;
5132		return true;
5133	}
5134
5135	function formatUser(o) {
5136		if (o.id) {
5137			o.picture = o.avatar_url;
5138			o.thumbnail = o.avatar_url;
5139			o.name = o.username || o.full_name;
5140		}
5141
5142		return o;
5143	}
5144
5145	// See: http://developers.soundcloud.com/docs/api/reference#activities
5146	function paging(res) {
5147		if ('next_href' in res) {
5148			res.paging = {
5149				next: res.next_href
5150			};
5151		}
5152	}
5153
5154})(hello);
5155
5156(function(hello) {
5157
5158	var base = 'https://api.twitter.com/';
5159
5160	hello.init({
5161
5162		twitter: {
5163
5164			// Ensure that you define an oauth_proxy
5165			oauth: {
5166				version: '1.0a',
5167				auth: base + 'oauth/authenticate',
5168				request: base + 'oauth/request_token',
5169				token: base + 'oauth/access_token'
5170			},
5171
5172			login: function(p) {
5173				// Reauthenticate
5174				// https://dev.twitter.com/oauth/reference/get/oauth/authenticate
5175				var prefix = '?force_login=true';
5176				this.oauth.auth = this.oauth.auth.replace(prefix, '') + (p.options.force ? prefix : '');
5177			},
5178
5179			base: base + '1.1/',
5180
5181			get: {
5182				me: 'account/verify_credentials.json',
5183				'me/friends': 'friends/list.json?count=@{limit|200}',
5184				'me/following': 'friends/list.json?count=@{limit|200}',
5185				'me/followers': 'followers/list.json?count=@{limit|200}',
5186
5187				// Https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline
5188				'me/share': 'statuses/user_timeline.json?count=@{limit|200}',
5189
5190				// Https://dev.twitter.com/rest/reference/get/favorites/list
5191				'me/like': 'favorites/list.json?count=@{limit|200}'
5192			},
5193
5194			post: {
5195				'me/share': function(p, callback) {
5196
5197					var data = p.data;
5198					p.data = null;
5199
5200					var status = [];
5201
5202					// Change message to status
5203					if (data.message) {
5204						status.push(data.message);
5205						delete data.message;
5206					}
5207
5208					// If link is given
5209					if (data.link) {
5210						status.push(data.link);
5211						delete data.link;
5212					}
5213
5214					if (data.picture) {
5215						status.push(data.picture);
5216						delete data.picture;
5217					}
5218
5219					// Compound all the components
5220					if (status.length) {
5221						data.status = status.join(' ');
5222					}
5223
5224					// Tweet media
5225					if (data.file) {
5226						data['media[]'] = data.file;
5227						delete data.file;
5228						p.data = data;
5229						callback('statuses/update_with_media.json');
5230					}
5231
5232					// Retweet?
5233					else if ('id' in data) {
5234						callback('statuses/retweet/' + data.id + '.json');
5235					}
5236
5237					// Tweet
5238					else {
5239						// Assign the post body to the query parameters
5240						hello.utils.extend(p.query, data);
5241						callback('statuses/update.json?include_entities=1');
5242					}
5243				},
5244
5245				// See: https://dev.twitter.com/rest/reference/post/favorites/create
5246				'me/like': function(p, callback) {
5247					var id = p.data.id;
5248					p.data = null;
5249					callback('favorites/create.json?id=' + id);
5250				}
5251			},
5252
5253			del: {
5254
5255				// See: https://dev.twitter.com/rest/reference/post/favorites/destroy
5256				'me/like': function() {
5257					p.method = 'post';
5258					var id = p.data.id;
5259					p.data = null;
5260					callback('favorites/destroy.json?id=' + id);
5261				}
5262			},
5263
5264			wrap: {
5265				me: function(res) {
5266					formatError(res);
5267					formatUser(res);
5268					return res;
5269				},
5270
5271				'me/friends': formatFriends,
5272				'me/followers': formatFriends,
5273				'me/following': formatFriends,
5274
5275				'me/share': function(res) {
5276					formatError(res);
5277					paging(res);
5278					if (!res.error && 'length' in res) {
5279						return {data: res};
5280					}
5281
5282					return res;
5283				},
5284
5285				'default': function(res) {
5286					res = arrayToDataResponse(res);
5287					paging(res);
5288					return res;
5289				}
5290			},
5291			xhr: function(p) {
5292
5293				// Rely on the proxy for non-GET requests.
5294				return (p.method !== 'get');
5295			}
5296		}
5297	});
5298
5299	function formatUser(o) {
5300		if (o.id) {
5301			if (o.name) {
5302				var m = o.name.split(' ');
5303				o.first_name = m.shift();
5304				o.last_name = m.join(' ');
5305			}
5306
5307			// See: https://dev.twitter.com/overview/general/user-profile-images-and-banners
5308			o.thumbnail = o.profile_image_url_https || o.profile_image_url;
5309		}
5310
5311		return o;
5312	}
5313
5314	function formatFriends(o) {
5315		formatError(o);
5316		paging(o);
5317		if (o.users) {
5318			o.data = o.users.map(formatUser);
5319			delete o.users;
5320		}
5321
5322		return o;
5323	}
5324
5325	function formatError(o) {
5326		if (o.errors) {
5327			var e = o.errors[0];
5328			o.error = {
5329				code: 'request_failed',
5330				message: e.message
5331			};
5332		}
5333	}
5334
5335	// Take a cursor and add it to the path
5336	function paging(res) {
5337		// Does the response include a 'next_cursor_string'
5338		if ('next_cursor_str' in res) {
5339			// See: https://dev.twitter.com/docs/misc/cursoring
5340			res.paging = {
5341				next: '?cursor=' + res.next_cursor_str
5342			};
5343		}
5344	}
5345
5346	function arrayToDataResponse(res) {
5347		return Array.isArray(res) ? {data: res} : res;
5348	}
5349
5350	/**
5351	// The documentation says to define user in the request
5352	// Although its not actually required.
5353
5354	var user_id;
5355
5356	function withUserId(callback){
5357		if(user_id){
5358			callback(user_id);
5359		}
5360		else{
5361			hello.api('twitter:/me', function(o){
5362				user_id = o.id;
5363				callback(o.id);
5364			});
5365		}
5366	}
5367
5368	function sign(url){
5369		return function(p, callback){
5370			withUserId(function(user_id){
5371				callback(url+'?user_id='+user_id);
5372			});
5373		};
5374	}
5375	*/
5376
5377})(hello);
5378
5379// Vkontakte (vk.com)
5380(function(hello) {
5381
5382	hello.init({
5383
5384		vk: {
5385			name: 'Vk',
5386
5387			// See https://vk.com/dev/oauth_dialog
5388			oauth: {
5389				version: 2,
5390				auth: 'https://oauth.vk.com/authorize',
5391				grant: 'https://oauth.vk.com/access_token'
5392			},
5393
5394			// Authorization scopes
5395			// See https://vk.com/dev/permissions
5396			scope: {
5397				email: 'email',
5398				friends: 'friends',
5399				photos: 'photos',
5400				videos: 'video',
5401				share: 'share',
5402				offline_access: 'offline'
5403			},
5404
5405			// Refresh the access_token
5406			refresh: true,
5407
5408			login: function(p) {
5409				p.qs.display = window.navigator &&
5410					window.navigator.userAgent &&
5411					/ipad|phone|phone|android/.test(window.navigator.userAgent.toLowerCase()) ? 'mobile' : 'popup';
5412			},
5413
5414			// API Base URL
5415			base: 'https://api.vk.com/method/',
5416
5417			// Map GET requests
5418			get: {
5419				me: function(p, callback) {
5420					p.query.fields = 'id,first_name,last_name,photo_max';
5421					callback('users.get');
5422				}
5423			},
5424
5425			wrap: {
5426				me: function(res, headers, req) {
5427					formatError(res);
5428					return formatUser(res, req);
5429				}
5430			},
5431
5432			// No XHR
5433			xhr: false,
5434
5435			// All requests should be JSONP as of missing CORS headers in https://api.vk.com/method/*
5436			jsonp: true,
5437
5438			// No form
5439			form: false
5440		}
5441	});
5442
5443	function formatUser(o, req) {
5444
5445		if (o !== null && 'response' in o && o.response !== null && o.response.length) {
5446			o = o.response[0];
5447			o.id = o.uid;
5448			o.thumbnail = o.picture = o.photo_max;
5449			o.name = o.first_name + ' ' + o.last_name;
5450
5451			if (req.authResponse && req.authResponse.email !== null)
5452				o.email = req.authResponse.email;
5453		}
5454
5455		return o;
5456	}
5457
5458	function formatError(o) {
5459
5460		if (o.error) {
5461			var e = o.error;
5462			o.error = {
5463				code: e.error_code,
5464				message: e.error_msg
5465			};
5466		}
5467	}
5468
5469})(hello);
5470
5471(function(hello) {
5472
5473	hello.init({
5474		windows: {
5475			name: 'Windows live',
5476
5477			// REF: http://msdn.microsoft.com/en-us/library/hh243641.aspx
5478			oauth: {
5479				version: 2,
5480				auth: 'https://login.live.com/oauth20_authorize.srf',
5481				grant: 'https://login.live.com/oauth20_token.srf'
5482			},
5483
5484			// Refresh the access_token once expired
5485			refresh: true,
5486
5487			logout: function() {
5488				return 'http://login.live.com/oauth20_logout.srf?ts=' + (new Date()).getTime();
5489			},
5490
5491			// Authorization scopes
5492			scope: {
5493				basic: 'wl.signin,wl.basic',
5494				email: 'wl.emails',
5495				birthday: 'wl.birthday',
5496				events: 'wl.calendars',
5497				photos: 'wl.photos',
5498				videos: 'wl.photos',
5499				friends: 'wl.contacts_emails',
5500				files: 'wl.skydrive',
5501				publish: 'wl.share',
5502				publish_files: 'wl.skydrive_update',
5503				share: 'wl.share',
5504				create_event: 'wl.calendars_update,wl.events_create',
5505				offline_access: 'wl.offline_access'
5506			},
5507
5508			// API base URL
5509			base: 'https://apis.live.net/v5.0/',
5510
5511			// Map GET requests
5512			get: {
5513
5514				// Friends
5515				me: 'me',
5516				'me/friends': 'me/friends',
5517				'me/following': 'me/contacts',
5518				'me/followers': 'me/friends',
5519				'me/contacts': 'me/contacts',
5520
5521				'me/albums': 'me/albums',
5522
5523				// Include the data[id] in the path
5524				'me/album': '@{id}/files',
5525				'me/photo': '@{id}',
5526
5527				// Files
5528				'me/files': '@{parent|me/skydrive}/files',
5529				'me/folders': '@{id|me/skydrive}/files',
5530				'me/folder': '@{id|me/skydrive}/files'
5531			},
5532
5533			// Map POST requests
5534			post: {
5535				'me/albums': 'me/albums',
5536				'me/album': '@{id}/files/',
5537
5538				'me/folders': '@{id|me/skydrive/}',
5539				'me/files': '@{parent|me/skydrive}/files'
5540			},
5541
5542			// Map DELETE requests
5543			del: {
5544				// Include the data[id] in the path
5545				'me/album': '@{id}',
5546				'me/photo': '@{id}',
5547				'me/folder': '@{id}',
5548				'me/files': '@{id}'
5549			},
5550
5551			wrap: {
5552				me: formatUser,
5553
5554				'me/friends': formatFriends,
5555				'me/contacts': formatFriends,
5556				'me/followers': formatFriends,
5557				'me/following': formatFriends,
5558				'me/albums': formatAlbums,
5559				'me/photos': formatDefault,
5560				'default': formatDefault
5561			},
5562
5563			xhr: function(p) {
5564				if (p.method !== 'get' && p.method !== 'delete' && !hello.utils.hasBinary(p.data)) {
5565
5566					// Does this have a data-uri to upload as a file?
5567					if (typeof (p.data.file) === 'string') {
5568						p.data.file = hello.utils.toBlob(p.data.file);
5569					}
5570					else {
5571						p.data = JSON.stringify(p.data);
5572						p.headers = {
5573							'Content-Type': 'application/json'
5574						};
5575					}
5576				}
5577
5578				return true;
5579			},
5580
5581			jsonp: function(p) {
5582				if (p.method !== 'get' && !hello.utils.hasBinary(p.data)) {
5583					p.data.method = p.method;
5584					p.method = 'get';
5585				}
5586			}
5587		}
5588	});
5589
5590	function formatDefault(o) {
5591		if ('data' in o) {
5592			o.data.forEach(function(d) {
5593				if (d.picture) {
5594					d.thumbnail = d.picture;
5595				}
5596
5597				if (d.images) {
5598					d.pictures = d.images
5599						.map(formatImage)
5600						.sort(function(a, b) {
5601							return a.width - b.width;
5602						});
5603				}
5604			});
5605		}
5606
5607		return o;
5608	}
5609
5610	function formatImage(image) {
5611		return {
5612			width: image.width,
5613			height: image.height,
5614			source: image.source
5615		};
5616	}
5617
5618	function formatAlbums(o) {
5619		if ('data' in o) {
5620			o.data.forEach(function(d) {
5621				d.photos = d.files = 'https://apis.live.net/v5.0/' + d.id + '/photos';
5622			});
5623		}
5624
5625		return o;
5626	}
5627
5628	function formatUser(o, headers, req) {
5629		if (o.id) {
5630			var token = req.query.access_token;
5631			if (o.emails) {
5632				o.email = o.emails.preferred;
5633			}
5634
5635			// If this is not an non-network friend
5636			if (o.is_friend !== false) {
5637				// Use the id of the user_id if available
5638				var id = (o.user_id || o.id);
5639				o.thumbnail = o.picture = 'https://apis.live.net/v5.0/' + id + '/picture?access_token=' + token;
5640			}
5641		}
5642
5643		return o;
5644	}
5645
5646	function formatFriends(o, headers, req) {
5647		if ('data' in o) {
5648			o.data.forEach(function(d) {
5649				formatUser(d, headers, req);
5650			});
5651		}
5652
5653		return o;
5654	}
5655
5656})(hello);
5657
5658(function(hello) {
5659
5660	hello.init({
5661
5662		yahoo: {
5663
5664			// Ensure that you define an oauth_proxy
5665			oauth: {
5666				version: '1.0a',
5667				auth: 'https://api.login.yahoo.com/oauth/v2/request_auth',
5668				request: 'https://api.login.yahoo.com/oauth/v2/get_request_token',
5669				token: 'https://api.login.yahoo.com/oauth/v2/get_token'
5670			},
5671
5672			// Login handler
5673			login: function(p) {
5674				// Change the default popup window to be at least 560
5675				// Yahoo does dynamically change it on the fly for the signin screen (only, what if your already signed in)
5676				p.options.popup.width = 560;
5677
5678				// Yahoo throws an parameter error if for whatever reason the state.scope contains a comma, so lets remove scope
5679				try {delete p.qs.state.scope;}
5680				catch (e) {}
5681			},
5682
5683			base: 'https://social.yahooapis.com/v1/',
5684
5685			get: {
5686				me: yql('select * from social.profile(0) where guid=me'),
5687				'me/friends': yql('select * from social.contacts(0) where guid=me'),
5688				'me/following': yql('select * from social.contacts(0) where guid=me')
5689			},
5690			wrap: {
5691				me: formatUser,
5692
5693				// Can't get IDs
5694				// It might be better to loop through the social.relationship table with has unique IDs of users.
5695				'me/friends': formatFriends,
5696				'me/following': formatFriends,
5697				'default': paging
5698			}
5699		}
5700	});
5701
5702	/*
5703		// Auto-refresh fix: bug in Yahoo can't get this to work with node-oauth-shim
5704		login : function(o){
5705			// Is the user already logged in
5706			var auth = hello('yahoo').getAuthResponse();
5707
5708			// Is this a refresh token?
5709			if(o.options.display==='none'&&auth&&auth.access_token&&auth.refresh_token){
5710				// Add the old token and the refresh token, including path to the query
5711				// See http://developer.yahoo.com/oauth/guide/oauth-refreshaccesstoken.html
5712				o.qs.access_token = auth.access_token;
5713				o.qs.refresh_token = auth.refresh_token;
5714				o.qs.token_url = 'https://api.login.yahoo.com/oauth/v2/get_token';
5715			}
5716		},
5717	*/
5718
5719	function formatError(o) {
5720		if (o && 'meta' in o && 'error_type' in o.meta) {
5721			o.error = {
5722				code: o.meta.error_type,
5723				message: o.meta.error_message
5724			};
5725		}
5726	}
5727
5728	function formatUser(o) {
5729
5730		formatError(o);
5731		if (o.query && o.query.results && o.query.results.profile) {
5732			o = o.query.results.profile;
5733			o.id = o.guid;
5734			o.last_name = o.familyName;
5735			o.first_name = o.givenName || o.nickname;
5736			var a = [];
5737			if (o.first_name) {
5738				a.push(o.first_name);
5739			}
5740
5741			if (o.last_name) {
5742				a.push(o.last_name);
5743			}
5744
5745			o.name = a.join(' ');
5746			o.email = (o.emails && o.emails[0]) ? o.emails[0].handle : null;
5747			o.thumbnail = o.image ? o.image.imageUrl : null;
5748		}
5749
5750		return o;
5751	}
5752
5753	function formatFriends(o, headers, request) {
5754		formatError(o);
5755		paging(o, headers, request);
5756		var contact;
5757		var field;
5758		if (o.query && o.query.results && o.query.results.contact) {
5759			o.data = o.query.results.contact;
5760			delete o.query;
5761
5762			if (!Array.isArray(o.data)) {
5763				o.data = [o.data];
5764			}
5765
5766			o.data.forEach(formatFriend);
5767		}
5768
5769		return o;
5770	}
5771
5772	function formatFriend(contact) {
5773		contact.id = null;
5774
5775		// #362: Reports of responses returning a single item, rather than an Array of items.
5776		// Format the contact.fields to be an array.
5777		if (contact.fields && !(contact.fields instanceof Array)) {
5778			contact.fields = [contact.fields];
5779		}
5780
5781		(contact.fields || []).forEach(function(field) {
5782			if (field.type === 'email') {
5783				contact.email = field.value;
5784			}
5785
5786			if (field.type === 'name') {
5787				contact.first_name = field.value.givenName;
5788				contact.last_name = field.value.familyName;
5789				contact.name = field.value.givenName + ' ' + field.value.familyName;
5790			}
5791
5792			if (field.type === 'yahooid') {
5793				contact.id = field.value;
5794			}
5795		});
5796	}
5797
5798	function paging(res, headers, request) {
5799
5800		// See: http://developer.yahoo.com/yql/guide/paging.html#local_limits
5801		if (res.query && res.query.count && request.options) {
5802			res.paging = {
5803				next: '?start=' + (res.query.count + (+request.options.start || 1))
5804			};
5805		}
5806
5807		return res;
5808	}
5809
5810	function yql(q) {
5811		return 'https://query.yahooapis.com/v1/yql?q=' + (q + ' limit @{limit|100} offset @{start|0}').replace(/\s/g, '%20') + '&format=json';
5812	}
5813
5814})(hello);
5815
5816// Register as anonymous AMD module
5817if (typeof define === 'function' && define.amd) {
5818	define(function() {
5819		return hello;
5820	});
5821}
5822
5823// CommonJS module for browserify
5824if (typeof module === 'object' && module.exports) {
5825	module.exports = hello;
5826}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.