diff --git a/.gitignore b/.gitignore index 4c553263..84cf3235 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ src/*.html src/**/*.html tests/specs/bundle.js .env.sh +dist/ diff --git a/build.sh b/build.sh index 5cb4a84a..e3ba5a88 100644 --- a/build.sh +++ b/build.sh @@ -6,6 +6,9 @@ version=`node -e "console.log(require('./package.json').version)"` # Year year=$(date +'%Y') +# Make dist directory if it doesn't exist +mkdir -p dist + # Set current working directory to src/ cd src/ diff --git a/dist/hello.all.js b/dist/hello.all.js deleted file mode 100644 index 392bebfc..00000000 --- a/dist/hello.all.js +++ /dev/null @@ -1,5812 +0,0 @@ -/*! hellojs v1.21.1 - (c) 2012-2026 Andrew Dodson - MIT https://adodson.com/hello.js/LICENSE */ -// ES5 Object.create -if (!Object.create) { - - // Shim, Object create - // A shim for Object.create(), it adds a prototype to a new object - Object.create = (function() { - - function F() {} - - return function(o) { - - if (arguments.length != 1) { - throw new Error('Object.create implementation only accepts one parameter.'); - } - - F.prototype = o; - return new F(); - }; - - })(); - -} - -// ES5 Object.keys -if (!Object.keys) { - Object.keys = function(o, k, r) { - r = []; - for (k in o) { - if (r.hasOwnProperty.call(o, k)) - r.push(k); - } - - return r; - }; -} -/* eslint-disable no-extend-native */ -// ES5 [].indexOf -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function(s) { - - for (var j = 0; j < this.length; j++) { - if (this[j] === s) { - return j; - } - } - - return -1; - }; -} - -// ES5 [].forEach -if (!Array.prototype.forEach) { - Array.prototype.forEach = function(fun/*, thisArg*/) { - - if (this === void 0 || this === null) { - throw new TypeError(); - } - - var t = Object(this); - var len = t.length >>> 0; - if (typeof fun !== 'function') { - throw new TypeError(); - } - - var thisArg = arguments.length >= 2 ? arguments[1] : void 0; - for (var i = 0; i < len; i++) { - if (i in t) { - fun.call(thisArg, t[i], i, t); - } - } - - return this; - }; -} - -// ES5 [].filter -if (!Array.prototype.filter) { - Array.prototype.filter = function(fun, thisArg) { - - var a = []; - this.forEach(function(val, i, t) { - if (fun.call(thisArg || void 0, val, i, t)) { - a.push(val); - } - }); - - return a; - }; -} - -// Production steps of ECMA-262, Edition 5, 15.4.4.19 -// Reference: http://es5.github.io/#x15.4.4.19 -if (!Array.prototype.map) { - - Array.prototype.map = function(fun, thisArg) { - - var a = []; - this.forEach(function(val, i, t) { - a.push(fun.call(thisArg || void 0, val, i, t)); - }); - - return a; - }; -} - -// ES5 isArray -if (!Array.isArray) { - - // Function Array.isArray - Array.isArray = function(o) { - return Object.prototype.toString.call(o) === '[object Array]'; - }; - -} - -// Test for location.assign -if (typeof window === 'object' && typeof window.location === 'object' && !window.location.assign) { - - window.location.assign = function(url) { - window.location = url; - }; - -} - -// Test for Function.bind -if (!Function.prototype.bind) { - - // MDN - // Polyfill IE8, does not support native Function.bind - Function.prototype.bind = function(b) { - - if (typeof this !== 'function') { - throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); - } - - function C() {} - - var a = [].slice; - var f = a.call(arguments, 1); - var _this = this; - var D = function() { - return _this.apply(this instanceof C ? this : b || window, f.concat(a.call(arguments))); - }; - - C.prototype = this.prototype; - D.prototype = new C(); - - return D; - }; - -} -/* eslint-enable no-extend-native */ -/** - * @hello.js - * - * HelloJS is a client side Javascript SDK for making OAuth2 logins and subsequent REST calls. - * - * @author Andrew Dodson - * @website https://adodson.com/hello.js/ - * - * @copyright Andrew Dodson, 2012 - 2015 - * @license MIT: You are free to use and modify this code for any use, on the condition that this copyright notice remains. - */ - -var hello = function(name) { - return hello.use(name); -}; - -hello.utils = { - - // Extend the first object with the properties and methods of the second - extend: function(r /*, a[, b[, ...]] */) { - const dangerousKeys = ['__proto__', 'constructor', 'prototype']; - - // Get the arguments as an array but ommit the initial item - Array.prototype.slice.call(arguments, 1).forEach(function(a) { - if (Array.isArray(r) && Array.isArray(a)) { - Array.prototype.push.apply(r, a); - } - else if (r && (r instanceof Object || typeof r === 'object') && a && (a instanceof Object || typeof a === 'object') && r !== a) { - for (var x in a) { - // Prevent prototype pollution - if (dangerousKeys.includes(x)) { - continue; - } - - r[x] = hello.utils.extend(r[x], a[x]); - } - } - else { - - if (Array.isArray(a)) { - // Clone it - a = a.slice(0); - } - - r = a; - } - }); - - return r; - } -}; - -// Core library -hello.utils.extend(hello, { - - settings: { - - // OAuth2 authentication defaults - redirect_uri: window.location.href.split('#')[0], - response_type: 'token', - display: 'popup', - state: '', - - // OAuth1 shim - // The path to the OAuth1 server for signing user requests - // Want to recreate your own? Checkout https://github.com/MrSwitch/node-oauth-shim - oauth_proxy: 'https://auth-server.herokuapp.com/proxy', - - // API timeout in milliseconds - timeout: 20000, - - // Popup Options - popup: { - resizable: 1, - scrollbars: 1, - width: 500, - height: 550 - }, - - // Default scope - // Many services require atleast a profile scope, - // HelloJS automatially includes the value of provider.scope_map.basic - // If that's not required it can be removed via hello.settings.scope.length = 0; - scope: ['basic'], - - // Scope Maps - // This is the default module scope, these are the defaults which each service is mapped too. - // By including them here it prevents the scope from being applied accidentally - scope_map: { - basic: '' - }, - - // Default service / network - default_service: null, - - // Force authentication - // When hello.login is fired. - // (null): ignore current session expiry and continue with login - // (true): ignore current session expiry and continue with login, ask for user to reauthenticate - // (false): if the current session looks good for the request scopes return the current session. - force: null, - - // Page URL - // When 'display=page' this property defines where the users page should end up after redirect_uri - // Ths could be problematic if the redirect_uri is indeed the final place, - // Typically this circumvents the problem of the redirect_url being a dumb relay page. - page_uri: window.location.href - }, - - // Service configuration objects - services: {}, - - // Use - // Define a new instance of the HelloJS library with a default service - use: function(service) { - - // Create self, which inherits from its parent - var self = Object.create(this); - - // Inherit the prototype from its parent - self.settings = Object.create(this.settings); - - // Define the default service - if (service) { - self.settings.default_service = service; - } - - // Create an instance of Events - self.utils.Event.call(self); - - return self; - }, - - // Initialize - // Define the client_ids for the endpoint services - // @param object o, contains a key value pair, service => clientId - // @param object opts, contains a key value pair of options used for defining the authentication defaults - // @param number timeout, timeout in seconds - init: function(services, options) { - - var utils = this.utils; - - if (!services) { - return this.services; - } - - // Define provider credentials - // Reformat the ID field - for (var x in services) {if (services.hasOwnProperty(x)) { - if (typeof (services[x]) !== 'object') { - services[x] = {id: services[x]}; - } - }} - - // Merge services if there already exists some - utils.extend(this.services, services); - - // Update the default settings with this one. - if (options) { - utils.extend(this.settings, options); - - // Do this immediatly incase the browser changes the current path. - if ('redirect_uri' in options) { - this.settings.redirect_uri = utils.url(options.redirect_uri).href; - } - } - - return this; - }, - - // Login - // Using the endpoint - // @param network stringify name to connect to - // @param options object (optional) {display mode, is either none|popup(default)|page, scope: email,birthday,publish, .. } - // @param callback function (optional) fired on signin - login: function() { - - // Create an object which inherits its parent as the prototype and constructs a new event chain. - var _this = this; - var utils = _this.utils; - var error = utils.error; - var {promise, reject, resolve} = utils.createDeferredPromise(); - - // Get parameters - var p = utils.args({network: 's', options: 'o', callback: 'f'}, arguments); - - // Local vars - var url; - - // Get all the custom options and store to be appended to the querystring - var qs = utils.diffKey(p.options, _this.settings); - - // Merge/override options with app defaults - var opts = p.options = utils.merge(_this.settings, p.options || {}); - - // Merge/override options with app defaults - opts.popup = utils.merge(_this.settings.popup, p.options.popup || {}); - - // Network - p.network = p.network || _this.settings.default_service; - - // Bind callback to both reject and fulfill states - promise.then(p.callback, p.callback); - - // Trigger an event on the global listener - function emit(s, value) { - hello.emit(s, value); - } - - promise.then(emit.bind(this, 'auth.login auth'), emit.bind(this, 'auth.failed auth')); - - // Is our service valid? - if (typeof (p.network) !== 'string' || !(p.network in _this.services)) { - // Trigger the default login. - // Ahh we dont have one. - reject(error('invalid_network', 'The provided network was not recognized')); - return promise; - } - - var provider = _this.services[p.network]; - - // Create a global listener to capture events triggered out of scope - var callbackId = utils.globalEvent(function(obj) { - - // The responseHandler returns a string, lets save this locally - if (obj) { - if (typeof (obj) == 'string') { - obj = JSON.parse(obj); - } - } - else { - obj = error('cancelled', 'The authentication was not completed'); - } - - // Handle these response using the local - // Trigger on the parent - if (!obj.error) { - - // Save on the parent window the new credentials - // This fixes an IE10 bug i think... atleast it does for me. - utils.store(obj.network, obj); - - // Fulfill a successful login - resolve({ - network: obj.network, - authResponse: obj - }); - } - else { - // Reject a successful login - reject(obj); - } - }); - - var redirectUri = utils.url(opts.redirect_uri).href; - - // May be a space-delimited list of multiple, complementary types - var responseType = provider.oauth.response_type || opts.response_type; - - // Fallback to token if the module hasn't defined a grant url - if (/\bcode\b/.test(responseType) && !provider.oauth.grant) { - responseType = responseType.replace(/\bcode\b/, 'token'); - } - - // Query string parameters, we may pass our own arguments to form the querystring - p.qs = utils.merge(qs, { - client_id: encodeURIComponent(provider.id), - response_type: encodeURIComponent(responseType), - redirect_uri: encodeURIComponent(redirectUri), - state: { - client_id: provider.id, - network: p.network, - display: opts.display, - callback: callbackId, - state: opts.state, - redirect_uri: redirectUri - } - }); - - // Get current session for merging scopes, and for quick auth response - var session = utils.store(p.network); - - // Scopes (authentication permisions) - // Ensure this is a string - IE has a problem moving Arrays between windows - // Append the setup scope - var SCOPE_SPLIT = /[,\s]+/; - - // Include default scope settings (cloned). - var scope = _this.settings.scope ? [_this.settings.scope.toString()] : []; - - // Extend the providers scope list with the default - var scopeMap = utils.merge(_this.settings.scope_map, provider.scope || {}); - - // Add user defined scopes... - if (opts.scope) { - scope.push(opts.scope.toString()); - } - - // Append scopes from a previous session. - // This helps keep app credentials constant, - // Avoiding having to keep tabs on what scopes are authorized - if (session && 'scope' in session && session.scope instanceof String) { - scope.push(session.scope); - } - - // Join and Split again - scope = scope.join(',').split(SCOPE_SPLIT); - - // Format remove duplicates and empty values - scope = utils.unique(scope).filter(filterEmpty); - - // Save the the scopes to the state with the names that they were requested with. - p.qs.state.scope = scope.join(','); - - // Map scopes to the providers naming convention - scope = scope.map(function(item) { - // Does this have a mapping? - return (item in scopeMap) ? scopeMap[item] : item; - }); - - // Stringify and Arrayify so that double mapped scopes are given the chance to be formatted - scope = scope.join(',').split(SCOPE_SPLIT); - - // Again... - // Format remove duplicates and empty values - scope = utils.unique(scope).filter(filterEmpty); - - // Join with the expected scope delimiter into a string - p.qs.scope = scope.join(provider.scope_delim || ','); - - // Is the user already signed in with the appropriate scopes, valid access_token? - if (opts.force === false) { - - if (session && 'access_token' in session && session.access_token && 'expires' in session && session.expires > ((new Date()).getTime() / 1e3)) { - // What is different about the scopes in the session vs the scopes in the new login? - var diff = utils.diff((session.scope || '').split(SCOPE_SPLIT), (p.qs.state.scope || '').split(SCOPE_SPLIT)); - if (diff.length === 0) { - - // OK trigger the callback - resolve({ - unchanged: true, - network: p.network, - authResponse: session - }); - - // Nothing has changed - return promise; - } - } - } - - // Page URL - if (opts.display === 'page' && opts.page_uri) { - // Add a page location, place to endup after session has authenticated - p.qs.state.page_uri = utils.url(opts.page_uri).href; - } - - // Bespoke - // Override login querystrings from auth_options - if ('login' in provider && typeof (provider.login) === 'function') { - // Format the paramaters according to the providers formatting function - provider.login(p); - } - - // Add OAuth to state - // Where the service is going to take advantage of the oauth_proxy - if (!/\btoken\b/.test(responseType) || - parseInt(provider.oauth.version, 10) < 2 || - (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token)) { - - // Add the oauth endpoints - p.qs.state.oauth = provider.oauth; - - // Add the proxy url - p.qs.state.oauth_proxy = opts.oauth_proxy; - - } - - // Convert state to a string - if (provider.oauth.base64_state) { - p.qs.state = window.btoa(JSON.stringify(p.qs.state)); - } - else { - p.qs.state = encodeURIComponent(JSON.stringify(p.qs.state)); - } - - // URL - if (parseInt(provider.oauth.version, 10) === 1) { - - // Turn the request to the OAuth Proxy for 3-legged auth - url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction); - } - - // Refresh token - else if (opts.display === 'none' && provider.oauth.grant && session && session.refresh_token) { - - // Add the refresh_token to the request - p.qs.refresh_token = session.refresh_token; - - // Define the request path - url = utils.qs(opts.oauth_proxy, p.qs, encodeFunction); - } - else { - url = utils.qs(provider.oauth.auth, p.qs, encodeFunction); - } - - // Broadcast this event as an auth:init - emit('auth.init', p); - - // Execute - // Trigger how we want self displayed - if (opts.display === 'none') { - // Sign-in in the background, iframe - utils.iframe(url, redirectUri); - } - - // Triggering popup? - else if (opts.display === 'popup') { - - var popup = utils.popup(url, redirectUri, opts.popup); - - var timer = setInterval(function() { - if (!popup || popup.closed) { - clearInterval(timer); - - var response = error('cancelled', 'Login has been cancelled'); - - if (!popup) { - response = error('blocked', 'Popup was blocked'); - } - - response.network = p.network; - - reject(response); - } - }, 100); - } - - else { - window.location = url; - } - - return promise; - - function encodeFunction(s) {return s;} - - function filterEmpty(s) {return !!s;} - }, - - // Remove any data associated with a given service - // @param string name of the service - // @param function callback - logout: function() { - - var _this = this; - var utils = _this.utils; - var error = utils.error; - - // Create a new promise - var {promise, resolve, reject} = utils.createDeferredPromise(); - - var p = utils.args({name: 's', options: 'o', callback: 'f'}, arguments); - - p.options = p.options || {}; - - // Add callback to events - promise.then(p.callback, p.callback); - - // Trigger an event on the global listener - function emit(s, value) { - hello.emit(s, value); - } - - promise.then(emit.bind(this, 'auth.logout auth'), emit.bind(this, 'error')); - - // Network - p.name = p.name || this.settings.default_service; - p.authResponse = utils.store(p.name); - - if (p.name && !(p.name in _this.services)) { - - reject(error('invalid_network', 'The network was unrecognized')); - - } - else if (p.name && p.authResponse) { - - // Define the callback - var callback = function(opts) { - - // Remove from the store - utils.store(p.name, null); - - // Emit events by default - resolve(hello.utils.merge({network: p.name}, opts || {})); - }; - - // Run an async operation to remove the users session - var _opts = {}; - if (p.options.force) { - var logout = _this.services[p.name].logout; - if (logout) { - // Convert logout to URL string, - // If no string is returned, then this function will handle the logout async style - if (typeof (logout) === 'function') { - logout = logout(callback, p); - } - - // If logout is a string then assume URL and open in iframe. - if (typeof (logout) === 'string') { - utils.iframe(logout); - _opts.force = null; - _opts.message = 'Logout success on providers site was indeterminate'; - } - else if (logout === undefined) { - // The callback function will handle the response. - return promise; - } - } - } - - // Remove local credentials - callback(_opts); - } - else { - reject(error('invalid_session', 'There was no session to remove')); - } - - return promise; - }, - - // Returns all the sessions that are subscribed too - // @param string optional, name of the service to get information about. - getAuthResponse: function(service) { - - // If the service doesn't exist - service = service || this.settings.default_service; - - if (!service || !(service in this.services)) { - return null; - } - - return this.utils.store(service) || null; - }, - - // Events: placeholder for the events - events: {} -}); - -// Core utilities -hello.utils.extend(hello.utils, { - - // Error - error: function(code, message) { - return { - error: { - code: code, - message: message - } - }; - }, - - // Append the querystring to a url - // @param string url - // @param object parameters - qs: function(url, params, formatFunction) { - - if (params) { - - // Set default formatting function - formatFunction = formatFunction || encodeURIComponent; - - // Override the items in the URL which already exist - for (var x in params) { - var str = '([\\?\\&])' + x + '=[^\\&]*'; - var reg = new RegExp(str); - if (url.match(reg)) { - url = url.replace(reg, '$1' + x + '=' + formatFunction(params[x])); - delete params[x]; - } - } - } - - if (!this.isEmpty(params)) { - return url + (url.indexOf('?') > -1 ? '&' : '?') + this.param(params, formatFunction); - } - - return url; - }, - - // Param - // Explode/encode the parameters of an URL string/object - // @param string s, string to decode - param: function(s, formatFunction) { - var b; - var a = {}; - var m; - - if (typeof (s) === 'string') { - - formatFunction = formatFunction || decodeURIComponent; - - m = s.replace(/^[\#\?]/, '').match(/([^=\/\&]+)=([^\&]+)/g); - if (m) { - for (var i = 0; i < m.length; i++) { - b = m[i].match(/([^=]+)=(.*)/); - a[b[1]] = formatFunction(b[2]); - } - } - - return a; - } - else { - - formatFunction = formatFunction || encodeURIComponent; - - var o = s; - - a = []; - - for (var x in o) {if (o.hasOwnProperty(x)) { - if (o.hasOwnProperty(x)) { - a.push([x, o[x] === '?' ? '?' : formatFunction(o[x])].join('=')); - } - }} - - return a.join('&'); - } - }, - - // Local storage facade - store: (function() { - - var a = ['localStorage', 'sessionStorage']; - var i = -1; - var prefix = 'test'; - - // Set LocalStorage - var localStorage; - - while (a[++i]) { - try { - // In Chrome with cookies blocked, calling localStorage throws an error - localStorage = window[a[i]]; - localStorage.setItem(prefix + i, i); - localStorage.removeItem(prefix + i); - break; - } - catch (e) { - localStorage = null; - } - } - - if (!localStorage) { - - var cache = null; - - localStorage = { - getItem: function(prop) { - prop = prop + '='; - var m = document.cookie.split(';'); - for (var i = 0; i < m.length; i++) { - var _m = m[i].replace(/(^\s+|\s+$)/, ''); - if (_m && _m.indexOf(prop) === 0) { - return _m.substr(prop.length); - } - } - - return cache; - }, - - setItem: function(prop, value) { - cache = value; - document.cookie = prop + '=' + value; - } - }; - - // Fill the cache up - cache = localStorage.getItem('hello'); - } - - function get() { - var json = {}; - try { - json = JSON.parse(localStorage.getItem('hello')) || {}; - } - catch (e) {} - - return json; - } - - function set(json) { - localStorage.setItem('hello', JSON.stringify(json)); - } - - // Check if the browser support local storage - return function(name, value, days) { - - // Local storage - var json = get(); - - if (name && value === undefined) { - return json[name] || null; - } - else if (name && value === null) { - try { - delete json[name]; - } - catch (e) { - json[name] = null; - } - } - else if (name) { - json[name] = value; - } - else { - return json; - } - - set(json); - - return json || null; - }; - - })(), - - // Create and Append new DOM elements - // @param node string - // @param attr object literal - // @param dom/string - append: function(node, attr, target) { - - var n = typeof (node) === 'string' ? document.createElement(node) : node; - - if (typeof (attr) === 'object') { - if ('tagName' in attr) { - target = attr; - } - else { - for (var x in attr) {if (attr.hasOwnProperty(x)) { - if (typeof (attr[x]) === 'object') { - for (var y in attr[x]) {if (attr[x].hasOwnProperty(y)) { - n[x][y] = attr[x][y]; - }} - } - else if (x === 'html') { - n.innerHTML = attr[x]; - } - - // IE doesn't like us setting methods with setAttribute - else if (!/^on/.test(x)) { - n.setAttribute(x, attr[x]); - } - else { - n[x] = attr[x]; - } - }} - } - } - - if (target === 'body') { - (function self() { - if (document.body) { - document.body.appendChild(n); - } - else { - setTimeout(self, 16); - } - })(); - } - else if (typeof (target) === 'object') { - target.appendChild(n); - } - else if (typeof (target) === 'string') { - document.getElementsByTagName(target)[0].appendChild(n); - } - - return n; - }, - - // An easy way to create a hidden iframe - // @param string src - iframe: function(src) { - this.append('iframe', {src: src, style: {position: 'absolute', left: '-1000px', bottom: 0, height: '1px', width: '1px'}}, 'body'); - }, - - // Recursive merge two objects into one, second parameter overides the first - // @param a array - merge: function(/* Args: a, b, c, .. n */) { - var args = Array.prototype.slice.call(arguments); - args.unshift({}); - return this.extend.apply(null, args); - }, - - // Makes it easier to assign parameters, where some are optional - // @param o object - // @param a arguments - args: function(o, args) { - - var p = {}; - var i = 0; - var t = null; - var x = null; - - // 'x' is the first key in the list of object parameters - for (x in o) {if (o.hasOwnProperty(x)) { - break; - }} - - // Passing in hash object of arguments? - // Where the first argument can't be an object - if ((args.length === 1) && (typeof (args[0]) === 'object') && o[x] != 'o!') { - - // Could this object still belong to a property? - // Check the object keys if they match any of the property keys - for (x in args[0]) {if (o.hasOwnProperty(x)) { - // Does this key exist in the property list? - if (x in o) { - // Yes this key does exist so its most likely this function has been invoked with an object parameter - // Return first argument as the hash of all arguments - return args[0]; - } - }} - } - - // Else loop through and account for the missing ones. - for (x in o) {if (o.hasOwnProperty(x)) { - - t = typeof (args[i]); - - if ((typeof (o[x]) === 'function' && o[x].test(args[i])) || (typeof (o[x]) === 'string' && ( - (o[x].indexOf('s') > -1 && t === 'string') || - (o[x].indexOf('o') > -1 && t === 'object') || - (o[x].indexOf('i') > -1 && t === 'number') || - (o[x].indexOf('a') > -1 && t === 'object') || - (o[x].indexOf('f') > -1 && t === 'function') - )) - ) { - p[x] = args[i++]; - } - - else if (typeof (o[x]) === 'string' && o[x].indexOf('!') > -1) { - return false; - } - }} - - return p; - }, - - // Returns a URL instance - url: function(path) { - - // If the path is empty - if (!path) { - return window.location; - } - - // Chrome and FireFox support new URL() to extract URL objects - else if (window.URL && URL instanceof Function && URL.length !== 0) { - return new URL(path, window.location); - } - - // Ugly shim, it works! - else { - var a = document.createElement('a'); - a.href = path; - return a.cloneNode(false); - } - }, - - diff: function(a, b) { - return b.filter(function(item) { - return a.indexOf(item) === -1; - }); - }, - - // Get the different hash of properties unique to `a`, and not in `b` - diffKey: function(a, b) { - if (a || !b) { - var r = {}; - for (var x in a) { - // Does the property not exist? - if (!(x in b)) { - r[x] = a[x]; - } - } - - return r; - } - - return a; - }, - - // Unique - // Remove duplicate and null values from an array - // @param a array - unique: function(a) { - if (!Array.isArray(a)) { return []; } - - return a.filter(function(item, index) { - // Is this the first location of item - return a.indexOf(item) === index; - }); - }, - - isEmpty: function(obj) { - - // Scalar - if (!obj) - return true; - - // Array - if (Array.isArray(obj)) { - return !obj.length; - } - else if (typeof (obj) === 'object') { - // Object - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - return false; - } - } - } - - return true; - }, - - // Create a deferred promise with externally accessible resolve/reject functions - // Returns an object with {promise, resolve, reject} - createDeferredPromise: function() { - // Use modern Promise.withResolvers if available, otherwise polyfill - if (Promise.withResolvers) { - return Promise.withResolvers(); - } - - // Polyfill for older browsers - var resolve; - var reject; - var promise = new Promise(function(res, rej) { - resolve = res; - reject = rej; - }); - - return { - promise: promise, - resolve: resolve, - reject: reject - }; - }, - - - // Event - // A contructor superclass for adding event menthods, on, off, emit. - Event: function() { - - var separator = /[\s\,]+/; - - // If this doesn't support getPrototype then we can't get prototype.events of the parent - // So lets get the current instance events, and add those to a parent property - this.parent = { - events: this.events, - findEvents: this.findEvents, - parent: this.parent, - utils: this.utils - }; - - this.events = {}; - - // On, subscribe to events - // @param evt string - // @param callback function - this.on = function(evt, callback) { - - if (callback && typeof (callback) === 'function') { - var a = evt.split(separator); - for (var i = 0; i < a.length; i++) { - - // Has this event already been fired on this instance? - this.events[a[i]] = [callback].concat(this.events[a[i]] || []); - } - } - - return this; - }; - - // Off, unsubscribe to events - // @param evt string - // @param callback function - this.off = function(evt, callback) { - - this.findEvents(evt, function(name, index) { - if (!callback || this.events[name][index] === callback) { - this.events[name][index] = null; - } - }); - - return this; - }; - - // Emit - // Triggers any subscribed events - this.emit = function(evt /*, data, ... */) { - - // Get arguments as an Array, knock off the first one - var args = Array.prototype.slice.call(arguments, 1); - args.push(evt); - - // Handler - var handler = function(name, index) { - - // Replace the last property with the event name - args[args.length - 1] = (name === '*' ? evt : name); - - // Trigger - this.events[name][index].apply(this, args); - }; - - // Find the callbacks which match the condition and call - var _this = this; - while (_this && _this.findEvents) { - - // Find events which match - _this.findEvents(evt + ',*', handler); - _this = _this.parent; - } - - return this; - }; - - // - // Easy functions - this.emitAfter = function() { - var _this = this; - var args = arguments; - setTimeout(function() { - _this.emit.apply(_this, args); - }, 0); - - return this; - }; - - this.findEvents = function(evt, callback) { - - var a = evt.split(separator); - - for (var name in this.events) {if (this.events.hasOwnProperty(name)) { - - if (a.indexOf(name) > -1) { - - for (var i = 0; i < this.events[name].length; i++) { - - // Does the event handler exist? - if (this.events[name][i]) { - // Emit on the local instance of this - callback.call(this, name, i); - } - } - } - }} - }; - - return this; - }, - - // Global Events - // Attach the callback to the window object - // Return its unique reference - globalEvent: function(callback, guid) { - // If the guid has not been supplied then create a new one. - guid = guid || '_hellojs_' + parseInt(Math.random() * 1e12, 10).toString(36); - - // Define the callback function - window[guid] = function() { - // Trigger the callback - try { - if (callback.apply(this, arguments)) { - delete window[guid]; - } - } - catch (e) { - console.error(e); - } - }; - - return guid; - }, - - // Trigger a clientside popup - // This has been augmented to support PhoneGap - popup: function(url, redirectUri, options) { - - var documentElement = document.documentElement; - - // Multi Screen Popup Positioning (http://stackoverflow.com/a/16861050) - // Credit: http://www.xtf.dk/2011/08/center-new-popup-window-even-on.html - // Fixes dual-screen position Most browsers Firefox - - if (options.height && options.top === undefined) { - var dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top; - var height = screen.height || window.innerHeight || documentElement.clientHeight; - options.top = parseInt((height - options.height) / 2, 10) + dualScreenTop; - } - - if (options.width && options.left === undefined) { - var dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left; - var width = screen.width || window.innerWidth || documentElement.clientWidth; - options.left = parseInt((width - options.width) / 2, 10) + dualScreenLeft; - } - - // Convert options into an array - var optionsArray = []; - Object.keys(options).forEach(function(name) { - var value = options[name]; - optionsArray.push(name + (value !== null ? '=' + value : '')); - }); - - // Call the open() function with the initial path - // - // OAuth redirect, fixes URI fragments from being lost in Safari - // (URI Fragments within 302 Location URI are lost over HTTPS) - // Loading the redirect.html before triggering the OAuth Flow seems to fix it. - // - // Firefox decodes URL fragments when calling location.hash. - // - This is bad if the value contains break points which are escaped - // - Hence the url must be encoded twice as it contains breakpoints. - if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) { - url = redirectUri + '#oauth_redirect=' + encodeURIComponent(encodeURIComponent(url)); - } - - var popup = window.open( - url, - '_blank', - optionsArray.join(',') - ); - - if (popup && popup.focus) { - popup.focus(); - } - - return popup; - }, - - // OAuth and API response handler - responseHandler: function(window, parent) { - - var _this = this; - var p; - var location = window.location; - - // Is this an auth relay message which needs to call the proxy? - p = _this.param(location.search); - - // OAuth2 or OAuth1 server response? - if (p && p.state && (p.code || p.oauth_token)) { - - try { - var state = JSON.parse(p.state); - - // Add this path as the redirect_uri - p.redirect_uri = state.redirect_uri || location.href.replace(/[\?\#].*$/, ''); - - // Redirect to the host - var path = _this.qs(state.oauth_proxy, p); - - - if (isValidUrl(path)) { - location.assign(path); - } - - return; - } - catch (e) { - console.error('Could not decode state parameter', e); - return; - } - } - - // Save session, from redirected authentication - // #access_token has come in? - // - // FACEBOOK is returning auth errors within as a query_string... thats a stickler for consistency. - // SoundCloud is the state in the querystring and the token in the hashtag, so we'll mix the two together - - p = _this.merge(_this.param(location.search || ''), _this.param(location.hash || '')); - - // If p.state - if (p && 'state' in p) { - - // Remove any addition information - // E.g. p.state = 'facebook.page'; - try { - var a = JSON.parse(p.state); - _this.extend(p, a); - } - catch (e) { - var stateDecoded = decodeURIComponent(p.state); - try { - var b = JSON.parse(stateDecoded); - _this.extend(p, b); - } - catch (e) { - console.error('Could not decode state parameter'); - } - } - - // Access_token? - if (('access_token' in p && p.access_token) && p.network) { - - if (!p.expires_in || parseInt(p.expires_in, 10) === 0) { - // If p.expires_in is unset, set to 0 - p.expires_in = 0; - } - - p.expires_in = parseInt(p.expires_in, 10); - p.expires = ((new Date()).getTime() / 1e3) + (p.expires_in || (60 * 60 * 24 * 365)); - - // Lets use the "state" to assign it to one of our networks - authCallback(p, window, parent); - } - - // Error=? - // &error_description=? - // &state=? - else if (('error' in p && p.error) && p.network) { - - p.error = { - code: p.error, - message: p.error_message || p.error_description - }; - - // Let the state handler handle it - authCallback(p, window, parent); - } - - // API call, or a cancelled login - // Result is serialized JSON string - else if (p.callback && p.callback in parent) { - - // Trigger a function in the parent - var res = 'result' in p && p.result ? JSON.parse(p.result) : false; - - // Trigger the callback on the parent - callback(parent, p.callback)(res); - closeWindow(); - } - - // If this page is still open - if (p.page_uri && isValidUrl(p.page_uri)) { - location.assign(p.page_uri); - } - } - - // OAuth redirect, fixes URI fragments from being lost in Safari - // (URI Fragments within 302 Location URI are lost over HTTPS) - // Loading the redirect.html before triggering the OAuth Flow seems to fix it. - else if ('oauth_redirect' in p) { - var url = decodeURIComponent(p.oauth_redirect); - - if (isValidUrl(url)) { - location.assign(url); - } - - return; - } - - function isValidUrl(url) { - var regexp = /^https?:/; - return regexp.test(url) - - // If `HELLOJS_REDIRECT_URL` is defined in the window context, validate that the URL matches it. - && ( - !Object.prototype.hasOwnProperty.call(window, 'HELLOJS_REDIRECT_URL') - || - url.match(window.HELLOJS_REDIRECT_URL) - ); - } - - // Trigger a callback to authenticate - function authCallback(obj, window, parent) { - - var cb = obj.callback; - var network = obj.network; - - // Trigger the callback on the parent - _this.store(network, obj); - - // If this is a page request it has no parent or opener window to handle callbacks - if (('display' in obj) && obj.display === 'page') { - return; - } - - // Remove from session object - if (parent && cb && cb in parent) { - - try { - delete obj.callback; - } - catch (e) {} - - // Update store - _this.store(network, obj); - - // Call the globalEvent function on the parent - // It's safer to pass back a string to the parent, - // Rather than an object/array (better for IE8) - var str = JSON.stringify(obj); - - try { - callback(parent, cb)(str); - } - catch (e) { - // Error thrown whilst executing parent callback - } - } - - closeWindow(); - } - - function callback(parent, callbackID) { - if (callbackID.indexOf('_hellojs_') !== 0) { - return function() { - throw 'Could not execute callback ' + callbackID; - }; - } - - return parent[callbackID]; - } - - function closeWindow() { - - if (window.frameElement) { - // Inside an iframe, remove from parent - parent.document.body.removeChild(window.frameElement); - } - else { - // Close this current window - try { - window.close(); - } - catch (e) {} - - // IOS bug wont let us close a popup if still loading - if (window.addEventListener) { - window.addEventListener('load', function() { - window.close(); - }); - } - } - - } - } -}); - -// Events -// Extend the hello object with its own event instance -hello.utils.Event.call(hello); - -/////////////////////////////////// -// Monitoring session state -// Check for session changes -/////////////////////////////////// - -(function(hello) { - - // Monitor for a change in state and fire - var oldSessions = {}; - - // Hash of expired tokens - var expired = {}; - - // Listen to other triggers to Auth events, use these to update this - hello.on('auth.login, auth.logout', function(auth) { - if (auth && typeof (auth) === 'object' && auth.network) { - oldSessions[auth.network] = hello.utils.store(auth.network) || {}; - } - }); - - (function self() { - - var CURRENT_TIME = ((new Date()).getTime() / 1e3); - var emit = function(eventName) { - hello.emit('auth.' + eventName, { - network: name, - authResponse: session - }); - }; - - // Loop through the services - for (var name in hello.services) {if (hello.services.hasOwnProperty(name)) { - - if (!hello.services[name].id) { - // We haven't attached an ID so dont listen. - continue; - } - - // Get session - var session = hello.utils.store(name) || {}; - var provider = hello.services[name]; - var oldSess = oldSessions[name] || {}; - - // Listen for globalEvents that did not get triggered from the child - if (session && 'callback' in session) { - - // To do remove from session object... - var cb = session.callback; - try { - delete session.callback; - } - catch (e) {} - - // Update store - // Removing the callback - hello.utils.store(name, session); - - // Emit global events - try { - window[cb](session); - } - catch (e) {} - } - - // Refresh token - if (session && ('expires' in session) && session.expires < CURRENT_TIME) { - - // If auto refresh is possible - // Either the browser supports - var refresh = provider.refresh || session.refresh_token; - - // Has the refresh been run recently? - if (refresh && (!(name in expired) || expired[name] < CURRENT_TIME)) { - // Try to resignin - hello.emit('notice', name + ' has expired trying to resignin'); - hello.login(name, {display: 'none', force: false}); - - // Update expired, every 10 minutes - expired[name] = CURRENT_TIME + 600; - } - - // Does this provider not support refresh - else if (!refresh && !(name in expired)) { - // Label the event - emit('expired'); - expired[name] = true; - } - - // If session has expired then we dont want to store its value until it can be established that its been updated - continue; - } - - // Has session changed? - else if (oldSess.access_token === session.access_token && - oldSess.expires === session.expires) { - continue; - } - - // Access_token has been removed - else if (!session.access_token && oldSess.access_token) { - emit('logout'); - } - - // Access_token has been created - else if (session.access_token && !oldSess.access_token) { - emit('login'); - } - - // Access_token has been updated - else if (session.expires !== oldSess.expires) { - emit('update'); - } - - // Updated stored session - oldSessions[name] = session; - - // Remove the expired flags - if (name in expired) { - delete expired[name]; - } - }} - - // Check error events - setTimeout(self, 1000); - })(); - -})(hello); - -// EOF CORE lib -////////////////////////////////// - -///////////////////////////////////////// -// API -// @param path string -// @param query object (optional) -// @param method string (optional) -// @param data object (optional) -// @param timeout integer (optional) -// @param callback function (optional) - -hello.api = function() { - - // Shorthand - var _this = this; - var utils = _this.utils; - var error = utils.error; - - // Construct a new Promise object with external resolvers - var {promise, resolve, reject} = utils.createDeferredPromise(); - - // Arguments - var p = utils.args({path: 's!', query: 'o', method: 's', data: 'o', timeout: 'i', callback: 'f'}, arguments); - - // Method - p.method = (p.method || 'get').toLowerCase(); - - // Headers - p.headers = p.headers || {}; - - // Query - p.query = p.query || {}; - - // If get, put all parameters into query - if (p.method === 'get' || p.method === 'delete') { - utils.extend(p.query, p.data); - p.data = {}; - } - - var data = p.data = p.data || {}; - - // Completed event callback - promise.then(p.callback, p.callback); - - // Remove the network from path, e.g. facebook:/me/friends - // Results in { network : facebook, path : me/friends } - if (!p.path) { - reject(error('invalid_path', 'Missing the path parameter from the request')); - return promise; - } - - p.path = p.path.replace(/^\/+/, ''); - var a = (p.path.split(/[\/\:]/, 2) || [])[0].toLowerCase(); - - if (a in _this.services) { - p.network = a; - var reg = new RegExp('^' + a + ':?\/?'); - p.path = p.path.replace(reg, ''); - } - - // Network & Provider - // Define the network that this request is made for - p.network = _this.settings.default_service = p.network || _this.settings.default_service; - var o = _this.services[p.network]; - - // INVALID - // Is there no service by the given network name? - if (!o) { - reject(error('invalid_network', 'Could not match the service requested: ' + p.network)); - return promise; - } - - // PATH - // As long as the path isn't flagged as unavaiable, e.g. path == false - - if (!(!(p.method in o) || !(p.path in o[p.method]) || o[p.method][p.path] !== false)) { - reject(error('invalid_path', 'The provided path is not available on the selected network')); - return promise; - } - - // PROXY - // OAuth1 calls always need a proxy - - if (!p.oauth_proxy) { - p.oauth_proxy = _this.settings.oauth_proxy; - } - - if (!('proxy' in p)) { - p.proxy = p.oauth_proxy && o.oauth && parseInt(o.oauth.version, 10) === 1; - } - - // TIMEOUT - // Adopt timeout from global settings by default - - if (!('timeout' in p)) { - p.timeout = _this.settings.timeout; - } - - // Format response - // Whether to run the raw response through post processing. - if (!('formatResponse' in p)) { - p.formatResponse = true; - } - - // Get the current session - // Append the access_token to the query - p.authResponse = _this.getAuthResponse(p.network); - if (p.authResponse && p.authResponse.access_token) { - p.query.access_token = p.authResponse.access_token; - } - - var url = p.path; - var m; - - // Store the query as options - // This is used to populate the request object before the data is augmented by the prewrap handlers. - p.options = utils.clone(p.query); - - // Clone the data object - // Prevent this script overwriting the data of the incoming object. - // Ensure that everytime we run an iteration the callbacks haven't removed some data - p.data = utils.clone(data); - - // URL Mapping - // Is there a map for the given URL? - var actions = o[{'delete': 'del'}[p.method] || p.method] || {}; - - // Extrapolate the QueryString - // Provide a clean path - // Move the querystring into the data - if (p.method === 'get') { - - var query = url.split(/[\?#]/)[1]; - if (query) { - utils.extend(p.query, utils.param(query)); - - // Remove the query part from the URL - url = url.replace(/\?.*?(#|$)/, '$1'); - } - } - - // Is the hash fragment defined - if ((m = url.match(/#(.+)/, ''))) { - url = url.split('#')[0]; - p.path = m[1]; - } - else if (url in actions) { - p.path = url; - url = actions[url]; - } - else if ('default' in actions) { - url = actions['default']; - } - - // Redirect Handler - // This defines for the Form+Iframe+Hash hack where to return the results too. - p.redirect_uri = _this.settings.redirect_uri; - - // Define FormatHandler - // The request can be procesed in a multitude of ways - // Here's the options - depending on the browser and endpoint - p.xhr = o.xhr; - p.jsonp = o.jsonp; - p.form = o.form; - - // Make request - if (typeof (url) === 'function') { - // Does self have its own callback? - url(p, getPath); - } - else { - // Else the URL is a string - getPath(url); - } - - return promise; - - // If url needs a base - // Wrap everything in - function getPath(url) { - - // Format the string if it needs it - url = url.replace(/\@\{([a-z\_\-]+)(\|.*?)?\}/gi, function(m, key, defaults) { - var val = defaults ? defaults.replace(/^\|/, '') : ''; - if (key in p.query) { - val = p.query[key]; - delete p.query[key]; - } - else if (p.data && key in p.data) { - val = p.data[key]; - delete p.data[key]; - } - else if (!defaults) { - reject(error('missing_attribute', 'The attribute ' + key + ' is missing from the request')); - } - - return val; - }); - - // Add base - if (!url.match(/^https?:\/\//)) { - url = o.base + url; - } - - // Define the request URL - p.url = url; - - // Make the HTTP request with the curated request object - // CALLBACK HANDLER - // @ response object - // @ statusCode integer if available - utils.request(p, function(r, headers) { - - // Is this a raw response? - if (!p.formatResponse) { - // Bad request? error statusCode or otherwise contains an error response vis JSONP? - if (typeof headers === 'object' ? (headers.statusCode >= 400) : (typeof r === 'object' && 'error' in r)) { - reject(r); - } - else { - resolve(r); - } - - return; - } - - // Should this be an object - if (r === true) { - r = {success: true}; - } - else if (!r) { - r = {}; - } - - // The delete callback needs a better response - if (p.method === 'delete') { - r = (!r || utils.isEmpty(r)) ? {success: true} : r; - } - - // FORMAT RESPONSE? - // Does self request have a corresponding formatter - if (o.wrap && ((p.path in o.wrap) || ('default' in o.wrap))) { - var wrap = (p.path in o.wrap ? p.path : 'default'); - var time = (new Date()).getTime(); - - // FORMAT RESPONSE - var b = o.wrap[wrap](r, headers, p); - - // Has the response been utterly overwritten? - // Typically self augments the existing object.. but for those rare occassions - if (b) { - r = b; - } - } - - // Is there a next_page defined in the response? - if (r && 'paging' in r && r.paging.next) { - - // Add the relative path if it is missing from the paging/next path - if (r.paging.next[0] === '?') { - r.paging.next = p.path + r.paging.next; - } - - // The relative path has been defined, lets markup the handler in the HashFragment - else { - r.paging.next += '#' + p.path; - } - } - - // Dispatch to listeners - // Emit events which pertain to the formatted response - if (!r || 'error' in r) { - reject(r); - } - else { - resolve(r); - } - }); - } -}; - -// API utilities -hello.utils.extend(hello.utils, { - - // Make an HTTP request - request: function(p, callback) { - - var _this = this; - var error = _this.error; - - // This has to go through a POST request - if (!_this.isEmpty(p.data) && !('FileList' in window) && _this.hasBinary(p.data)) { - - // Disable XHR and JSONP - p.xhr = false; - p.jsonp = false; - } - - // Check if the browser and service support CORS - var cors = this.request_cors(function() { - // If it does then run this... - return ((p.xhr === undefined) || (p.xhr && (typeof (p.xhr) !== 'function' || p.xhr(p, p.query)))); - }); - - if (cors) { - - formatUrl(p, function(url) { - - var x = _this.xhr(p.method, url, p.headers, p.data, callback); - x.onprogress = p.onprogress || null; - - // Windows Phone does not support xhr.upload, see #74 - // Feature detect - if (x.upload && p.onuploadprogress) { - x.upload.onprogress = p.onuploadprogress; - } - - }); - - return; - } - - // Clone the query object - // Each request modifies the query object and needs to be tared after each one. - var _query = p.query; - - p.query = _this.clone(p.query); - - // Assign a new callbackID - p.callbackID = _this.globalEvent(); - - // JSONP - if (p.jsonp !== false) { - - // Clone the query object - p.query.callback = p.callbackID; - - // If the JSONP is a function then run it - if (typeof (p.jsonp) === 'function') { - p.jsonp(p, p.query); - } - - // Lets use JSONP if the method is 'get' - if (p.method === 'get') { - - formatUrl(p, function(url) { - _this.jsonp(url, callback, p.callbackID, p.timeout); - }); - - return; - } - else { - // It's not compatible reset query - p.query = _query; - } - - } - - // Otherwise we're on to the old school, iframe hacks and JSONP - if (p.form !== false) { - - // Add some additional query parameters to the URL - // We're pretty stuffed if the endpoint doesn't like these - p.query.redirect_uri = p.redirect_uri; - p.query.state = JSON.stringify({callback: p.callbackID}); - - var opts; - - if (typeof (p.form) === 'function') { - - // Format the request - opts = p.form(p, p.query); - } - - if (p.method === 'post' && opts !== false) { - - formatUrl(p, function(url) { - _this.post(url, p.data, opts, callback, p.callbackID, p.timeout); - }); - - return; - } - } - - // None of the methods were successful throw an error - callback(error('invalid_request', 'There was no mechanism for handling this request')); - - return; - - // Format URL - // Constructs the request URL, optionally wraps the URL through a call to a proxy server - // Returns the formatted URL - function formatUrl(p, callback) { - - // Are we signing the request? - var sign; - - // OAuth1 - // Remove the token from the query before signing - if (p.authResponse && p.authResponse.oauth && parseInt(p.authResponse.oauth.version, 10) === 1) { - - // OAUTH SIGNING PROXY - sign = p.query.access_token; - - // Remove the access_token - delete p.query.access_token; - - // Enfore use of Proxy - p.proxy = true; - } - - // POST body to querystring - if (p.data && (p.method === 'get' || p.method === 'delete')) { - // Attach the p.data to the querystring. - _this.extend(p.query, p.data); - p.data = null; - } - - // Construct the path - var path = _this.qs(p.url, p.query); - - // Proxy the request through a server - // Used for signing OAuth1 - // And circumventing services without Access-Control Headers - if (p.proxy) { - // Use the proxy as a path - path = _this.qs(p.oauth_proxy, { - path: path, - access_token: sign || '', - - // This will prompt the request to be signed as though it is OAuth1 - then: p.proxy_response_type || (p.method.toLowerCase() === 'get' ? 'redirect' : 'proxy'), - method: p.method.toLowerCase(), - suppress_response_codes: p.suppress_response_codes || true - }); - } - - callback(path); - } - }, - - // Test whether the browser supports the CORS response - request_cors: function(callback) { - return 'withCredentials' in new XMLHttpRequest() && callback(); - }, - - // Return the type of DOM object - domInstance: function(type, data) { - var test = 'HTML' + (type || '').replace( - /^[a-z]/, - function(m) { - return m.toUpperCase(); - } - - ) + 'Element'; - - if (!data) { - return false; - } - - if (window[test]) { - return data instanceof window[test]; - } - else if (window.Element) { - return data instanceof window.Element && (!type || (data.tagName && data.tagName.toLowerCase() === type)); - } - else { - return (!(data instanceof Object || data instanceof Array || data instanceof String || data instanceof Number) && data.tagName && data.tagName.toLowerCase() === type); - } - }, - - // Create a clone of an object - clone: function(obj) { - // Does not clone DOM elements, nor Binary data, e.g. Blobs, Filelists - if (obj === null || typeof (obj) !== 'object' || obj instanceof Date || 'nodeName' in obj || this.isBinary(obj) || (typeof FormData === 'function' && obj instanceof FormData)) { - return obj; - } - - if (Array.isArray(obj)) { - // Clone each item in the array - return obj.map(this.clone.bind(this)); - } - - // But does clone everything else. - var clone = {}; - for (var x in obj) { - clone[x] = this.clone(obj[x]); - } - - return clone; - }, - - // XHR: uses CORS to make requests - xhr: function(method, url, headers, data, callback) { - - var r = new XMLHttpRequest(); - var error = this.error; - - // Binary? - var binary = false; - if (method === 'blob') { - binary = method; - method = 'GET'; - } - - method = method.toUpperCase(); - - // Xhr.responseType 'json' is not supported in any of the vendors yet. - r.onload = function(e) { - var json = r.response; - try { - json = JSON.parse(r.responseText); - } - catch (_e) { - if (r.status === 401) { - json = error('access_denied', r.statusText); - } - } - - var headers = headersToJSON(r.getAllResponseHeaders()); - headers.statusCode = r.status; - - callback(json || (method === 'GET' ? error('empty_response', 'Could not get resource') : {}), headers); - }; - - r.onerror = function(e) { - var json = r.responseText; - try { - json = JSON.parse(r.responseText); - } - catch (_e) {} - - callback(json || error('access_denied', 'Could not get resource')); - }; - - var x; - - // Should we add the query to the URL? - if (method === 'GET' || method === 'DELETE') { - data = null; - } - else if (data && typeof (data) !== 'string' && !(data instanceof FormData) && !(data instanceof File) && !(data instanceof Blob)) { - // Loop through and add formData - var f = new FormData(); - for (x in data) if (data.hasOwnProperty(x)) { - if (data[x] instanceof HTMLInputElement) { - if ('files' in data[x] && data[x].files.length > 0) { - f.append(x, data[x].files[0]); - } - } - else if (data[x] instanceof Blob) { - f.append(x, data[x], data.name); - } - else { - f.append(x, data[x]); - } - } - - data = f; - } - - // Open the path, async - r.open(method, url, true); - - if (binary) { - if ('responseType' in r) { - r.responseType = binary; - } - else { - r.overrideMimeType('text/plain; charset=x-user-defined'); - } - } - - // Set any bespoke headers - if (headers) { - for (x in headers) { - r.setRequestHeader(x, headers[x]); - } - } - - r.send(data); - - return r; - - // Headers are returned as a string - function headersToJSON(s) { - var r = {}; - var reg = /([a-z\-]+):\s?(.*);?/gi; - var m; - while ((m = reg.exec(s))) { - r[m[1]] = m[2]; - } - - return r; - } - }, - - // JSONP - // Injects a script tag into the DOM to be executed and appends a callback function to the window object - // @param string/function pathFunc either a string of the URL or a callback function pathFunc(querystringhash, continueFunc); - // @param function callback a function to call on completion; - jsonp: function(url, callback, callbackID, timeout) { - - var _this = this; - var error = _this.error; - - // Change the name of the callback - var bool = 0; - var head = document.getElementsByTagName('head')[0]; - var operaFix; - var result = error('server_error', 'server_error'); - var cb = function() { - if (!(bool++)) { - window.setTimeout(function() { - callback(result); - head.removeChild(script); - }, 0); - } - - }; - - // Add callback to the window object - callbackID = _this.globalEvent(function(json) { - result = json; - return true; - - // Mark callback as done - }, callbackID); - - // The URL is a function for some cases and as such - // Determine its value with a callback containing the new parameters of this function. - url = url.replace(new RegExp('=\\?(&|$)'), '=' + callbackID + '$1'); - - // Build script tag - var script = _this.append('script', { - id: callbackID, - name: callbackID, - src: url, - async: true, - onload: cb, - onerror: cb, - onreadystatechange: function() { - if (/loaded|complete/i.test(this.readyState)) { - cb(); - } - } - }); - - // Opera fix error - // Problem: If an error occurs with script loading Opera fails to trigger the script.onerror handler we specified - // - // Fix: - // By setting the request to synchronous we can trigger the error handler when all else fails. - // This action will be ignored if we've already called the callback handler "cb" with a successful onload event - if (window.navigator.userAgent.toLowerCase().indexOf('opera') > -1) { - operaFix = _this.append('script', { - text: 'document.getElementById(\'' + callbackID + '\').onerror();' - }); - script.async = false; - } - - // Add timeout - if (timeout) { - window.setTimeout(function() { - result = error('timeout', 'timeout'); - cb(); - }, timeout); - } - - // TODO: add fix for IE, - // However: unable recreate the bug of firing off the onreadystatechange before the script content has been executed and the value of "result" has been defined. - // Inject script tag into the head element - head.appendChild(script); - - // Append Opera Fix to run after our script - if (operaFix) { - head.appendChild(operaFix); - } - }, - - // Post - // Send information to a remote location using the post mechanism - // @param string uri path - // @param object data, key value data to send - // @param function callback, function to execute in response - post: function(url, data, options, callback, callbackID, timeout) { - - var _this = this; - var error = _this.error; - var doc = document; - - // This hack needs a form - var form = null; - var reenableAfterSubmit = []; - var newform; - var i = 0; - var x = null; - var bool = 0; - var cb = function(r) { - if (!(bool++)) { - callback(r); - } - }; - - // What is the name of the callback to contain - // We'll also use this to name the iframe - _this.globalEvent(cb, callbackID); - - // Build the iframe window - var win; - try { - // IE7 hack, only lets us define the name here, not later. - win = doc.createElement('