From 3e9ee9cfc2a6d9e36497b6ed6c976239d94330e6 Mon Sep 17 00:00:00 2001 From: coderaiser Date: Mon, 17 Dec 2012 10:52:33 -0500 Subject: [PATCH] added ability to upload files to dropbox --- ChangeLog | 3 + config.json | 2 + lib/client/menu.js | 72 +- lib/client/storage/_dropbox.js | 181 ++- lib/client/storage/_dropbox_chooser.js | 55 + lib/client/storage/_gdrive.js | 113 ++ lib/client/storage/_github.js | 17 +- lib/client/storage/dropbox/LICENSE.txt | 19 + lib/client/storage/dropbox/README.md | 73 ++ .../storage/dropbox/doc/auth_drivers.md | 167 +++ lib/client/storage/dropbox/doc/coffee_faq.md | 84 ++ lib/client/storage/dropbox/doc/development.md | 89 ++ .../storage/dropbox/doc/getting_started.md | 272 ++++ lib/client/storage/dropbox/lib/README.md | 10 + lib/client/storage/dropbox/lib/dropbox.min.js | 5 + lib/client/storage/dropbox/package.json | 46 + .../storage/dropbox/src/000-dropbox.coffee | 6 + .../storage/dropbox/src/api_error.coffee | 49 + lib/client/storage/dropbox/src/base64.coffee | 72 ++ lib/client/storage/dropbox/src/client.coffee | 1104 +++++++++++++++++ lib/client/storage/dropbox/src/drivers.coffee | 477 +++++++ lib/client/storage/dropbox/src/hmac.coffee | 180 +++ lib/client/storage/dropbox/src/oauth.coffee | 162 +++ lib/client/storage/dropbox/src/prod.coffee | 36 + .../storage/dropbox/src/pulled_changes.coffee | 100 ++ .../storage/dropbox/src/references.coffee | 86 ++ lib/client/storage/dropbox/src/stat.coffee | 127 ++ .../storage/dropbox/src/user_info.coffee | 79 ++ lib/client/storage/dropbox/src/xhr.coffee | 450 +++++++ .../storage/dropbox/src/zzz-export.coffee | 21 + lib/client/storage/dropbox/txt.vimrc.txt | 6 + 31 files changed, 4034 insertions(+), 129 deletions(-) create mode 100644 lib/client/storage/_dropbox_chooser.js create mode 100644 lib/client/storage/_gdrive.js create mode 100644 lib/client/storage/dropbox/LICENSE.txt create mode 100644 lib/client/storage/dropbox/README.md create mode 100644 lib/client/storage/dropbox/doc/auth_drivers.md create mode 100644 lib/client/storage/dropbox/doc/coffee_faq.md create mode 100644 lib/client/storage/dropbox/doc/development.md create mode 100644 lib/client/storage/dropbox/doc/getting_started.md create mode 100644 lib/client/storage/dropbox/lib/README.md create mode 100644 lib/client/storage/dropbox/lib/dropbox.min.js create mode 100644 lib/client/storage/dropbox/package.json create mode 100644 lib/client/storage/dropbox/src/000-dropbox.coffee create mode 100644 lib/client/storage/dropbox/src/api_error.coffee create mode 100644 lib/client/storage/dropbox/src/base64.coffee create mode 100644 lib/client/storage/dropbox/src/client.coffee create mode 100644 lib/client/storage/dropbox/src/drivers.coffee create mode 100644 lib/client/storage/dropbox/src/hmac.coffee create mode 100644 lib/client/storage/dropbox/src/oauth.coffee create mode 100644 lib/client/storage/dropbox/src/prod.coffee create mode 100644 lib/client/storage/dropbox/src/pulled_changes.coffee create mode 100644 lib/client/storage/dropbox/src/references.coffee create mode 100644 lib/client/storage/dropbox/src/stat.coffee create mode 100644 lib/client/storage/dropbox/src/user_info.coffee create mode 100644 lib/client/storage/dropbox/src/xhr.coffee create mode 100644 lib/client/storage/dropbox/src/zzz-export.coffee create mode 100644 lib/client/storage/dropbox/txt.vimrc.txt diff --git a/ChangeLog b/ChangeLog index 46a23f05..96288fd6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,6 +26,9 @@ jquery loaded after ie.js, should be before. inside event function varible called event do not exist (everithing ok on webkit). +* Added ability to upload files to dropbox. + + 2012.12.12, Version 0.1.8 * Added ability to shutdown Cloud Commander diff --git a/config.json b/config.json index 2dd79a63..c014fd05 100644 --- a/config.json +++ b/config.json @@ -11,6 +11,8 @@ "github_key" : "891c251b925e4e967fa9", "github_secret" : "afe9bed1e810c5dc44c4c2a953fc6efb1e5b0545", "dropbox_key" : "0nd3ssnp5fp7tqs", + "dropbox_secret" : "r61lxpchmk8l06o", + "dropbox_encoded_key" : "DkMz4FYHQTA=|GW6pf2dONkrGvckMwBsl1V1vysrCPktPiUWN7UpDjw==", "dropbox_chooser_key" : "o7d6llji052vijk", "logs" : false, "show_keys_panel" : true, diff --git a/lib/client/menu.js b/lib/client/menu.js index ea34ed92..164f4cc6 100644 --- a/lib/client/menu.js +++ b/lib/client/menu.js @@ -34,6 +34,24 @@ var CloudCommander, Util, DOM, $; } } + function getContent(pCallBack){ + return DOM.getCurrentFileContent(function(pData){ + var lName = DOM.getCurrentName(); + if( Util.isObject(pData) ){ + pData = JSON.stringify(pData, null, 4); + + var lExt = '.json'; + if( !Util.checkExtension(lName, lExt) ) + lName += lExt; + } + + Util.exec(pCallBack, { + data: pData, + name: lName + }); + }); + } + /** function return configureation for menu */ function getConfig (){ return{ @@ -76,24 +94,17 @@ var CloudCommander, Util, DOM, $; 'gist': { name: 'Gist', callback: function(key, opt){ - DOM.getCurrentFileContent(function(pData){ - var lName = DOM.getCurrentName(); - if( Util.isObject(pData) ){ - pData = JSON.stringify(pData, null, 4); - - var lExt = '.json'; - if( !Util.checkExtension(lName, lExt) ) - lName += lExt; - } - - var lGitHub = cloudcmd.GitHub; + getContent(function(pParams){ + var lGitHub = cloudcmd.GitHub, + lData = pParams.data, + lName = pParams.name; if('init' in lGitHub) - lGitHub.createGist(pData, lName); + lGitHub.createGist(lData, lName); else Util.exec(cloudcmd.GitHub, function(){ - lGitHub.createGist(pData, lName); + lGitHub.createGist(lData, lName); }); }); @@ -103,28 +114,37 @@ var CloudCommander, Util, DOM, $; 'gdrive': { name: 'GDrive', - callback: function(key, opt){ - DOM.getCurrentFileContent(function(data){ - var lName = DOM.getCurrentName(); - - if( Util.isObject(data) ) - data = JSON.stringify(data, null, 4); - var lData = { - data: data, - name: lName - }; - + getContent(function(pParams){ var lGDrive = cloudcmd.GDrive; if('init' in lGDrive) - lGDrive.init(lData); + lGDrive.init(pParams); else - Util.exec(cloudcmd.GDrive, lData); + Util.exec(cloudcmd.GDrive, pParams); }); Util.log('Uploading to gdrive...'); } + }, + 'dropbox':{ + name: 'DropBox', + callback: function(key, opt){ + getContent(function(pParams){ + var lDropBox = cloudcmd.DropBox, + lData = pParams.data, + lName = pParams.name; + + if('init' in lDropBox) + lDropBox.uploadFile(lData, lName); + else + Util.exec(lDropBox, function(){ + cloudcmd.DropBox.uploadFile(lData, lName); + }); + }); + + Util.log('Uploading to dropbox...'); + } } } }, diff --git a/lib/client/storage/_dropbox.js b/lib/client/storage/_dropbox.js index bedf6c3d..d1c8b9ee 100644 --- a/lib/client/storage/_dropbox.js +++ b/lib/client/storage/_dropbox.js @@ -1,113 +1,102 @@ -var CloudCommander, Util, DOM, gapi; +var CloudCommander, Util, DOM, CloudFunc, Dropbox, cb, Client; +/* module for work with github */ (function(){ "use strict"; - var cloudcmd = CloudCommander, - GDrive = {}; - + var cloudcmd = CloudCommander, + //Client, + DropBoxStore = {}; - function authorize(pData){ - /* https://developers.google.com/drive/credentials */ - Util.setTimeout({ - func : function(pCallBack){ - var lCLIENT_ID = '255175681917.apps.googleusercontent.com', - lSCOPES = 'https://www.googleapis.com/auth/drive', - lParams = { - 'client_id' : lCLIENT_ID, - 'scope' : lSCOPES, - 'immediate' : true - }; - - gapi.auth.authorize(lParams, pCallBack); - }, - - callback : function(pAuthResult){ - var lRet; - if (pAuthResult && !pAuthResult.error){ - uploadFile(pData); - - lRet = true; - } - return lRet; - } - }); - } + /* temporary callback function for work with github */ + cb = function (err, data){ console.log(err || data);}; - function load(pData){ - var lUrl = 'https://apis.google.com/js/client.js'; - - DOM.jsload(lUrl, function(){ - authorize(pData); - }); - } - - /** - * Start the file upload. - * - * @param {Object} evt Arguments from the file selector. - */ - function uploadFile(pData) { - gapi.client.load('drive', 'v2', function() { - GDrive.uploadFile(pData); - }); - } + /* PRIVATE FUNCTIONS */ /** - * Insert new file. - * - * @param {File} fileData {name, data} File object to read data from. - * @param {Function} callback Function to call when the request is complete. + * function loads dropbox.js */ - GDrive.uploadFile = function(pData, callback) { - var lData = pData.data, - lName = pData.name, - boundary = '-------314159265358979323846', - delimiter = "\r\n--" + boundary + "\r\n", - close_delim = "\r\n--" + boundary + "--", - - contentType = pData.type || 'application/octet-stream', - metadata = { - 'title' : lName, - 'mimeType' : contentType - }, - - base64Data = btoa(lData), - - multipartRequestBody = - delimiter + - 'Content-Type: application/json\r\n\r\n' + - JSON.stringify(metadata) + - delimiter + - 'Content-Type: ' + contentType + '\r\n' + - 'Content-Transfer-Encoding: base64\r\n' + - '\r\n' + - base64Data + - close_delim; - - var request = gapi.client.request({ - 'path': '/upload/drive/v2/files', - 'method': 'POST', - 'params': {'uploadType': 'multipart'}, - 'headers': { - 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"' - }, - - 'body': multipartRequestBody + function load(pCallBack){ + console.time('dropbox load'); + + var lSrc = '//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.7.1/dropbox.min.js', + lLocal = CloudFunc.LIBDIRCLIENT + 'storage/dropbox/lib/dropbox.min.js', + lOnload = function(){ + console.timeEnd('dropbox load'); + DOM.Images.hideLoad(); + + Util.exec(pCallBack); + }; + + DOM.jsload(lSrc, { + onload : lOnload, + error : DOM.retJSLoad(lLocal, lOnload) }); - if (!callback) - callback = function(file) { - console.log(file); + } + + function getUserData(pCallBack){ + var lName, + lShowUserInfo = function(pError, pData){ + if(!pError){ + lName = pData.name; + console.log('Hello ' + lName + ' :)!'); + } }; - request.execute(callback); + Client.getUserInfo(lShowUserInfo); + + Util.exec(pCallBack); + } + /** + * function logins on dropbox + * + * @param pData = {key, secret} + */ + DropBoxStore.login = function(pCallBack){ + cloudcmd.getConfig(function(pConfig){ + Client = new Dropbox.Client({ + key: pConfig.dropbox_encoded_key + }); + Client.authDriver(new Dropbox.Drivers.Redirect({rememberUser: true})); + + Client.authenticate(function(pError, pClient) { + Util.log(pError); + Client = pClient; + Util.exec(pCallBack); + }); + + }); + + }; + /** + * upload file to DropBox + */ + DropBoxStore.uploadFile = function(pContent, pFileName){ + if(pContent){ + DOM.Images.showLoad(); + if(!pFileName) + pFileName = Util.getDate(); + + Client.writeFile(pFileName, pContent, function(pError, pData){ + DOM.Images.hideLoad(); + console.log(pError || pData); + }); + } + + return pContent; }; - - GDrive.init = function(pData){ - load(pData); + DropBoxStore.init = function(pCallBack){ + Util.loadOnLoad([ + Util.retExec(pCallBack), + getUserData, + DropBoxStore.login, + load + ]); + + cloudcmd.DropBox.init = null; }; - cloudcmd.GDrive = GDrive; -})(); \ No newline at end of file + cloudcmd.DropBox = DropBoxStore; +})(); diff --git a/lib/client/storage/_dropbox_chooser.js b/lib/client/storage/_dropbox_chooser.js new file mode 100644 index 00000000..5532e1a9 --- /dev/null +++ b/lib/client/storage/_dropbox_chooser.js @@ -0,0 +1,55 @@ +var CloudCommander, DOM, Dropbox; +/* module for work with github */ + +(function(){ + "use strict"; + + var cloudcmd = CloudCommander, + CHOOSER_API = 'https://www.dropbox.com/static/api/1/dropbox.js', + CLIENT_ID, + DropBoxStore = {}, + options = { + linkType: "direct", + success: function(files) { + console.log("Here's the file link:" + files[0].link); + }, + cancel: function() { + console.log('Chose something'); + } + }; + + /* PRIVATE FUNCTIONS */ + + /** + * function loads dropbox.js + */ + function load(){ + console.time('dropbox load'); + + cloudcmd.getConfig(function(pConfig){ + var lElement = DOM.anyload({ + src : CHOOSER_API, + not_append : true, + id : 'dropboxjs', + func : DropBoxStore.choose + + }); + + var lDropBoxId = pConfig.dropbox_chooser_key; + lElement.setAttribute('data-app-key', lDropBoxId); + document.body.appendChild(lElement); + + console.timeEnd('dropbox load'); + }); + } + + DropBoxStore.choose = function(){ + Dropbox.choose(options); + }; + + DropBoxStore.init = function(){ + load(); + }; + + cloudcmd.DropBox = DropBoxStore; +})(); diff --git a/lib/client/storage/_gdrive.js b/lib/client/storage/_gdrive.js new file mode 100644 index 00000000..bedf6c3d --- /dev/null +++ b/lib/client/storage/_gdrive.js @@ -0,0 +1,113 @@ +var CloudCommander, Util, DOM, gapi; + +(function(){ + "use strict"; + + var cloudcmd = CloudCommander, + GDrive = {}; + + + function authorize(pData){ + /* https://developers.google.com/drive/credentials */ + Util.setTimeout({ + func : function(pCallBack){ + var lCLIENT_ID = '255175681917.apps.googleusercontent.com', + lSCOPES = 'https://www.googleapis.com/auth/drive', + lParams = { + 'client_id' : lCLIENT_ID, + 'scope' : lSCOPES, + 'immediate' : true + }; + + gapi.auth.authorize(lParams, pCallBack); + }, + + callback : function(pAuthResult){ + var lRet; + if (pAuthResult && !pAuthResult.error){ + uploadFile(pData); + + lRet = true; + } + return lRet; + } + }); + } + + function load(pData){ + var lUrl = 'https://apis.google.com/js/client.js'; + + DOM.jsload(lUrl, function(){ + authorize(pData); + }); + } + + /** + * Start the file upload. + * + * @param {Object} evt Arguments from the file selector. + */ + function uploadFile(pData) { + gapi.client.load('drive', 'v2', function() { + GDrive.uploadFile(pData); + }); + } + + /** + * Insert new file. + * + * @param {File} fileData {name, data} File object to read data from. + * @param {Function} callback Function to call when the request is complete. + */ + GDrive.uploadFile = function(pData, callback) { + var lData = pData.data, + lName = pData.name, + boundary = '-------314159265358979323846', + delimiter = "\r\n--" + boundary + "\r\n", + close_delim = "\r\n--" + boundary + "--", + + contentType = pData.type || 'application/octet-stream', + metadata = { + 'title' : lName, + 'mimeType' : contentType + }, + + base64Data = btoa(lData), + + multipartRequestBody = + delimiter + + 'Content-Type: application/json\r\n\r\n' + + JSON.stringify(metadata) + + delimiter + + 'Content-Type: ' + contentType + '\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + '\r\n' + + base64Data + + close_delim; + + var request = gapi.client.request({ + 'path': '/upload/drive/v2/files', + 'method': 'POST', + 'params': {'uploadType': 'multipart'}, + 'headers': { + 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"' + }, + + 'body': multipartRequestBody + }); + + if (!callback) + callback = function(file) { + console.log(file); + }; + + request.execute(callback); + }; + + + GDrive.init = function(pData){ + load(pData); + }; + + cloudcmd.GDrive = GDrive; +})(); \ No newline at end of file diff --git a/lib/client/storage/_github.js b/lib/client/storage/_github.js index 24f426f3..0f7ba500 100644 --- a/lib/client/storage/_github.js +++ b/lib/client/storage/_github.js @@ -62,11 +62,7 @@ var CloudCommander, Util, DOM, $, Github, cb; if ( Util.isContainStr(lCode, '?code=') ){ lCode = lCode.replace('?code=',''); - DOM.ajax({ - type : 'put', - url : AuthURL, - data: lCode, - success: function(pData){ + var lSuccess = function(pData){ if(pData && pData.token){ lToken = pData.token; @@ -76,8 +72,15 @@ var CloudCommander, Util, DOM, $, Github, cb; } else Util.log("Worning: token not getted..."); - } - }); + }, + lData = { + type : 'put', + url : AuthURL, + data : lCode, + success : lSuccess + }; + + DOM.ajax(lData); } else //window.open('welcome.html', 'welcome','width=300,height=200,menubar=yes,status=yes')"> diff --git a/lib/client/storage/dropbox/LICENSE.txt b/lib/client/storage/dropbox/LICENSE.txt new file mode 100644 index 00000000..03350194 --- /dev/null +++ b/lib/client/storage/dropbox/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2012 Dropbox, Inc., http://www.dropbox.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/client/storage/dropbox/README.md b/lib/client/storage/dropbox/README.md new file mode 100644 index 00000000..cbca9f8b --- /dev/null +++ b/lib/client/storage/dropbox/README.md @@ -0,0 +1,73 @@ +# Client Library for the Dropbox API + +This is a JavaScript client library for the Dropbox API, +[written in CoffeeScript](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md), +suitable for use in both modern browsers and in server-side code running under +[node.js](http://nodejs.org/). + + +## Supported Platforms + +This library is tested against the following JavaScript platforms + +* [node.js](http://nodejs.org/) 0.6 and 0.8 +* [Chrome](https://www.google.com/chrome) 23 +* [Firefox](www.mozilla.org/firefox) 17 +* Internet Explorer 9 + +Keep in mind that the versions above are not hard requirements. + + +## Installation and Usage + +The +[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md) +will help you get your first dropbox.js application up and running. + +Peruse the source code of the +[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples), +and borrow as much as you need. + +The +[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html) +can be a good place to bookmark while building your application. + +If you run into a problem, take a look at +[the dropbox.js GitHub issue list](https://github.com/dropbox/dropbox-js/issues). +Please open a new issue if your problem wasn't already reported. + + +## Development + +This library is written in CoffeeScript. +[This document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md) +can help you understand if that matters to you. + +The +[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md) +will make your life easier if you need to change the source code. + + +## Platform-Specific Issues + +This lists the most serious problems that you might run into while using +`dropbox.js`. See +[the GitHub issue list](https://github.com/dropbox/dropbox-js/issues) for a +full list of outstanding problems. + +### node.js + +Reading and writing binary files is currently broken. + +### Internet Explorer 9 + +The library only works when used from `https://` pages, due to +[these issues](http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx). + +Reading and writing binary files is unsupported. + + +## Copyright and License + +The library is Copyright (c) 2012 Dropbox Inc., and distributed under the MIT +License. diff --git a/lib/client/storage/dropbox/doc/auth_drivers.md b/lib/client/storage/dropbox/doc/auth_drivers.md new file mode 100644 index 00000000..5507f058 --- /dev/null +++ b/lib/client/storage/dropbox/doc/auth_drivers.md @@ -0,0 +1,167 @@ +# Authentication Drivers + +This document explains the structure and functionality of a `dropbox.js` OAuth +driver, and describes the drivers that ship with the library. + +## The OAuth Driver Interface + +An OAuth driver is a JavaScript object that implements the methods documented +in the +[Dropbox.AuthDriver class](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/AuthDriver.html). +This class exists solely for the purpose of documenting these methods. + +A simple driver can get away with implementing `url` and `doAuthorize`. The +following example shows an awfully unusable node.js driver that asks the user +to visit the authorization URL in a browser. + +```javascript +var util = require("util"); +var simpleDriver = { + url: function() { return ""; }, + doAuthorize: function(authUrl, token, tokenSecret, callback) { + util.print("Visit the following in a browser, then press Enter\n" + + authUrl + "\n"); + var onEnterKey = function() { + process.stdin.removeListener("data", onEnterKey); + callback(token); + } + process.stdin.addListener("data", onEnterKey); + process.stdin.resume(); + } +}; +``` + +Complex drivers can take control of the OAuth process by implementing +`onAuthStateChange`. Implementations of this method should read the `authState` +field of the `Dropbox.Client` instance they are given to make decisions. +Implementations should call the `credentials` and `setCredentials` methods on +the client to control the OAuth process. + +See the +[Dropbox.Drivers.Redirect source](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee) +for a sample implementation of `onAuthStateChange`. + + +### The OAuth Process Steps + +The `authenticate` method in `Dropbox.Client` implements the OAuth process as a +finite state machine (FSM). The current state is available in the `authState` +field. + +The authentication FSM has the following states. + +* `Dropbox.Client.RESET` is the initial state, where the client has no OAuth +tokens; after `onAuthStateChange` is triggered, the client will attempt to +obtain an OAuth request token +* `Dropbox.Client.REQUEST` indicates that the client has obtained an OAuth +request token; after `onAuthStateChange` is triggered, the client will call +`doAuthorize` on the OAuth driver, to get the OAuth request token authorized by +the user +* `Dropbox.Client.AUTHORIZED` is reached after the `doAuthorize` calls its +callback, indicating that the user has authorized the OAuth request token; +after `onAuthStateChange` is triggered, the client will attempt to exchange the +request token for an OAuth access token +* `Dropbox.Client.DONE` indicates that the OAuth process has completed, and the +client has an OAuth access token that can be used in API calls; after +`onAuthStateChange` is triggered, `authorize` will call its callback function, +and report success +* `Dropbox.Client.SIGNED_OFF` is reached when the client's `signOut` method is +called, after the API call succeeds; after `onAuthStateChange` is triggered, +`signOut` will call its callback function, and report success +* `Dropbox.Client.ERROR` is reached if any of the Dropbox API calls used by +`authorize` or `signOut` results in an error; after `onAuthStateChange` is +triggered, `authorize` or `signOut` will call its callback function and report +the error + + +## Built-in OAuth Drivers + +`dropbox.js` ships with the OAuth drivers below. + +### Dropbox.Drivers.Redirect + +The recommended built-in driver for browser applications completes the OAuth +token authorization step by redirecting the browser to the Dropbox page that +performs the authorization and having that page redirect back to the +application page. + +This driver's constructor takes the following options. + +* `useQuery` should be set to true for applications that use the URL fragment +(the part after `#`) to store state information +* `rememberUser` can be set to true to have the driver store the user's OAuth +token in `localStorage`, so the user doesn't have to authorize the application +on every request + +Although it seems that `rememberUser` should be true by default, it brings a +couple of drawbacks. The user's token will still be valid after signing off the +Dropbox web site, so your application will still recognize the user and access +their Dropbox. This behavior is unintuitive to users. A reasonable compromise +for apps that use `rememberUser` is to provide a `Sign out` button that calls +the `signOut` method on the app's `Dropbox.Client` instance. + +The +[checkbox.js](https://github.com/dropbox/dropbox-js/tree/master/samples/checkbox.js) +sample application uses `rememberUser`, and implements signing off as described +above. + + +### Dropbox.Drivers.Popup + +This driver may be useful for browser applications that can't handle the +redirections peformed by `Dropbox.Drivers.Redirect`. This driver avoids +changing the location of the application's browser window by popping up a +separate window, and loading the Dropbox authorization page in that window. + +The popup method has a couple of serious drawbacks. Most browsers will not +display the popup window by default, and instead will show a hard-to-notice +warning that the user must interact with to display the popup. The driver's +code for communicating between the popup and the main application window does +not work on IE9 and below, so applications that use it will only work on +Chrome, Firefox and IE10+. + +If the drawbacks above are more acceptable than restructuring your application +to handle redirects, create a page on your site that contains the +[receiver code](https://github.com/dropbox/dropbox-js/blob/master/test/html/oauth_receiver.html), +and point the `Dropbox.Drivers.Popup` constructor to it. + +```javascript +client.authDriver(new Dropbox.Drivers.Popup({receiverUrl: "https://url.to/receiver.html"})); +``` + +The popup driver adds a `#` (fragment hash) to the receiver URL if necessary, +to ensure that the user's Dropbox uid and OAuth token are passed to the +receiver in a URL fragment. This measure may improve your users' privacy, as it +reduces the chance that their uid or token ends up in a server log. + +If you have a good reason to disable the behavior above, set the `noFragment` +option to true. + +```javascript +client.authDriver(new Dropbox.Drivers.Popup({receiverUrl: "https://url.to/receiver.html", noFragment: true})); +``` + + +### Dropbox.Drivers.NodeServer + +This driver is designed for use in the automated test suites of node.js +applications. It completes the OAuth token authorization step by opening the +Dropbox authorization page in a new browser window, and "catches" the OAuth +redirection by setting up a small server using the `https` built-in node.js +library. + +The driver's constructor takes the following options. + +* `port` is the HTTP port number; the default is 8192, and works well with the +Chrome extension described below +* `favicon` is a path to a file that will be served in response to requests to +`/favicon.ico`; setting this to a proper image will avoid some warnings in the +browsers' consoles + +To fully automate your test suite, you need to load up the Chrome extension +bundled in the `dropbox.js` source tree. The extension automatically clicks on +the "Authorize" button in the Dropbox token authorization page, and closes the +page after the token authorization process completes. Follow the steps in the +[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md) +to build and install the extension. + diff --git a/lib/client/storage/dropbox/doc/coffee_faq.md b/lib/client/storage/dropbox/doc/coffee_faq.md new file mode 100644 index 00000000..b4814244 --- /dev/null +++ b/lib/client/storage/dropbox/doc/coffee_faq.md @@ -0,0 +1,84 @@ +# dropbox.js and CoffeeScript FAQ + +dropbox.js is written in [CoffeeScript](http://coffeescript.org/), which +compiles into very readable JavaScript. This document addresses the concerns +that are commonly raised by JavaScript developers that do not use or wish to +learn CoffeeScript. + + +## Do I need to learn CoffeeScript to use the library? + +**No.** + +The examples in the +[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md) +are all written in JavaScript. The +[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html) +covers the entire library, so you should not need to read the library source +code to understand how to use it. + +_Please open an issue if the documentation is unclear!_ + +The +[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples), +are written in CoffeeScript. Please use the `Try CoffeeScript` button on the +[CoffeeScript](http://coffeescript.org/) home page to quickly compile the +sample CoffeeScript into very readable JavaScript. + + +## Do I need to learn CoffeeScript to know how dropbox.js works? + +**No.** + +You can follow the +[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md) +to build the un-minified JavaScript library in `lib/dropbox.js` and then use +your editor's find feature to get to the source code for the methods that you +are interested in. + +The building instructions in the development guide do not require familiarity +with CoffeeScript. + + +## Do I need to learn CoffeeScript to modify dropbox.js? + +**Yes, but you might not need to modify the library.** + +You do need to learn CoffeeScript to change the `dropbox.js` source code. At +the same time, you can take advantage of the library hooks and the dynamic +nature of the JavaScript language to change the behavior of `dropbox.js` +without touching the source code. + +* You can implement your OAuth strategy. +* You can add methods to the prototype classes such as `Dropbox.Client` to +implement custom operations. _Please open an issue if you think your addition +is generally useful!_ +* You can replace internal classes such as `Dropbox.Xhr` (or selectively +replace methods) with wrappers that tweak the original behavior + + +## Can I contribute to dropbox.js without learning CoffeeScript? + +**Yes.** + +Most of the development time is spent on API design, developing tests, +documentation and sample code. Contributing a good testing strategy with a bug +report can save us 90% of the development time. A feature request that also +includes a well thought-out API change proposal and testing strategy can also +save us 90-95% of the implementation time. + +At the same time, _please open issues for bugs and feature requests even if you +don't have time to include any of the above_. Knowing of a problem is the first +step towards fixing it. + +Last, please share your constructive suggestions on how to make `dropbox.js` +easier to use for JavaScript developers that don't speak CoffeeScript. + + +## Can I complain to get dropbox.js to switch away from CoffeeScript? + +**No.** + +At the moment, 100% of the library's development comes from unpaid, voluntary +efforts. Switching to JavaScript would reduce the efficiency of these efforts, +and it would kill developer motivation. diff --git a/lib/client/storage/dropbox/doc/development.md b/lib/client/storage/dropbox/doc/development.md new file mode 100644 index 00000000..1429d64b --- /dev/null +++ b/lib/client/storage/dropbox/doc/development.md @@ -0,0 +1,89 @@ +# dropbox.js Development + +Read this document if you want to build `dropbox.js` or modify its source code. +If you want to write applications using dropbox.js, check out the +[Getting Started](getting_started.md). + +The library is written using [CoffeeScript](http://coffeescript.org/), built +using [cake](http://coffeescript.org/documentation/docs/cake.html), minified +using [uglify.js](https://github.com/mishoo/UglifyJS/), tested using +[mocha](http://visionmedia.github.com/mocha/) and +[chai.js](http://chaijs.com/), and packaged using [npm](https://npmjs.org/). + +If you don't "speak" CoffeeScript, +[this document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md) +might address some of your concerns. + + +## Dev Environment Setup + +Install [node.js](http://nodejs.org/#download) to get `npm` (the node +package manager), then use it to install the libraries required by the test +suite. + +```bash +git clone https://github.com/dropbox/dropbox-js.git +cd dropbox-js +npm install +``` + +## Build + +Run `npm pack` and ignore any deprecation warnings that might come up. + +```bash +npm pack +``` + +The build output is in the `lib/` directory. `dropbox.js` is the compiled +library that ships in the npm package, and `dropbox.min.js` is a minified +version, optimized for browser apps. + + +## Test + +First, you will need to obtain a couple of Dropbox tokens that will be used by +the automated tests. + +```bash +cake tokens +``` + +Re-run the command above if the tests fail due to authentication errors. + +Once you have Dropbox tokens, you can run the test suite in node.js or in your +default browser. + +```bash +cake test +cake webtest +``` + +The library is automatically re-built when running tests, so you don't need to +run `npm pack`. Please run the tests in both node.js and a browser before +submitting pull requests. + +The tests store all their data in folders named along the lines of +`js tests.0.ac1n6lgs0e3lerk9`. If tests fail, you might have to clean up these +folders yourself. + + +## Testing Chrome Extension + +The test suite opens up a couple of Dropbox authorization pages, and a page +that cannot close itself. dropbox.js ships with a Google Chrome extension that +can fully automate the testing process on Chrome. + +The extension is written in CoffeeScript, so you will have to compile it. + +```bash +cake extension +``` + +After compilation, have Chrome load the unpacked extension at +`test/chrome_extension` and click on the scary-looking toolbar icon to activate +the extension. The icon's color should turn red, to indicate that it is active. + +The extension performs some checks to prevent against attacks. However, for +best results, you should disable the automation (by clicking on the extension +icon) when you're not testing dropbox.js. diff --git a/lib/client/storage/dropbox/doc/getting_started.md b/lib/client/storage/dropbox/doc/getting_started.md new file mode 100644 index 00000000..8fdea598 --- /dev/null +++ b/lib/client/storage/dropbox/doc/getting_started.md @@ -0,0 +1,272 @@ +# Getting Started + +This is a guide to writing your first dropbox.js application. + + +## Library Setup + +This section describes how to get the library hooked up into your application. + +### Browser Applications + +To get started right away, place this snippet in your page's ``. + +```html + +``` + +The snippet is not a typo. [cdnjs](https://cdnjs.com) recommends using +[protocol-relative URLs](http://paulirish.com/2010/the-protocol-relative-url/). + +To get the latest development build of dropbox.js, follow the steps in the +[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md). + + +#### "Powered by Dropbox" Static Web Apps + +Before writing any source code, use the +[console app](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/powered_by.js/public/index.html) +to set up your Dropbox. After adding an application, place the source code at +`/Apps/Static Web Apps/my_awesome_app/public`. You should find a pre-generated +`index.html` file in there. + +### node.js Applications + +First, install the `dropbox` [npm](https://npmjs.org/) package. + +```bash +npm install dropbox +``` + +Once the npm package is installed, the following `require` statement lets you +access the same API as browser applications + +```javascript +var Dropbox = require("dropbox"); +``` + + +## Initialization + +[Register your application](https://www.dropbox.com/developers/apps) to obtain +an API key. Read the brief +[API core concepts intro](https://www.dropbox.com/developers/start/core). + +Once you have an API key, use it to create a `Dropbox.Client`. + +```javascript +var client = new Dropbox.Client({ + key: "your-key-here", secret: "your-secret-here", sandbox: true +}); +``` + +If your application requires full Dropbox access, leave out the `sandbox: true` +parameter. + + +### Browser and Open-Source Applications + +The Dropbox API guidelines ask that the API key and secret is never exposed in +cleartext. This is an issue for browser-side and open-source applications. + +To meet this requirement, +[encode your API key](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/api_keys.js/public/index.html) +and pass the encoded key string to the `Dropbox.Client` constructor. + +```javascript +var client = new Dropbox.Client({ + key: "encoded-key-string|it-is-really-really-long", sandbox: true +}); +``` + + +## Authentication + +Before you can make any API calls, you need to authenticate your application's +user with Dropbox, and have them authorize your app's to access their Dropbox. + +This process follows the [OAuth 1.0](http://tools.ietf.org/html/rfc5849) +protocol, which entails sending the user to a Web page on `www.dropbox.com`, +and then having them redirected back to your application. Each Web application +has its requirements, so `dropbox.js` lets you customize the authentication +process by implementing an +[OAuth driver](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee). + +At the same time, dropbox.js ships with a couple of OAuth drivers, and you +should take advantage of them as you prototype your application. + +Read the +[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md) +for further information about writing an OAuth driver, and to learn about all +the drivers that ship with `dropbox.js`. + +### Browser Setup + +The following snippet will set up the recommended driver. + +```javascript +client.authDriver(new Dropbox.Drivers.Redirect()); +``` + +The +[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md) +describes some useful options that you can pass to the +`Dropbox.Drivers.Redirect` constructor. + +### node.js Setup + +Single-process node.js applications should create one driver to authenticate +all the clients. + +```javascript +client.authDriver(new Dropbox.Drivers.NodeServer(8191)); +``` + +The +[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md) +has useful tips on using the `NodeServer` driver. + +### Shared Code + +After setting up an OAuth driver, authenticating the user is one method call +away. + +```javascript +client.authenticate(function(error, client) { + if (error) { + // Replace with a call to your own error-handling code. + // + // Don't forget to return from the callback, so you don't execute the code + // that assumes everything went well. + return showError(error); + } + + // Replace with a call to your own application code. + // + // The user authorized your app, and everything went well. + // client is a Dropbox.Client instance that you can use to make API calls. + doSomethingCool(client); +}); +``` + +## Error Handlng + +When Dropbox API calls fail, dropbox.js methods pass a `Dropbox.Error` instance +as the first parameter in their callbacks. This parameter is named `error` in +all the code snippets on this page. + +If `error` is a truthy value, you should either recover from the error, or +notify the user that an error occurred. The `status` field in the +`Dropbox.Error` instance contains the HTTP error code, which should be one of +the +[error codes in the REST API](https://www.dropbox.com/developers/reference/api#error-handling). + +The snippet below is a template for an extensive error handler. + +```javascript +var showError = function(error) { + if (window.console) { // Skip the "if" in node.js code. + console.error(error); + } + + switch (error.status) { + case 401: + // If you're using dropbox.js, the only cause behind this error is that + // the user token expired. + // Get the user through the authentication flow again. + break; + + case 404: + // The file or folder you tried to access is not in the user's Dropbox. + // Handling this error is specific to your application. + break; + + case 507: + // The user is over their Dropbox quota. + // Tell them their Dropbox is full. Refreshing the page won't help. + break; + + case 503: + // Too many API requests. Tell the user to try again later. + // Long-term, optimize your code to use fewer API calls. + break; + + case 400: // Bad input parameter + case 403: // Bad OAuth request. + case 405: // Request method not expected + default: + // Caused by a bug in dropbox.js, in your application, or in Dropbox. + // Tell the user an error occurred, ask them to refresh the page. + } +}; +``` + + +## The Fun Part + +Authentication was the hard part of the API integration, and error handling was +the most boring part. Now that these are both behind us, you can interact +with the user's Dropbox and focus on coding up your application! + +The following sections have some commonly used code snippets. The +[Dropbox.Client API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/Client.html) +will help you navigate less common scenarios, and the +[Dropbox REST API reference](https://www.dropbox.com/developers/reference/api) +describes the underlying HTTP protocol, and can come in handy when debugging +your application, or if you want to extend dropbox.js. + +### User Info + +```javascript +client.getUserInfo(function(error, userInfo) { + if (error) { + return showError(error); // Something went wrong. + } + + alert("Hello, " + userInfo.name + "!"); +}); +``` + +### Write a File + +```javascript +client.writeFile("hello_world.txt", "Hello, world!\n", function(error, stat) { + if (error) { + return showError(error); // Something went wrong. + } + + alert("File saved as revision " + stat.revisionTag); +}); +``` + +### Read a File + +```javascript +client.readFile("hello_world.txt", function(error, data) { + if (error) { + return showError(error); // Something went wrong. + } + + alert(data); // data has the file's contents +}); +``` + +### List a Directory's Contents + +```javascript +client.readdir("/", function(error, entries) { + if (error) { + return showError(error); // Something went wrong. + } + + alert("Your Dropbox contains " + entries.join(", ")); +}); +``` + +### Sample Applications + +Check out the +[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples) +to see how all these concepts play out together. + diff --git a/lib/client/storage/dropbox/lib/README.md b/lib/client/storage/dropbox/lib/README.md new file mode 100644 index 00000000..6a273731 --- /dev/null +++ b/lib/client/storage/dropbox/lib/README.md @@ -0,0 +1,10 @@ +# dropbox.js Build Directory + +dropbox.js is written in [CoffeeScript](http://coffeescript.org/), and its +source code is in the `src/` directory. The `lib/` directory contains the +compiled JavaScript produced by the build process. + +The +[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md) +contains a step-by-step guide to the library's build process, and will help you +populate this directory. diff --git a/lib/client/storage/dropbox/lib/dropbox.min.js b/lib/client/storage/dropbox/lib/dropbox.min.js new file mode 100644 index 00000000..86ef3b5f --- /dev/null +++ b/lib/client/storage/dropbox/lib/dropbox.min.js @@ -0,0 +1,5 @@ +/* + * dropbox 0.7.1 + */ +(function(){var t,e,r,n,o,i,s,a,h,u,l,p,c,d,f,y,v,m,g,w,S,b,T={}.hasOwnProperty,E=function(t,e){function r(){this.constructor=t}for(var n in e)T.call(e,n)&&(t[n]=e[n]);return r.prototype=e.prototype,t.prototype=new r,t.__super__=e.prototype,t};if(t=function(){function t(t){this.client=new e(t)}return t}(),t.ApiError=function(){function t(t,e,r){var n;if(this.method=e,this.url=r,this.status=t.status,n=t.responseType?t.response||t.responseText:t.responseText)try{this.responseText=""+n,this.response=JSON.parse(n)}catch(o){this.response=null}else this.responseText="(no response)",this.response=null}return t.prototype.toString=function(){return"Dropbox API error "+this.status+" from "+this.method+" "+this.url+" :: "+this.responseText},t.prototype.inspect=function(){return""+this},t}(),"undefined"!=typeof window&&null!==window?window.atob&&window.btoa?(h=function(t){return window.atob(t)},d=function(t){return window.btoa(t)}):(l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f=function(t,e,r){var n,o;for(o=3-e,t<<=8*o,n=3;n>=o;)r.push(l.charAt(63&t>>6*n)),n-=1;for(n=e;3>n;)r.push("="),n+=1;return null},u=function(t,e,r){var n,o;for(o=4-e,t<<=6*o,n=2;n>=o;)r.push(String.fromCharCode(255&t>>8*n)),n-=1;return null},d=function(t){var e,r,n,o,i,s;for(o=[],e=0,r=0,n=i=0,s=t.length;s>=0?s>i:i>s;n=s>=0?++i:--i)e=e<<8|t.charCodeAt(n),r+=1,3===r&&(f(e,r,o),e=r=0);return r>0&&f(e,r,o),o.join("")},h=function(t){var e,r,n,o,i,s,a;for(i=[],e=0,n=0,o=s=0,a=t.length;(a>=0?a>s:s>a)&&(r=t.charAt(o),"="!==r);o=a>=0?++s:--s)e=e<<6|l.indexOf(r),n+=1,4===n&&(u(e,n,i),e=n=0);return n>0&&u(e,n,i),i.join("")}):(h=function(t){var e,r;return e=new Buffer(t,"base64"),function(){var t,n,o;for(o=[],r=t=0,n=e.length;n>=0?n>t:t>n;r=n>=0?++t:--t)o.push(String.fromCharCode(e[r]));return o}().join("")},d=function(t){var e,r;return e=new Buffer(function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(t.charCodeAt(r));return o}()),e.toString("base64")}),t.Client=function(){function r(e){this.sandbox=e.sandbox||!1,this.apiServer=e.server||this.defaultApiServer(),this.authServer=e.authServer||this.defaultAuthServer(),this.fileServer=e.fileServer||this.defaultFileServer(),this.downloadServer=e.downloadServer||this.defaultDownloadServer(),this.oauth=new t.Oauth(e),this.driver=null,this.filter=null,this.uid=null,this.authState=null,this.authError=null,this._credentials=null,this.setCredentials(e),this.setupUrls()}return r.prototype.authDriver=function(t){return this.driver=t,this},r.prototype.xhrFilter=function(t){return this.filter=t,this},r.prototype.dropboxUid=function(){return this.uid},r.prototype.credentials=function(){return this._credentials||this.computeCredentials(),this._credentials},r.prototype.authenticate=function(r){var n,o,i=this;return n=null,o=function(){var s;if(n!==i.authState&&(n=i.authState,i.driver.onAuthStateChange))return i.driver.onAuthStateChange(i,o);switch(i.authState){case e.RESET:return i.requestToken(function(t,r){var n,s;return t?(i.authError=t,i.authState=e.ERROR):(n=r.oauth_token,s=r.oauth_token_secret,i.oauth.setToken(n,s),i.authState=e.REQUEST),i._credentials=null,o()});case e.REQUEST:return s=i.authorizeUrl(i.oauth.token),i.driver.doAuthorize(s,i.oauth.token,i.oauth.tokenSecret,function(){return i.authState=e.AUTHORIZED,i._credentials=null,o()});case e.AUTHORIZED:return i.getAccessToken(function(t,r){return t?(i.authError=t,i.authState=e.ERROR):(i.oauth.setToken(r.oauth_token,r.oauth_token_secret),i.uid=r.uid,i.authState=e.DONE),i._credentials=null,o()});case e.DONE:return r(null,i);case t.SIGNED_OFF:return i.reset(),o();case e.ERROR:return r(i.authError)}},o(),this},r.prototype.signOut=function(r){var n,o=this;return n=new t.Xhr("POST",this.urls.signOut),n.signWithOauth(this.oauth),this.dispatchXhr(n,function(t){return t?r(t):(o.reset(),o.authState=e.SIGNED_OFF,o.driver.onAuthStateChange?o.driver.onAuthStateChange(o,function(){return r(t)}):r(t))})},r.prototype.signOff=function(t){return this.signOut(t)},r.prototype.getUserInfo=function(e){var r;return r=new t.Xhr("GET",this.urls.accountInfo),r.signWithOauth(this.oauth),this.dispatchXhr(r,function(r,n){return e(r,t.UserInfo.parse(n),n)})},r.prototype.readFile=function(e,r,n){var o,i,s,a,h,u,l;return n||"function"!=typeof r||(n=r,r=null),i={},u=null,o=null,a=null,r&&(r.versionTag?i.rev=r.versionTag:r.rev&&(i.rev=r.rev),r.blob&&(u="blob"),r.binary&&(u="b"),r.length?(null!=r.start?(h=r.start,s=r.start+r.length-1):(h="",s=r.length),a="bytes="+h+"-"+s):null!=r.start&&(a="bytes="+r.start+"-"),r.modifiedSince&&(o=new Date(r.modifiedSince).toUTCString())),l=new t.Xhr("GET",""+this.urls.getFile+"/"+this.urlEncodePath(e)),l.setParams(i).signWithOauth(this.oauth).setResponseType(u),o&&l.setHeader("If-Modified-Since",o),a&&l.setHeader("Range",a),this.dispatchXhr(l,function(e,r,o){return n(e,r,t.Stat.parse(o))})},r.prototype.writeFile=function(e,r,n,o){var i;return o||"function"!=typeof n||(o=n,n=null),i=t.Xhr.canSendForms&&"object"==typeof r,i?this.writeFileUsingForm(e,r,n,o):this.writeFileUsingPut(e,r,n,o)},r.prototype.writeFileUsingForm=function(e,r,n,o){var i,s,a,h;return a=e.lastIndexOf("/"),-1===a?(i=e,e=""):(i=e.substring(a),e=e.substring(0,a)),s={file:i},n&&(n.noOverwrite&&(s.overwrite="false"),n.lastVersionTag?s.parent_rev=n.lastVersionTag:(n.parentRev||n.parent_rev)&&(s.parent_rev=n.parentRev||n.parent_rev)),h=new t.Xhr("POST",""+this.urls.postFile+"/"+this.urlEncodePath(e)),h.setParams(s).signWithOauth(this.oauth).setFileField("file",i,r,"application/octet-stream"),delete s.file,this.dispatchXhr(h,function(e,r){return o(e,t.Stat.parse(r))})},r.prototype.writeFileUsingPut=function(e,r,n,o){var i,s;return i={},n&&(n.noOverwrite&&(i.overwrite="false"),n.lastVersionTag?i.parent_rev=n.lastVersionTag:(n.parentRev||n.parent_rev)&&(i.parent_rev=n.parentRev||n.parent_rev)),s=new t.Xhr("POST",""+this.urls.putFile+"/"+this.urlEncodePath(e)),s.setBody(r).setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){return o(e,t.Stat.parse(r))})},r.prototype.stat=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o={},r&&(null!=r.version&&(o.rev=r.version),(r.removed||r.deleted)&&(o.include_deleted="true"),r.readDir&&(o.list="true",r.readDir!==!0&&(o.file_limit=""+r.readDir)),r.cacheHash&&(o.hash=r.cacheHash)),o.include_deleted||(o.include_deleted="false"),o.list||(o.list="false"),i=new t.Xhr("GET",""+this.urls.metadata+"/"+this.urlEncodePath(e)),i.setParams(o).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){var o,i,s;return s=t.Stat.parse(r),o=(null!=r?r.contents:void 0)?function(){var e,n,o,s;for(o=r.contents,s=[],e=0,n=o.length;n>e;e++)i=o[e],s.push(t.Stat.parse(i));return s}():void 0,n(e,s,o)})},r.prototype.readdir=function(t,e,r){var n;return r||"function"!=typeof e||(r=e,e=null),n={readDir:!0},e&&(null!=e.limit&&(n.readDir=e.limit),e.versionTag&&(n.versionTag=e.versionTag)),this.stat(t,n,function(t,e,n){var o,i;return o=n?function(){var t,e,r;for(r=[],t=0,e=n.length;e>t;t++)i=n[t],r.push(i.name);return r}():null,r(t,o,e,n)})},r.prototype.metadata=function(t,e,r){return this.stat(t,e,r)},r.prototype.makeUrl=function(e,r,n){var o,i,s,a,h;return n||"function"!=typeof r||(n=r,r=null),i=r&&(r["long"]||r.longUrl||r.downloadHack)?{short_url:"false"}:{},e=this.urlEncodePath(e),s=""+this.urls.shares+"/"+e,o=!1,a=!1,r&&(r.downloadHack?(o=!0,a=!0):r.download&&(o=!0,s=""+this.urls.media+"/"+e)),h=new t.Xhr("POST",s).setParams(i).signWithOauth(this.oauth),this.dispatchXhr(h,function(e,r){return a&&r&&r.url&&(r.url=r.url.replace(this.authServer,this.downloadServer)),n(e,t.PublicUrl.parse(r,o))})},r.prototype.history=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o={},r&&null!=r.limit&&(o.rev_limit=r.limit),i=new t.Xhr("GET",""+this.urls.revisions+"/"+this.urlEncodePath(e)),i.setParams(o).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){var o,i;return i=r?function(){var e,n,i;for(i=[],e=0,n=r.length;n>e;e++)o=r[e],i.push(t.Stat.parse(o));return i}():void 0,n(e,i)})},r.prototype.revisions=function(t,e,r){return this.history(t,e,r)},r.prototype.thumbnailUrl=function(t,e){var r;return r=this.thumbnailXhr(t,e),r.paramsToUrl().url},r.prototype.readThumbnail=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o="b",r&&r.blob&&(o="blob"),i=this.thumbnailXhr(e,r),i.setResponseType(o),this.dispatchXhr(i,function(e,r,o){return n(e,r,t.Stat.parse(o))})},r.prototype.thumbnailXhr=function(e,r){var n,o;return n={},r&&(r.format?n.format=r.format:r.png&&(n.format="png"),r.size&&(n.size=r.size)),o=new t.Xhr("GET",""+this.urls.thumbnails+"/"+this.urlEncodePath(e)),o.setParams(n).signWithOauth(this.oauth)},r.prototype.revertFile=function(e,r,n){var o;return o=new t.Xhr("POST",""+this.urls.restore+"/"+this.urlEncodePath(e)),o.setParams({rev:r}).signWithOauth(this.oauth),this.dispatchXhr(o,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.restore=function(t,e,r){return this.revertFile(t,e,r)},r.prototype.findByName=function(e,r,n,o){var i,s;return o||"function"!=typeof n||(o=n,n=null),i={query:r},n&&(null!=n.limit&&(i.file_limit=n.limit),(n.removed||n.deleted)&&(i.include_deleted=!0)),s=new t.Xhr("GET",""+this.urls.search+"/"+this.urlEncodePath(e)),s.setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){var n,i;return i=r?function(){var e,o,i;for(i=[],e=0,o=r.length;o>e;e++)n=r[e],i.push(t.Stat.parse(n));return i}():void 0,o(e,i)})},r.prototype.search=function(t,e,r,n){return this.findByName(t,e,r,n)},r.prototype.makeCopyReference=function(e,r){var n;return n=new t.Xhr("GET",""+this.urls.copyRef+"/"+this.urlEncodePath(e)),n.signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.CopyReference.parse(n))})},r.prototype.copyRef=function(t,e){return this.makeCopyReference(t,e)},r.prototype.pullChanges=function(e,r){var n,o;return r||"function"!=typeof e||(r=e,e=null),n=e?e.cursorTag?{cursor:e.cursorTag}:{cursor:e}:{},o=new t.Xhr("POST",this.urls.delta),o.setParams(n).signWithOauth(this.oauth),this.dispatchXhr(o,function(e,n){return r(e,t.PulledChanges.parse(n))})},r.prototype.delta=function(t,e){return this.pullChanges(t,e)},r.prototype.mkdir=function(e,r){var n;return n=new t.Xhr("POST",this.urls.fileopsCreateFolder),n.setParams({root:this.fileRoot,path:this.normalizePath(e)}).signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.Stat.parse(n))})},r.prototype.remove=function(e,r){var n;return n=new t.Xhr("POST",this.urls.fileopsDelete),n.setParams({root:this.fileRoot,path:this.normalizePath(e)}).signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.Stat.parse(n))})},r.prototype.unlink=function(t,e){return this.remove(t,e)},r.prototype["delete"]=function(t,e){return this.remove(t,e)},r.prototype.copy=function(e,r,n){var o,i,s;return n||"function"!=typeof o||(n=o,o=null),i={root:this.fileRoot,to_path:this.normalizePath(r)},e instanceof t.CopyReference?i.from_copy_ref=e.tag:i.from_path=this.normalizePath(e),s=new t.Xhr("POST",this.urls.fileopsCopy),s.setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.move=function(e,r,n){var o,i;return n||"function"!=typeof o||(n=o,o=null),i=new t.Xhr("POST",this.urls.fileopsMove),i.setParams({root:this.fileRoot,from_path:this.normalizePath(e),to_path:this.normalizePath(r)}).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.reset=function(){return this.uid=null,this.oauth.setToken(null,""),this.authState=e.RESET,this.authError=null,this._credentials=null,this},r.prototype.setCredentials=function(t){return this.oauth.reset(t),this.uid=t.uid||null,this.authState=t.authState?t.authState:t.token?e.DONE:e.RESET,this.authError=null,this._credentials=null,this},r.prototype.appHash=function(){return this.oauth.appHash()},r.prototype.setupUrls=function(){return this.fileRoot=this.sandbox?"sandbox":"dropbox",this.urls={requestToken:""+this.apiServer+"/1/oauth/request_token",authorize:""+this.authServer+"/1/oauth/authorize",accessToken:""+this.apiServer+"/1/oauth/access_token",signOut:""+this.apiServer+"/1/unlink_access_token",accountInfo:""+this.apiServer+"/1/account/info",getFile:""+this.fileServer+"/1/files/"+this.fileRoot,postFile:""+this.fileServer+"/1/files/"+this.fileRoot,putFile:""+this.fileServer+"/1/files_put/"+this.fileRoot,metadata:""+this.apiServer+"/1/metadata/"+this.fileRoot,delta:""+this.apiServer+"/1/delta",revisions:""+this.apiServer+"/1/revisions/"+this.fileRoot,restore:""+this.apiServer+"/1/restore/"+this.fileRoot,search:""+this.apiServer+"/1/search/"+this.fileRoot,shares:""+this.apiServer+"/1/shares/"+this.fileRoot,media:""+this.apiServer+"/1/media/"+this.fileRoot,copyRef:""+this.apiServer+"/1/copy_ref/"+this.fileRoot,thumbnails:""+this.fileServer+"/1/thumbnails/"+this.fileRoot,fileopsCopy:""+this.apiServer+"/1/fileops/copy",fileopsCreateFolder:""+this.apiServer+"/1/fileops/create_folder",fileopsDelete:""+this.apiServer+"/1/fileops/delete",fileopsMove:""+this.apiServer+"/1/fileops/move"}},r.ERROR=0,r.RESET=1,r.REQUEST=2,r.AUTHORIZED=3,r.DONE=4,r.SIGNED_OFF=5,r.prototype.urlEncodePath=function(e){return t.Xhr.urlEncodeValue(this.normalizePath(e)).replace(/%2F/gi,"/")},r.prototype.normalizePath=function(t){var e;if("/"===t.substring(0,1)){for(e=1;"/"===t.substring(e,e+1);)e+=1;return t.substring(e)}return t},r.prototype.requestToken=function(e){var r;return r=new t.Xhr("POST",this.urls.requestToken).signWithOauth(this.oauth),this.dispatchXhr(r,e)},r.prototype.authorizeUrl=function(e){var r;return r={oauth_token:e,oauth_callback:this.driver.url()},""+this.urls.authorize+"?"+t.Xhr.urlEncode(r)},r.prototype.getAccessToken=function(e){var r;return r=new t.Xhr("POST",this.urls.accessToken).signWithOauth(this.oauth),this.dispatchXhr(r,e)},r.prototype.dispatchXhr=function(t,e){var r;return t.setCallback(e),t.prepare(),r=t.xhr,this.filter&&!this.filter(r,t)?r:(t.send(),r)},r.prototype.defaultApiServer=function(){return"https://api.dropbox.com"},r.prototype.defaultAuthServer=function(){return this.apiServer.replace("api.","www.")},r.prototype.defaultFileServer=function(){return this.apiServer.replace("api.","api-content.")},r.prototype.defaultDownloadServer=function(){return this.apiServer.replace("api.","dl.")},r.prototype.computeCredentials=function(){var t;return t={key:this.oauth.key,sandbox:this.sandbox},this.oauth.secret&&(t.secret=this.oauth.secret),this.oauth.token&&(t.token=this.oauth.token,t.tokenSecret=this.oauth.tokenSecret),this.uid&&(t.uid=this.uid),this.authState!==e.ERROR&&this.authState!==e.RESET&&this.authState!==e.DONE&&this.authState!==e.SIGNED_OFF&&(t.authState=this.authState),this.apiServer!==this.defaultApiServer()&&(t.server=this.apiServer),this.authServer!==this.defaultAuthServer()&&(t.authServer=this.authServer),this.fileServer!==this.defaultFileServer()&&(t.fileServer=this.fileServer),this.downloadServer!==this.defaultDownloadServer()&&(t.downloadServer=this.downloadServer),this._credentials=t},r}(),e=t.Client,t.AuthDriver=function(){function t(){}return t.prototype.url=function(){return"https://some.url"},t.prototype.doAuthorize=function(t,e,r,n){return n("oauth-token")},t.prototype.onAuthStateChange=function(t,e){return e()},t}(),t.Drivers={},t.Drivers.BrowserBase=function(){function t(t){this.rememberUser=(null!=t?t.rememberUser:void 0)||!1,this.scope=(null!=t?t.scope:void 0)||"default"}return t.prototype.onAuthStateChange=function(t,r){var n,o=this;switch(this.setStorageKey(t),t.authState){case e.RESET:return(n=this.loadCredentials())?n.authState?(t.setCredentials(n),r()):this.rememberUser?(t.setCredentials(n),t.getUserInfo(function(e){return e&&(t.reset(),o.forgetCredentials()),r()})):(this.forgetCredentials(),r()):r();case e.REQUEST:return this.storeCredentials(t.credentials()),r();case e.DONE:return this.rememberUser?this.storeCredentials(t.credentials()):this.forgetCredentials(),r();case e.SIGNED_OFF:return this.forgetCredentials(),r();case e.ERROR:return this.forgetCredentials(),r();default:return r()}},t.prototype.setStorageKey=function(t){return this.storageKey="dropbox-auth:"+this.scope+":"+t.appHash(),this},t.prototype.storeCredentials=function(t){return localStorage.setItem(this.storageKey,JSON.stringify(t))},t.prototype.loadCredentials=function(){var t;if(t=localStorage.getItem(this.storageKey),!t)return null;try{return JSON.parse(t)}catch(e){return null}},t.prototype.forgetCredentials=function(){return localStorage.removeItem(this.storageKey)},t.currentLocation=function(){return window.location.href},t}(),t.Drivers.Redirect=function(r){function n(t){n.__super__.constructor.call(this,t),this.useQuery=(null!=t?t.useQuery:void 0)||!1,this.receiverUrl=this.computeUrl(t),this.tokenRe=RegExp("(#|\\?|&)oauth_token=([^&#]+)(&|#|$)")}return E(n,r),n.prototype.onAuthStateChange=function(t,r){var o;return this.setStorageKey(t),t.authState===e.RESET&&(o=this.loadCredentials())&&o.authState&&(o.token===this.locationToken()&&o.authState===e.REQUEST?(o.authState=e.AUTHORIZED,this.storeCredentials(o)):this.forgetCredentials()),n.__super__.onAuthStateChange.call(this,t,r)},n.prototype.url=function(){return this.receiverUrl},n.prototype.doAuthorize=function(t){return window.location.assign(t)},n.prototype.computeUrl=function(){var e,r,n,o;return o="_dropboxjs_scope="+encodeURIComponent(this.scope),r=t.Drivers.BrowserBase.currentLocation(),-1===r.indexOf("#")?e=null:(n=r.split("#",2),r=n[0],e=n[1]),this.useQuery?r+=-1===r.indexOf("?")?"?"+o:"&"+o:e="?"+o,e?r+"#"+e:r},n.prototype.locationToken=function(){var e,r,n;return e=t.Drivers.BrowserBase.currentLocation(),n="_dropboxjs_scope="+encodeURIComponent(this.scope)+"&",-1===("function"==typeof e.indexOf?e.indexOf(n):void 0)?null:(r=this.tokenRe.exec(e),r?decodeURIComponent(r[2]):null)},n}(t.Drivers.BrowserBase),t.Drivers.Popup=function(r){function n(t){n.__super__.constructor.call(this,t),this.receiverUrl=this.computeUrl(t),this.tokenRe=RegExp("(#|\\?|&)oauth_token=([^&#]+)(&|#|$)")}return E(n,r),n.prototype.onAuthStateChange=function(t,r){var o;return this.setStorageKey(t),t.authState===e.RESET&&(o=this.loadCredentials())&&o.authState&&this.forgetCredentials(),n.__super__.onAuthStateChange.call(this,t,r)},n.prototype.doAuthorize=function(t,e,r,n){return this.listenForMessage(e,n),this.openWindow(t)},n.prototype.url=function(){return this.receiverUrl},n.prototype.computeUrl=function(e){var r;if(e){if(e.receiverUrl)return e.noFragment||-1!==e.receiverUrl.indexOf("#")?e.receiverUrl:e.receiverUrl+"#";if(e.receiverFile)return r=t.Drivers.BrowserBase.currentLocation().split("/"),r[r.length-1]=e.receiverFile,e.noFragment?r.join("/"):r.join("/")+"#"}return t.Drivers.BrowserBase.currentLocation()},n.prototype.openWindow=function(t){return window.open(t,"_dropboxOauthSigninWindow",this.popupWindowSpec(980,980))},n.prototype.popupWindowSpec=function(t,e){var r,n,o,i,s,a,h,u,l,p;return s=null!=(h=window.screenX)?h:window.screenLeft,a=null!=(u=window.screenY)?u:window.screenTop,i=null!=(l=window.outerWidth)?l:document.documentElement.clientWidth,r=null!=(p=window.outerHeight)?p:document.documentElement.clientHeight,n=Math.round(s+(i-t)/2),o=Math.round(a+(r-e)/2.5),"width="+t+",height="+e+","+("left="+n+",top="+o)+"dialog=yes,dependent=yes,scrollbars=yes,location=yes"},n.prototype.listenForMessage=function(t,e){var r,n;return n=this.tokenRe,r=function(o){var i;return i=n.exec(""+o.data),i&&decodeURIComponent(i[2])===t?(window.removeEventListener("message",r),e()):void 0},window.addEventListener("message",r,!1)},n}(t.Drivers.BrowserBase),t.Drivers.NodeServer=function(){function t(t){this.port=(null!=t?t.port:void 0)||8912,this.faviconFile=(null!=t?t.favicon:void 0)||null,this.fs=require("fs"),this.http=require("http"),this.open=require("open"),this.callbacks={},this.urlRe=RegExp("^/oauth_callback\\?"),this.tokenRe=RegExp("(\\?|&)oauth_token=([^&]+)(&|$)"),this.createApp()}return t.prototype.url=function(){return"http://localhost:"+this.port+"/oauth_callback"},t.prototype.doAuthorize=function(t,e,r,n){return this.callbacks[e]=n,this.openBrowser(t)},t.prototype.openBrowser=function(t){if(!t.match(/^https?:\/\//))throw Error("Not a http/https URL: "+t);return this.open(t)},t.prototype.createApp=function(){var t=this;return this.app=this.http.createServer(function(e,r){return t.doRequest(e,r)}),this.app.listen(this.port)},t.prototype.closeServer=function(){return this.app.close()},t.prototype.doRequest=function(t,e){var r,n,o,i=this;return this.urlRe.exec(t.url)&&(n=this.tokenRe.exec(t.url),n&&(o=decodeURIComponent(n[2]),this.callbacks[o]&&(this.callbacks[o](),delete this.callbacks[o]))),r="",t.on("data",function(t){return r+=t}),t.on("end",function(){return i.faviconFile&&"/favicon.ico"===t.url?i.sendFavicon(e):i.closeBrowser(e)})},t.prototype.closeBrowser=function(t){var e;return e='\n\n

Please close this window.

',t.writeHead(200,{"Content-Length":e.length,"Content-Type":"text/html"}),t.write(e),t.end},t.prototype.sendFavicon=function(t){return this.fs.readFile(this.faviconFile,function(e,r){return t.writeHead(200,{"Content-Length":r.length,"Content-Type":"image/x-icon"}),t.write(r),t.end})},t}(),p=function(t,e){return a(m(S(t),S(e),t.length,e.length))},c=function(t){return a(w(S(t),t.length))},("undefined"==typeof window||null===window)&&(y=require("crypto"),p=function(t,e){var r;return r=y.createHmac("sha1",e),r.update(t),r.digest("base64")},c=function(t){var e;return e=y.createHash("sha1"),e.update(t),e.digest("base64")}),m=function(t,e,r,n){var o,i,s,a;return e.length>16&&(e=w(e,n)),s=function(){var t,r;for(r=[],i=t=0;16>t;i=++t)r.push(909522486^e[i]);return r}(),a=function(){var t,r;for(r=[],i=t=0;16>t;i=++t)r.push(1549556828^e[i]);return r}(),o=w(s.concat(t),64+r),w(a.concat(o),84)},w=function(t,e){var r,n,o,i,a,h,u,l,p,c,d,f,y,v,m,w,S,b;for(t[e>>2]|=1<<31-((3&e)<<3),t[(e+8>>6<<4)+15]=e<<3,w=Array(80),r=1732584193,o=-271733879,a=-1732584194,u=271733878,p=-1009589776,f=0,m=t.length;m>f;){for(n=r,i=o,h=a,l=u,c=p,y=b=0;80>b;y=++b)w[y]=16>y?t[f+y]:g(w[y-3]^w[y-8]^w[y-14]^w[y-16],1),20>y?(d=o&a|~o&u,v=1518500249):40>y?(d=o^a^u,v=1859775393):60>y?(d=o&a|o&u|a&u,v=-1894007588):(d=o^a^u,v=-899497514),S=s(s(g(r,5),d),s(s(p,w[y]),v)),p=u,u=a,a=g(o,30),o=r,r=S;r=s(r,n),o=s(o,i),a=s(a,h),u=s(u,l),p=s(p,c),f+=16}return[r,o,a,u,p]},g=function(t,e){return t<>>32-e},s=function(t,e){var r,n;return n=(65535&t)+(65535&e),r=(t>>16)+(e>>16)+(n>>16),r<<16|65535&n},a=function(t){var e,r,n,o,i;for(o="",e=0,n=4*t.length;n>e;)r=e,i=(255&t[r>>2]>>(3-(3&r)<<3))<<16,r+=1,i|=(255&t[r>>2]>>(3-(3&r)<<3))<<8,r+=1,i|=255&t[r>>2]>>(3-(3&r)<<3),o+=b[63&i>>18],o+=b[63&i>>12],e+=1,o+=e>=n?"=":b[63&i>>6],e+=1,o+=e>=n?"=":b[63&i],e+=1;return o},b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",S=function(t){var e,r,n,o,i;for(e=[],n=255,r=o=0,i=t.length;i>=0?i>o:o>i;r=i>=0?++o:--o)e[r>>2]|=(t.charCodeAt(r)&n)<<(3-(3&r)<<3);return e},t.Oauth=function(){function e(t){this.key=this.k=null,this.secret=this.s=null,this.token=null,this.tokenSecret=null,this._appHash=null,this.reset(t)}return e.prototype.reset=function(t){var e,r,n,o;if(t.secret)this.k=this.key=t.key,this.s=this.secret=t.secret,this._appHash=null;else if(t.key)this.key=t.key,this.secret=null,n=h(v(this.key).split("|",2)[1]),o=n.split("?",2),e=o[0],r=o[1],this.k=decodeURIComponent(e),this.s=decodeURIComponent(r),this._appHash=null;else if(!this.k)throw Error("No API key supplied");return t.token?this.setToken(t.token,t.tokenSecret):this.setToken(null,"")},e.prototype.setToken=function(e,r){if(e&&!r)throw Error("No secret supplied with the user token");return this.token=e,this.tokenSecret=r||"",this.hmacKey=t.Xhr.urlEncodeValue(this.s)+"&"+t.Xhr.urlEncodeValue(r),null},e.prototype.authHeader=function(e,r,n){var o,i,s,a,h,u;this.addAuthParams(e,r,n),i=[];for(s in n)a=n[s],"oauth_"===s.substring(0,6)&&i.push(s);for(i.sort(),o=[],h=0,u=i.length;u>h;h++)s=i[h],o.push(t.Xhr.urlEncodeValue(s)+'="'+t.Xhr.urlEncodeValue(n[s])+'"'),delete n[s];return"OAuth "+o.join(",")},e.prototype.addAuthParams=function(t,e,r){return this.boilerplateParams(r),r.oauth_signature=this.signature(t,e,r),r},e.prototype.boilerplateParams=function(t){return t.oauth_consumer_key=this.k,t.oauth_nonce=this.nonce(),t.oauth_signature_method="HMAC-SHA1",this.token&&(t.oauth_token=this.token),t.oauth_timestamp=Math.floor(Date.now()/1e3),t.oauth_version="1.0",t},e.prototype.nonce=function(){return Date.now().toString(36)+Math.random().toString(36)},e.prototype.signature=function(e,r,n){var o;return o=e.toUpperCase()+"&"+t.Xhr.urlEncodeValue(r)+"&"+t.Xhr.urlEncodeValue(t.Xhr.urlEncode(n)),p(o,this.hmacKey)},e.prototype.appHash=function(){return this._appHash?this._appHash:this._appHash=c(this.k).replace(/\=/g,"")},e}(),null==Date.now&&(Date.now=function(){return(new Date).getTime()}),v=function(t,e){var r,n,o,i,s,a,u,l,p,c,f,y;for(e?(e=[encodeURIComponent(t),encodeURIComponent(e)].join("?"),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length/2;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(16*(15&t.charCodeAt(2*r))+(15&t.charCodeAt(2*r+1)));return o}()):(c=t.split("|",2),t=c[0],e=c[1],t=h(t),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(t.charCodeAt(r));return o}(),e=h(e)),i=function(){for(y=[],l=0;256>l;l++)y.push(l);return y}.apply(this),a=0,s=p=0;256>p;s=++p)a=(a+i[r]+t[s%t.length])%256,f=[i[a],i[s]],i[s]=f[0],i[a]=f[1];return s=a=0,o=function(){var t,r,o,h;for(h=[],u=t=0,r=e.length;r>=0?r>t:t>r;u=r>=0?++t:--t)s=(s+1)%256,a=(a+i[s])%256,o=[i[a],i[s]],i[s]=o[0],i[a]=o[1],n=i[(i[s]+i[a])%256],h.push(String.fromCharCode((n^e.charCodeAt(u))%256));return h}(),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(String.fromCharCode(t[r]));return o}(),[d(t.join("")),d(o.join(""))].join("|")},t.PulledChanges=function(){function e(e){var r;this.blankSlate=e.reset||!1,this.cursorTag=e.cursor,this.shouldPullAgain=e.has_more,this.shouldBackOff=!this.shouldPullAgain,this.changes=e.cursor&&e.cursor.length?function(){var n,o,i,s;for(i=e.entries,s=[],n=0,o=i.length;o>n;n++)r=i[n],s.push(t.PullChange.parse(r));return s}():[]}return e.parse=function(e){return e&&"object"==typeof e?new t.PulledChanges(e):e},e.prototype.blankSlate=void 0,e.prototype.cursorTag=void 0,e.prototype.changes=void 0,e.prototype.shouldPullAgain=void 0,e.prototype.shouldBackOff=void 0,e}(),t.PullChange=function(){function e(e){this.path=e[0],this.stat=t.Stat.parse(e[1]),this.stat?this.wasRemoved=!1:(this.stat=null,this.wasRemoved=!0)}return e.parse=function(e){return e&&"object"==typeof e?new t.PullChange(e):e},e.prototype.path=void 0,e.prototype.wasRemoved=void 0,e.prototype.stat=void 0,e}(),t.PublicUrl=function(){function e(t,e){this.url=t.url,this.expiresAt=new Date(Date.parse(t.expires)),this.isDirect=e===!0?!0:e===!1?!1:864e5>=Date.now()-this.expiresAt,this.isPreview=!this.isDirect}return e.parse=function(e,r){return e&&"object"==typeof e?new t.PublicUrl(e,r):e},e.prototype.url=void 0,e.prototype.expiresAt=void 0,e.prototype.isDirect=void 0,e.prototype.isPreview=void 0,e}(),t.CopyReference=function(){function e(t){"object"==typeof t?(this.tag=t.copy_ref,this.expiresAt=new Date(Date.parse(t.expires))):(this.tag=t,this.expiresAt=new Date)}return e.parse=function(e){return!e||"object"!=typeof e&&"string"!=typeof e?e:new t.CopyReference(e)},e.prototype.tag=void 0,e.prototype.expiresAt=void 0,e}(),t.Stat=function(){function e(t){var e,r,n,o;switch(this.path=t.path,"/"!==this.path.substring(0,1)&&(this.path="/"+this.path),e=this.path.length-1,e>=0&&"/"===this.path.substring(e)&&(this.path=this.path.substring(0,e)),r=this.path.lastIndexOf("/"),this.name=this.path.substring(r+1),this.isFolder=t.is_dir||!1,this.isFile=!this.isFolder,this.isRemoved=t.is_deleted||!1,this.typeIcon=t.icon,this.modifiedAt=(null!=(n=t.modified)?n.length:void 0)?new Date(Date.parse(t.modified)):null,this.clientModifiedAt=(null!=(o=t.client_mtime)?o.length:void 0)?new Date(Date.parse(t.client_mtime)):null,t.root){case"dropbox":this.inAppFolder=!1;break;case"app_folder":this.inAppFolder=!0;break;default:this.inAppFolder=null}this.size=t.bytes||0,this.humanSize=t.size||"",this.hasThumbnail=t.thumb_exists||!1,this.isFolder?(this.versionTag=t.hash,this.mimeType=t.mime_type||"inode/directory"):(this.versionTag=t.rev,this.mimeType=t.mime_type||"application/octet-stream")}return e.parse=function(e){return e&&"object"==typeof e?new t.Stat(e):e},e.prototype.path=null,e.prototype.name=null,e.prototype.inAppFolder=null,e.prototype.isFolder=null,e.prototype.isFile=null,e.prototype.isRemoved=null,e.prototype.typeIcon=null,e.prototype.versionTag=null,e.prototype.mimeType=null,e.prototype.size=null,e.prototype.humanSize=null,e.prototype.hasThumbnail=null,e.prototype.modifiedAt=null,e.prototype.clientModifiedAt=null,e}(),t.UserInfo=function(){function e(t){var e;this.name=t.display_name,this.email=t.email,this.countryCode=t.country||null,this.uid=""+t.uid,t.public_app_url?(this.publicAppUrl=t.public_app_url,e=this.publicAppUrl.length-1,e>=0&&"/"===this.publicAppUrl.substring(e)&&(this.publicAppUrl=this.publicAppUrl.substring(0,e))):this.publicAppUrl=null,this.referralUrl=t.referral_link,this.quota=t.quota_info.quota,this.privateBytes=t.quota_info.normal||0,this.sharedBytes=t.quota_info.shared||0,this.usedQuota=this.privateBytes+this.sharedBytes}return e.parse=function(e){return e&&"object"==typeof e?new t.UserInfo(e):e},e.prototype.name=null,e.prototype.email=null,e.prototype.countryCode=null,e.prototype.uid=null,e.prototype.referralUrl=null,e.prototype.publicAppUrl=null,e.prototype.quota=null,e.prototype.usedQuota=null,e.prototype.privateBytes=null,e.prototype.sharedBytes=null,e}(),"undefined"!=typeof window&&null!==window?(!window.XDomainRequest||"withCredentials"in new XMLHttpRequest?(i=window.XMLHttpRequest,o=!1,r=-1===window.navigator.userAgent.indexOf("Firefox")):(i=window.XDomainRequest,o=!0,r=!1),n=!0):(i=require("xmlhttprequest").XMLHttpRequest,o=!1,r=!1,n=!1),t.Xhr=function(){function e(t,e){this.method=t,this.isGet="GET"===this.method,this.url=e,this.headers={},this.params=null,this.body=null,this.preflight=!(this.isGet||"POST"===this.method),this.signed=!1,this.responseType=null,this.callback=null,this.xhr=null}return e.Request=i,e.ieMode=o,e.canSendForms=r,e.doesPreflight=n,e.prototype.setParams=function(t){if(this.signed)throw Error("setParams called after addOauthParams or addOauthHeader");if(this.params)throw Error("setParams cannot be called twice");return this.params=t,this},e.prototype.setCallback=function(t){return this.callback=t,this},e.prototype.signWithOauth=function(e){return t.Xhr.ieMode||t.Xhr.doesPreflight&&!this.preflight?this.addOauthParams(e):this.addOauthHeader(e)},e.prototype.addOauthParams=function(t){if(this.signed)throw Error("Request already has an OAuth signature");return this.params||(this.params={}),t.addAuthParams(this.method,this.url,this.params),this.signed=!0,this},e.prototype.addOauthHeader=function(t){if(this.signed)throw Error("Request already has an OAuth signature");return this.params||(this.params={}),this.signed=!0,this.setHeader("Authorization",t.authHeader(this.method,this.url,this.params))},e.prototype.setBody=function(t){if(this.isGet)throw Error("setBody cannot be called on GET requests");if(null!==this.body)throw Error("Request already has a body");return this.preflight||"undefined"!=typeof FormData&&null!==FormData&&t instanceof FormData||(this.preflight=!0),this.body=t,this},e.prototype.setResponseType=function(t){return this.responseType=t,this},e.prototype.setHeader=function(t,e){var r;if(this.headers[t])throw r=this.headers[t],Error("HTTP header "+t+" already set to "+r);if("Content-Type"===t)throw Error("Content-Type is automatically computed based on setBody");return this.preflight=!0,this.headers[t]=e,this},e.prototype.setFileField=function(t,e,r,n){var o,i;if(null!==this.body)throw Error("Request already has a body"); +if(this.isGet)throw Error("paramsToBody cannot be called on GET requests");return i="object"==typeof r&&("undefined"!=typeof Blob&&null!==Blob&&r instanceof Blob||"undefined"!=typeof File&&null!==File&&r instanceof File),i?(this.body=new FormData,this.body.append(t,r,e)):(n||(n="application/octet-stream"),o=this.multipartBoundary(),this.headers["Content-Type"]="multipart/form-data; boundary="+o,this.body=["--",o,"\r\n",'Content-Disposition: form-data; name="',t,'"; filename="',e,'"\r\n',"Content-Type: ",n,"\r\n","Content-Transfer-Encoding: binary\r\n\r\n",r,"\r\n","--",o,"--","\r\n"].join(""))},e.prototype.multipartBoundary=function(){return[Date.now().toString(36),Math.random().toString(36)].join("----")},e.prototype.paramsToUrl=function(){var e;return this.params&&(e=t.Xhr.urlEncode(this.params),0!==e.length&&(this.url=[this.url,"?",e].join("")),this.params=null),this},e.prototype.paramsToBody=function(){if(this.params){if(null!==this.body)throw Error("Request already has a body");if(this.isGet)throw Error("paramsToBody cannot be called on GET requests");this.headers["Content-Type"]="application/x-www-form-urlencoded",this.body=t.Xhr.urlEncode(this.params),this.params=null}return this},e.prototype.prepare=function(){var e,r,n,o,i=this;if(r=t.Xhr.ieMode,this.isGet||null!==this.body||r?(this.paramsToUrl(),null!==this.body&&"string"==typeof this.body&&(this.headers["Content-Type"]="text/plain; charset=utf8")):this.paramsToBody(),this.xhr=new t.Xhr.Request,r?(this.xhr.onload=function(){return i.onLoad()},this.xhr.onerror=function(){return i.onError()}):this.xhr.onreadystatechange=function(){return i.onReadyStateChange()},this.xhr.open(this.method,this.url,!0),!r){o=this.headers;for(e in o)T.call(o,e)&&(n=o[e],this.xhr.setRequestHeader(e,n))}return this.responseType&&("b"===this.responseType?this.xhr.overrideMimeType&&this.xhr.overrideMimeType("text/plain; charset=x-user-defined"):this.xhr.responseType=this.responseType),this},e.prototype.send=function(t){return this.callback=t||this.callback,null!==this.body?this.xhr.send(this.body):this.xhr.send(),this},e.urlEncode=function(t){var e,r,n;e=[];for(r in t)n=t[r],e.push(this.urlEncodeValue(r)+"="+this.urlEncodeValue(n));return e.sort().join("&")},e.urlEncodeValue=function(t){return encodeURIComponent(""+t).replace(/\!/g,"%21").replace(/'/g,"%27").replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/\*/g,"%2A")},e.urlDecode=function(t){var e,r,n,o,i,s;for(r={},s=t.split("&"),o=0,i=s.length;i>o;o++)n=s[o],e=n.split("="),r[decodeURIComponent(e[0])]=decodeURIComponent(e[1]);return r},e.prototype.onReadyStateChange=function(){var e,r,n,o,i,s,a,h,u;if(4!==this.xhr.readyState)return!0;if(200>this.xhr.status||this.xhr.status>=300)return e=new t.ApiError(this.xhr,this.method,this.url),this.callback(e),!0;if(s=this.xhr.getResponseHeader("x-dropbox-metadata"),null!=s?s.length:void 0)try{i=JSON.parse(s)}catch(l){i=void 0}else i=void 0;if(this.responseType){if("b"===this.responseType){for(n=null!=this.xhr.responseText?this.xhr.responseText:this.xhr.response,r=[],o=h=0,u=n.length;u>=0?u>h:h>u;o=u>=0?++h:--h)r.push(String.fromCharCode(255&n.charCodeAt(o)));a=r.join(""),this.callback(null,a,i)}else this.callback(null,this.xhr.response,i);return!0}switch(a=null!=this.xhr.responseText?this.xhr.responseText:this.xhr.response,this.xhr.getResponseHeader("Content-Type")){case"application/x-www-form-urlencoded":this.callback(null,t.Xhr.urlDecode(a),i);break;case"application/json":case"text/javascript":this.callback(null,JSON.parse(a),i);break;default:this.callback(null,a,i)}return!0},e.prototype.onLoad=function(){var e;switch(e=this.xhr.responseText,this.xhr.contentType){case"application/x-www-form-urlencoded":this.callback(null,t.Xhr.urlDecode(e),void 0);break;case"application/json":case"text/javascript":this.callback(null,JSON.parse(e),void 0);break;default:this.callback(null,e,void 0)}return!0},e.prototype.onError=function(){var e;return e=new t.ApiError(this.xhr,this.method,this.url),this.callback(e),!0},e}(),null!=("undefined"!=typeof module&&null!==module?module.exports:void 0))module.exports=t;else{if("undefined"==typeof window||null===window)throw Error("This library only supports node.js and modern browsers.");window.Dropbox=t}t.atob=h,t.btoa=d,t.hmac=p,t.sha1=c,t.encodeKey=v}).call(this); \ No newline at end of file diff --git a/lib/client/storage/dropbox/package.json b/lib/client/storage/dropbox/package.json new file mode 100644 index 00000000..dfa72764 --- /dev/null +++ b/lib/client/storage/dropbox/package.json @@ -0,0 +1,46 @@ +{ + "name": "dropbox", + "version": "0.7.2", + "description": "Client library for the Dropbox API", + "keywords": ["dropbox", "filesystem", "storage"], + "homepage": "http://github.com/dropbox/dropbox-js", + "author": "Victor Costan (http://www.costan.us)", + "license": "MIT", + "contributors": [ + "Aakanksha Sarda " + ], + "repository": { + "type": "git", + "url": "https://github.com/dropbox/dropbox-js.git" + }, + "engines": { + "node": ">= 0.6" + }, + "dependencies": { + "open": ">= 0.0.2", + "xmlhttprequest": ">= 1.5.0" + }, + "devDependencies": { + "async": ">= 0.1.22", + "chai": ">= 1.4.0", + "codo": ">= 1.5.2", + "coffee-script": ">= 1.4.0", + "express": ">= 3.0.4", + "mocha": ">= 1.7.4", + "remove": ">= 0.1.5", + "sinon": ">= 1.5.2", + "sinon-chai": ">= 2.2.0", + "uglify-js": ">= 2.2.2" + }, + "main": "lib/dropbox.js", + "directories": { + "doc": "doc", + "lib": "lib", + "src": "src", + "test": "test" + }, + "scripts": { + "prepublish": "cake build", + "test": "cake test" + } +} diff --git a/lib/client/storage/dropbox/src/000-dropbox.coffee b/lib/client/storage/dropbox/src/000-dropbox.coffee new file mode 100644 index 00000000..183d00c2 --- /dev/null +++ b/lib/client/storage/dropbox/src/000-dropbox.coffee @@ -0,0 +1,6 @@ +# Main entry point to the Dropbox API. +class Dropbox + constructor: (options) -> + @client = new DropboxClient options + + # NOTE: this is not yet implemented. diff --git a/lib/client/storage/dropbox/src/api_error.coffee b/lib/client/storage/dropbox/src/api_error.coffee new file mode 100644 index 00000000..131fb9ec --- /dev/null +++ b/lib/client/storage/dropbox/src/api_error.coffee @@ -0,0 +1,49 @@ +# Information about a failed call to the Dropbox API. +class Dropbox.ApiError + # @property {Number} the HTTP error code (e.g., 403) + status: undefined + + # @property {String} the HTTP method of the failed request (e.g., 'GET') + method: undefined + + # @property {String} the URL of the failed request + url: undefined + + # @property {?String} the body of the HTTP error response; can be null if + # the error was caused by a network failure or by a security issue + responseText: undefined + + # @property {?Object} the result of parsing the JSON in the HTTP error + # response; can be null if the API server didn't return JSON, or if the + # HTTP response body is unavailable + response: undefined + + # Wraps a failed XHR call to the Dropbox API. + # + # @param {String} method the HTTP verb of the API request (e.g., 'GET') + # @param {String} url the URL of the API request + # @param {XMLHttpRequest} xhr the XMLHttpRequest instance of the failed + # request + constructor: (xhr, @method, @url) -> + @status = xhr.status + if xhr.responseType + text = xhr.response or xhr.responseText + else + text = xhr.responseText + if text + try + @responseText = text.toString() + @response = JSON.parse text + catch e + @response = null + else + @responseText = '(no response)' + @response = null + + # Used when the error is printed out by developers. + toString: -> + "Dropbox API error #{@status} from #{@method} #{@url} :: #{@responseText}" + + # Used by some testing frameworks. + inspect: -> + @toString() diff --git a/lib/client/storage/dropbox/src/base64.coffee b/lib/client/storage/dropbox/src/base64.coffee new file mode 100644 index 00000000..fa165d3a --- /dev/null +++ b/lib/client/storage/dropbox/src/base64.coffee @@ -0,0 +1,72 @@ +# node.js implementation of atob and btoa + +if window? + if window.atob and window.btoa + atob = (string) -> window.atob string + btoa = (base64) -> window.btoa base64 + else + # IE < 10 doesn't implement the standard atob / btoa functions. + base64Digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + + btoaNibble = (accumulator, bytes, result) -> + limit = 3 - bytes + accumulator <<= limit * 8 + i = 3 + while i >= limit + result.push base64Digits.charAt((accumulator >> (i * 6)) & 0x3F) + i -= 1 + i = bytes + while i < 3 + result.push '=' + i += 1 + null + atobNibble = (accumulator, digits, result) -> + limit = 4 - digits + accumulator <<= limit * 6 + i = 2 + while i >= limit + result.push String.fromCharCode((accumulator >> (8 * i)) & 0xFF) + i -= 1 + null + + btoa = (string) -> + result = [] + accumulator = 0 + bytes = 0 + for i in [0...string.length] + accumulator = (accumulator << 8) | string.charCodeAt(i) + bytes += 1 + if bytes is 3 + btoaNibble accumulator, bytes, result + accumulator = bytes = 0 + + if bytes > 0 + btoaNibble accumulator, bytes, result + result.join '' + + atob = (base64) -> + result = [] + accumulator = 0 + digits = 0 + for i in [0...base64.length] + digit = base64.charAt i + break if digit is '=' + accumulator = (accumulator << 6) | base64Digits.indexOf(digit) + digits += 1 + if digits is 4 + atobNibble accumulator, digits, result + accumulator = digits = 0 + + if digits > 0 + atobNibble accumulator, digits, result + result.join '' + +else + # NOTE: the npm packages atob and btoa don't do base64-encoding correctly. + atob = (arg) -> + buffer = new Buffer arg, 'base64' + (String.fromCharCode(buffer[i]) for i in [0...buffer.length]).join '' + btoa = (arg) -> + buffer = new Buffer(arg.charCodeAt(i) for i in [0...arg.length]) + buffer.toString 'base64' diff --git a/lib/client/storage/dropbox/src/client.coffee b/lib/client/storage/dropbox/src/client.coffee new file mode 100644 index 00000000..c465e4bc --- /dev/null +++ b/lib/client/storage/dropbox/src/client.coffee @@ -0,0 +1,1104 @@ +# Represents a user accessing the application. +class Dropbox.Client + # Dropbox client representing an application. + # + # For an optimal user experience, applications should use a single client for + # all Dropbox interactions. + # + # @param {Object} options the application type and API key + # @option options {Boolean} sandbox true for applications that request + # sandbox access (access to a single folder exclusive to the app) + # @option options {String} key the Dropbox application's key; browser-side + # applications should use Dropbox.encodeKey to obtain an encoded + # key string, and pass it as the key option + # @option options {String} secret the Dropbox application's secret; + # browser-side applications should not use the secret option; instead, + # they should pass the result of Dropbox.encodeKey as the key option + # @option options {String} token if set, the user's access token + # @option options {String} tokenSecret if set, the secret for the user's + # access token + # @option options {String} uid if set, the user's Dropbox UID + # @option options {Number} authState if set, indicates that the token and + # tokenSecret are in an intermediate state in the authentication process; + # this option should never be set by hand, however it may be returned by + # calls to credentials() + constructor: (options) -> + @sandbox = options.sandbox or false + @apiServer = options.server or @defaultApiServer() + @authServer = options.authServer or @defaultAuthServer() + @fileServer = options.fileServer or @defaultFileServer() + @downloadServer = options.downloadServer or @defaultDownloadServer() + + @oauth = new Dropbox.Oauth options + @driver = null + @filter = null + @uid = null + @authState = null + @authError = null + @_credentials = null + @setCredentials options + + @setupUrls() + + # Plugs in the OAuth / application integration code. + # + # @param {DropboxAuthDriver} driver provides the integration between the + # application and the Dropbox OAuth flow; most applications should be + # able to use instances of Dropbox.Driver.Redirect, Dropbox.Driver.Popup, + # or Dropbox.Driver.NodeServer + # @return {Dropbox.Client} this, for easy call chaining + authDriver: (driver) -> + @driver = driver + @ + + # Plugs in a filter for all XMLHttpRequests issued by this client. + # + # Whenever possible, filter implementations should only use the native + # XMHttpRequest object received as the first argument. The Dropbox.Xhr API + # implemented by the second argument is not yet stabilized. + # + # @param {function(XMLHttpRequest, Dropbox.Xhr): boolean} filter called every + # time the client is about to send a network request; the filter can + # inspect and modify the XMLHttpRequest; if the filter returns a falsey + # value, the XMLHttpRequest will not be sent + # @return {Drobpox.Client} this, for easy call chaining + xhrFilter: (filter) -> + @filter = filter + @ + + # The authenticated user's Dropbx user ID. + # + # This user ID is guaranteed to be consistent across API calls from the same + # application (not across applications, though). + # + # @return {?String} a short ID that identifies the user, or null if no user + # is authenticated + dropboxUid: -> + @uid + + # Get the client's OAuth credentials. + # + # @param {?Object} the result of a prior call to credentials() + # @return {Object} a plain object whose properties can be passed to the + # Dropbox.Client constructor to reuse this client's login credentials + credentials: () -> + @computeCredentials() unless @_credentials + @_credentials + + # Authenticates the app's user to Dropbox' API server. + # + # @param {function(?Dropbox.ApiError, ?Dropbox.Client)} callback called when + # the authentication completes; if successful, the second parameter is + # this client and the first parameter is null + # @return {Dropbox.Client} this, for easy call chaining + authenticate: (callback) -> + oldAuthState = null + + # Advances the authentication FSM by one step. + _fsmStep = => + if oldAuthState isnt @authState + oldAuthState = @authState + if @driver.onAuthStateChange + return @driver.onAuthStateChange(@, _fsmStep) + + switch @authState + when DropboxClient.RESET # No user credentials -> request token. + @requestToken (error, data) => + if error + @authError = error + @authState = DropboxClient.ERROR + else + token = data.oauth_token + tokenSecret = data.oauth_token_secret + @oauth.setToken token, tokenSecret + @authState = DropboxClient.REQUEST + @_credentials = null + _fsmStep() + + when DropboxClient.REQUEST # Have request token, get it authorized. + authUrl = @authorizeUrl @oauth.token + @driver.doAuthorize authUrl, @oauth.token, @oauth.tokenSecret, => + @authState = DropboxClient.AUTHORIZED + @_credentials = null + _fsmStep() + + when DropboxClient.AUTHORIZED + # Request token authorized, switch it for an access token. + @getAccessToken (error, data) => + if error + @authError = error + @authState = DropboxClient.ERROR + else + @oauth.setToken data.oauth_token, data.oauth_token_secret + @uid = data.uid + @authState = DropboxClient.DONE + @_credentials = null + _fsmStep() + + when DropboxClient.DONE # We have an access token. + callback null, @ + + when Dropbox.SIGNED_OFF # The user signed off, restart the flow. + @reset() + _fsmStep() + + when DropboxClient.ERROR # An error occurred during authentication. + callback @authError + + _fsmStep() # Start up the state machine. + @ + + # Revokes the user's Dropbox credentials. + # + # This should be called when the user explictly signs off from your + # application, to meet the users' expectation that after they sign off, their + # access tokens will be persisted on the machine. + # + # @param {function(?Dropbox.ApiError)} callback called when + # the authentication completes; if successful, the error parameter is + # null + # @return {XMLHttpRequest} the XHR object used for this API call + signOut: (callback) -> + xhr = new Dropbox.Xhr 'POST', @urls.signOut + xhr.signWithOauth @oauth + @dispatchXhr xhr, (error) => + return callback(error) if error + + @reset() + @authState = DropboxClient.SIGNED_OFF + if @driver.onAuthStateChange + @driver.onAuthStateChange @, -> + callback error + else + callback error + + # Alias for signOut. + signOff: (callback) -> + @signOut callback + + # Retrieves information about the logged in user. + # + # @param {function(?Dropbox.ApiError, ?Dropbox.UserInfo, ?Object)} callback + # called with the result of the /account/info HTTP request; if the call + # succeeds, the second parameter is a Dropbox.UserInfo instance, the + # third parameter is the parsed JSON data behind the Dropbox.UserInfo + # instance, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + getUserInfo: (callback) -> + xhr = new Dropbox.Xhr 'GET', @urls.accountInfo + xhr.signWithOauth @oauth + @dispatchXhr xhr, (error, userData) -> + callback error, Dropbox.UserInfo.parse(userData), userData + + # Retrieves the contents of a file stored in Dropbox. + # + # Some options are silently ignored in Internet Explorer 9 and below, due to + # insufficient support in its proprietary XDomainRequest replacement for XHR. + # Currently, the options are: arrayBuffer, blob, length, start. + # + # @param {String} path the path of the file to be read, relative to the + # user's Dropbox or to the application's folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {String} versionTag the tag string for the desired version + # of the file contents; the most recent version is retrieved by default + # @option options {String} rev alias for "versionTag" that matches the HTTP + # API + # @option options {Boolean} arrayBuffer if true, the file's contents will be + # passed to the callback in an ArrayBuffer; this is a good method of + # reading non-UTF8 data, such as images; requires XHR Level 2 support, + # which is not available in IE <= 9 + # @option options {Boolean} blob if true, the file's contents will be + # passed to the callback in a Blob; this is a good method of reading + # non-UTF8 data, such as images; requires XHR Level 2 support, which is not + # available in IE <= 9 + # @option options {Boolean} binary if true, the file will be retrieved as a + # binary string; the default is an UTF-8 encoded string; this relies on + # browser hacks and should not be used if the environment supports the Blob + # API + # @option options {Number} length the number of bytes to be retrieved from + # the file; if the start option is not present, the last "length" bytes + # will be read (after issue #30 is closed); by default, the entire file is + # read + # @option options {Number} start the 0-based offset of the first byte to be + # retrieved; if the length option is not present, the bytes between + # "start" and the file's end will be read; by default, the entire + # file is read + # @param {function(?Dropbox.ApiError, ?String, ?Dropbox.Stat)} callback + # called with the result of the /files (GET) HTTP request; the second + # parameter is the contents of the file, the third parameter is a + # Dropbox.Stat instance describing the file, and the first parameter is + # null + # @return {XMLHttpRequest} the XHR object used for this API call + readFile: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + params = {} + responseType = null + rangeHeader = null + if options + if options.versionTag + params.rev = options.versionTag + else if options.rev + params.rev = options.rev + + if options.arrayBuffer + responseType = 'arraybuffer' + else if options.blob + responseType = 'blob' + else if options.binary + responseType = 'b' # See the Dropbox.Xhr.request2 docs + + if options.length + if options.start? + rangeStart = options.start + rangeEnd = options.start + options.length - 1 + else + rangeStart = '' + rangeEnd = options.length + rangeHeader = "bytes=#{rangeStart}-#{rangeEnd}" + else if options.start? + rangeHeader = "bytes=#{options.start}-" + + xhr = new Dropbox.Xhr 'GET', "#{@urls.getFile}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth(@oauth).setResponseType(responseType) + xhr.setHeader 'Range', rangeHeader if rangeHeader + @dispatchXhr xhr, (error, data, metadata) -> + callback error, data, Dropbox.Stat.parse(metadata) + + # Store a file into a user's Dropbox. + # + # @param {String} path the path of the file to be created, relative to the + # user's Dropbox or to the application's folder + # @param {String, ArrayBuffer, ArrayBufferView, Blob, File} data the contents + # written to the file; if a File is passed, its name is ignored + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {String} lastVersionTag the identifier string for the + # version of the file's contents that was last read by this program, used + # for conflict resolution; for best results, use the versionTag attribute + # value from the Dropbox.Stat instance provided by readFile + # @option options {String} parentRev alias for "lastVersionTag" that matches + # the HTTP API + # @option options {Boolean} noOverwrite if set, the write will not overwrite + # a file with the same name that already exsits; instead the contents + # will be written to a similarly named file (e.g. "notes (1).txt" + # instead of "notes.txt") + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /files (POST) HTTP request; the second paramter is a + # Dropbox.Stat instance describing the newly created file, and the first + # parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + writeFile: (path, data, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + useForm = Dropbox.Xhr.canSendForms and typeof data is 'object' + if useForm + @writeFileUsingForm path, data, options, callback + else + @writeFileUsingPut path, data, options, callback + + # writeFile implementation that uses the POST /files API. + # + # @private + # This method is more demanding in terms of CPU and browser support, but does + # not require CORS preflight, so it always completes in 1 HTTP request. + writeFileUsingForm: (path, data, options, callback) -> + # Break down the path into a file/folder name and the containing folder. + slashIndex = path.lastIndexOf '/' + if slashIndex is -1 + fileName = path + path = '' + else + fileName = path.substring slashIndex + path = path.substring 0, slashIndex + + params = { file: fileName } + if options + if options.noOverwrite + params.overwrite = 'false' + if options.lastVersionTag + params.parent_rev = options.lastVersionTag + else if options.parentRev or options.parent_rev + params.parent_rev = options.parentRev or options.parent_rev + # TODO: locale support would edit the params here + + xhr = new Dropbox.Xhr 'POST', "#{@urls.postFile}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth(@oauth).setFileField('file', fileName, + data, 'application/octet-stream') + + # NOTE: the Dropbox API docs ask us to replace the 'file' parameter after + # signing the request; the hack below works as intended + delete params.file + + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # writeFile implementation that uses the /files_put API. + # + # @private + # This method is less demanding on CPU, and makes fewer assumptions about + # browser support, but it takes 2 HTTP requests for binary files, because it + # needs CORS preflight. + writeFileUsingPut: (path, data, options, callback) -> + params = {} + if options + if options.noOverwrite + params.overwrite = 'false' + if options.lastVersionTag + params.parent_rev = options.lastVersionTag + else if options.parentRev or options.parent_rev + params.parent_rev = options.parentRev or options.parent_rev + # TODO: locale support would edit the params here + xhr = new Dropbox.Xhr 'POST', "#{@urls.putFile}/#{@urlEncodePath(path)}" + xhr.setBody(data).setParams(params).signWithOauth(@oauth) + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # Reads the metadata of a file or folder in a user's Dropbox. + # + # @param {String} path the path to the file or folder whose metadata will be + # read, relative to the user's Dropbox or to the application's folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Number} version if set, the call will return the metadata + # for the given revision of the file / folder; the latest version is used + # by default + # @option options {Boolean} removed if set to true, the results will include + # files and folders that were deleted from the user's Dropbox + # @option options {Boolean} deleted alias for "removed" that matches the HTTP + # API; using this alias is not recommended, because it may cause confusion + # with JavaScript's delete operation + # @option options {Boolean, Number} readDir only meaningful when stat-ing + # folders; if this is set, the API call will also retrieve the folder's + # contents, which is passed into the callback's third parameter; if this + # is a number, it specifies the maximum number of files and folders that + # should be returned; the default limit is 10,000 items; if the limit is + # exceeded, the call will fail with an error + # @option options {String} versionTag used for saving bandwidth when getting + # a folder's contents; if this value is specified and it matches the + # folder's contents, the call will fail with a 304 (Contents not changed) + # error code; a folder's version identifier can be obtained from the + # versionTag attribute of a Dropbox.Stat instance describing it + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat, ?Array)} + # callback called with the result of the /metadata HTTP request; if the + # call succeeds, the second parameter is a Dropbox.Stat instance + # describing the file / folder, and the first parameter is null; + # if the readDir option is true and the call succeeds, the third + # parameter is an array of Dropbox.Stat instances describing the folder's + # entries + # @return {XMLHttpRequest} the XHR object used for this API call + stat: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + params = {} + if options + if options.version? + params.rev = options.version + if options.removed or options.deleted + params.include_deleted = 'true' + if options.readDir + params.list = 'true' + if options.readDir isnt true + params.file_limit = options.readDir.toString() + if options.cacheHash + params.hash = options.cacheHash + params.include_deleted ||= 'false' + params.list ||= 'false' + # TODO: locale support would edit the params here + xhr = new Dropbox.Xhr 'GET', "#{@urls.metadata}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth @oauth + @dispatchXhr xhr, (error, metadata) -> + stat = Dropbox.Stat.parse metadata + if metadata?.contents + entries = (Dropbox.Stat.parse(entry) for entry in metadata.contents) + else + entries = undefined + callback error, stat, entries + + # Lists the files and folders inside a folder in a user's Dropbox. + # + # @param {String} path the path to the folder whose contents will be + # retrieved, relative to the user's Dropbox or to the application's + # folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Boolean} removed if set to true, the results will include + # files and folders that were deleted from the user's Dropbox + # @option options {Boolean} deleted alias for "removed" that matches the HTTP + # API; using this alias is not recommended, because it may cause confusion + # with JavaScript's delete operation + # @option options {Boolean, Number} limit the maximum number of files and + # folders that should be returned; the default limit is 10,000 items; if + # the limit is exceeded, the call will fail with an error + # @option options {String} versionTag used for saving bandwidth; if this + # option is specified, and its value matches the folder's version tag, + # the call will fail with a 304 (Contents not changed) error code + # instead of returning the contents; a folder's version identifier can be + # obtained from the versionTag attribute of a Dropbox.Stat instance + # describing it + # @param {function(?Dropbox.ApiError, ?Array, ?Dropbox.Stat, + # ?Array)} callback called with the result of the /metadata + # HTTP request; if the call succeeds, the second parameter is an array + # containing the names of the files and folders in the given folder, the + # third parameter is a Dropbox.Stat instance describing the folder, the + # fourth parameter is an array of Dropbox.Stat instances describing the + # folder's entries, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + readdir: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + statOptions = { readDir: true } + if options + if options.limit? + statOptions.readDir = options.limit + if options.versionTag + statOptions.versionTag = options.versionTag + @stat path, statOptions, (error, stat, entry_stats) -> + if entry_stats + entries = (entry_stat.name for entry_stat in entry_stats) + else + entries = null + callback error, entries, stat, entry_stats + + # Alias for "stat" that matches the HTTP API. + metadata: (path, options, callback) -> + @stat path, options, callback + + # Creates a publicly readable URL to a file or folder in the user's Dropbox. + # + # @param {String} path the path to the file or folder that will be linked to; + # the path is relative to the user's Dropbox or to the application's + # folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Boolean} download if set, the URL will be a direct + # download URL, instead of the usual Dropbox preview URLs; direct + # download URLs are short-lived (currently 4 hours), whereas regular URLs + # virtually have no expiration date (currently set to 2030); no direct + # download URLs can be generated for directories + # @option options {Boolean} downloadHack if set, a long-living download URL + # will be generated by asking for a preview URL and using the officially + # documented hack at https://www.dropbox.com/help/201 to turn the preview + # URL into a download URL + # @option options {Boolean} long if set, the URL will not be shortened using + # Dropbox's shortner; the download and downloadHack options imply long + # @option options {Boolean} longUrl synonym for long; makes life easy for + # RhinoJS users + # @param {function(?Dropbox.ApiError, ?Dropbox.PublicUrl)} callback called + # with the result of the /shares or /media HTTP request; if the call + # succeeds, the second parameter is a Dropbox.PublicUrl instance, and the + # first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + makeUrl: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + # NOTE: cannot use options.long; normally, the CoffeeScript compiler + # escapes keywords for us; although long isn't really a keyword, the + # Rhino VM thinks it is; this hack can be removed when the bug below + # is fixed: + # https://github.com/mozilla/rhino/issues/93 + if options and (options['long'] or options.longUrl or options.downloadHack) + params = { short_url: 'false' } + else + params = {} + + path = @urlEncodePath path + url = "#{@urls.shares}/#{path}" + isDirect = false + useDownloadHack = false + if options + if options.downloadHack + isDirect = true + useDownloadHack = true + else if options.download + isDirect = true + url = "#{@urls.media}/#{path}" + + # TODO: locale support would edit the params here + xhr = new Dropbox.Xhr('POST', url).setParams(params).signWithOauth(@oauth) + @dispatchXhr xhr, (error, urlData) -> + if useDownloadHack and urlData and urlData.url + urlData.url = urlData.url.replace(@authServer, @downloadServer) + callback error, Dropbox.PublicUrl.parse(urlData, isDirect) + + # Retrieves the revision history of a file in a user's Dropbox. + # + # @param {String} path the path to the file whose revision history will be + # retrieved, relative to the user's Dropbox or to the application's + # folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Number} limit if specified, the call will return at most + # this many versions + # @param {function(?Dropbox.ApiError, ?Array)} callback called + # with the result of the /revisions HTTP request; if the call succeeds, + # the second parameter is an array with one Dropbox.Stat instance per + # file version, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + history: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + params = {} + if options and options.limit? + params.rev_limit = options.limit + + xhr = new Dropbox.Xhr 'GET', "#{@urls.revisions}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth(@oauth) + @dispatchXhr xhr, (error, versions) -> + if versions + stats = (Dropbox.Stat.parse(metadata) for metadata in versions) + else + stats = undefined + callback error, stats + + # Alias for "history" that matches the HTTP API. + revisions: (path, options, callback) -> + @history path, options, callback + + # Computes a URL that generates a thumbnail for a file in the user's Dropbox. + # + # @param {String} path the path to the file whose thumbnail image URL will be + # computed, relative to the user's Dropbox or to the application's + # folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Boolean} png if true, the thumbnail's image will be a PNG + # file; the default thumbnail format is JPEG + # @option options {String} format value that gets passed directly to the API; + # this is intended for newly added formats that the API may not support; + # use options such as "png" when applicable + # @option options {String} sizeCode specifies the image's dimensions; this + # gets passed directly to the API; currently, the following values are + # supported: 'small' (32x32), 'medium' (64x64), 'large' (128x128), + # 's' (64x64), 'm' (128x128), 'l' (640x480), 'xl' (1024x768); the default + # value is "small" + # @return {String} a URL to an image that can be used as the thumbnail for + # the given file + thumbnailUrl: (path, options) -> + xhr = @thumbnailXhr path, options + xhr.paramsToUrl().url + + # Retrieves the image data of a thumbnail for a file in the user's Dropbox. + # + # This method is intended to be used with low-level painting APIs. Whenever + # possible, it is easier to place the result of thumbnailUrl in a DOM + # element, and rely on the browser to fetch the file. + # + # @param {String} path the path to the file whose thumbnail image URL will be + # computed, relative to the user's Dropbox or to the application's + # folder + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Boolean} png if true, the thumbnail's image will be a PNG + # file; the default thumbnail format is JPEG + # @option options {String} format value that gets passed directly to the API; + # this is intended for newly added formats that the API may not support; + # use options such as "png" when applicable + # @option options {String} sizeCode specifies the image's dimensions; this + # gets passed directly to the API; currently, the following values are + # supported: 'small' (32x32), 'medium' (64x64), 'large' (128x128), + # 's' (64x64), 'm' (128x128), 'l' (640x480), 'xl' (1024x768); the default + # value is "small" + # @option options {Boolean} blob if true, the file will be retrieved as a + # Blob, instead of a String; this requires XHR Level 2 support, which is + # not available in IE <= 9 + # @param {function(?Dropbox.ApiError, ?Object, ?Dropbox.Stat)} callback + # called with the result of the /thumbnails HTTP request; if the call + # succeeds, the second parameter is the image data as a String or Blob, + # the third parameter is a Dropbox.Stat instance describing the + # thumbnailed file, and the first argument is null + # @return {XMLHttpRequest} the XHR object used for this API call + readThumbnail: (path, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + responseType = 'b' + if options + responseType = 'blob' if options.blob + + xhr = @thumbnailXhr path, options + xhr.setResponseType responseType + @dispatchXhr xhr, (error, data, metadata) -> + callback error, data, Dropbox.Stat.parse(metadata) + + # Sets up an XHR for reading a thumbnail for a file in the user's Dropbox. + # + # @see Dropbox.Client#thumbnailUrl + # @return {Dropbox.Xhr} an XHR request configured for fetching the thumbnail + thumbnailXhr: (path, options) -> + params = {} + if options + if options.format + params.format = options.format + else if options.png + params.format = 'png' + if options.size + # Can we do something nicer here? + params.size = options.size + + xhr = new Dropbox.Xhr 'GET', "#{@urls.thumbnails}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth(@oauth) + + # Reverts a file's contents to a previous version. + # + # This is an atomic, bandwidth-optimized equivalent of reading the file + # contents at the given file version (readFile), and then using it to + # overwrite the file (writeFile). + # + # @param {String} path the path to the file whose contents will be reverted + # to a previous version, relative to the user's Dropbox or to the + # application's folder + # @param {String} versionTag the tag of the version that the file will be + # reverted to; maps to the "rev" parameter in the HTTP API + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /restore HTTP request; if the call succeeds, the + # second parameter is a Dropbox.Stat instance describing the file after + # the revert operation, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + revertFile: (path, versionTag, callback) -> + xhr = new Dropbox.Xhr 'POST', "#{@urls.restore}/#{@urlEncodePath(path)}" + xhr.setParams(rev: versionTag).signWithOauth @oauth + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # Alias for "revertFile" that matches the HTTP API. + restore: (path, versionTag, callback) -> + @revertFile path, versionTag, callback + + # Finds files / folders whose name match a pattern, in the user's Dropbox. + # + # @param {String} path the path to the file whose contents will be reverted + # to a previous version, relative to the user's Dropbox or to the + # application's folder + # @param {String} namePattern the string that file / folder names must + # contain in order to match the search criteria; + # @param {?Object} options the advanced settings below; for the default + # settings, skip the argument or pass null + # @option options {Number} limit if specified, the call will return at most + # this many versions + # @option options {Boolean} removed if set to true, the results will include + # files and folders that were deleted from the user's Dropbox; the default + # limit is the maximum value of 1,000 + # @option options {Boolean} deleted alias for "removed" that matches the HTTP + # API; using this alias is not recommended, because it may cause confusion + # with JavaScript's delete operation + # @param {function(?Dropbox.ApiError, ?Array)} callback called + # with the result of the /search HTTP request; if the call succeeds, the + # second parameter is an array with one Dropbox.Stat instance per search + # result, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + findByName: (path, namePattern, options, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + params = { query: namePattern } + if options + if options.limit? + params.file_limit = options.limit + if options.removed or options.deleted + params.include_deleted = true + + xhr = new Dropbox.Xhr 'GET', "#{@urls.search}/#{@urlEncodePath(path)}" + xhr.setParams(params).signWithOauth(@oauth) + @dispatchXhr xhr, (error, results) -> + if results + stats = (Dropbox.Stat.parse(metadata) for metadata in results) + else + stats = undefined + callback error, stats + + # Alias for "findByName" that matches the HTTP API. + search: (path, namePattern, options, callback) -> + @findByName path, namePattern, options, callback + + # Creates a reference used to copy a file to another user's Dropbox. + # + # @param {String} path the path to the file whose contents will be + # referenced, relative to the uesr's Dropbox or to the application's + # folder + # @param {function(?Dropbox.ApiError, ?Dropbox.CopyReference)} callback + # called with the result of the /copy_ref HTTP request; if the call + # succeeds, the second parameter is a Dropbox.CopyReference instance, and + # the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + makeCopyReference: (path, callback) -> + xhr = new Dropbox.Xhr 'GET', "#{@urls.copyRef}/#{@urlEncodePath(path)}" + xhr.signWithOauth @oauth + @dispatchXhr xhr, (error, refData) -> + callback error, Dropbox.CopyReference.parse(refData) + + # Alias for "makeCopyReference" that matches the HTTP API. + copyRef: (path, callback) -> + @makeCopyReference path, callback + + # Fetches a list of changes in the user's Dropbox since the last call. + # + # This method is intended to make full sync implementations easier and more + # performant. Each call returns a cursor that can be used in a future call + # to obtain all the changes that happened in the user's Dropbox (or + # application directory) between the two calls. + # + # @param {Dropbox.PulledChanges, String} cursorTag the result of a previous + # call to pullChanges, or a string containing a tag representing the + # Dropbox state that is used as the baseline for the change list; this + # should be obtained from a previous call to pullChanges, or be set to null + # / ommitted on the first call to pullChanges + # @param {function(?Dropbox.ApiError, ?Dropbox.PulledChanges)} callback + # called with the result of the /delta HTTP request; if the call + # succeeds, the second parameter is a Dropbox.PulledChanges describing + # the changes to the user's Dropbox since the pullChanges call that + # produced the given cursor, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + pullChanges: (cursor, callback) -> + if (not callback) and (typeof cursor is 'function') + callback = cursor + cursor = null + + if cursor + if cursor.cursorTag + params = { cursor: cursor.cursorTag } + else + params = { cursor: cursor } + else + params = {} + + xhr = new Dropbox.Xhr 'POST', @urls.delta + xhr.setParams(params).signWithOauth @oauth + @dispatchXhr xhr, (error, deltaInfo) -> + callback error, Dropbox.PulledChanges.parse(deltaInfo) + + # Alias for "pullChanges" that matches the HTTP API. + delta: (cursor, callback) -> + @pullChanges cursor, callback + + # Creates a folder in a user's Dropbox. + # + # @param {String} path the path of the folder that will be created, relative + # to the user's Dropbox or to the application's folder + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /fileops/create_folder HTTP request; if the call + # succeeds, the second parameter is a Dropbox.Stat instance describing + # the newly created folder, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + mkdir: (path, callback) -> + xhr = new Dropbox.Xhr 'POST', @urls.fileopsCreateFolder + xhr.setParams(root: @fileRoot, path: @normalizePath(path)). + signWithOauth(@oauth) + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # Removes a file or diretory from a user's Dropbox. + # + # @param {String} path the path of the file to be read, relative to the + # user's Dropbox or to the application's folder + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /fileops/delete HTTP request; if the call succeeds, + # the second parameter is a Dropbox.Stat instance describing the removed + # file or folder, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + remove: (path, callback) -> + xhr = new Dropbox.Xhr 'POST', @urls.fileopsDelete + xhr.setParams(root: @fileRoot, path: @normalizePath(path)). + signWithOauth(@oauth) + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # node.js-friendly alias for "remove". + unlink: (path, callback) -> + @remove path, callback + + # Alias for "remove" that matches the HTTP API. + delete: (path, callback) -> + @remove path, callback + + # Copies a file or folder in the user's Dropbox. + # + # This method's "from" parameter can be either a path or a copy reference + # obtained by a previous call to makeCopyRef. The method uses a crude + # heuristic to interpret the "from" string -- if it doesn't contain any + # slash (/) or dot (.) character, it is assumed to be a copy reference. The + # easiest way to work with it is to prepend "/" to every path passed to the + # method. The method will process paths that start with multiple /s + # correctly. + # + # @param {String, Dropbox.CopyReference} from the path of the file or folder + # that will be copied, or a Dropbox.CopyReference instance obtained by + # calling makeCopyRef or Dropbox.CopyReference.parse; if this is a path, it + # is relative to the user's Dropbox or to the application's folder + # @param {String} toPath the path that the file or folder will have after the + # method call; the path is relative to the user's Dropbox or to the + # application folder + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /fileops/copy HTTP request; if the call succeeds, the + # second parameter is a Dropbox.Stat instance describing the file or folder + # created by the copy operation, and the first parameter is null + # @return {XMLHttpRequest} the XHR object used for this API call + copy: (from, toPath, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + params = { root: @fileRoot, to_path: @normalizePath(toPath) } + if from instanceof Dropbox.CopyReference + params.from_copy_ref = from.tag + else + params.from_path = @normalizePath from + # TODO: locale support would edit the params here + + xhr = new Dropbox.Xhr 'POST', @urls.fileopsCopy + xhr.setParams(params).signWithOauth @oauth + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # Moves a file or folder to a different location in a user's Dropbox. + # + # @param {String} fromPath the path of the file or folder that will be moved, + # relative to the user's Dropbox or to the application's folder + # @param {String} toPath the path that the file or folder will have after + # the method call; the path is relative to the user's Dropbox or to the + # application's folder + # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with + # the result of the /fileops/move HTTP request; if the call succeeds, the + # second parameter is a Dropbox.Stat instance describing the moved + # file or folder at its new location, and the first parameter is + # null + # @return {XMLHttpRequest} the XHR object used for this API call + move: (fromPath, toPath, callback) -> + if (not callback) and (typeof options is 'function') + callback = options + options = null + + xhr = new Dropbox.Xhr 'POST', @urls.fileopsMove + xhr.setParams( + root: @fileRoot, from_path: @normalizePath(fromPath), + to_path: @normalizePath(toPath)).signWithOauth @oauth + @dispatchXhr xhr, (error, metadata) -> + callback error, Dropbox.Stat.parse(metadata) + + # Removes all login information. + # + # @return {Dropbox.Client} this, for easy call chaining + reset: -> + @uid = null + @oauth.setToken null, '' + @authState = DropboxClient.RESET + @authError = null + @_credentials = null + @ + + # Change the client's OAuth credentials. + # + # @param {?Object} the result of a prior call to credentials() + # @return {Dropbox.Client} this, for easy call chaining + setCredentials: (credentials) -> + @oauth.reset credentials + @uid = credentials.uid or null + if credentials.authState + @authState = credentials.authState + else + if credentials.token + @authState = DropboxClient.DONE + else + @authState = DropboxClient.RESET + @authError = null + @_credentials = null + @ + + # @return {String} a string that uniquely identifies the Dropbox application + # of this client + appHash: -> + @oauth.appHash() + + # Computes the URLs of all the Dropbox API calls. + # + # @private + # This is called by the constructor, and used by the other methods. It should + # not be used directly. + setupUrls: -> + @fileRoot = if @sandbox then 'sandbox' else 'dropbox' + + @urls = + # Authentication. + requestToken: "#{@apiServer}/1/oauth/request_token" + authorize: "#{@authServer}/1/oauth/authorize" + accessToken: "#{@apiServer}/1/oauth/access_token" + signOut: "#{@apiServer}/1/unlink_access_token" + + # Accounts. + accountInfo: "#{@apiServer}/1/account/info" + + # Files and metadata. + getFile: "#{@fileServer}/1/files/#{@fileRoot}" + postFile: "#{@fileServer}/1/files/#{@fileRoot}" + putFile: "#{@fileServer}/1/files_put/#{@fileRoot}" + metadata: "#{@apiServer}/1/metadata/#{@fileRoot}" + delta: "#{@apiServer}/1/delta" + revisions: "#{@apiServer}/1/revisions/#{@fileRoot}" + restore: "#{@apiServer}/1/restore/#{@fileRoot}" + search: "#{@apiServer}/1/search/#{@fileRoot}" + shares: "#{@apiServer}/1/shares/#{@fileRoot}" + media: "#{@apiServer}/1/media/#{@fileRoot}" + copyRef: "#{@apiServer}/1/copy_ref/#{@fileRoot}" + thumbnails: "#{@fileServer}/1/thumbnails/#{@fileRoot}" + + # File operations. + fileopsCopy: "#{@apiServer}/1/fileops/copy" + fileopsCreateFolder: "#{@apiServer}/1/fileops/create_folder" + fileopsDelete: "#{@apiServer}/1/fileops/delete" + fileopsMove: "#{@apiServer}/1/fileops/move" + + # authState value for a client that experienced an authentication error. + @ERROR: 0 + + # authState value for a properly initialized client with no user credentials. + @RESET: 1 + + # authState value for a client with a request token that must be authorized. + @REQUEST: 2 + + # authState value for a client whose request token was authorized. + @AUTHORIZED: 3 + + # authState value for a client that has an access token. + @DONE: 4 + + # authState value for a client that voluntarily invalidated its access token. + @SIGNED_OFF: 5 + + # Normalizes a Dropobx path and encodes it for inclusion in a request URL. + # + # @private + # This is called internally by the other client functions, and should not be + # used outside the {Dropbox.Client} class. + urlEncodePath: (path) -> + Dropbox.Xhr.urlEncodeValue(@normalizePath(path)).replace /%2F/gi, '/' + + # Normalizes a Dropbox path for API requests. + # + # @private + # This is an internal method. It is used by all the client methods that take + # paths as arguments. + # + # @param {String} path a path + normalizePath: (path) -> + if path.substring(0, 1) is '/' + i = 1 + while path.substring(i, i + 1) is '/' + i += 1 + path.substring i + else + path + + # Generates an OAuth request token. + # + # @private + # This a low-level method called by authorize. Users should call authorize. + # + # @param {function(error, data)} callback called with the result of the + # /oauth/request_token HTTP request + requestToken: (callback) -> + xhr = new Dropbox.Xhr('POST', @urls.requestToken).signWithOauth(@oauth) + @dispatchXhr xhr, callback + + # The URL for /oauth/authorize, embedding the user's token. + # + # @private + # This a low-level method called by authorize. Users should call authorize. + # + # @param {String} token the oauth_token obtained from an /oauth/request_token + # call + # @return {String} the URL that the user's browser should be redirected to in + # order to perform an /oauth/authorize request + authorizeUrl: (token) -> + params = { oauth_token: token, oauth_callback: @driver.url() } + "#{@urls.authorize}?" + Dropbox.Xhr.urlEncode(params) + + # Exchanges an OAuth request token with an access token. + # + # @private + # This a low-level method called by authorize. Users should call authorize. + # + # @param {function(error, data)} callback called with the result of the + # /oauth/access_token HTTP request + getAccessToken: (callback) -> + xhr = new Dropbox.Xhr('POST', @urls.accessToken).signWithOauth(@oauth) + @dispatchXhr xhr, callback + + # Prepares an XHR before it is sent to the server. + # + # @private + # This is a low-level method called by other client methods. + dispatchXhr: (xhr, callback) -> + xhr.setCallback callback + xhr.prepare() + nativeXhr = xhr.xhr + if @filter + return nativeXhr unless @filter(nativeXhr, xhr) + xhr.send() + nativeXhr + + # @private + # @return {String} the URL to the default value for the "server" option + defaultApiServer: -> + 'https://api.dropbox.com' + + # @private + # @return {String} the URL to the default value for the "authServer" option + defaultAuthServer: -> + @apiServer.replace 'api.', 'www.' + + # @private + # @return {String} the URL to the default value for the "fileServer" option + defaultFileServer: -> + @apiServer.replace 'api.', 'api-content.' + + # @private + # @return {String} the URL to the default value for the "downloadServer" + # option + defaultDownloadServer: -> + @apiServer.replace 'api.', 'dl.' + + # Computes the cached value returned by credentials. + # + # @private + # @see Dropbox.Client#computeCredentials + computeCredentials: -> + value = + key: @oauth.key + sandbox: @sandbox + value.secret = @oauth.secret if @oauth.secret + if @oauth.token + value.token = @oauth.token + value.tokenSecret = @oauth.tokenSecret + value.uid = @uid if @uid + if @authState isnt DropboxClient.ERROR and + @authState isnt DropboxClient.RESET and + @authState isnt DropboxClient.DONE and + @authState isnt DropboxClient.SIGNED_OFF + value.authState = @authState + if @apiServer isnt @defaultApiServer() + value.server = @apiServer + if @authServer isnt @defaultAuthServer() + value.authServer = @authServer + if @fileServer isnt @defaultFileServer() + value.fileServer = @fileServer + if @downloadServer isnt @defaultDownloadServer() + value.downloadServer = @downloadServer + @_credentials = value + +DropboxClient = Dropbox.Client diff --git a/lib/client/storage/dropbox/src/drivers.coffee b/lib/client/storage/dropbox/src/drivers.coffee new file mode 100644 index 00000000..d48184e4 --- /dev/null +++ b/lib/client/storage/dropbox/src/drivers.coffee @@ -0,0 +1,477 @@ +# Documentation for the interface to a Dropbox OAuth driver. +class Dropbox.AuthDriver + # The callback URL that should be supplied to the OAuth /authorize call. + # + # The driver must be able to intercept redirects to the returned URL, in + # order to know when a user has completed the authorization flow. + # + # @return {String} an absolute URL + url: -> + 'https://some.url' + + # Redirects users to /authorize and waits for them to complete the flow. + # + # This method is called when the OAuth process reaches the REQUEST state, + # meaning the client has a request token that must be authorized by the user. + # + # @param {String} authUrl the URL that users should be sent to in order to + # authorize the application's token; this points to a Web page on + # Dropbox' servers + # @param {String} token the OAuth token that the user is authorizing; this + # will be provided by the Dropbox servers as a query parameter when the + # user is redirected to the URL returned by the driver's url() method + # @param {String} tokenSecret the secret associated with the given OAuth + # token; the driver may store this together with the token + # @param {function()} callback called when users have completed the + # authorization flow; the driver should call this when Dropbox redirects + # users to the URL returned by the url() method, and the 'token' query + # parameter matches the value of the token parameter + doAuthorize: (authUrl, token, tokenSecret, callback) -> + callback 'oauth-token' + + # Called when there is some progress in the OAuth process. + # + # The OAuth process goes through the following states: + # + # * Dropbox.Client.RESET - the client has no OAuth token, and is about to + # ask for a request token + # * Dropbox.Client.REQUEST - the client has a request OAuth token, and the + # user must go to an URL on the Dropbox servers to authorize the token + # * Dropbox.Client.AUTHORIZED - the client has a request OAuth token that + # was authorized by the user, and is about to exchange it for an access + # token + # * Dropbox.Client.DONE - the client has an access OAuth token that can be + # used for all API calls; the OAuth process is complete, and the callback + # passed to authorize is about to be called + # * Dropbox.Client.SIGNED_OFF - the client's Dropbox.Client#signOut() was + # called, and the client's OAuth token was invalidated + # * Dropbox.Client.ERROR - the client encounered an error during the OAuth + # process; the callback passed to authorize is about to be called with the + # error information + # + # @param {Dropbox.Client} client the client performing the OAuth process + # @param {function()} callback called when onAuthStateChange acknowledges the + # state change + onAuthStateChange: (client, callback) -> + callback() + +# Namespace for authentication drivers. +Dropbox.Drivers = {} + +# Base class for drivers that run in the browser. +# +# Inheriting from this class makes a driver use HTML5 localStorage to preserve +# OAuth tokens across page reloads. +class Dropbox.Drivers.BrowserBase + # Sets up the OAuth driver. + # + # Subclasses should pass the options object they receive to the superclass + # constructor. + # + # @param {?Object} options the advanced settings below + # @option options {Boolean} rememberUser if true, the user's OAuth tokens are + # saved in localStorage; if you use this, you MUST provide a UI item that + # calls signOut() on Dropbox.Client, to let the user "log out" of the + # application + # @option options {String} scope embedded in the localStorage key that holds + # the authentication data; useful for having multiple OAuth tokens in a + # single application + constructor: (options) -> + @rememberUser = options?.rememberUser or false + @scope = options?.scope or 'default' + + # The magic happens here. + onAuthStateChange: (client, callback) -> + @setStorageKey client + + switch client.authState + when DropboxClient.RESET + @loadCredentials (credentials) => + return callback() unless credentials + + if credentials.authState # Incomplete authentication. + client.setCredentials credentials + return callback() + + # There is an old access token. Only use it if the app supports + # logout. + unless @rememberUser + @forgetCredentials() + return callback() + + # Verify that the old access token still works. + client.setCredentials credentials + client.getUserInfo (error) => + if error + client.reset() + @forgetCredentials callback + else + callback() + when DropboxClient.REQUEST + @storeCredentials client.credentials(), callback + when DropboxClient.DONE + if @rememberUser + return @storeCredentials(client.credentials(), callback) + @forgetCredentials callback + when DropboxClient.SIGNED_OFF + @forgetCredentials callback + when DropboxClient.ERROR + @forgetCredentials callback + else + callback() + @ + + # Computes the @storageKey used by loadCredentials and forgetCredentials. + # + # @private + # This is called by onAuthStateChange. + # + # @param {Dropbox.Client} client the client instance that is running the + # authorization process + # @return {Dropbox.Driver} this, for easy call chaining + setStorageKey: (client) -> + # NOTE: the storage key is dependent on the app hash so that multiple apps + # hosted off the same server don't step on eachother's toes + @storageKey = "dropbox-auth:#{@scope}:#{client.appHash()}" + @ + + # Stores a Dropbox.Client's credentials to localStorage. + # + # @private + # onAuthStateChange calls this method during the authentication flow. + # + # @param {Object} credentials the result of a Drobpox.Client#credentials call + # @param {function()} callback called when the storing operation is complete + # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining + storeCredentials: (credentials, callback) -> + localStorage.setItem @storageKey, JSON.stringify(credentials) + callback() + @ + + # Retrieves a token and secret from localStorage. + # + # @private + # onAuthStateChange calls this method during the authentication flow. + # + # @param {function(?Object)} callback supplied with the credentials object + # stored by a previous call to + # Dropbox.Drivers.BrowserBase#storeCredentials; null if no credentials were + # stored, or if the previously stored credentials were deleted + # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining + loadCredentials: (callback) -> + jsonString = localStorage.getItem @storageKey + unless jsonString + callback null + return @ + + try + callback JSON.parse(jsonString) + catch e + # Parse errors. + callback null + @ + + # Deletes information previously stored by a call to storeToken. + # + # @private + # onAuthStateChange calls this method during the authentication flow. + # + # @param {function()} callback called after the credentials are deleted + # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining + forgetCredentials: (callback) -> + localStorage.removeItem @storageKey + callback() + @ + + # Wrapper for window.location, for testing purposes. + # + # @return {String} the current page's URL + @currentLocation: -> + window.location.href + +# OAuth driver that uses a redirect and localStorage to complete the flow. +class Dropbox.Drivers.Redirect extends Dropbox.Drivers.BrowserBase + # Sets up the redirect-based OAuth driver. + # + # @param {?Object} options the advanced settings below + # @option options {Boolean} useQuery if true, the page will receive OAuth + # data as query parameters; by default, the page receives OAuth data in + # the fragment part of the URL (the string following the #, + # available as document.location.hash), to avoid confusing the server + # generating the page + # @option options {Boolean} rememberUser if true, the user's OAuth tokens are + # saved in localStorage; if you use this, you MUST provide a UI item that + # calls signOut() on Dropbox.Client, to let the user "log out" of the + # application + # @option options {String} scope embedded in the localStorage key that holds + # the authentication data; useful for having multiple OAuth tokens in a + # single application + constructor: (options) -> + super options + @useQuery = options?.useQuery or false + @receiverUrl = @computeUrl options + @tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)" + + # Forwards the authentication process from REQUEST to AUTHORIZED on redirect. + onAuthStateChange: (client, callback) -> + superCall = do => => super client, callback + @setStorageKey client + if client.authState is DropboxClient.RESET + @loadCredentials (credentials) => + if credentials and credentials.authState # Incomplete authentication. + if credentials.token is @locationToken() and + credentials.authState is DropboxClient.REQUEST + # locationToken matched, so the redirect happened + credentials.authState = DropboxClient.AUTHORIZED + return @storeCredentials credentials, superCall + else + # The authentication process broke down, start over. + return @forgetCredentials superCall + superCall() + else + superCall() + + # URL of the current page, since the user will be sent right back. + url: -> + @receiverUrl + + # Redirects to the authorize page. + doAuthorize: (authUrl) -> + window.location.assign authUrl + + # Pre-computes the return value of url. + computeUrl: -> + querySuffix = "_dropboxjs_scope=#{encodeURIComponent @scope}" + location = Dropbox.Drivers.BrowserBase.currentLocation() + if location.indexOf('#') is -1 + fragment = null + else + locationPair = location.split '#', 2 + location = locationPair[0] + fragment = locationPair[1] + if @useQuery + if location.indexOf('?') is -1 + location += "?#{querySuffix}" # No query string in the URL. + else + location += "&#{querySuffix}" # The URL already has a query string. + else + fragment = "?#{querySuffix}" + + if fragment + location + '#' + fragment + else + location + + # Figures out if the user completed the OAuth flow based on the current URL. + # + # @return {?String} the OAuth token that the user just authorized, or null if + # the user accessed this directly, without having authorized a token + locationToken: -> + location = Dropbox.Drivers.BrowserBase.currentLocation() + + # Check for the scope. + scopePattern = "_dropboxjs_scope=#{encodeURIComponent @scope}&" + return null if location.indexOf?(scopePattern) is -1 + + # Extract the token. + match = @tokenRe.exec location + if match then decodeURIComponent(match[2]) else null + +# OAuth driver that uses a popup window and postMessage to complete the flow. +class Dropbox.Drivers.Popup extends Dropbox.Drivers.BrowserBase + # Sets up a popup-based OAuth driver. + # + # @param {?Object} options one of the settings below; leave out the argument + # to use the current location for redirecting + # @option options {Boolean} rememberUser if true, the user's OAuth tokens are + # saved in localStorage; if you use this, you MUST provide a UI item that + # calls signOut() on Dropbox.Client, to let the user "log out" of the + # application + # @option options {String} scope embedded in the localStorage key that holds + # the authentication data; useful for having multiple OAuth tokens in a + # single application + # @option options {String} receiverUrl URL to the page that receives the + # /authorize redirect and performs the postMessage + # @option options {Boolean} noFragment if true, the receiverUrl will be used + # as given; by default, a hash "#" is appended to URLs that don't have + # one, so the OAuth token is received as a URL fragment and does not hit + # the file server + # @option options {String} receiverFile the URL to the receiver page will be + # computed by replacing the file name (everything after the last /) of + # the current location with this parameter's value + constructor: (options) -> + super options + @receiverUrl = @computeUrl options + @tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)" + + # Removes credentials stuck in the REQUEST stage. + onAuthStateChange: (client, callback) -> + superCall = do => => super client, callback + @setStorageKey client + if client.authState is DropboxClient.RESET + @loadCredentials (credentials) -> + if credentials and credentials.authState # Incomplete authentication. + # The authentication process broke down, start over. + return @forgetCredentials superCall + superCall() + else + superCall() + + # Shows the authorization URL in a pop-up, waits for it to send a message. + doAuthorize: (authUrl, token, tokenSecret, callback) -> + @listenForMessage token, callback + @openWindow authUrl + + # URL of the redirect receiver page, which posts a message back to this page. + url: -> + @receiverUrl + + # Pre-computes the return value of url. + computeUrl: (options) -> + if options + if options.receiverUrl + if options.noFragment or options.receiverUrl.indexOf('#') isnt -1 + return options.receiverUrl + else + return options.receiverUrl + '#' + else if options.receiverFile + fragments = Dropbox.Drivers.BrowserBase.currentLocation().split '/' + fragments[fragments.length - 1] = options.receiverFile + if options.noFragment + return fragments.join('/') + else + return fragments.join('/') + '#' + Dropbox.Drivers.BrowserBase.currentLocation() + + # Creates a popup window. + # + # @param {String} url the URL that will be loaded in the popup window + # @return {?DOMRef} reference to the opened window, or null if the call + # failed + openWindow: (url) -> + window.open url, '_dropboxOauthSigninWindow', @popupWindowSpec(980, 700) + + # Spec string for window.open to create a nice popup. + # + # @param {Number} popupWidth the desired width of the popup window + # @param {Number} popupHeight the desired height of the popup window + # @return {String} spec string for the popup window + popupWindowSpec: (popupWidth, popupHeight) -> + # Metrics for the current browser window. + x0 = window.screenX ? window.screenLeft + y0 = window.screenY ? window.screenTop + width = window.outerWidth ? document.documentElement.clientWidth + height = window.outerHeight ? document.documentElement.clientHeight + + # Computed popup window metrics. + popupLeft = Math.round x0 + (width - popupWidth) / 2 + popupTop = Math.round y0 + (height - popupHeight) / 2.5 + popupLeft = x0 if popupLeft < x0 + popupTop = y0 if popupTop < y0 + + # The specification string. + "width=#{popupWidth},height=#{popupHeight}," + + "left=#{popupLeft},top=#{popupTop}" + + 'dialog=yes,dependent=yes,scrollbars=yes,location=yes' + + # Listens for a postMessage from a previously opened popup window. + # + # @param {String} token the token string that must be received from the popup + # window + # @param {function()} called when the received message matches the token + listenForMessage: (token, callback) -> + listener = (event) => + match = @tokenRe.exec event.data.toString() + if match and decodeURIComponent(match[2]) is token + window.removeEventListener 'message', listener + callback() + window.addEventListener 'message', listener, false + + +# OAuth driver that redirects the browser to a node app to complete the flow. +# +# This is useful for testing node.js libraries and applications. +class Dropbox.Drivers.NodeServer + # Starts up the node app that intercepts the browser redirect. + # + # @param {?Object} options one or more of the options below + # @option options {Number} port the number of the TCP port that will receive + # HTTP requests + # @param {String} faviconFile the path to a file that will be served at + # /favicon.ico + constructor: (options) -> + @port = options?.port or 8912 + @faviconFile = options?.favicon or null + # Calling require in the constructor because this doesn't work in browsers. + @fs = require 'fs' + @http = require 'http' + @open = require 'open' + + @callbacks = {} + @urlRe = new RegExp "^/oauth_callback\\?" + @tokenRe = new RegExp "(\\?|&)oauth_token=([^&]+)(&|$)" + @createApp() + + # URL to the node.js OAuth callback handler. + url: -> + "http://localhost:#{@port}/oauth_callback" + + # Opens the token + doAuthorize: (authUrl, token, tokenSecret, callback) -> + @callbacks[token] = callback + @openBrowser authUrl + + # Opens the given URL in a browser. + openBrowser: (url) -> + unless url.match /^https?:\/\// + throw new Error("Not a http/https URL: #{url}") + @open url + + # Creates and starts up an HTTP server that will intercept the redirect. + createApp: -> + @app = @http.createServer (request, response) => + @doRequest request, response + @app.listen @port + + # Shuts down the HTTP server. + # + # The driver will become unusable after this call. + closeServer: -> + @app.close() + + # Reads out an /authorize callback. + doRequest: (request, response) -> + if @urlRe.exec request.url + match = @tokenRe.exec request.url + if match + token = decodeURIComponent match[2] + if @callbacks[token] + @callbacks[token]() + delete @callbacks[token] + data = '' + request.on 'data', (dataFragment) -> data += dataFragment + request.on 'end', => + if @faviconFile and (request.url is '/favicon.ico') + @sendFavicon response + else + @closeBrowser response + + # Renders a response that will close the browser window used for OAuth. + closeBrowser: (response) -> + closeHtml = """ + + +

Please close this window.

+ """ + response.writeHead(200, + {'Content-Length': closeHtml.length, 'Content-Type': 'text/html' }) + response.write closeHtml + response.end + + # Renders the favicon file. + sendFavicon: (response) -> + @fs.readFile @faviconFile, (error, data) -> + response.writeHead(200, + { 'Content-Length': data.length, 'Content-Type': 'image/x-icon' }) + response.write data + response.end diff --git a/lib/client/storage/dropbox/src/hmac.coffee b/lib/client/storage/dropbox/src/hmac.coffee new file mode 100644 index 00000000..b26be463 --- /dev/null +++ b/lib/client/storage/dropbox/src/hmac.coffee @@ -0,0 +1,180 @@ +# HMAC-SHA1 implementation heavily inspired from +# http://pajhome.org.uk/crypt/md5/sha1.js + +# Base64-encoded HMAC-SHA1. +# +# @param {String} string the ASCII string to be signed +# @param {String} key the HMAC key +# @return {String} a base64-encoded HMAC of the given string and key +base64HmacSha1 = (string, key) -> + arrayToBase64 hmacSha1(stringToArray(string), stringToArray(key), + string.length, key.length) + +# Base64-encoded SHA1. +# +# @param {String} string the ASCII string to be hashed +# @return {String} a base64-encoded SHA1 hash of the given string +base64Sha1 = (string) -> + arrayToBase64 sha1(stringToArray(string), string.length) + +# SHA1 and HMAC-SHA1 versions that use the node.js builtin crypto. +unless window? + crypto = require 'crypto' + base64HmacSha1 = (string, key) -> + hmac = crypto.createHmac 'sha1', key + hmac.update string + hmac.digest 'base64' + base64Sha1 = (string) -> + hash = crypto.createHash 'sha1' + hash.update string + hash.digest 'base64' + +# HMAC-SHA1 implementation. +# +# @param {Array} string the HMAC input, as an array of 32-bit numbers +# @param {Array} key the HMAC input, as an array of 32-bit numbers +# @param {Number} length the length of the HMAC input, in bytes +# @return {Array} the HMAC output, as an array of 32-bit numbers +hmacSha1 = (string, key, length, keyLength) -> + if key.length > 16 + key = sha1 key, keyLength + + ipad = (key[i] ^ 0x36363636 for i in [0...16]) + opad = (key[i] ^ 0x5C5C5C5C for i in [0...16]) + + hash1 = sha1 ipad.concat(string), 64 + length + sha1 opad.concat(hash1), 64 + 20 + +# SHA1 implementation. +# +# @param {Array} string the SHA1 input, as an array of 32-bit numbers; the +# computation trashes the array +# @param {Number} length the number of bytes in the SHA1 input; used in the +# SHA1 padding algorithm +# @return {Array} the SHA1 output, as an array of 32-bit numbers +sha1 = (string, length) -> + string[length >> 2] |= 1 << (31 - ((length & 0x03) << 3)) + string[(((length + 8) >> 6) << 4) + 15] = length << 3 + + state = Array 80 + a = 1732584193 # 0x67452301 + b = -271733879 # 0xefcdab89 + c = -1732584194 # 0x98badcfe + d = 271733878 # 0x10325476 + e = -1009589776 # 0xc3d2e1f0 + + i = 0 + limit = string.length + # Uncomment the line below to debug packing. + # console.log string.map(xxx) + while i < limit + a0 = a + b0 = b + c0 = c + d0 = d + e0 = e + + for j in [0...80] + if j < 16 + state[j] = string[i + j] + else + state[j] = rotateLeft32 state[j - 3] ^ state[j - 8] ^ state[j - 14] ^ + state[j - 16], 1 + if j < 20 + ft = (b & c) | ((~b) & d) + kt = 1518500249 # 0x5a827999 + else if j < 40 + ft = b ^ c ^ d + kt = 1859775393 # 0x6ed9eba1 + else if j < 60 + ft = (b & c) | (b & d) | (c & d) + kt = -1894007588 # 0x8f1bbcdc + else + ft = b ^ c ^ d + kt = -899497514 # 0xca62c1d6 + t = add32 add32(rotateLeft32(a, 5), ft), add32(add32(e, state[j]), kt) + e = d + d = c + c = rotateLeft32 b, 30 + b = a + a = t + # Uncomment the line below to debug block computation. + # console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)] + a = add32 a, a0 + b = add32 b, b0 + c = add32 c, c0 + d = add32 d, d0 + e = add32 e, e0 + i += 16 + # Uncomment the line below to see the input to the base64 encoder. + # console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)] + [a, b, c, d, e] + +### +# Uncomment the definition below for debugging. +# +# Returns the hexadecimal representation of a 32-bit number. +xxx = (n) -> + if n < 0 + n = (1 << 30) * 4 + n + n.toString 16 +### + +# Rotates a 32-bit word. +# +# @param {Number} value the 32-bit number to be rotated +# @param {Number} count the number of bits (0..31) to rotate by +# @return {Number} the rotated value +rotateLeft32 = (value, count) -> + (value << count) | (value >>> (32 - count)) + +# 32-bit unsigned addition. +# +# @param {Number} a, b the 32-bit numbers to be added modulo 2^32 +# @return {Number} the 32-bit representation of a + b +add32 = (a, b) -> + low = (a & 0xFFFF) + (b & 0xFFFF) + high = (a >> 16) + (b >> 16) + (low >> 16) + (high << 16) | (low & 0xFFFF) + +# Converts a 32-bit number array into a base64-encoded string. +# +# @param {Array} an array of big-endian 32-bit numbers +# @return {String} base64 encoding of the given array of numbers +arrayToBase64 = (array) -> + string = "" + i = 0 + limit = array.length * 4 + while i < limit + i2 = i + trit = ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 16 + i2 += 1 + trit |= ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 8 + i2 += 1 + trit |= (array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF + + string += _base64Digits[(trit >> 18) & 0x3F] + string += _base64Digits[(trit >> 12) & 0x3F] + i += 1 + if i >= limit + string += '=' + else + string += _base64Digits[(trit >> 6) & 0x3F] + i += 1 + if i >= limit + string += '=' + else + string += _base64Digits[trit & 0x3F] + i += 1 + string + +_base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +# Converts an ASCII string into array of 32-bit numbers. +stringToArray = (string) -> + array = [] + mask = 0xFF + for i in [0...string.length] + array[i >> 2] |= (string.charCodeAt(i) & mask) << ((3 - (i & 3)) << 3) + array + diff --git a/lib/client/storage/dropbox/src/oauth.coffee b/lib/client/storage/dropbox/src/oauth.coffee new file mode 100644 index 00000000..823f90a5 --- /dev/null +++ b/lib/client/storage/dropbox/src/oauth.coffee @@ -0,0 +1,162 @@ +# Stripped-down OAuth implementation that works with the Dropbox API server. +class Dropbox.Oauth + # Creates an Oauth instance that manages an application's keys and token. + # + # @param {Object} options the following properties + # @option options {String} key the Dropbox application's key (consumer key, + # in OAuth vocabulary); browser-side applications should use + # Dropbox.encodeKey to obtain an encoded key string, and pass it as the + # key option + # @option options {String} secret the Dropbox application's secret (consumer + # secret, in OAuth vocabulary); browser-side applications should not use + # the secret option; instead, they should pass the result of + # Dropbox.encodeKey as the key option + constructor: (options) -> + @key = @k = null + @secret = @s = null + @token = null + @tokenSecret = null + @_appHash = null + @reset options + + # Creates an Oauth instance that manages an application's keys and token. + # + # @see Dropbox.Oauth#constructor for options + reset: (options) -> + if options.secret + @k = @key = options.key + @s = @secret = options.secret + @_appHash = null + else if options.key + @key = options.key + @secret = null + secret = atob dropboxEncodeKey(@key).split('|', 2)[1] + [k, s] = secret.split '?', 2 + @k = decodeURIComponent k + @s = decodeURIComponent s + @_appHash = null + else + unless @k + throw new Error('No API key supplied') + + if options.token + @setToken options.token, options.tokenSecret + else + @setToken null, '' + + # Sets the OAuth token to be used for future requests. + setToken: (token, tokenSecret) -> + if token and (not tokenSecret) + throw new Error('No secret supplied with the user token') + + @token = token + @tokenSecret = tokenSecret || '' + + # This is part of signing, but it's set here so it can be cached. + @hmacKey = Dropbox.Xhr.urlEncodeValue(@s) + '&' + + Dropbox.Xhr.urlEncodeValue(tokenSecret) + null + + # Computes the value of the Authorization HTTP header. + # + # This method mutates the params object, and removes all the OAuth-related + # parameters from it. + # + # @param {String} method the HTTP method used to make the request ('GET', + # 'POST', etc) + # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos") + # that receives the request + # @param {Object} params an associative array (hash) containing the HTTP + # request parameters; the parameters should include the oauth_ + # parameters generated by calling {Dropbox.Oauth#boilerplateParams} + # @return {String} the value to be used for the Authorization HTTP header + authHeader: (method, url, params) -> + @addAuthParams method, url, params + + # Collect all the OAuth parameters. + oauth_params = [] + for param, value of params + if param.substring(0, 6) == 'oauth_' + oauth_params.push param + oauth_params.sort() + + # Remove the parameters from the params hash and add them to the header. + header = [] + for param in oauth_params + header.push Dropbox.Xhr.urlEncodeValue(param) + '="' + + Dropbox.Xhr.urlEncodeValue(params[param]) + '"' + delete params[param] + + # NOTE: the space after the comma is optional in the OAuth spec, so we'll + # skip it to save some bandwidth + 'OAuth ' + header.join(',') + + # Generates OAuth-required HTTP parameters. + # + # This method mutates the params object, and adds the OAuth-related + # parameters to it. + # + # @param {String} method the HTTP method used to make the request ('GET', + # 'POST', etc) + # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos") + # that receives the request + # @param {Object} params an associative array (hash) containing the HTTP + # request parameters; the parameters should include the oauth_ + # parameters generated by calling {Dropbox.Oauth#boilerplateParams} + # @return {String} the value to be used for the Authorization HTTP header + addAuthParams: (method, url, params) -> + # Augment params with OAuth parameters. + @boilerplateParams params + params.oauth_signature = @signature method, url, params + params + + # Adds boilerplate OAuth parameters to a request's parameter list. + # + # This should be called right before signing a request, to maximize the + # chances that the OAuth timestamp will be fresh. + # + # @param {Object} params an associative array (hash) containing the + # parameters for an OAuth request; the boilerplate parameters will be + # added to this hash + # @return {Object} params + boilerplateParams: (params) -> + params.oauth_consumer_key = @k + params.oauth_nonce = @nonce() + params.oauth_signature_method = 'HMAC-SHA1' + params.oauth_token = @token if @token + params.oauth_timestamp = Math.floor(Date.now() / 1000) + params.oauth_version = '1.0' + params + + # Generates a nonce for an OAuth request. + # + # @return {String} the nonce to be used as the oauth_nonce parameter + nonce: -> + Date.now().toString(36) + Math.random().toString(36) + + # Computes the signature for an OAuth request. + # + # @param {String} method the HTTP method used to make the request ('GET', + # 'POST', etc) + # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos") + # that receives the request + # @param {Object} params an associative array (hash) containing the HTTP + # request parameters; the parameters should include the oauth_ + # parameters generated by calling {Dropbox.Oauth#boilerplateParams} + # @return {String} the signature, ready to be used as the oauth_signature + # OAuth parameter + signature: (method, url, params) -> + string = method.toUpperCase() + '&' + Dropbox.Xhr.urlEncodeValue(url) + + '&' + Dropbox.Xhr.urlEncodeValue(Dropbox.Xhr.urlEncode(params)) + base64HmacSha1 string, @hmacKey + + # @return {String} a string that uniquely identifies the OAuth application + appHash: -> + return @_appHash if @_appHash + @_appHash = base64Sha1(@k).replace(/\=/g, '') + + +# Polyfill for Internet Explorer 8. +unless Date.now? + Date.now = () -> + (new Date()).getTime() diff --git a/lib/client/storage/dropbox/src/prod.coffee b/lib/client/storage/dropbox/src/prod.coffee new file mode 100644 index 00000000..3cc177b9 --- /dev/null +++ b/lib/client/storage/dropbox/src/prod.coffee @@ -0,0 +1,36 @@ +# Necessary bits to get a browser-side app in production. + +# Packs up a key and secret into a string, to bring script kiddies some pain. +# +# @param {String} key the application's API key +# @param {String} secret the application's API secret +# @return {String} encoded key string that can be passed as the key option to +# the Dropbox.Client constructor +dropboxEncodeKey = (key, secret) -> + if secret + secret = [encodeURIComponent(key), encodeURIComponent(secret)].join('?') + key = for i in [0...(key.length / 2)] + ((key.charCodeAt(i * 2) & 15) * 16) + (key.charCodeAt(i * 2 + 1) & 15) + else + [key, secret] = key.split '|', 2 + key = atob key + key = (key.charCodeAt(i) for i in [0...key.length]) + secret = atob secret + + s = [0...256] + y = 0 + for x in [0...256] + y = (y + s[i] + key[x % key.length]) % 256 + [s[x], s[y]] = [s[y], s[x]] + + x = y = 0 + result = for z in [0...secret.length] + x = (x + 1) % 256 + y = (y + s[x]) % 256 + [s[x], s[y]] = [s[y], s[x]] + k = s[(s[x] + s[y]) % 256] + String.fromCharCode((k ^ secret.charCodeAt(z)) % 256) + + key = (String.fromCharCode(key[i]) for i in [0...key.length]) + [btoa(key.join('')), btoa(result.join(''))].join '|' + diff --git a/lib/client/storage/dropbox/src/pulled_changes.coffee b/lib/client/storage/dropbox/src/pulled_changes.coffee new file mode 100644 index 00000000..0a324d24 --- /dev/null +++ b/lib/client/storage/dropbox/src/pulled_changes.coffee @@ -0,0 +1,100 @@ +# Wraps the result of pullChanges, describing the changes in a user's Dropbox. +class Dropbox.PulledChanges + # Creates a new Dropbox.PulledChanges instance from a /delta API call result. + # + # @param {?Object} deltaInfo the parsed JSON of a /delta API call result + # @return {?Dropbox.PulledChanges} a Dropbox.PulledChanges instance wrapping + # the given information; if the parameter does not look like parsed JSON, + # it is returned as is + @parse: (deltaInfo) -> + if deltaInfo and typeof deltaInfo is 'object' + new Dropbox.PulledChanges deltaInfo + else + deltaInfo + + # @property {Boolean} if true, the application should reset its copy of the + # user's Dropbox before applying the changes described by this instance + blankSlate: undefined + + # @property {String} encodes a cursor in the list of changes to a user's + # Dropbox; a pullChanges call returns some changes at the cursor, and then + # advance the cursor to account for the returned changes; the new cursor is + # returned by pullChanges, and meant to be used by a subsequent pullChanges + # call + cursorTag: undefined + + # @property {Array an array with one entry for each + # change to the user's Dropbox returned by a pullChanges call. + changes: undefined + + # @property {Boolean} if true, the pullChanges call returned a subset of the + # available changes, and the application should repeat the call + # immediately to get more changes + shouldPullAgain: undefined + + # @property {Boolean} if true, the API call will not have any more changes + # available in the nearby future, so the application should wait for at + # least 5 miuntes before issuing another pullChanges request + shouldBackOff: undefined + + # Creates a new Dropbox.PulledChanges instance from a /delta API call result. + # + # @private + # This constructor is used by Dropbox.PulledChanges, and should not be called + # directly. + # + # @param {?Object} deltaInfo the parsed JSON of a /delta API call result + constructor: (deltaInfo) -> + @blankSlate = deltaInfo.reset or false + @cursorTag = deltaInfo.cursor + @shouldPullAgain = deltaInfo.has_more + @shouldBackOff = not @shouldPullAgain + if deltaInfo.cursor and deltaInfo.cursor.length + @changes = (Dropbox.PullChange.parse entry for entry in deltaInfo.entries) + else + @changes = [] + +# Wraps a single change in a pullChanges result. +class Dropbox.PullChange + # Creates a Dropbox.PullChange instance wrapping an entry in a /delta result. + # + # @param {?Object} entry the parsed JSON of a single entry in a /delta API + # call result + # @return {?Dropbox.PullChange} a Dropbox.PullChange instance wrapping the + # given entry of a /delta API call; if the parameter does not look like + # parsed JSON, it is returned as is + @parse: (entry) -> + if entry and typeof entry is 'object' + new Dropbox.PullChange entry + else + entry + + # @property {String} the path of the changed file or folder + path: undefined + + # @property {Boolean} if true, this change is a deletion of the file or folder + # at the change's path; if a folder is deleted, all its contents (files + # and sub-folders) were also be deleted; pullChanges might not return + # separate changes expressing for the files or sub-folders + wasRemoved: undefined + + # @property {?Dropbox.Stat} a Stat instance containing updated information for + # the file or folder; this is null if the change is a deletion + stat: undefined + + # Creates a Dropbox.PullChange instance wrapping an entry in a /delta result. + # + # @private + # This constructor is used by Dropbox.PullChange.parse, and should not be + # called directly. + # + # @param {?Object} entry the parsed JSON of a single entry in a /delta API + # call result + constructor: (entry) -> + @path = entry[0] + @stat = Dropbox.Stat.parse entry[1] + if @stat + @wasRemoved = false + else + @stat = null + @wasRemoved = true diff --git a/lib/client/storage/dropbox/src/references.coffee b/lib/client/storage/dropbox/src/references.coffee new file mode 100644 index 00000000..f5e62437 --- /dev/null +++ b/lib/client/storage/dropbox/src/references.coffee @@ -0,0 +1,86 @@ +# Wraps an URL to a Dropbox file or folder that can be publicly shared. +class Dropbox.PublicUrl + # Creates a PublicUrl instance from a raw API response. + # + # @param {?Object} urlData the parsed JSON describing a public URL + # @param {Boolean} isDirect true if this is a direct download link, false if + # is a file / folder preview link + # @return {?Dropbox.PublicUrl} a PublicUrl instance wrapping the given public + # link info; parameters that don't look like parsed JSON are returned as + # they are + @parse: (urlData, isDirect) -> + if urlData and typeof urlData is 'object' + new Dropbox.PublicUrl urlData, isDirect + else + urlData + + # @property {String} the public URL + url: undefined + + # @property {Date} after this time, the URL is not usable + expiresAt: undefined + + # @property {Boolean} true if this is a direct download URL, false for URLs to + # preview pages in the Dropbox web app; folders do not have direct link + # + isDirect: undefined + + # @property {Boolean} true if this is URL points to a file's preview page in + # Dropbox, false for direct links + isPreview: undefined + + # Creates a PublicUrl instance from a raw API response. + # + # @private + # This constructor is used by Dropbox.PublicUrl.parse, and should not be + # called directly. + # + # @param {?Object} urlData the parsed JSON describing a public URL + # @param {Boolean} isDirect true if this is a direct download link, false if + # is a file / folder preview link + constructor: (urlData, isDirect) -> + @url = urlData.url + @expiresAt = new Date Date.parse(urlData.expires) + + if isDirect is true + @isDirect = true + else if isDirect is false + @isDirect = false + else + @isDirect = Date.now() - @expiresAt <= 86400000 # 1 day + @isPreview = !@isDirect + +# Reference to a file that can be used to make a copy across users' Dropboxes. +class Dropbox.CopyReference + # Creates a CopyReference instance from a raw reference or API response. + # + # @param {?Object, ?String} refData the parsed JSON descring a copy + # reference, or the reference string + @parse: (refData) -> + if refData and (typeof refData is 'object' or typeof refData is 'string') + new Dropbox.CopyReference refData + else + refData + + # @property {String} the raw reference, for use with Dropbox APIs + tag: undefined + + # @property {Date} deadline for using the reference in a copy operation + expiresAt: undefined + + # Creates a CopyReference instance from a raw reference or API response. + # + # @private + # This constructor is used by Dropbox.CopyReference.parse, and should not be + # called directly. + # + # @param {?Object, ?String} refData the parsed JSON descring a copy + # reference, or the reference string + constructor: (refData) -> + if typeof refData is 'object' + @tag = refData.copy_ref + @expiresAt = new Date Date.parse(refData.expires) + else + @tag = refData + @expiresAt = new Date() + diff --git a/lib/client/storage/dropbox/src/stat.coffee b/lib/client/storage/dropbox/src/stat.coffee new file mode 100644 index 00000000..6fe45c44 --- /dev/null +++ b/lib/client/storage/dropbox/src/stat.coffee @@ -0,0 +1,127 @@ +# The result of stat-ing a file or directory in a user's Dropbox. +class Dropbox.Stat + # Creates a Stat instance from a raw "metadata" response. + # + # @param {?Object} metadata the result of parsing JSON API responses that are + # called "metadata" in the API documentation + # @return {?Dropbox.Stat} a Stat instance wrapping the given API response; + # parameters that aren't parsed JSON objects are returned as they are + @parse: (metadata) -> + if metadata and typeof metadata is 'object' + new Dropbox.Stat metadata + else + metadata + + # @property {String} the path of this file or folder, relative to the user's + # Dropbox or to the application's folder + path: null + + # @property {String} the name of this file or folder + name: null + + # @property {Boolean} if true, the file or folder's path is relative to the + # application's folder; otherwise, the path is relative to the user's + # Dropbox + inAppFolder: null + + # @property {Boolean} if true, this Stat instance describes a folder + isFolder: null + + # @property {Boolean} if true, this Stat instance describes a file + isFile: null + + # @property {Boolean} if true, the file or folder described by this Stat + # instance was from the user's Dropbox, and was obtained by an API call + # that returns deleted items + isRemoved: null + + # @property {String} name of an icon in Dropbox's icon library that most + # accurately represents this file or folder + # + # See the Dropbox API documentation to obtain the Dropbox icon library. + # https://www.dropbox.com/developers/reference/api#metadata + typeIcon: null + + # @property {String} an identifier for the contents of the described file or + # directories; this can used to be restored a file's contents to a + # previous version, or to save bandwidth by not retrieving the same + # folder contents twice + versionTag: null + + # @property {String} a guess of the MIME type representing the file or folder's + # contents + mimeType: null + + # @property {Number} the size of the file, in bytes; null for folders + size: null + + # @property {String} the size of the file, in a human-readable format, such as + # "225.4KB"; the format of this string is influenced by the API client's + # locale + humanSize: null + + # @property {Boolean} if false, the URL generated by thumbnailUrl does not + # point to a valid image, and should not be used + hasThumbnail: null + + # @property {Date} the file or folder's last modification time + modifiedAt: null + + # @property {?Date} the file or folder's last modification time, as reported + # by the Dropbox client that uploaded the file; this time should not be + # trusted, but can be used for UI (display, sorting); null if the server + # does not report any time + clientModifiedAt: null + + # Creates a Stat instance from a raw "metadata" response. + # + # @private + # This constructor is used by Dropbox.Stat.parse, and should not be called + # directly. + # + # @param {Object} metadata the result of parsing JSON API responses that are + # called "metadata" in the API documentation + constructor: (metadata) -> + @path = metadata.path + # Ensure there is a trailing /, to make path processing reliable. + @path = '/' + @path if @path.substring(0, 1) isnt '/' + # Strip any trailing /, to make path joining predictable. + lastIndex = @path.length - 1 + if lastIndex >= 0 and @path.substring(lastIndex) is '/' + @path = @path.substring 0, lastIndex + + nameSlash = @path.lastIndexOf '/' + @name = @path.substring nameSlash + 1 + + @isFolder = metadata.is_dir || false + @isFile = !@isFolder + @isRemoved = metadata.is_deleted || false + @typeIcon = metadata.icon + if metadata.modified?.length + @modifiedAt = new Date Date.parse(metadata.modified) + else + @modifiedAt = null + if metadata.client_mtime?.length + @clientModifiedAt = new Date Date.parse(metadata.client_mtime) + else + @clientModifiedAt = null + + switch metadata.root + when 'dropbox' + @inAppFolder = false + when 'app_folder' + @inAppFolder = true + else + # New "root" value that we're not aware of. + @inAppFolder = null + + @size = metadata.bytes or 0 + @humanSize = metadata.size or '' + @hasThumbnail = metadata.thumb_exists or false + + if @isFolder + @versionTag = metadata.hash + @mimeType = metadata.mime_type || 'inode/directory' + else + @versionTag = metadata.rev + @mimeType = metadata.mime_type || 'application/octet-stream' diff --git a/lib/client/storage/dropbox/src/user_info.coffee b/lib/client/storage/dropbox/src/user_info.coffee new file mode 100644 index 00000000..fa61f212 --- /dev/null +++ b/lib/client/storage/dropbox/src/user_info.coffee @@ -0,0 +1,79 @@ +# Information about a Dropbox user. +class Dropbox.UserInfo + # Creates a UserInfo instance from a raw API response. + # + # @param {?Object} userInfo the result of parsing a JSON API response that + # describes a user + # @return {Dropbox.UserInfo} a UserInfo instance wrapping the given API + # response; parameters that aren't parsed JSON objects are returned as + # the are + @parse: (userInfo) -> + if userInfo and typeof userInfo is 'object' + new Dropbox.UserInfo userInfo + else + userInfo + + # @property {String} the user's name, in a form that is fit for display + name: null + + # @property {?String} the user's email; this is not in the official API + # documentation, so it might not be supported + email: null + + # @property {?String} two-letter country code, or null if unavailable + countryCode: null + + # @property {String} unique ID for the user; this ID matches the unique ID + # returned by the authentication process + uid: null + + # @property {String} + referralUrl: null + + # Specific to applications whose access type is "public app folder". + # + # @property {String} prefix for URLs to the application's files + publicAppUrl: null + + # @property {Number} the maximum amount of bytes that the user can store + quota: null + + # @property {Number} the number of bytes taken up by the user's data + usedQuota: null + + # @property {Number} the number of bytes taken up by the user's data that is + # not shared with other users + privateBytes: null + + # @property {Number} the number of bytes taken up by the user's data that is + # shared with other users + sharedBytes: null + + # Creates a UserInfo instance from a raw API response. + # + # @private + # This constructor is used by Dropbox.UserInfo.parse, and should not be + # called directly. + # + # @param {?Object} userInfo the result of parsing a JSON API response that + # describes a user + constructor: (userInfo) -> + @name = userInfo.display_name + @email = userInfo.email + @countryCode = userInfo.country or null + @uid = userInfo.uid.toString() + if userInfo.public_app_url + @publicAppUrl = userInfo.public_app_url + lastIndex = @publicAppUrl.length - 1 + # Strip any trailing /, to make path joining predictable. + if lastIndex >= 0 and @publicAppUrl.substring(lastIndex) is '/' + @publicAppUrl = @publicAppUrl.substring 0, lastIndex + else + @publicAppUrl = null + + @referralUrl = userInfo.referral_link + @quota = userInfo.quota_info.quota + @privateBytes = userInfo.quota_info.normal or 0 + @sharedBytes = userInfo.quota_info.shared or 0 + @usedQuota = @privateBytes + @sharedBytes + diff --git a/lib/client/storage/dropbox/src/xhr.coffee b/lib/client/storage/dropbox/src/xhr.coffee new file mode 100644 index 00000000..ef630f67 --- /dev/null +++ b/lib/client/storage/dropbox/src/xhr.coffee @@ -0,0 +1,450 @@ +if window? + if window.XDomainRequest and not ('withCredentials' of new XMLHttpRequest()) + DropboxXhrRequest = window.XDomainRequest + DropboxXhrIeMode = true + # IE's XDR doesn't allow setting requests' Content-Type to anything other + # than text/plain, so it can't send _any_ forms. + DropboxXhrCanSendForms = false + else + DropboxXhrRequest = window.XMLHttpRequest + DropboxXhrIeMode = false + # Firefox doesn't support adding named files to FormData. + # https://bugzilla.mozilla.org/show_bug.cgi?id=690659 + DropboxXhrCanSendForms = + window.navigator.userAgent.indexOf('Firefox') is -1 + DropboxXhrDoesPreflight = true +else + # Node.js needs an adapter for the XHR API. + DropboxXhrRequest = require('xmlhttprequest').XMLHttpRequest + DropboxXhrIeMode = false + # Node.js doesn't have FormData. We wouldn't want to bother putting together + # upload forms in node.js anyway, because it doesn't do CORS preflight + # checks, so we can use PUT requests without a performance hit. + DropboxXhrCanSendForms = false + # Node.js is a server so it doesn't do annoying browser checks. + DropboxXhrDoesPreflight = false + +# ArrayBufferView isn't available in the global namespce. +# +# Using the hack suggested in +# https://code.google.com/p/chromium/issues/detail?id=60449 +if typeof Uint8Array is 'undefined' + DropboxXhrArrayBufferView = null +else + DropboxXhrArrayBufferView = + (new Uint8Array(0)).__proto__.__proto__.constructor + +# Dispatches low-level AJAX calls (XMLHttpRequests). +class Dropbox.Xhr + # The object used to perform AJAX requests (XMLHttpRequest). + @Request = DropboxXhrRequest + # Set to true when using the XDomainRequest API. + @ieMode = DropboxXhrIeMode + # Set to true if the platform has proper support for FormData. + @canSendForms = DropboxXhrCanSendForms + # Set to true if the platform performs CORS preflight checks. + @doesPreflight = DropboxXhrDoesPreflight + @ArrayBufferView = DropboxXhrArrayBufferView + + # Sets up an AJAX request. + # + # @param {String} method the HTTP method used to make the request ('GET', + # 'POST', 'PUT', etc.) + # @param {String} baseUrl the URL that receives the request; this URL might + # be modified, e.g. by appending parameters for GET requests + constructor: (@method, baseUrl) -> + @isGet = @method is 'GET' + @url = baseUrl + @headers = {} + @params = null + @body = null + @preflight = not (@isGet or (@method is 'POST')) + @signed = false + @responseType = null + @callback = null + @xhr = null + + # Sets the parameters (form field values) that will be sent with the request. + # + # @param {?Object} params an associative array (hash) containing the HTTP + # request parameters + # @return {Dropbox.Xhr} this, for easy call chaining + setParams: (params) -> + if @signed + throw new Error 'setParams called after addOauthParams or addOauthHeader' + if @params + throw new Error 'setParams cannot be called twice' + @params = params + @ + + # Sets the function called when the XHR completes. + # + # This function can also be set when calling Dropbox.Xhr#send. + # + # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when + # the XHR completes; if an error occurs, the first parameter will be a + # Dropbox.ApiError instance; otherwise, the second parameter will be an + # instance of the required response type (e.g., String, Blob), and the + # third parameter will be the JSON-parsed 'x-dropbox-metadata' header + # @return {Dropbox.Xhr} this, for easy call chaining + setCallback: (@callback) -> + @ + + # Ammends the request parameters to include an OAuth signature. + # + # The OAuth signature will become invalid if the parameters are changed after + # the signing process. + # + # This method automatically decides the best way to add the OAuth signature + # to the current request. Modifying the request in any way (e.g., by adding + # headers) might result in a valid signature that is applied in a sub-optimal + # fashion. For best results, call this right before Dropbox.Xhr#prepare. + # + signWithOauth: (oauth) -> + if Dropbox.Xhr.ieMode or (Dropbox.Xhr.doesPreflight and (not @preflight)) + @addOauthParams oauth + else + @addOauthHeader oauth + + # Ammends the request parameters to include an OAuth signature. + # + # The OAuth signature will become invalid if the parameters are changed after + # the signing process. + # + # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be + # used to sign the request + # @return {Dropbox.Xhr} this, for easy call chaining + addOauthParams: (oauth) -> + if @signed + throw new Error 'Request already has an OAuth signature' + + @params or= {} + oauth.addAuthParams @method, @url, @params + @signed = true + @ + + # Adds an Authorize header containing an OAuth signature. + # + # The OAuth signature will become invalid if the parameters are changed after + # the signing process. + # + # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be + # used to sign the request + # @return {Dropbox.Xhr} this, for easy call chaining + addOauthHeader: (oauth) -> + if @signed + throw new Error 'Request already has an OAuth signature' + + @params or= {} + @signed = true + @setHeader 'Authorization', oauth.authHeader(@method, @url, @params) + + # Sets the body (piece of data) that will be sent with the request. + # + # @param {String, Blob, ArrayBuffer} body the body to be sent in a request; + # GET requests cannot have a body + # @return {Dropbox.Xhr} this, for easy call chaining + setBody: (body) -> + if @isGet + throw new Error 'setBody cannot be called on GET requests' + if @body isnt null + throw new Error 'Request already has a body' + + unless @preflight + unless (typeof FormData isnt 'undefined') and (body instanceof FormData) + @preflight = true + + @body = body + @ + + # Sends off an AJAX request and requests a custom response type. + # + # This method requires XHR Level 2 support, which is not available in IE + # versions <= 9. If these browsers must be supported, it is recommended to + # check whether window.Blob is truthy. + # + # @param {String} responseType the value that will be assigned to the XHR's + # responseType property + # @return {Dropbox.Xhr} this, for easy call chaining + setResponseType: (@responseType) -> + @ + + # Sets the value of a custom HTTP header. + # + # Custom HTTP headers require a CORS preflight in browsers, so requests that + # use them will take more time to complete, especially on high-latency mobile + # connections. + # + # @param {String} headerName the name of the HTTP header + # @param {String} value the value that the header will be set to + # @return {Dropbox.Xhr} this, for easy call chaining + setHeader: (headerName, value) -> + if @headers[headerName] + oldValue = @headers[headerName] + throw new Error "HTTP header #{headerName} already set to #{oldValue}" + if headerName is 'Content-Type' + throw new Error 'Content-Type is automatically computed based on setBody' + @preflight = true + @headers[headerName] = value + @ + + # Simulates having an being sent with the request. + # + # @param {String} fieldName the name of the form field / parameter (not of + # the uploaded file) + # @param {String} fileName the name of the uploaded file (not the name of the + # form field / parameter) + # @param {String, Blob, File} fileData contents of the file to be uploaded + # @param {?String} contentType the MIME type of the file to be uploaded; if + # fileData is a Blob or File, its MIME type is used instead + setFileField: (fieldName, fileName, fileData, contentType) -> + if @body isnt null + throw new Error 'Request already has a body' + + if @isGet + throw new Error 'paramsToBody cannot be called on GET requests' + + if typeof(fileData) is 'object' and typeof Blob isnt 'undefined' + if ArrayBuffer? and fileData instanceof ArrayBuffer + fileData = new Uint8Array fileData + if Dropbox.Xhr.ArrayBufferView and + fileData instanceof Dropbox.Xhr.ArrayBufferView + contentType or= 'application/octet-stream' + fileData = new Blob [fileData], type: contentType + # Workaround for http://crbug.com/165095 + if typeof File isnt 'undefined' and fileData instanceof File + fileData = new Blob [fileData], type: fileData.type + #fileData = fileData + useFormData = fileData instanceof Blob + else + useFormData = false + + if useFormData + @body = new FormData() + @body.append fieldName, fileData, fileName + else + contentType or= 'application/octet-stream' + boundary = @multipartBoundary() + @headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}" + @body = ['--', boundary, "\r\n", + 'Content-Disposition: form-data; name="', fieldName, + '"; filename="', fileName, "\"\r\n", + 'Content-Type: ', contentType, "\r\n", + "Content-Transfer-Encoding: binary\r\n\r\n", + fileData, + "\r\n", '--', boundary, '--', "\r\n"].join '' + + # @private + # @return {String} a nonce suitable for use as a part boundary in a multipart + # MIME message + multipartBoundary: -> + [Date.now().toString(36), Math.random().toString(36)].join '----' + + # Moves this request's parameters to its URL. + # + # @private + # @return {Dropbox.Xhr} this, for easy call chaining + paramsToUrl: -> + if @params + queryString = Dropbox.Xhr.urlEncode @params + if queryString.length isnt 0 + @url = [@url, '?', queryString].join '' + @params = null + @ + + # Moves this request's parameters to its body. + # + # @private + # @return {Dropbox.Xhr} this, for easy call chaining + paramsToBody: -> + if @params + if @body isnt null + throw new Error 'Request already has a body' + if @isGet + throw new Error 'paramsToBody cannot be called on GET requests' + @headers['Content-Type'] = 'application/x-www-form-urlencoded' + @body = Dropbox.Xhr.urlEncode @params + @params = null + @ + + # Sets up an XHR request. + # + # This method completely sets up a native XHR object and stops short of + # calling its send() method, so the API client has a chance of customizing + # the XHR. After customizing the XHR, Dropbox.Xhr#send should be called. + # + # + # @return {Dropbox.Xhr} this, for easy call chaining + prepare: -> + ieMode = Dropbox.Xhr.ieMode + if @isGet or @body isnt null or ieMode + @paramsToUrl() + if @body isnt null and typeof @body is 'string' + @headers['Content-Type'] = 'text/plain; charset=utf8' + else + @paramsToBody() + + @xhr = new Dropbox.Xhr.Request() + if ieMode + @xhr.onload = => @onLoad() + @xhr.onerror = => @onError() + else + @xhr.onreadystatechange = => @onReadyStateChange() + @xhr.open @method, @url, true + + unless ieMode + for own header, value of @headers + @xhr.setRequestHeader header, value + + if @responseType + if @responseType is 'b' + if @xhr.overrideMimeType + @xhr.overrideMimeType 'text/plain; charset=x-user-defined' + else + @xhr.responseType = @responseType + + @ + + # Fires off the prepared XHR request. + # + # Dropbox.Xhr#prepare should be called exactly once before this method. + # + # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when + # the XHR completes; if an error occurs, the first parameter will be a + # Dropbox.ApiError instance; otherwise, the second parameter will be an + # instance of the required response type (e.g., String, Blob), and the + # third parameter will be the JSON-parsed 'x-dropbox-metadata' header + # @return {Dropbox.Xhr} this, for easy call chaining + send: (callback) -> + @callback = callback or @callback + + if @body isnt null + body = @body + # send() in XHR doesn't like naked ArrayBuffers + if Dropbox.Xhr.ArrayBufferView and body instanceof ArrayBuffer + body = new Uint8Array body + + try + @xhr.send body + catch e + # Firefox doesn't support sending ArrayBufferViews. + # Node.js doesn't implement Blob. + if typeof Blob isnt 'undefined' and Dropbox.Xhr.ArrayBufferView and + body instanceof Dropbox.Xhr.ArrayBufferView + body = new Blob [body], type: 'application/octet-stream' + @xhr.send body + else + throw e + else + @xhr.send() + @ + + # Encodes an associative array (hash) into a x-www-form-urlencoded String. + # + # For consistency, the keys are sorted in alphabetical order in the encoded + # output. + # + # @param {Object} object the JavaScript object whose keys will be encoded + # @return {String} the object's keys and values, encoded using + # x-www-form-urlencoded + @urlEncode: (object) -> + chunks = [] + for key, value of object + chunks.push @urlEncodeValue(key) + '=' + @urlEncodeValue(value) + chunks.sort().join '&' + + # Encodes an object into a x-www-form-urlencoded key or value. + # + # @param {Object} object the object to be encoded; the encoding calls + # toString() on the object to obtain its string representation + # @return {String} encoded string, suitable for use as a key or value in an + # x-www-form-urlencoded string + @urlEncodeValue: (object) -> + encodeURIComponent(object.toString()).replace(/\!/g, '%21'). + replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'). + replace(/\*/g, '%2A') + + # Decodes an x-www-form-urlencoded String into an associative array (hash). + # + # @param {String} string the x-www-form-urlencoded String to be decoded + # @return {Object} an associative array whose keys and values are all strings + @urlDecode: (string) -> + result = {} + for token in string.split '&' + kvp = token.split '=' + result[decodeURIComponent(kvp[0])] = decodeURIComponent kvp[1] + result + + # Handles the XHR readystate event. + onReadyStateChange: -> + return true if @xhr.readyState isnt 4 # XMLHttpRequest.DONE is 4 + + if @xhr.status < 200 or @xhr.status >= 300 + apiError = new Dropbox.ApiError @xhr, @method, @url + @callback apiError + return true + + metadataJson = @xhr.getResponseHeader 'x-dropbox-metadata' + if metadataJson?.length + try + metadata = JSON.parse metadataJson + catch e + # Make sure the app doesn't crash if the server goes crazy. + metadata = undefined + else + metadata = undefined + + if @responseType + if @responseType is 'b' + dirtyText = if @xhr.responseText? + @xhr.responseText + else + @xhr.response + ### + jsString = ['["'] + for i in [0...dirtyText.length] + hexByte = (dirtyText.charCodeAt(i) & 0xFF).toString(16) + if hexByte.length is 2 + jsString.push "\\u00#{hexByte}" + else + jsString.push "\\u000#{hexByte}" + jsString.push '"]' + console.log jsString + text = JSON.parse(jsString.join(''))[0] + ### + bytes = [] + for i in [0...dirtyText.length] + bytes.push String.fromCharCode(dirtyText.charCodeAt(i) & 0xFF) + text = bytes.join '' + @callback null, text, metadata + else + @callback null, @xhr.response, metadata + return true + + text = if @xhr.responseText? then @xhr.responseText else @xhr.response + switch @xhr.getResponseHeader('Content-Type') + when 'application/x-www-form-urlencoded' + @callback null, Dropbox.Xhr.urlDecode(text), metadata + when 'application/json', 'text/javascript' + @callback null, JSON.parse(text), metadata + else + @callback null, text, metadata + true + + # Handles the XDomainRequest onload event. (IE 8, 9) + onLoad: -> + text = @xhr.responseText + switch @xhr.contentType + when 'application/x-www-form-urlencoded' + @callback null, Dropbox.Xhr.urlDecode(text), undefined + when 'application/json', 'text/javascript' + @callback null, JSON.parse(text), undefined + else + @callback null, text, undefined + true + + # Handles the XDomainRequest onload event. (IE 8, 9) + onError: -> + apiError = new Dropbox.ApiError @xhr, @method, @url + @callback apiError + return true diff --git a/lib/client/storage/dropbox/src/zzz-export.coffee b/lib/client/storage/dropbox/src/zzz-export.coffee new file mode 100644 index 00000000..45e47036 --- /dev/null +++ b/lib/client/storage/dropbox/src/zzz-export.coffee @@ -0,0 +1,21 @@ +# All changes to the global namespace happen here. + +# This file's name is set up in such a way that it will always show up last in +# the source directory. This makes coffee --join work as intended. + +if module?.exports? + # We're a node.js module, so export the Dropbox class. + module.exports = Dropbox +else if window? + # We're in a browser, so add Dropbox to the global namespace. + window.Dropbox = Dropbox +else + throw new Error('This library only supports node.js and modern browsers.') + +# These are mostly useful for testing. Clients shouldn't use internal stuff. +Dropbox.atob = atob +Dropbox.btoa = btoa +Dropbox.hmac = base64HmacSha1 +Dropbox.sha1 = base64Sha1 +Dropbox.encodeKey = dropboxEncodeKey + diff --git a/lib/client/storage/dropbox/txt.vimrc.txt b/lib/client/storage/dropbox/txt.vimrc.txt new file mode 100644 index 00000000..b6779843 --- /dev/null +++ b/lib/client/storage/dropbox/txt.vimrc.txt @@ -0,0 +1,6 @@ +" Indentation settings for the project: 2-space indentation, no tabs. +set tabstop=2 +set softtabstop=2 +set shiftwidth=2 +set expandtab +