box-chrome-sdk
Version:
A Chrome App SDK for the Box V2 API
1,131 lines (1,077 loc) • 117 kB
JavaScript
(function() {
/**
* @fileoverview Chrome module and angular service.
* @author jmeadows
*/
angular.module('chrome', []).factory('chrome', function() {
return window.chrome;
});
})();
//----------------------------
(function() {
/**
* @fileoverview Chrome downloads module and angular service. @see https://developer.chrome.com/extensions/downloads
* @author jmeadows
*/
var downloads = angular.module('chrome.downloads', ['chrome', 'rx']);
downloads.service('chromeDownloads', ['chrome', 'rx', function(chrome, rx) {
/**
* Download a file.
* @param {Object} options Options specifying how to perform the download. See https://developer.chrome.com/extensions/downloads#method-download
* @returns {Observable} An observable containing the id of the Chrome DownloadItem started by this call to download.
*/
this.download = function(options) {
return rx.Observable.fromChromeCallback(chrome.downloads.download, null, chrome.downloads)(options);
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Chrome Identity module and angular service. @see See https://developer.chrome.com/apps/identity
* @author jmeadows
*/
var downloads = angular.module('chrome.identity', ['chrome', 'rx']);
downloads.service('chromeIdentity', ['chrome', 'rx', function(chrome, rx) {
/**
* Download a file.
* @param {Object} options Options for how to launch the web auth flow. See https://developer.chrome.com/apps/identity#method-launchWebAuthFlow
* @returns {Observable} An observable containing the id of the Chrome DownloadItem started by this call to download.
*/
this.login = function(options) {
if (angular.isDefined(chrome.identity)) {
return rx.Observable.fromChromeCallback(chrome.identity.launchWebAuthFlow, null, chrome.identity)(options);
} else {
return rx.Observable.fromChromeCallback(chrome.runtime.sendMessage, null, chrome.runtime)({
'function': 'identity.launchWebAuthFlow',
params: [options]
});
}
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview chrome.storage An Rx abstraction of the
* [chrome.storage API](https://developer.chrome.com/extensions/storage)
* @author jmeadows
*/
var storage = angular.module('chrome.storage', ['chrome', 'rx']);
storage.service('chromeStorage', ['chrome', 'rx', function(chrome, rx) {
/**
* Gets an item from the Chrome local item store.
* @param {String} name The name of the item to get from the local item store.
* @returns {Observable} An observable containing the object from the local item store with the given name.
*/
this.getLocal = function(name) {
return rx.Observable.fromChromeCallback(chrome.storage.local.get, null, chrome.storage.local)(name)
.map(function(result) {
return result[name];
});
};
/**
* Sets an item in the Chrome local item store.
* @param {Object} value The items to save in the local item store.
* @returns {Observable} An observable containing a boolean value indicating whether the storage was successful.
*/
this.setLocal = function(value) {
return rx.Observable.fromChromeCallback(chrome.storage.local.set, null, chrome.storage.local)(value);
};
/**
* Removes an item from the Chrome local item store.
* @param {String} name The name of the item to get from the local item store.
* @returns {Observable} An observable containing a boolean value indicating whether the removal was successful.
*/
this.removeLocal = function(name) {
return rx.Observable.fromChromeCallback(chrome.storage.local.remove, null, chrome.storage.local)(name);
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Module and angular service wrapping window.crypto.
* @author jmeadows
*/
angular.module('crypto', ['rx']).factory('crypto', ['rx', function(rx) {
return {
'getRandomValues': function(arr) {
return window.crypto.getRandomValues(arr);
},
'digest': function(algorithm, content) {
return rx.Observable.fromPromise(window.crypto.subtle.digest(algorithm, content));
}
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Module and angular service wrapping json.patch.
* @author jmeadows
*/
angular.module('json.patch', []).factory('jsonpatch', function() {
return window.jsonpatch;
});
})();
//----------------------------
(function() {
/**
* @fileoverview Module and angular service wrapping momentJs.
* @author jmeadows
*/
angular.module('moment', []).factory('moment', function() {
return window.moment;
});
})();
//----------------------------
(function() {
/**
* @fileoverview Extensions to RxJS for working with Chrome callbacks and events.
* @author jmeadows
*/
/**
* Converts a chrome.* API function that takes a callback to an observable sequence, raising an error if
* chrome.runtime.lastError is set.
*
* @param {Function} func Function with a callback as the last parameter to convert to an Observable sequence.
* @param {Scheduler} [scheduler] Scheduler to run the function on. If not specified, defaults to Scheduler.timeout.
* @param {Mixed} [context] The context for the func parameter to be executed. If not specified, defaults to undefined.
* @param {Function} [selector] A selector which takes the arguments from the callback to produce a single item to yield on next.
* @returns {Function} A function, when executed with the required parameters minus the callback, produces an Observable sequence with a single value of the arguments to the callback as an array.
*/
Rx.Observable.fromChromeCallback = function (func, scheduler, context, selector) {
if (!scheduler) {
scheduler = Rx.Scheduler.immediate;
}
return function () {
var args = Array.prototype.slice.call(arguments, 0);
return new Rx.AnonymousObservable(function (observer) {
return scheduler.schedule(function () {
function handler(e) {
if (chrome.runtime.lastError) {
observer.onError(chrome.runtime.lastError.message);
observer.onCompleted();
}
var results = e || [undefined];
if (selector) {
try {
results = selector(arguments);
} catch (err) {
observer.onError(err);
return;
}
} else {
if (results.length === 1) {
results = results[0];
}
}
observer.onNext(results);
observer.onCompleted();
}
args.push(handler);
func.apply(context, args);
});
});
};
};
/**
* Creates an observable sequence from an chrome.event.
* @param {Function} event The function to add a handler to the emitter.
* @param {Function} [selector] A selector which takes the arguments from the event handler to produce a single item to yield on next.
* @returns {Observable} An observable sequence which wraps an event from an event emitter
*/
Rx.Observable.fromChromeEvent = function(event, selector) {
return Rx.Observable.fromEventPattern(
function(handler) {
event.addListener(handler);
},
function(handler) {
event.removeListener(handler);
},
selector
);
};
})();
//----------------------------
(function() {
/**
* @fileoverview An Rx abstraction of Angular's $http service.
* @author jmeadows
*/
var rxHttp = angular.module('rx.http', ['rx']);
rxHttp.factory('http', ['$http', 'rx', function($http, rx) {
/**
* Creates a FormData object for sending a request that contains a file.
* @param {Object} data An object containing key-value pairs to be sent with the request.
* @returns {FormData} An object that can be sent as a multipart form request.
*/
function makeFormData(data) {
var form = new FormData();
for (var param in data) {
if (data.hasOwnProperty(param) && angular.isDefined(data[param])) {
var value = data[param];
// This param is a file - the first element is the file object and the second is the filename
if (Array.isArray(value)) {
value.unshift(param);
form.append.apply(form, value);
} else {
form.append(param, value);
}
}
}
return form;
}
/**
* Turns an Angular promise from the $http service into an Rx observable.
* @param {String} method The HTTP verb to use in making the request.
* @param {String} url The URL for the request.
* @param {Object} config Configuration object for how the request should be sent.
* @param {Object} data Data to be sent with the request.
* @returns {Observable} An observable containing the result of the HTTP request.
*/
function getObservable(method, url, config, data) {
config = config || {};
config.headers = config.headers || {};
return rx.Observable.fromPromise(
$http(angular.extend(
config,
{
url: url,
method: method
},
data ? {
data: makeFormData(data),
transformRequest: angular.identity,
headers: angular.extend(config.headers, {'Content-Type': undefined})
} : {}
)));
}
/**
* Make an HTTP request.
* @param {String} method The HTTP verb to use in making the request.
* @param {String} url The URL for the request.
* @param {Object} config Configuration object for how the request should be sent.
* @param {Object} data Data to be sent with the request.
* @returns {Observable} An observable containing the result of the HTTP request.
*/
function request(method, url, config, data) {
var observable = getObservable(method, url, config, data),
subject = new rx.AsyncSubject();
observable.subscribe(subject);
return subject.asObservable();
}
return {
getObservable: getObservable,
request: request
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview An angular module and service providing OAuth2 access to the SDK.
* @author jmeadows
*/
var auth = angular.module('box.auth', ['rx', 'box.conf', 'chrome.identity', 'crypto']);
auth.provider('boxApiAuth', [
'authUrl', 'clientId', 'clientSecret', 'redirectUri',
function(authUrl, clientId, clientSecret, redirectUri) {
var webAuthParams = {
url: authUrl + '/authorize?client_id=' + clientId + '&response_type=code&redirect_uri=' + redirectUri,
interactive: true
};
var throwError;
throwError = function() {
throw new Error('unknown error extracting code');
};
function parseWebAuthResponseWithState(state) {
return function parseWebAuthResponse(responseUrl) {
if (responseUrl) {
var error = responseUrl.match(/[&\?]error=([^&]+)/);
if (error) {
throw new Error('Error extracting code');
}
var csrfState = responseUrl.match(/[&\?]state=([\w\/\-]+)/);
if (! csrfState || csrfState[1] !== state) {
throw new Error('Returned csrf token does not match the one sent!');
}
return responseUrl.match(/[&\?]code=([\w\/\-]+)/)[1];
} else {
return throwError();
}
};
}
this.setThrowError = function(throwErrorFunc) {
throwError = throwErrorFunc;
};
var refreshObservable = null;
this.$get = ['rx', 'chromeIdentity', 'http', 'crypto', function(rx, chromeIdentity, http, crypto) {
return {
/**
* Present the user with a login form from Box, requesting their login and that they grant access to
* your application.
* @returns {Observable} An observable that will contain an authorization code if the user performs
* the login to Box successfully and grants access to your application.
*/
login: function() {
var randomArray = new Uint8Array(16);
crypto.getRandomValues(randomArray);
var state = 'box_csrf_token_' + Array.prototype.map.call(randomArray, function(i) {
var alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return alphabet.charAt(i % alphabet.length);
}).join('');
var params = JSON.parse(JSON.stringify(webAuthParams));
params.url += '&state=' + state;
return chromeIdentity.login(params).map(parseWebAuthResponseWithState(state));
},
/**
* Exchange an authorization code for an authorization token.
* @param {String} code An authorization code returned from @see login
* @returns {Observable} An observable that will contain the HTTP result from the request
* to url https://www.box.com/api/oauth2 containing an authorization token and a refresh token.
*/
getToken: function(code) {
return http.request(
'POST',
authUrl + '/token',
null,
{
/*eslint-disable camelcase*/
grant_type: 'authorization_code',
code: code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri
/*eslint-enable camelcase*/
}
);
},
/**
* Exchange a refresh token for a new authorization token.
* @param {String} refreshToken A refresh token returned from @see getToken or this function.
* @returns {Observable} An observable that will contain the HTTP result from the request
* to url https://www.box.com/api/oauth2 containing an authorization token and a refresh token.
*/
refreshToken: function(refreshToken) {
var subject = new rx.Subject();
if (refreshObservable === null) {
refreshObservable = http.request(
'POST',
authUrl + '/token',
null,
{
/*eslint-disable camelcase*/
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri
/*eslint-enable camelcase*/
}
)
.publish()
.refCount();
refreshObservable.subscribe(
angular.noop,
angular.noop,
function() {
refreshObservable = null;
}
);
}
refreshObservable.subscribe(subject);
return subject.asObservable();
}
};
}];
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Configuration information for the SDK.
* @author jmeadows
*/
var conf = angular.module('box.conf', []);
//conf.constant('clientSecret', 'uII-----------------------------');
//conf.constant('clientId', 'i3p-----------------------------');
var runtime = chrome.runtime || {};
var runtimeId = runtime.id;
conf.constant('authUrl', 'https://www.box.com/api/oauth2');
conf.constant('redirectUri', 'https://' + runtimeId + '.chromiumapp.org/provider_cb');
conf.constant('apiUrl', 'https://api.box.com/2.0');
conf.constant('uploadUrl', 'https://upload.box.com/api/2.0');
})();
//----------------------------
(function() {
/**
* @fileoverview An Rx abstraction of Angular's $http service, with authorization for the Box API.
* @author jmeadows
*/
var http = angular.module('box.http', ['rx', 'rx.http', 'box.conf', 'chrome.storage', 'crypto', 'moment']);
/**
* Injects auth token header to http requests. Automatically retrieves new tokens.
*
* If we don't have an auth token, see if we have a refresh token and try to exchange for an auth token.
* If we don't have a refresh token, we need to log in via oauth2.
* If we do have an auth token that isn't expired, append it to all outgoing requests.
* @param {Object} rx RxJS namespace object
* @param {Object} chromeStorage Chrome storage service
* @param {Object} http HTTP service
* @param {Object} boxApiAuth Box auth service
* @param {Object} moment Angular service for moment.js
* @returns {Object} Box HTTP service
*/
http.factory('boxHttp', ['rx', 'chromeStorage', 'http', 'boxApiAuth', 'moment', function(rx, chromeStorage, http, boxApiAuth, moment) {
function storeTokens(result) {
var refreshMoment = moment(), accessMoment = moment();
refreshMoment.add('days', 60);
accessMoment.add('seconds', result.expires_in);
chromeStorage.setLocal({
/*eslint-disable camelcase*/
refresh_token: {
token: result.refresh_token,
expires_at: refreshMoment.toDate().toString()
},
access_token: {
token: result.access_token,
expires_at: accessMoment.toDate().toString()
}
/*eslint-enable camelcase*/
}).subscribe(angular.noop);
}
function ejectTokens() {
chromeStorage.removeLocal('access_token')
.concat(chromeStorage.removeLocal('refresh_token'))
.subscribe(angular.noop);
}
function tryLogin() {
return boxApiAuth.login()
.flatMap(function(code) {
return boxApiAuth.getToken(code);
})
.map(function(result) {
return result.data;
})
.do(storeTokens);
}
function shouldRejectToken(data) {
return !angular.isDefined(data) || !angular.isDefined(data.token) || !angular.isDefined(data.expires_at) || moment().isAfter(moment(data.expires_at));
}
function tryRefresh(noLogin) {
return chromeStorage.getLocal('refresh_token')
.flatMap(function(data) {
function loginIfAllowed() {
if (noLogin) {
return rx.Observable.throw(new Error('Not logged in!'));
}
return tryLogin();
}
if (shouldRejectToken(data)) {
return loginIfAllowed();
}
return boxApiAuth.refreshToken(data.token)
.map(function(result) {
return result.data;
})
.onErrorResumeNext(rx.Observable.defer(loginIfAllowed).do(ejectTokens))
.take(1);
})
.do(storeTokens)
.map(function(result) {
return {
token: result.access_token
};
});
}
function tryGetAuthToken(noLogin) {
return chromeStorage.getLocal('access_token')
.flatMap(function(data) {
if (shouldRejectToken(data)) {
return tryRefresh(noLogin);
}
return rx.Observable.return(data);
})
.map(function(data) {
return data.token;
});
}
function makeRequest(obs, method, url, config, data) {
return obs
.flatMap(function(token) {
angular.extend(config.headers, {Authorization: 'Bearer ' + token});
return http.getObservable(method, url, config, data);
})
.flatMap(function(response) {
switch (response.status) {
case 401: // Unauthorized - need to login again
return makeRequest(
tryLogin().map(function(data) {
return data.access_token;
}),
method, url, config, data
);
case 202: // Download not ready yet - try it later
case 429: // Too many requests - wait some time and then try again
return makeRequest(
rx.Observable.timer(parseInt(response.headers['Retry-After'], 10) * 1000)
.flatMap(function() {
return tryGetAuthToken();
}),
method, url, config, data
);
default:
return rx.Observable.return(response.data);
}
});
}
function BoxHttp() {
}
function makeHelper(method) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
args.unshift(method);
return this.request.apply(this, args);
};
}
BoxHttp.prototype = {
request: function(method, url, config, data) {
var subject = new rx.AsyncSubject();
config = config || {};
config.headers = config.headers || this.defaultHeaders;
makeRequest(tryGetAuthToken(), method, url, config, data).subscribe(subject);
return subject.asObservable();
},
get: makeHelper('GET'),
post: makeHelper('POST'),
put: makeHelper('PUT'),
delete: makeHelper('DELETE'),
options: makeHelper('OPTIONS'),
auth: tryGetAuthToken,
defaultHeaders: {}
};
BoxHttp.prototype.constructor = BoxHttp;
return new BoxHttp();
}]);
http.factory('boxHttpResponseInterceptor',['$q', 'apiUrl', function($q, apiUrl){
return {
responseError: function(rejection) {
try {
if (rejection.config.url.indexOf(apiUrl) === 0 && (rejection.status === 401 || rejection.status === 429)) {
return rejection;
}
} catch(err) {
return $q.reject(rejection);
}
return $q.reject(rejection);
}
};
}]);
http.config(['$httpProvider',function($httpProvider) {
//Http Intercpetor to check auth failures for xhr requests
$httpProvider.interceptors.push('boxHttpResponseInterceptor');
}]);
})();
//----------------------------
(function() {
/**
* @namespace box
*/
})();
//----------------------------
(function() {
/**
* @fileoverview Box SDK public interface.
* @author jmeadows
*/
var sdk = angular.module('box.sdk', ['rx', 'chrome.storage', 'box.auth', 'box.conf', 'box.http', 'box.objects', 'box.util']);
sdk.factory('boxSdk', [
'$timeout',
'rx',
'boxHttp',
'apiUrl',
'authUrl',
'crypto',
'BoxCollaboration',
'BoxFile',
'BoxFolder',
'BoxTask',
'BoxUser',
'BoxGroup',
'getAll',
'clientId',
'clientSecret',
'chromeStorage',
'BoxComment',
'http',
'boxObjectBase',
'responseTranslator',
'BoxEvent',
function(
$timeout,
rx,
boxHttp,
apiUrl,
authUrl,
crypto,
BoxCollaboration,
BoxFile,
BoxFolder,
BoxTask,
BoxUser,
BoxGroup,
getAll,
clientId,
clientSecret,
chromeStorage,
BoxComment,
http,
boxObjectBase,
responseTranslator,
BoxEvent
) {
function Sdk() {
this.boxHttp = boxHttp;
}
Sdk.prototype = angular.extend(Object.create(boxObjectBase), {
/**
* Logs out from Box, revoking the current API auth and refresh tokens.
* @returns {Observable} An observable containing the result of the API request.
*/
logout: function() {
return chromeStorage.getLocal('refresh_token')
.flatMap(function(data) {
return chromeStorage.removeLocal('access_token')
.concat(chromeStorage.removeLocal('refresh_token'))
.concat(http.request(
'POST',
authUrl + '/revoke',
null,
{
/*eslint-disable camelcase*/
client_id: clientId,
client_secret: clientSecret,
/*eslint-enable camelcase*/
token: data.token
}
));
})
.takeLast(1);
},
/**
* Get the folder object with a given id.
* @param {String} id The Box folder id identifying the requested folder
* @returns {Observable} An observable containing the requested folder object.
*/
getFolder: function(id) {
return this.boxHttp.get(apiUrl + '/folders/' + id).map(function(result) {
return new BoxFolder(result);
});
},
/**
* Get the file object with a given id.
* @param {String} id The Box file id identifying the requested file
* @returns {Observable} An observable containing the requested file object.
*/
getFile: function(id) {
return this.boxHttp.get(apiUrl + '/files/' + id).map(function(result) {
return new BoxFile(result);
});
},
/**
* Get the folder object with a given id from the trash.
* @param {String} id The Box folder id identifying the requested trashed folder
* @returns {Observable} An observable containing the requested trashed folder object.
*/
getTrashedFolder: function(id) {
return this.boxHttp.get(apiUrl + '/folders/' + id + '/trash').map(function(result) {
return new BoxFolder(result);
});
},
/**
* Get the file object with a given id from the trash.
* @param {String} id The Box file id identifying the requested trashed file
* @returns {Observable} An observable containing the requested trashed file object.
*/
getTrashedFile: function(id) {
return this.boxHttp.get(apiUrl + '/files/' + id + '/trash').map(function(result) {
return new BoxFile(result);
});
},
/**
* Get a list of all items in the trash.
* @param {String} fields A comma separated list of fields that should be returned for each trashed item
* @returns {Observable} An observable sequence of files and/or folders that are in the trash.
*/
getTrashedItems: function(fields) {
var boxHttp = this.boxHttp;
return getAll(
function(limit, offset) {
return boxHttp.get(apiUrl + '/folders/trash/items', {
fields: fields,
limit: limit,
offset: offset
});
},
responseTranslator.translateResponse
);
},
/**
* Get a list of all pending collaborations.
* @returns {Observable} An observable sequence of collaboration objects that are still pending.
*/
getPendingCollaborations: function() {
return this.boxHttp.get(apiUrl + '/collaborations?pending').flatMap(function(result) {
return rx.Observable.fromArray(result.entries.map(function(entry) {
return new BoxCollaboration(entry);
}));
});
},
/**
* Search Box for content. Read more here [http://developers.box.com/docs/#search].
* @param {String} query The string to search for
* @param {Object} params Specifies how the query will be executed.
* @returns {Observable} An observable sequence of files and folders.
*/
search: function(query, params) {
var boxHttp = this.boxHttp;
return getAll(
function(limit, offset) {
return boxHttp.get(apiUrl + '/search', {
params: angular.extend(params || {}, {query: query, limit: limit, offset: offset})
});
},
responseTranslator.translateResponse
);
},
/**
* Create a new task.
* @param {File|Object} item The file the task will be associated with.
* @param {Object} params Can include message (string) and/or due_at (timestamp).
* @returns {Observable} An observable containing the newly created Task.
*/
createTask: function(item, params) {
return this.boxHttp.post(apiUrl + '/tasks', null,
angular.extend(params, {
item: {
type: 'file',
id: item.id
},
action: 'review'
})
).map(function(result) {
return new BoxTask(result);
});
},
/**
* Gets information about the logged-in user.
* @returns {Observable} An observable containing the User object for the logged-in user.
*/
getUserInfo: function() {
return this.boxHttp.get(apiUrl + '/users/me').map(function(result) {
return new BoxUser(result);
});
},
/**
* Gets all users in the current user's enterprise if the current user is an enterprise admin.
* @param {String} filter A string used to filter the results to only users starting with the filter in either the name or the login
* @returns {Observable} An observable stream of user objects in the current enterprise.
*/
getUsers: function(filter) {
var boxHttp = this.boxHttp;
return getAll(
function(limit, offset) {
return boxHttp.get(apiUrl + '/users', {
/*eslint-disable camelcase*/
params: { filter_term: filter, limit: limit, offset: offset}
/*eslint-enable camelcase*/
});
},
function(result) {
return new BoxUser(result);
}
);
},
/**
* Creates a new enterprise user if the current user is an enterprise admin.
* @param {Object} params A hash of properties for the user. Must contain at least login and name.
* @returns {Observable} An observable containing the new user object.
*/
createUser: function(params) {
return this.boxHttp.post(apiUrl + '/users', null, params)
.map(function(result) {
return new BoxUser(result);
});
},
/**
* Get all of the groups for the logged-in user.
* @returns {Observable} An observable sequence of groups for the logged-in user.
*/
getGroups: function() {
return this.boxHttp.get(apiUrl + '/groups').flatMap(function(result) {
return rx.Observable.fromArray(result.entries.map(function(entry) {
return new BoxGroup(entry);
}));
});
},
/**
* Create a new group.
* @param {String} name The name for the new group
* @returns {Observable} An observable containing the newly created group.
*/
createGroup: function(name) {
return this.boxHttp.post(apiUrl + '/groups', null, {name: name})
.map(function(result) {
return new BoxGroup(result);
});
},
/**
* Subscribe to events for the current user.
* @param {String} streamPosition The stream position from which to start streaming events.
* @returns {Observable} An observable sequence of BoxEvent objects. Disposing of this sequence unsubscribes.
*/
subscribeToEvents: function(streamPosition) {
var boxHttp = this.boxHttp;
function subscribe(streamPosition) {
return boxHttp.options(apiUrl + '/events')
.flatMap(function(result) {
return boxHttp.get(result.entries[0].url + '&stream_position=' + streamPosition)
.flatMap(function(response) {
if (response.message === 'new_change') {
var chunk = -1;
return rx.Observable.while(
function() {
return chunk !== 0;
},
rx.Observable.defer(function() {
return rx.Observable.return(streamPosition);
})
.flatMap(function(position) {
return boxHttp.get(apiUrl + '/events?stream_position=' + position)
.do(function(result) {
chunk = result.chunk_size;
streamPosition = result.next_stream_position;
})
.flatMap(function(result) {
return rx.Observable.fromArray(result.entries)
.map(function(entry) {
return new BoxEvent(entry, result.next_stream_position);
});
});
})
);
} else if (response.message === 'reconnect') {
return rx.Observable.empty();
} else {
return rx.Observable.empty();
}
})
.timeout(result.entries[0].retry_timeout * 1000, rx.Observable.defer(function() {
return rx.Observable.empty();
}))
.map(function(result) {
return {
result: result,
streamPosition: streamPosition
};
});
});
}
var obs = rx.Observable.return(streamPosition);
if (!angular.isDefined(streamPosition)) {
obs = boxHttp.get(apiUrl + '/events?stream_position=now')
.map(function(result) {
return result.next_stream_position;
});
}
var subject = new rx.ReplaySubject();
obs.subscribe(function(streamPosition) {
var currentStreamPosition = streamPosition;
function sub() {
subscribe(currentStreamPosition)
.do(function(result) {
currentStreamPosition = result.streamPosition;
})
.subscribe(
function onNext(result) {
subject.onNext(result.result);
},
function onError(error) {
subject.onError(error);
},
function onCompleted() {
$timeout(sub, 1);
}
);
}
sub();
});
return subject.asObservable();
}
});
return new Sdk();
}]);
sdk.config(['boxApiAuthProvider', function(boxApiAuthProvider) {
boxApiAuthProvider.setThrowError(function() {
throw chrome.runtime.lastError;
});
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Utility functions needed by the SDK.
* @author jmeadows
*/
var util = angular.module('box.util', ['rx']);
util.provider('getAll', function() {
var defaultLimit = 20;
this.setDefaultLimit = function(limit) {
defaultLimit = limit;
};
this.$get = ['rx', function(rx) {
/**
* For a paginated API, get all results.
* @param {Function} observableFactory A function that can create a request given a limit and offset.
* @param {Function} entryTranslator A function that can turn JSON responses into Box objects.
* @returns {Observable} An observable sequence of results.
*/
return function(observableFactory, entryTranslator) {
var limit = defaultLimit, offset = 0, total = -1;
return rx.Observable.while(
function() {
return total === -1 || total > offset;
},
rx.Observable.defer(function() {
return rx.Observable.return({
limit: limit,
offset: offset
});
})
.flatMap(function(params) {
return observableFactory(params.limit, params.offset)
.do(function(result) {
offset += limit;
total = result.total_count;
})
.flatMap(function(result) {
return rx.Observable.fromArray(
result.entries
.map(entryTranslator)
.filter(function(entry) {
return entry !== null;
})
);
});
})
);
};
}];
});
util.service('responseTranslator', [function() {
var translators = [];
this.registerTranslator = function(translator) {
translators.push(translator);
};
this.translateResponse = function(response) {
var objects = translators.map(function(translator) {
return translator(response);
}).filter(angular.identity);
if (objects.length > 0) {
return objects[0];
} else {
return response;
}
};
this.registerDefaultTranslator = function(type, Constructor) {
this.registerTranslator(function(response) {
return response.type === type ? new Constructor(response) : null;
});
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview The base class for Box objects.
* @author jmeadows
*/
var objects = angular.module('box.objects', ['rx', 'box.conf', 'box.http', 'box.util', 'chrome.downloads']);
objects.factory('boxObjectBase',
['apiUrl', 'boxHttp', function(apiUrl, boxHttp) {
function BoxHttpProxy(user) {
var defaultHeaders = Object.create(this.defaultHeaders || {});
this.defaultHeaders = angular.extend(defaultHeaders, {
'As-User': user.id
});
}
var Constructor = Object.getPrototypeOf(boxHttp).constructor;
BoxHttpProxy.prototype = Object.create(new Constructor());
return {
url: function() {
return apiUrl + '/' + this.urlType + '/' + this.id;
},
/**
* Allows enterprise administrators to make API calls for their managed users.
* @param {BoxUser} user The user that operations should be made as
* @returns {Object} A new instance of the same type as this box object, on which operations will be performed as the specified user.
*/
asUser: function(user) {
var constructor = this.constructor, args = [];
function BoxObjectAsUser() {
constructor.apply(this, args);
}
BoxObjectAsUser.prototype = Object.create(constructor.prototype);
for (var i = 0; i < constructor.length - 1; i++) {
args.push(null);
}
args.push(this);
var boxObjectAsUser = new BoxObjectAsUser();
boxObjectAsUser.boxHttp = new BoxHttpProxy(user);
return boxObjectAsUser;
}
};
}]);
})();
//----------------------------
(function() {
/**
* @fileoverview Box Collaboration object. @see http://developers.box.com/docs/#collaborations
* @author jmeadows
*/
/**
* @module BoxCollaboration
*/
angular.module('box.objects').factory('BoxCollaboration', ['boxHttp', 'boxObjectBase', 'responseTranslator', function(boxHttp, boxObjectBase, responseTranslator) {
/**
* Box Collaboration object, representing an access control list.
* [Learn more](http://developers.box.com/docs/#collaborations).
* @param {Object} json Information about the collaboration from an API request.
* @constructor
*/
function Collaboration(json) {
angular.extend(this, json);
this.boxHttp = boxHttp;
}
Collaboration.prototype = angular.extend(Object.create(boxObjectBase), {
urlType: 'collaborations',
/**
* Edit the collaboration, changing its [role](https://support.box.com/entries/20366031-what-are-the-different-collaboration-permissions-and-what-access-do-they-provide)
* or whether or not the collaboration has been accepted.
* @param {String} role The new role for the collaboration.
* @param {Boolean} [status] The new status for the collaboration.
* @returns {Observable} An observable containing a new, updated collaboration object.
*/
edit: function(role, status) {
return this.boxHttp.put(this.url(), {
role: role,
status: status
}).map(function(result) {
return new Collaboration(result);
});
},
/**
* Delete the collaboration.
* @returns {Observable} An observable containing the Box API respons