UNPKG

box-chrome-sdk

Version:

A Chrome App SDK for the Box V2 API

2,625 lines 117 kB
(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-----------------------------');
    
    conf.constant('authUrl', 'https://www.box.com/api/oauth2');
    conf.constant('redirectUri', 'https://' + chrome.runtime.id + '.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 response for this request.
             */
            delete: function() {
                return this.boxHttp.delete(this.url());
            },
            /**
             * Get information about the collaboration.
             * @returns {Observable} An observable containing a new collaboration object with this collaboration's
             * full information.
             */
            getInfo: function() {
                return this.boxHttp.get(this.url()).map(function(result) {
                    return new Collaboration(result);
                });
            }
        });
        Collaboration.prototype.constructor = Collaboration;
        responseTranslator.registerDefaultTranslator('collaboration', Collaboration);
        return Collaboration;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Comment object. @see http://developers.box.com/docs/#comments
     * @author jmeadows
     */
    
    /**
     * @module BoxComment
     */
    
    angular.module('box.objects').factory('BoxComment', ['boxHttp', 'boxObjectBase', 'responseTranslator', function(boxHttp, boxObjectBase, responseTranslator) {
        /**
         * Represents a message from a Box user about a Box file.
         * @param {Object} json Information about the comment from an API request.
         * @constructor
         */
        function Comment(json) {
            angular.extend(this, json);
            this.boxHttp = boxHttp;
        }
        Comment.prototype = angular.extend(Object.create(boxObjectBase), {
            urlType: 'comments',
            /**
             * Updates the comment's message.
             * @param {String} message The new message that should be in the comment
             * @returns {Observable} An observable containing the updated comment.
             */
            updateMessage: function(message) {
                return this.boxHttp.put(this.url(), null, {
                    message: message
                }).map(function(result) {
                    return new Comment(result);
                });
            },
            /**
             * Get information about the comment.
             * @returns {Observable} An observable containing a new comment object with this comment's
             * full information.
             */
            getInfo: function() {
                return this.boxHttp.get(this.url()).map(function(result) {
                    return new Comment(result);
                });
            },
            /**
             * Delete the comment.
             * @returns {Observable} An observable containing the Box API response for this request.
             */
            delete: function() {
                return this.boxHttp.delete(this.url());
            }
        });
        Comment.prototype.constructor = Comment;
        responseTranslator.registerDefaultTranslator('comment', Comment);
        return Comment;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Event object. @see http://developers.box.com/docs/#events
     * @author jmeadows
     */
    
    /**
     * @module BoxEvent
     */
    
    angular.module('box.objects').factory('BoxEvent', ['boxHttp', 'boxObjectBase', 'responseTranslator', function(boxHttp, boxObjectBase, responseTranslator) {
        /**
         * Represents an event - something happening in a Box account.
         * @param {Object} json Information about the event.
         * @param {String} streamPosition Indicates a moment in time near where this event occurred.
         * @constructor
         */
        function Event(json, streamPosition) {
            angular.extend(this, json);
            this.streamPosition = streamPosition;
            if (angular.isDefined(this.source)) {
                this.source = responseTranslator.translateResponse(this.source);
            }
        }
        Event.prototype = angular.extend(Object.create(boxObjectBase), {
        });
        Event.prototype.constructor = Event;
        return Event;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box File object. @see http://developers.box.com/docs/#files
     * @author jmeadows
     */
    
    /**
     * @module BoxFile
     */
    
    angular.module('box.objects').factory('BoxFile', [
        'rx', 'crypto', 'chromeDownloads', 'apiUrl', 'uploadUrl', 'boxHttp', 'boxItem', 'BoxFileVersion', 'BoxComment', 'BoxTask', 'BoxMetadata', 'responseTranslator',
        function(rx, crypto, chromeDownloads, apiUrl, uploadUrl, boxHttp, boxItem, BoxFileVersion, BoxComment, BoxTask, BoxMetadata, responseTranslator) {
            /**
             * An individual file on Box. For more information, see [http://developers.box.com/docs/#files](File Object).
             * @param {Object} json Information about the file from an API request.
             * @constructor
             */
            function File(json) {
                angular.extend(this, json);
                this.boxHttp = boxHttp;
            }
            File.prototype = angular.extend(Object.create(boxItem), {
                urlType: 'files',
                /**
                 * Download the file.
                 * @param {Object} params Can contain a version (Integer) value specifying which version to download
                 * @returns {Observable} An observable containing the integer ID identifying the download
                 */
                download: function(params) {
                    var url = this.url() + '/content';
                    if (params && params.version) {
                        url += '?version=' + params.version;
                    }
                    return this.boxHttp.auth().flatMap(function(token) {
                        return chromeDownloads.download({
                            url: url,
                            headers: [{'Authorization': 'Bearer ' + token}]
                        });
                    });
                },
                /**
                 * Get the content of the file.
                 * @param {Object} params Parameters related to file download. See https://developers.box.com/docs/#files-download-a-file
                 * @returns {Observable} An observable containing the file content.
                 */
                getContent: function(params) {
                    return this.boxHttp.get(this.url() + '/content', {
                        params: params,
                        responseType: 'blob'
                    });
                },
                /**
                 * Upload a new version of the file.
                 * @param {string} name The name of the file
                 * @param {ArrayBuffer|Blob} content The file content to upload
                 * @param {String} mtime Timestamp that will be assigned to the file's modified_at property
                 * @param {Boolean} ifMatch Whether or not to send an If-Match header with the request
                 * @returns {Observable} An observable containing the new file object.
                 */
                replace: function(name, content, mtime, ifMatch) {
                    var boxHttp = this.boxHttp, etag = this.etag, fileId = this.id;
                    return crypto.digest('SHA-1', content).map(function(sha1) {
                        return Array.prototype.map.call(new Uint8Array(sha1), function(i) {
                            return ('0' + i.toString(16)).slice(-2);
                        }).join('');
                    }).flatMap(function(sha1) {
                        return boxHttp.post(uploadUrl + '/files/' + fileId + '/content',
                            {
                                headers: angular.extend(
                                    {'Content-MD5': sha1},
                                    ifMatch ? {'If-Match': etag} : {}
                                )
                            },
                            {
                                filename: [new Blob([content]), name],
                                /*eslint-disable camelcase*/
                                content_modified_at: mtime
                                /*eslint-enable camelcase*/
                            }
                        ).map(function(result) {
                            return new File(result.entries[0]);
                        });
                    });
                },
                /**
                 * Get a list of file versions for the file.
                 * @returns {Observable} An observable sequence of file version objects for the file.
                 */
                getHistory: function() {
                    var that = this;
                    return this.boxHttp.get(this.url() + '/versions').flatMap(function(result) {
                        return rx.Observable.fromArray(result.entries.map(function(entry) {
                            return new BoxFileVersion(that, entry);
                        }));
                    });
                },
                /**
                 * Get image data for the file's thumbnail.
                 * @param {String} extension The file extension to retrieve.  Can be png, gif, or jpg.
                 * @param {Object} params Parameters specifying the thumbnail's dimensions.
                 * @returns {Observable} An observable containing the thumbnail image data as a Blob.
                 */
                getThumbnail: function(extension, params) {
                    return this.boxHttp.get(this.url() + '/thumbnail.' + extension,
                        {
                            responseType: 'blob',
                            params: params
                        }
                    );
                },
                /**
                 * Get a list of comments for the file.
                 * @returns {Observable} An observable sequence of comment objects for the file.
                 */
                getComments: function() {
                    return this.boxHttp.get(this.url() + '/comments')
                        .flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries.map(function(entry) {
                                return new BoxComment(entry);
                            }));
                        });
                },
                /**
                 * Get a list of tasks for the file.
                 * @returns {Observable} An observable sequence of task objects for the file.
                 */
                getTasks: function() {
                    return this.boxHttp.get(this.url() + '/tasks')
                        .flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries.map(function(entry) {
                                return new BoxTask(entry);
                            }));
                        });
                },
                /**
                 * Get the file's associated metadata.
                 * @param {String} type Type of metadata. Must be properties.
                 * @returns {Observable} An observable containing the metadata object.
                 */
                getMetadata: function(type) {
                    var that = this;
                    return this.boxHttp.get(this.url() + '/metadata/' + type)
                        .map(function(result) {
                            return new BoxMetadata(that, result);
                        });
                },
                /**
                 * Create metadata associated with the file.
                 * @param {String} type Type of metadata to create. Must be properties.
                 * @param {Object} value Hash containing key-value pairs to be stored as metadata for the file
                 * @returns {Observable} An observable containing the created metadata object.
                 */
                createMetadata: function(type, value) {
                    var that = this;
                    return this.boxHttp.post(this.url() + '/metadata/' + type, null, value)
                        .map(function(result) {
                            return new BoxMetadata(that, result);
                        });
                }
            });
            File.prototype.constructor = File;
            responseTranslator.registerDefaultTranslator('file', File);
            return File;
        }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box File Version object. @see http://developers.box.com/docs/#files-view-versions-of-a-file
     * @author jmeadows
     */
    
    /**
     * @module BoxFileVersion
     */
    
    angular.module('box.objects').factory('BoxFileVersion', ['boxHttp', 'boxObjectBase', 'responseTranslator', function(boxHttp, boxObjectBase, responseTranslator) {
        /**
         * Represents a previous version of a Box file.
         * @param {File} file The Box file object this file version object is associated with
         * @param {Object} json Information about the file version from an API request.
         * @constructor
         */
        function FileVersion(file, json) {
            this.file = file;
            angular.extend(this, json);
            this.boxHttp = boxHttp;
        }
        FileVersion.prototype = angular.extend(Object.create(boxObjectBase), {
            url: function() {
                return this.file.url() + '/versions';
            },
            /**
             * Promotes this version of the file to the current version.
             * @returns {Observable} An observable containing a new file version object.
             */
            promoteToCurrent: function() {
                var file = this.file;
                return this.boxHttp.post(this.url() + '/current', null, {
                    type: 'file_version',
                    id: this.id
                }).map(function(result) {
                    return new FileVersion(file, result);
                });
            },
            /**
             * Delete the file version.
             * @returns {Observable} An observable containing the Box API response for this request.
             */
            delete: function() {
                return this.boxHttp.delete(this.url() + '/' + this.id);
            },
            /**
             * Download the file.
             * @returns {Observable} An observable containing the integer ID identifying the download.
             */
            download: function() {
                return this.file.download({version: this.id});
            }
        });
        FileVersion.prototype.constructor = FileVersion;
        responseTranslator.registerDefaultTranslator('file_version', FileVersion);
        return FileVersion;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Folder object. @see http://developers.box.com/docs/#folders
     * @author jmeadows
     */
    
    /**
     * @module BoxFolder
     */
    
    angular.module('box.objects').factory('BoxFolder',
        ['rx', 'crypto', 'apiUrl', 'uploadUrl', 'boxHttp', 'boxItem', 'BoxCollaboration', 'BoxFile', 'getAll', 'responseTranslator',
            function(rx, crypto, apiUrl, uploadUrl, boxHttp, boxItem, BoxCollaboration, BoxFile, getAll, responseTranslator) {
                /**
                 * An individual folder on Box, containing other folders and/or files.
                 * @param {Object} json information about the folder from an API request
                 * @constructor
                 */
                function Folder(json) {
                    angular.extend(this, json);
                    this.boxHttp = boxHttp;
                }
                Folder.prototype = angular.extend(Object.create(boxItem), {
                    urlType: 'folders',
                    /**
                     * Get a folder's items.
                     * @param {String} fields Comma separated list of fields that should be returned about each item
                     * @returns {Observable} An observable sequence of the folder's items.
                     */
                    getItems: function(fields) {
                        var url = this.url(), boxHttp = this.boxHttp;
                        return getAll(
                            function(limit, offset) {
                                return boxHttp.get(url + '/items', {
                                    params: {
                                        fields: fields,
                                        offset: offset,
                                        limit: limit
                                    }
                                });
                            },
                            responseTranslator.translateResponse
                        );
                    },
                    /**
                     * Create a subfolder with the folder as its parent.
                     * @param {String} name The name of the new subfolder
                     * @returns {Observable} An observable containing the new folder object.
                     */
                    createSubfolder: function(name) {
                        return this.boxHttp.post(apiUrl + '/folders', null, {
                            name: name,
                            parent: {id: this.id}
                        }).map(function(result) {
                            return new Folder(result);
                        });
                    },
                    /**
                     * Get a list of collaborations on the folder.
                     * @returns {Observable} An observable sequence of the folder's collaborations.
                     */
                    getCollaborations: function() {
                        return this.boxHttp.get(this.url() + '/collaborations')
                            .flatMap(function(result) {
                                return rx.Observable.fromArray(result.entries.map(function(entry) {
                                    return new BoxCollaboration(entry);
                                }));
                            });
                    },
                    /**
                     * Invite a collaborator to a folder.
                     * @param {String} role The [role](https://support.box.com/entries/20366031-what-are-the-different-collaboration-permissions-and-what-access-do-they-provide) granted to the invited collaborator
                     * @param {User|Group|Object} collaborator The collaborator that will be invited to the folder.  Must have properties of type ("user" or "group"), and id or login (email address)
                     * @param {Boolean} notify whether or not the user should receive an email notification
                     * @returns {Observable} An observable containing the new collaboration object created by the request.
                     */
                    addCollaboration: function(role, collaborator, notify) {
                        return this.boxHttp.post(apiUrl + '/collaborations',
                            angular.isDefined(notify) ? {notify: notify} : null,
                            {
                                item: {
                                    type: this.type,
                                    id: this.id
                                },
                                /*eslint-disable camelcase*/
                                accessible_by: collaborator,
                                /*eslint-enable camelcase*/
                                role: role
                            }
                        ).map(function(result) {
                            return new BoxCollaboration(result);
                        });
                    },
                    /**
                     * Upload a file to the folder.
                     * @param {String} name The name of the file
                     * @param {ArrayBuffer|Blob} content The file content
                     * @param {Timestamp} ctime Timestamp the new file will have as its created at time
                     * @param {Timestamp} mtime Timestamp the new file will have as its modified at time
                     * @returns {Observable} An observable containing the new file object.
                     */
                    uploadFileTo: function(name, content, ctime, mtime) {
                        var boxHttp = this.boxHttp, parentId = this.id;
                        return crypto.digest('SHA-1', content).map(function(sha1) {
                            return Array.prototype.map.call(new Uint8Array(sha1), function(i) {
                                return ('0' + i.toString(16)).slice(-2);
                            }).join('');
                        }).flatMap(function(sha1) {
                            return boxHttp.post(uploadUrl + '/files/content', {
                                    headers: {
                                        'Content-MD5': sha1
                                    }
                                },
                                {
                                    filename: [new Blob([content]), name],
                                    /*eslint-disable camelcase*/
                                    parent_id: parentId,
                                    content_created_at: ctime,
                                    content_modified_at: mtime
                                    /*eslint-enable camelcase*/
                                }
                            ).map(function(result) {
                                return new BoxFile(result.entries[0]);
                            });
                        });
                    }
                });
                Folder.prototype.constructor = Folder;
                responseTranslator.registerDefaultTranslator('folder', Folder);
                return Folder;
            }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Group object. @see http://developers.box.com/docs/#groups
     * @author jmeadows
     */
    
    /**
     * @module BoxGroup
     */
    
    angular.module('box.objects').factory('BoxGroup',
        ['boxHttp', 'boxObjectBase', 'apiUrl', 'BoxCollaboration', 'rx', 'responseTranslator',
            function(boxHttp, boxObjectBase, apiUrl, BoxCollaboration, rx, responseTranslator) {
                function Group(json) {
                    angular.extend(this, json);
                    this.boxHttp = boxHttp;
                }
                Group.prototype = angular.extend(Object.create(boxObjectBase), {
                    urlType: 'groups',
                    /**
                     * Update the group's information.
                     * @param {Object} params An object containing parameters to update  (currently just name is supported)
                     * @returns {Observable} An observable containing a new Group object with the updated parameters.
                     */
                    update: function(params) {
                        return this.boxHttp.put(
                            this.url(),
                            null,
                            params
                        ).map(function(result) {
                            return new Group(result);
                        });
                    },
                    /**
                     * Deletes the group.
                     * @returns {Observable} An observable containing the Box API response for this request.
                     */
                    delete: function() {
                        return this.boxHttp.delete(this.url());
                    },
                    /**
                     * Get a group's memberships, specifying a user, a group, and the user's role in the group.
                     * @returns {Observable} An observable sequence of membership objects.
                     */
                    getMemberships: function() {
                        return this.boxHttp.get(this.url() + '/memberships').flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries);
                        });
                    },
                    /**
                     * Add a new email alias to the user's account.
                     * @param {User} user The user to add to the group
                     * @param {String} role The role of the user in the group
                     * @returns {Observable} An observable containing the newly created membership object.
                     */
                    addMember: function(user, role) {
                        return this.boxHttp.post(apiUrl + '/group_memberships', null, {
                            user: {id: user.id},
                            group: {id: this.id},
                            role: role
                        });
                    },
                    /**
                     * Update a group membership.
                     * @param {Object} membership A membership object to update
                     * @param {String} role The role of the user in the group
                     * @returns {Observable} An observable containing the updated membership object.
                     */
                    updateMembership: function(membership, role) {
                        return this.boxHttp.put(apiUrl + '/group_memberships/' + membership.id, null, {role: role});
                    },
                    /**
                     * Remove a member from the group.
                     * @param {Object} membership A membership object to delete
                     * @returns {Observable} An empty observable.
                     */
                    deleteMembership: function(membership) {
                        return this.boxHttp.delete(apiUrl + '/group_memberships/' + membership.id);
                    },
                    /**
                     * Gets a collection of collaboration objects for the task.
                     * @returns {Observable} An observable sequence of Box Collaboration objects.
                     */
                    getCollaborations: function() {
                        return this.boxHttp.get(this.url() + '/collaborations').flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries.map(function(entry) {
                                return new BoxCollaboration(entry);
                            }));
                        });
                    }
                });
                Group.prototype.constructor = Group;
                responseTranslator.registerDefaultTranslator('group', Group);
                return Group;
            }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Item object. Base class for files and folders.
     * @author jmeadows
     */
    
    /**
     * @module Item
     */
    
    angular.module('box.objects').factory('boxItem', ['boxHttp', 'boxObjectBase', 'BoxComment', 'apiUrl', function(boxHttp, boxObjectBase, BoxComment, apiUrl) {
        return angular.extend(Object.create(boxObjectBase), {
            /**
             * Update this item's information.
             * @param {Object} params Specifies the information to update.  See [http://developers.box.com/docs/#files-update-a-files-information](Updating files) and [http://developers.box.com/docs/#folders-update-information-about-a-folder](Updating folders).
             * @param {Boolean} ifMatch Whether or not to send an If-Match header
             * @returns {Observable} An observable containing the updated item.
             */
            updateInfo: function(params, ifMatch) {
                var Construct = this.constructor;
                return this.boxHttp.put(this.url(),
                    {
                        headers: ifMatch ? {'If-Match': this.etag} : {}
                    },
                    params
                ).map(function(result) {
                    return new Construct(result);
                });
            },
            /**
             * Get a link that can be safely shared with others. See the [http://blog.box.com/2012/04/share-your-stuff-and-stay-in-control-using-box-shared-links/](shared link blog post).
             * @param {String} access Controls who may access the shared link. Can be one of open, company, collaborators, or null (default).
             * @param {Timestamp} unshareDateTime When the shared link will automatically expire
             * @param {Object} permissions Controls whether users of the shared link can preview or download the item
             * @returns {Observable} An observable containing the shared link.
             */
            getSharedLink: function(access, unshareDateTime, permissions) {
                return this.updateInfo({
                    'shared_link': {
                        access: access,
                        /*eslint-disable camelcase*/
                        unshared_at: unshareDateTime,
                        /*eslint-enable camelcase*/
                        permissions: permissions
                    }
                });
            },
            /**
             * Copy an item to another parent folder.
             * @param {Folder} parentFolder The destination Box folder for the copy operation
             * @param {String} name The new name for the copied item
             * @returns {Observable} An observable containing the new copied item.
             */
            copyTo: function(parentFolder, name) {
                var Construct = this.constructor;
                return this.boxHttp.post(this.url() + '/copy', null, {
                    parent: {
                        id: parentFolder.id
                    },
                    name: name || this.name
                }).map(function(result) {
                    return new Construct(result);
                });
            },
            /**
             * Delete the item.
             * @param {Boolean} recursive Whether or not the delete should be recursive (only valid for deleting folders)
             * @param {Boolean} ifMatch Whether or not to send an If-Match header
             * @returns {Observable} An observable containing the Box API response for this request.
             */
            delete: function(recursive, ifMatch) {
                return this.boxHttp.delete(this.url(), {
                    headers: ifMatch ? {'If-Match': this.etag} : {},
                    params: {recursive: !!recursive}
                });
            },
            /**
             * Adds a comment to a file or folder.
             * @param {String} message The message to leave as a comment
             * @returns {Observable} An observable containing the newly created Box comment object.
             */
            commentOn: function(message) {
                return this.boxHttp.post(apiUrl + '/comments', null, {
                    item: {
                        type: this.type,
                        id: this.id
                    },
                    message: message
                }).map(function(result) {
                    return new BoxComment(result);
                });
            },
            /**
             * Restore an item from the trash.
             * @param {Folder} parentFolder The destination Box folder to restore the item to
             * @param {String} name The new name for the restored item
             * @returns {Observable} An observable containing the restored item.
             */
            restoreFromTrash: function(parentFolder, name) {
                var Construct = this.constructor;
                return this.boxHttp.post(this.url(), null, {
                    parent: {
                        id: parentFolder.id
                    },
                    name: name || this.name
                }).map(function(result) {
                    return new Construct(result);
                });
            },
            /**
             * Permanently delete the item from the trash.
             * @param {Boolean} ifMatch Whether or not to send an If-Match header
             * @returns {Observable} An observable containing no result. The onCompleted callback will signal that the item was successfully deleted.
             */
            deleteFromTrash: function(ifMatch) {
                return this.boxHttp.delete(this.url() + '/trash', {
                    headers: ifMatch ? {'If-Match': this.etag} : {}
                });
            }
        });
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Metadata object.
     * @author jmeadows
     */
    
    /**
     * @module BoxMetadata
     */
    
    angular.module('box.objects').factory('BoxMetadata', ['boxHttp', 'boxObjectBase', function(boxHttp, boxObjectBase) {
        /**
         * Custom data associated with a Box file.  For more information, see
         * [http://developers.box.com/metadata-api/](Box Metadata API).
         * @param {File} file The Box file with which this metadata is associated
         * @param {Object} json Information about the metadata from an API request.
         * @constructor
         */
        function Metadata(file, json) {
            this.file = file;
            angular.extend(this, json);
            this.boxHttp = boxHttp;
        }
        function MetadataUpdate() {
            this.ops = [];
        }
        Metadata.prototype = angular.extend(Object.create(boxObjectBase), {
            url: function() {
                return this.file.url() + '/metadata/' + this.$type;
            },
            /**
             * Updates this metadata object with the instructions specified in the given update object.
             * @param {MetadataUpdate} update A metadata update object specifying what to update.
             * @returns {Observable} An observable containing the updated metadata object
             */
            sendUpdate: function(update) {
                var file = this.file;
                return this.boxHttp.put(
                    this.url(),
                    {
                        headers: {'Content-Type': 'application/json-patch+json'}
                    },
                    update.ops
                ).map(function(result) {
                    return new Metadata(file, result);
                });
            },
            /**
             * Delete this metadata object.
             * @returns {Observable} An observable containing the Box API response for this request.
             */
            delete: function() {
                return this.boxHttp.delete(this.url());
            },
            /**
             * Create a metadata update object that can be used to update this metadata object.
             * @returns {MetadataUpdate} The update object that can be used to update this metadata object.
             */
            startUpdate: function() {
                return new MetadataUpdate();
            }
        });
        MetadataUpdate.prototype = {
            /**
             * Adds an instruction to this update object to add a new key/value pair to the metadata object.
             * @param {String} path Specifies where to add the key
             * @param {String} value The value to add
             * @returns {undefined} void
             */
            add: function(path, value) {
                this.ops.push({
                    op: 'add',
                    path: path,
                    value: value
                });
            },
            /**
             * Adds an instruction to this update object to remove a key/value pair from the metadata object.
             * @param {String} path Specifies which key to remove
             * @param {String} oldValue If specified, will only execute the replace if the current value matches this parameter
             * @returns {undefined} void
             */
            remove: function(path, oldValue) {
                if (oldValue) {
                    this.test(path, oldValue);
                }
                this.ops.push({
                    op: 'remove',
                    path: path
                });
            },
            /**
             * Adds an instruction to this update object to replace
             * @param {String} path Specifies which key to replace
             * @param {String} value The new value for the specified key
             * @param {String} oldValue If specified, will only execute the replace if the current value matches this parameter
             * @returns {undefined} void
             */
            replace: function(path, value, oldValue) {
                if (oldValue) {
                    this.test(path, oldValue);
                }
                this.ops.push({
                    op: 'replace',
                    path: path,
                    value: value
                });
            },
            /**
             * Adds an instruction to this update object to test the metadata object for the existence of a key/value pair.
             * The next instruction will only be executed if this test passes.
             * @param {String} path Sepcifies which key to test
             * @param {String} value Specifies the value to test for
             * @returns {undefined} void
             */
            test: function(path, value) {
                this.ops.push({
                    op: 'test',
                    path: path,
                    value: value
                });
            }
        };
        Metadata.prototype.constructor = Metadata;
        return Metadata;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Task object. @see http://developers.box.com/docs/#tasks
     * @author jmeadows
     */
    
    /**
     * @module BoxTask
     */
    
    angular.module('box.objects').factory('BoxTask',
        ['rx', 'boxHttp', 'apiUrl', 'boxObjectBase', 'BoxTaskAssignment', 'responseTranslator',
            function(rx, boxHttp, apiUrl, boxObjectBase, BoxTaskAssignment, responseTranslator) {
                /**
                 * Represents a file-centric workflow object.  Read more at [https://support.box.com/hc/en-us/articles/200520868-Tasks](Tasks).
                 * @param {Object} json Information about the task from an API result.
                 * @constructor
                 */
                function Task(json) {
                    angular.extend(this, json);
                    this.boxHttp = boxHttp;
                }
                Task.prototype = angular.extend(Object.create(boxObjectBase), {
                    urlType: 'tasks',
                    /**
                     * Get information about the task
                     * @returns {Observable} An observable containing a new Task object containing the details of this task.
                     */
                    getInfo: function() {
                        return this.boxHttp.get(this.url()).map(function(result) {
                            return new Task(result);
                        });
                    },
                    /**
                     * Update the task's action, message, or due date.
                     * @param {Object} params Any or all of: action (String), message (String), and/or due_at (Timestamp)
                     * @returns {Observable} An observable containing a new Task object with the updated parameters.
                     */
                    update: function(params) {
                        return this.boxHttp.put(this.url(), null, params).map(function(result) {
                            return new Task(result);
                        });
                    },
                    /**
                     * Deletes the task.
                     * @returns {Observable} An observable containing the Box API response for this request.
                     */
                    delete: function() {
                        return this.boxHttp.delete(this.url());
                    },
                    /**
                     * Gets a collection of task assignment objects for the task.
                     * @returns {Observable} An observable sequence of Box Task Assignment objects.
                     */
                    getAssignments: function() {
                        var that = this;
                        return this.boxHttp.get(this.url() + '/assignments').flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries.map(function(entry) {
                                return new BoxTaskAssignment(that, entry);
                            }));
                        });
                    },
                    /**
                     * Assigns the task to a user.
                     * @param {User|Object} user The user object representing the user this task should be assigned to.
                     * @returns {Observable} An observable containing the Box API response for this request.
                     */
                    assignTo: function(user) {
                        return this.boxHttp.post(apiUrl + '/task_assignments', null, {
                            task: {
                                id: this.id,
                                type: 'task'
                            },
                            /*eslint-disable camelcase*/
                            assign_to: {
                            /*eslint-enable camelcase*/
                                id: user.id,
                                login: user.login
                            }
                        }).map(function(result) {
                                return new BoxTaskAssignment(result);
                            });
                    }
                });
                Task.prototype.constructor = Task;
                responseTranslator.registerDefaultTranslator('task', Task);
                return Task;
            }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box Task Assignment object. @see http://developers.box.com/docs/#tasks-get-the-assignments-for-a-task
     * @author jmeadows
     */
    
    /**
     * @module BoxTaskAssignment
     */
    
    angular.module('box.objects').factory('BoxTaskAssignment', ['boxHttp', 'boxObjectBase', 'responseTranslator', function(boxHttp, boxObjectBase, responseTranslator) {
        /**
         * Represents a task assignment, specifying the item the task is associated with and the user it is assigned to.
         * @param {Task} task The task specified by the task assignment
         * @param {Object} json Information about the task assignment from an API result.
         * @constructor
         */
        function TaskAssignment(task, json) {
            this.task = task;
            angular.extend(this, json);
            this.boxHttp = boxHttp;
        }
        TaskAssignment.prototype = angular.extend(Object.create(boxObjectBase), {
            urlType: 'task_assignments',
            /**
             * Get information about the task assignment
             * @returns {Observable} An observable containing a new TaskAssignment object containing the details of this task assignment.
             */
            getInfo: function() {
                var task = this.task;
                return this.boxHttp.get(this.url()).map(function(result) {
                    return new TaskAssignment(task, result);
                });
            },
            /**
             * Delete this task assignment, unassigning the associated task from the assignee.
             * @returns {Observable} An observable containing the Box API response for this request.
             */
            delete: function() {
                return this.boxHttp.delete(this.url());
            },
            /**
             * Update this task assignment's message or resolution state.
             * @param {String} message A message to the assignee about the associated task.
             * @param {String} state Can be completed, incomplete, approved, or rejected.
             * @returns {Observable} An observable containing a new TaskAssignment object containing the updated task assignment.
             */
            update: function(message, state) {
                return this.boxHttp.put(this.url(), null, {
                    message: message,
                    /*eslint-disable camelcase*/
                    resolution_state: state
                    /*eslint-enable camelcase*/
                })
                    .map(function(result) {
                        return new TaskAssignment(result);
                    });
            }
        });
        TaskAssignment.prototype.constructor = TaskAssignment;
        responseTranslator.registerDefaultTranslator('task_assignment', TaskAssignment);
        return TaskAssignment;
    }]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Box User object. @see http://developers.box.com/docs/#users
     * @author jmeadows
     */
    
    /**
     * @module BoxUser
     */
    
    angular.module('box.objects').factory('BoxUser',
        [
            'rx',
            'apiUrl',
            'boxHttp',
            'boxObjectBase',
            'BoxCollaboration',
            'BoxComment',
            'BoxFile',
            'BoxFileVersion',
            'BoxFolder',
            'BoxGroup',
            'boxItem',
            'BoxMetadata',
            'BoxTask',
            'BoxTaskAssignment',
            'responseTranslator',
            function(
                rx,
                apiUrl,
                boxHttp,
                boxObjectBase,
                BoxCollaboration,
                BoxComment,
                BoxFile,
                BoxFileVersion,
                BoxFolder,
                BoxGroup,
                boxItem,
                BoxMetadata,
                BoxTask,
                BoxTaskAssignment,
                responseTranslator
                ) {
                /**
                 * Represents a user with a Box account.
                 * @param {Object} json information about the user from an API request
                 * @constructor
                 */
                function User(json) {
                    angular.extend(this, json);
                    this.boxHttp = boxHttp;
                }
                User.prototype = angular.extend(Object.create(boxObjectBase), {
                    urlType: 'users',
                    /**
                     * Assigns a task to the user.
                     * @param {Task|Object} task The task to assign to the user.
                     * @returns {Observable} An observable containing the Box API response for this request.
                     */
                    assignTaskTo: function(task) {
                        return this.boxHttp.post(apiUrl + '/task_assignments', null, {
                            task: {
                                id: task.id,
                                type: 'task'
                            },
                            /*eslint-disable camelcase*/
                            assign_to: {
                            /*eslint-enable camelcase*/
                                id: this.id,
                                login: this.login
                            }
                        }).map(function(result) {
                            return new BoxTaskAssignment(result);
                        });
                    },
                    /**
                     * Update the enterprise user's information if the current user is an enterprise admin.
                     * @param {Object} params A hash containing key/value pairs of the user fields to update
                     * @param {Boolean} notify Whether to notify the affected user when they are rolled out of the enterprise.
                     * @returns {Observable} An observable containing a new User object with the updated parameters.
                     */
                    update: function(params, notify) {
                        return this.boxHttp.put(
                            this.url(),
                            angular.isDefined(notify) ? {notify: notify} : null,
                            params
                        ).map(function(result) {
                            return new User(result);
                        });
                    },
                    /**
                     * Delete the enterprise user if the current user is an enterprise admin.
                     * @param {Boolean} notify Whether to notify the affected user.
                     * @param {Boolean} force Whether or not to delete the user even if they still own files
                     * @returns {Observable} An empty observable.
                     */
                    delete: function(notify, force) {
                        return this.boxHttp.delete(
                            this.url(),
                            angular.extend({},
                                angular.isDefined(notify) ? {notify: notify} : {},
                                angular.isDefined(force) ? {force: force} : {}
                            )
                        );
                    },
                    giveRootFolderToUser: function(user) {
                        /*eslint-disable camelcase*/
                        return this.boxHttp.put(this.url() + '/folders/0', null, {owned_by: {id: user.id}})
                        /*eslint-enable camelcase*/
                            .map(function(result) {
                                return new BoxFolder(result);
                            });
                    },
                    /**
                     * Get a user's email address aliases.  Doesn't include the primary login, which is included in the User object.
                     * @returns {Observable} An observable sequence of email alias objects.
                     */
                    getEmailAliases: function() {
                        return this.boxHttp.get(this.url() + '/email_aliases').flatMap(function(result) {
                            return rx.Observable.fromArray(result.entries);
                        });
                    },
                    /**
                     * Add a new email alias to the user's account.
                     * @param {String} email The email address to add to the user's account
                     * @returns {Observable} An observable containing the newly created email alias object.
                     */
                    addEmailAlias: function(email) {
                        return this.boxHttp.post(this.url() + '/email_aliases', null, {email: email});
                    },
                    /**
                     * Delete the email alias from the user's account.
                     * @param {Object} emailAlias The email alias object to remove from the user's account.
                     * @returns {Observable} An empty observable.
                     */
                    deleteEmailAlias: function(emailAlias) {
                        return this.boxHttp.delete(this.url() + '/email_aliases/' + emailAlias.id);
                    }
                });
                User.prototype.constructor = User;
                responseTranslator.registerDefaultTranslator('user', User);
                return User;
            }]);
    
})();
//----------------------------
(function() {
    /**
     * @namespace box.ui
     * @memberOf box
     */
    angular.module('box.ui', ['rx', 'chrome', 'ui.bootstrap', 'box.sdk']);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Directives supporting the Box UI.
     * @author jmeadows
     */
    
    var mod = angular.module('box.ui');
    
    /**
     * @function boxBreadCrumbs
     * @memberOf box.ui
     * @description Builds a breadcrumb list from a user's root folder to the current item (file or folder). Must be inside
     * a @see BoxFolderView .
     * @param {Object} boxItemSelected Box item selected service
     * @returns {Object} directive for displaying breadcrumbs for an item.
     */
    function boxBreadCrumbs(boxItemSelected) {
        'use strict';
        return {
            scope: {
                item: '='
            },
            link: function(scope) {
                scope.selectFolder = function(item) {
                    boxItemSelected.selectItem(item);
                };
            },
            template: '<span ng-if="!!item" class="box-breadcrumb">' +
                '<span data-ng-repeat="parent in item.path_collection.entries">' +
                '<span class="box-breadcrumb box-breadcrumb-parent" data-ng-click="selectFolder(parent)">{{parent.name}}</span><span> / </span></span>' +
                '<span class="box-breadcrumb box-breadcrumb-leaf" data-ng-click="selectFolder(item)">{{item.name}}</span>' +
                '</span>',
            restrict: 'EA',
            replace: true
        };
    }
    
    mod.directive('boxBreadcrumbs', ['boxItemSelected', boxBreadCrumbs]);
    
    /**
     * @function boxItem
     * @memberOf box.ui
     * @description Directive for showing the details of a Box item, including its name, thumbnail, mtime/ctime, and byte size.
     * @param {Object} boxItemSelected Box item selected service
     * @returns {Object} directive for displaying a Box item (file or folder)
     */
    function boxItem(boxItemSelected) {
        'use strict';
        return {
            template: '<div class="box-item"><div ng-if="!!item">' +
                    '<div class="box-item-thumbnail"><span box-item-thumbnail item="item" thumbnail-size="thumbnailSize"></span></div>' +
                    '<div class="box-item-body" ng-click="select()" ng-dblclick="select() && $emit(\'closeModal\')">' +
                    '    <span class="box-item-body-title">{{item.name}}</span>' +
                    '    <span class="box-item-body-size">{{item.size | bytes}}</span>' +
                    '    <span class="box-item-body-date">{{item.modified_at | date : \'mediumDate\'}}</span>' +
                    '</div>' +
                '</div></div>',
            scope: {
                item: '=',
                thumbnailSize: '='
            },
            link: function(scope, element) {
                scope.select = function() {
                    boxItemSelected.selectItem(scope.item);
                    element.addClass('selected');
                    return true;
                };
                boxItemSelected.selectedItem.subscribe(function(item) {
                    if (item.id !== scope.item.id) {
                        element.removeClass('selected');
                    }
                });
            },
            replace: true,
            restrict: 'EA'
        };
    }
    mod.directive('boxItem', ['boxItemSelected', boxItem]);
    
    /**
     * @function boxItemThumbnail
     * @memberOf box.ui
     * @description Directive for showing an item's thumbnail.
     * @param {Object} chrome Service representing chrome APIs
     * @returns {Object} directive for displaying a Box item thumbnail
     */
    function boxItemThumbnail(chrome) {
        'use strict';
        return {
            scope: {
                item: '=',
                thumbnailSize: '='
            },
            template: '<span></span>',
            link: function(scope, element) {
                if (scope.item.type === 'folder') {
                    var image = new Image();
                    image.src = chrome.runtime.getURL('img/folder.jpg');
                    element.empty();
                    element.append(image);
                } else if (scope.item.type === 'file') {
                    scope.item.getThumbnail('png', scope.thumbnailSize).subscribe(function(thumbnail) {
                        var image = new Image();
                        image.height = scope.thumbnailSize.min_height;
                        image.width = scope.thumbnailSize.min_width;
                        // This is sort of wasteful, but it prevents a spurious warning from Chrome complaining that the blob's MIME type is text/xml.
                        if (thumbnail.type.indexOf('text/') !== -1) {
                            thumbnail = new Blob([thumbnail], {type: 'image/png'});
                        }
                        image.src = window.URL.createObjectURL(thumbnail);
                        image.onload = function() {
                            window.URL.revokeObjectURL(image.src);
                        };
                        element.empty();
                        element.append(image);
                    });
                }
            },
            replace: true,
            restrict: 'EA'
        };
    }
    mod.directive('boxItemThumbnail', ['chrome', boxItemThumbnail]);
    
    /**
     * @function boxAllFiles
     * @memberOf box.ui
     * @description Directive for showing a folder view of the user's root folder.
     * @param {Object} boxItemSelected Box item selected service
     * @returns {Object} directive for viewing all items in the root folder
     */
    function boxAllFiles(boxItemSelected) {
        'use strict';
        return {
            template: '<div><span box-folder-view folder-id="0"></span></div>',
            replace: true,
            restrict: 'EA',
            link: function() {
                boxItemSelected.selectItem({type: 'folder', id: '0'});
            }
        };
    }
    mod.directive('boxAllFiles', ['boxItemSelected', boxAllFiles]);
    
    /**
     * @function BoxFolderView
     * @memberOf box.ui
     * @description Directive for viewing a folder's items.
     * @param {Object} boxSdk Box SDK service
     * @param {Object} boxItemSelected Box item selected service
     * @returns {Object} directive for viewing a folder's items.
     */
    function BoxFolderView(boxSdk, boxItemSelected) {
        'use strict';
        return {
            template: '<div>' +
                    '<div class="item-view-header">' +
                    '    <span box-breadcrumbs item="folder"></span>' +
                    '</div>' +
                    '<div class="item-view-body">' +
                    '    <div class="folder-view-item-wrapper" ng-repeat="item in items">' +
                    '        <div box-item item="item" thumbnail-size="thumbnailSize"></div>' +
                    '    </div>' +
                    '</div>' +
                '</div>',
            scope: {
                folderId: '@'
            },
            replace: true,
            restrict: 'EA',
            link: function(scope) {
                angular.extend(scope, {
                    items: [],
                    thumbnailSize: {
                        'min_width': 32,
                        'max_width': 32,
                        'min_height': 32,
                        'max_height': 32
                    }
                });
                scope.getFolder = function(id) {
                    scope.items = [];
                    boxSdk.getFolder(id)
                        .do(function(folder) {
                            scope.name = folder.name;
                            scope.folder = folder;
                        })
                        .flatMap(function(folder) {
                            return folder.getItems('size,modified_at,name');
                        })
                        .subscribe(function(item) {
                            /*eslint-disable camelcase*/
                            item.modified_at = new Date(item.modified_at);
                            /*eslint-enable camelcase*/
                            scope.items.push(item);
                        });
                };
                scope.selectedItemDisposable = boxItemSelected.selectedItem.subscribe(function(item) {
                    if (item.type === 'folder') {
                        scope.folderId = item.id;
                        scope.getFolder(item.id);
                    } else {
                        scope.fileId = item.id;
                    }
                });
            }
        };
    }
    mod.directive('boxFolderView', ['boxSdk', 'boxItemSelected', BoxFolderView]);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Filters supporting the Box UI.
     * @author jmeadows
     */
    
    var mod = angular.module('box.ui');
    
    /**
     * @function bytes
     * @memberOf box.ui
     * @description Angular filter for formatting a number as a number of bytes, kB, MB, etc.
     * @returns {Function} Formatter for getting a representation of a number of bytes to a given precision.
     */
    function bytes() {
        'use strict';
        return function(bytes, precision) {
            if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
                return '-';
            }
            if (parseFloat(bytes) === 0) {
                return '0 bytes';
            }
            if (typeof precision === 'undefined') {
                precision = 1;
            }
            var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
                number = Math.floor(Math.log(bytes) / Math.log(1024));
            return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) +  ' ' + units[number];
        };
    }
    
    mod.filter('bytes', bytes);
    
})();
//----------------------------
(function() {
    /**
     * @fileoverview Services supporting the Box UI.
     * @author jmeadows
     */
    
    var mod = angular.module('box.ui');
    
    /**
     * @function boxItemSelected
     * @memberOf box.ui
     * @description Service that notifies subscribers when a new box item is selected.
     * @param {Object} rx RxJS namespace object
     * @returns {Object} Service with method selectItem and property selectedItem
     */
    function boxItemSelected(rx) {
        'use strict';
        var subject = new rx.ReplaySubject(1);
        return {
            selectItem: function(item) {
                subject.onNext(item);
            },
            selectedItem: subject.asObservable()
        };
    }
    mod.factory('boxItemSelected', ['rx', boxItemSelected]);
    
    /**
     * @function boxItemPicker
     * @memberOf box.ui
     * @description Service for opening a Box file or folder picker. Uses the @see $modal service from angular.ui.bootstrap.
     * Supports SAVE_AS, OPEN_FILE, and OPEN_FOLDER modes.
     * @param {Object} $modal ng-modal service
     * @param {Object} rx RxJS namespace object
     * @param {Object} boxItemSelected boxItemSelected service
     * @param {Function} BoxFolder Box Folder constructor
     * @returns {Object} service with method open and property modes
     */
    function boxItemPicker($modal, rx, boxItemSelected, BoxFolder) {
        'use strict';
        var modes = Object.freeze({
            SAVE_AS: 'Save File As',
            OPEN_FILE: 'Open File',
            OPEN_FOLDER: 'Open Folder'
        });
        return {
            mode: modes,
            open: function(mode, filename, modalOptions) {
                var modal = $modal.open(angular.extend(modalOptions || {}, {
                    template:
                        '<div>' +
                        '   <div class="modal-header">' +
                        '       <h3 class="modal-title">' + mode + '</h3>' +
                        '   </div>' +
                        '   <div class="modal-body">' +
                        '       <span box-all-files></span>' +
                        '       <div><label>Filename: <input type="text" ng-model="filename" /></label></div>' +
                        '   </div>' +
                        '   <div class="modal-footer">' +
                        '       <button class="btn btn-primary" ng-click="close()">OK</button>' +
                        '       <button class="btn btn-warning" ng-click="$dismiss()">Cancel</button>' +
                        '   </div>' +
                        '</div>',
                    controller: function($scope) {
                        $scope.filename = filename;
                        $scope.close = function() {
                            boxItemSelected.selectedItem
                                .map(function(item) {
                                    switch (mode) {
                                        case modes.SAVE_AS:
                                            item.filename = $scope.filename;
                                            return item;
                                        case modes.OPEN_FILE:
                                            if (item.type === 'folder') {
                                                throw new Error('Invalid selection - select a file.');
                                            } else {
                                                return item;
                                            }
                                            break;
                                        case modes.OPEN_FOLDER:
                                            if (item.type === 'folder') {
                                                return item;
                                            } else {
                                                return new BoxFolder(item.parent);
                                            }
                                            break;
                                        default:
                                            return item;
                                    }
                                })
                                .subscribe(modal.close.bind(modal), modal.dismiss.bind(modal), angular.noop);
                        };
                        $scope.$on('closeModal', $scope.close);
                        boxItemSelected.selectedItem
                            .subscribe(function(item) {
                                if (item.type === 'file') {
                                    $scope.filename = item.name;
                                }
                            });
                    }
                }));
                return rx.Observable.fromPromise(modal.result);
            }
        };
    }
    mod.factory('boxItemPicker', ['$modal', 'rx', 'boxItemSelected', 'BoxFolder', boxItemPicker]);
    
})();