updated dropbox library to v.0.8.1.

This commit is contained in:
coderaiser 2013-01-14 04:37:19 -05:00
parent 3e3e6dc97c
commit 72b2417195
92 changed files with 7115 additions and 753 deletions

View file

@ -73,6 +73,8 @@ keyStop: function(e, opt) {
* Fixed bug with setting curent cursor when
clicked on menu item.
* Updated dropbox library to v.0.8.1.
2012.12.12, Version 0.1.8

View file

@ -17,8 +17,7 @@ var CloudCommander, Util, DOM, Dropbox, cb, Client;
function load(pCallBack){
console.time('dropbox load');
var lSrc = '//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.7.1/dropbox.min.js',
//var lSrc = CloudCmd.LIBDIRCLIENT + 'storage/dropbox/lib/dropbox.js',
var lSrc = 'http://cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.8.1/dropbox.min.js',
lLocal = CloudCmd.LIBDIRCLIENT + 'storage/dropbox/lib/dropbox.min.js',
lOnload = function(){
console.timeEnd('dropbox load');

37
lib/client/storage/dropbox/.gitignore vendored Normal file
View file

@ -0,0 +1,37 @@
# Vim.
*.swp
# OSX
.DS_Store
# Npm modules.
node_modules
# Vendored javascript modules.
test/vendor
# Build output.
lib/dropbox.js
lib/dropbox.min.js
test/chrome_app/lib
test/chrome_app/manifest.json
test/chrome_app/node_modules
test/chrome_app/test
test/chrome_extension/*.js
test/chrome_profile
test/js
tmp/*.js
# Documentation output.
doc/*.html
doc/assets
doc/classes
doc/files
# Node packaging output.
dropbox-*.tgz
# Potentially sensitive credentials and keys used during testing.
test/.token
test/ssl

View file

@ -0,0 +1,45 @@
# Vim.
*.swp
.vimrc
# OSX
.DS_Store
# Git.
.git
# Npm modules.
node_modules
# Vendored javascript modules.
test/vendor
# Minified library.
lib/dropbox.min.js
# Sample apps.
samples
# Test build output.
test/chrome_profile
test/js
tmp/*.js
# Test code that is not related to node.js.
test/chrome_app
# Documentation output.
doc/*.html
doc/assets
doc/classes
doc/files
# Test app logos.
test/app_icon
# Test automating Chrome extension.
test/chrome_extension
# Potentially sensitive credentials and keys used during testing.
test/.token
test/ssl

View file

@ -1,131 +1,215 @@
async = require 'async'
{spawn, exec} = require 'child_process'
fs = require 'fs'
log = console.log
remove = require 'remove'
# Node 0.6 compatibility hack.
unless fs.existsSync
path = require 'path'
fs.existsSync = (filePath) -> path.existsSync filePath
task 'build', ->
build()
task 'test', ->
vendor ->
build ->
ssl_cert ->
tokens ->
run 'node_modules/mocha/bin/mocha --colors --slow 200 ' +
'--timeout 10000 --require test/js/helper.js test/js/*test.js'
task 'webtest', ->
vendor ->
build ->
ssl_cert ->
tokens ->
webFileServer = require './test/js/web_file_server.js'
webFileServer.openBrowser()
task 'cert', ->
remove.removeSync 'test/ssl', ignoreMissing: true
ssl_cert()
task 'vendor', ->
remove.removeSync './test/vendor', ignoreMissing: true
vendor()
task 'tokens', ->
remove.removeSync './test/.token', ignoreMissing: true
build ->
tokens ->
process.exit 0
task 'doc', ->
run 'node_modules/codo/bin/codo src'
task 'extension', ->
run 'node_modules/coffee-script/bin/coffee ' +
'--compile test/chrome_extension/*.coffee'
build = (callback) ->
commands = []
# Compile without --join for decent error messages.
commands.push 'node_modules/coffee-script/bin/coffee --output tmp ' +
'--compile src/*.coffee'
commands.push 'node_modules/coffee-script/bin/coffee --output lib ' +
'--compile --join dropbox.js src/*.coffee'
# Minify the javascript, for browser distribution.
commands.push 'node_modules/uglify-js/bin/uglifyjs --compress --mangle ' +
'--output lib/dropbox.min.js lib/dropbox.js'
commands.push 'node_modules/coffee-script/bin/coffee --output test/js ' +
'--compile test/src/*.coffee'
async.forEachSeries commands, run, ->
callback() if callback
ssl_cert = (callback) ->
fs.mkdirSync 'test/ssl' unless fs.existsSync 'test/ssl'
if fs.existsSync 'test/ssl/cert.pem'
callback() if callback?
return
run 'openssl req -new -x509 -days 365 -nodes -batch ' +
'-out test/ssl/cert.pem -keyout test/ssl/cert.pem ' +
'-subj /O=dropbox.js/OU=Testing/CN=localhost ', callback
vendor = (callback) ->
# All the files will be dumped here.
fs.mkdirSync 'test/vendor' unless fs.existsSync 'test/vendor'
# Embed the binary test image into a 7-bit ASCII JavaScript.
bytes = fs.readFileSync 'test/binary/dropbox.png'
fragments = []
for i in [0...bytes.length]
fragment = bytes.readUInt8(i).toString 16
while fragment.length < 4
fragment = '0' + fragment
fragments.push "\\u#{fragment}"
js = "window.testImageBytes = \"#{fragments.join('')}\";"
fs.writeFileSync 'test/vendor/favicon.js', js
downloads = [
# chai.js ships different builds for browsers vs node.js
['http://chaijs.com/chai.js', 'test/vendor/chai.js'],
# sinon.js also ships special builds for browsers
['http://sinonjs.org/releases/sinon.js', 'test/vendor/sinon.js'],
# ... and sinon.js ships an IE-only module
['http://sinonjs.org/releases/sinon-ie.js', 'test/vendor/sinon-ie.js']
]
async.forEachSeries downloads, download, ->
callback() if callback
tokens = (callback) ->
TokenStash = require './test/js/token_stash.js'
tokenStash = new TokenStash
(new TokenStash()).get ->
callback() if callback?
run = (args...) ->
for a in args
switch typeof a
when 'string' then command = a
when 'object'
if a instanceof Array then params = a
else options = a
when 'function' then callback = a
command += ' ' + params.join ' ' if params?
cmd = spawn '/bin/sh', ['-c', command], options
cmd.stdout.on 'data', (data) -> process.stdout.write data
cmd.stderr.on 'data', (data) -> process.stderr.write data
process.on 'SIGHUP', -> cmd.kill()
cmd.on 'exit', (code) -> callback() if callback? and code is 0
download = ([url, file], callback) ->
if fs.existsSync file
callback() if callback?
return
run "curl -o #{file} #{url}", callback
async = require 'async'
{spawn, exec} = require 'child_process'
fs = require 'fs'
glob = require 'glob'
log = console.log
path = require 'path'
remove = require 'remove'
# Node 0.6 compatibility hack.
unless fs.existsSync
fs.existsSync = (filePath) -> path.existsSync filePath
task 'build', ->
build()
task 'test', ->
vendor ->
build ->
ssl_cert ->
tokens ->
run 'node_modules/mocha/bin/mocha --colors --slow 200 ' +
'--timeout 10000 --require test/js/helper.js test/js/*test.js'
task 'webtest', ->
vendor ->
build ->
ssl_cert ->
tokens ->
webtest()
task 'cert', ->
remove.removeSync 'test/ssl', ignoreMissing: true
ssl_cert()
task 'vendor', ->
remove.removeSync './test/vendor', ignoreMissing: true
vendor()
task 'tokens', ->
remove.removeSync './test/.token', ignoreMissing: true
build ->
tokens ->
process.exit 0
task 'doc', ->
run 'node_modules/codo/bin/codo src'
task 'extension', ->
run 'node_modules/coffee-script/bin/coffee ' +
'--compile test/chrome_extension/*.coffee'
task 'chrome', ->
vendor ->
build ->
# The v2 Chrome App API isn't supported yet.
buildChromeApp 'app_v1'
task 'chrometest', ->
vendor ->
build ->
# The v2 Chrome App API isn't supported yet.
buildChromeApp 'app_v1', ->
testChromeApp()
build = (callback) ->
commands = []
# Ignoring ".coffee" when sorting.
# We want "driver.coffee" to sort before "driver-browser.coffee"
source_files = glob.sync 'src/*.coffee'
source_files.sort (a, b) ->
a.replace(/\.coffee$/, '').localeCompare b.replace(/\.coffee$/, '')
# Compile without --join for decent error messages.
commands.push 'node_modules/coffee-script/bin/coffee --output tmp ' +
"--compile #{source_files.join(' ')}"
commands.push 'node_modules/coffee-script/bin/coffee --output lib ' +
"--compile --join dropbox.js #{source_files.join(' ')}"
# Minify the javascript, for browser distribution.
commands.push 'node_modules/uglify-js/bin/uglifyjs --compress --mangle ' +
'--output lib/dropbox.min.js lib/dropbox.js'
commands.push 'node_modules/coffee-script/bin/coffee --output test/js ' +
'--compile test/src/*.coffee'
async.forEachSeries commands, run, ->
callback() if callback
webtest = (callback) ->
webFileServer = require './test/js/web_file_server.js'
if 'BROWSER' of process.env
if process.env['BROWSER'] is 'false'
url = webFileServer.testUrl()
console.log "Please open the URL below in your browser:\n #{url}"
else
webFileServer.openBrowser process.env['BROWSER']
else
webFileServer.openBrowser()
callback() if callback?
ssl_cert = (callback) ->
fs.mkdirSync 'test/ssl' unless fs.existsSync 'test/ssl'
if fs.existsSync 'test/ssl/cert.pem'
callback() if callback?
return
run 'openssl req -new -x509 -days 365 -nodes -batch ' +
'-out test/ssl/cert.pem -keyout test/ssl/cert.pem ' +
'-subj /O=dropbox.js/OU=Testing/CN=localhost ', callback
vendor = (callback) ->
# All the files will be dumped here.
fs.mkdirSync 'test/vendor' unless fs.existsSync 'test/vendor'
# Embed the binary test image into a 7-bit ASCII JavaScript.
bytes = fs.readFileSync 'test/binary/dropbox.png'
fragments = []
for i in [0...bytes.length]
fragment = bytes.readUInt8(i).toString 16
while fragment.length < 4
fragment = '0' + fragment
fragments.push "\\u#{fragment}"
js = "window.testImageBytes = \"#{fragments.join('')}\";"
fs.writeFileSync 'test/vendor/favicon.js', js
downloads = [
# chai.js ships different builds for browsers vs node.js
['http://chaijs.com/chai.js', 'test/vendor/chai.js'],
# sinon.js also ships special builds for browsers
['http://sinonjs.org/releases/sinon.js', 'test/vendor/sinon.js'],
# ... and sinon.js ships an IE-only module
['http://sinonjs.org/releases/sinon-ie.js', 'test/vendor/sinon-ie.js']
]
async.forEachSeries downloads, download, ->
callback() if callback
testChromeApp = (callback) ->
# Clean up the profile.
fs.mkdirSync 'test/chrome_profile' unless fs.existsSync 'test/chrome_profile'
command = "\"#{chromeCommand()}\" --load-extension=test/chrome_app " +
'--user-data-dir=test/chrome_profile --no-default-browser-check ' +
'--no-first-run --no-service-autorun --disable-default-apps ' +
'--homepage=about:blank --v=-1'
run command, ->
callback() if callback
buildChromeApp = (manifestFile, callback) ->
unless fs.existsSync 'test/chrome_app/test'
fs.mkdirSync 'test/chrome_app/test'
unless fs.existsSync 'test/chrome_app/node_modules'
fs.mkdirSync 'test/chrome_app/node_modules'
links = [
['lib', 'test/chrome_app/lib'],
['node_modules/mocha', 'test/chrome_app/node_modules/mocha'],
['node_modules/sinon-chai', 'test/chrome_app/node_modules/sinon-chai'],
['test/.token', 'test/chrome_app/test/.token'],
['test/binary', 'test/chrome_app/test/binary'],
['test/html', 'test/chrome_app/test/html'],
['test/js', 'test/chrome_app/test/js'],
['test/vendor', 'test/chrome_app/test/vendor'],
]
commands = [
"cp test/chrome_app/manifests/#{manifestFile}.json " +
'test/chrome_app/manifest.json'
]
for link in links
# fs.symlinkSync(path.resolve(link[0]), link[1]) unless fs.existsSync link[1]
commands.push "cp -r #{link[0]} #{path.dirname(link[1])}"
async.forEachSeries commands, run, ->
callback() if callback
chromeCommand = ->
paths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/MacOS/Contents/Chromium',
]
for path in paths
return path if fs.existsSync path
if 'process.platform' is 'win32'
'chrome'
else
'google-chrome'
tokens = (callback) ->
TokenStash = require './test/js/token_stash.js'
tokenStash = new TokenStash
(new TokenStash()).get ->
callback() if callback?
run = (args...) ->
for a in args
switch typeof a
when 'string' then command = a
when 'object'
if a instanceof Array then params = a
else options = a
when 'function' then callback = a
command += ' ' + params.join ' ' if params?
cmd = spawn '/bin/sh', ['-c', command], options
cmd.stdout.on 'data', (data) -> process.stdout.write data
cmd.stderr.on 'data', (data) -> process.stderr.write data
process.on 'SIGHUP', -> cmd.kill()
cmd.on 'exit', (code) -> callback() if callback? and code is 0
download = ([url, file], callback) ->
if fs.existsSync file
callback() if callback?
return
run "curl -o #{file} #{url}", callback

View file

@ -100,7 +100,8 @@ 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
The driver's constructor takes a `receiverPath` option t
[checkbox.js](https://github.com/dropbox/dropbox-js/tree/master/samples/checkbox.js)
sample application uses `rememberUser`, and implements signing off as described
above.
@ -123,10 +124,12 @@ 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.
change the code to reflect the location of `dropbox.js` on your site, and point
the `Dropbox.Drivers.Popup` constructor to it.
```javascript
client.authDriver(new Dropbox.Drivers.Popup({receiverUrl: "https://url.to/receiver.html"}));
client.authDriver(new Dropbox.Drivers.Popup({
receiverUrl: "https://url.to/oauth_receiver.html"}));
```
The popup driver adds a `#` (fragment hash) to the receiver URL if necessary,
@ -138,9 +141,42 @@ 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}));
client.authDriver(new Dropbox.Drivers.Popup({
receiverUrl: "https://url.to/receiver.html", noFragment: true}));
```
The popup driver implements the `rememberUser` option with the same semantics
and caveats as the redirecting driver.
### Dropbox.Drivers.Chrome
Google Chrome [extensions](http://developer.chrome.com/extensions/) and
[applications](developer.chrome.com/apps/) are supported by a driver that opens
a new browser tab (in the case of extensions and legacy applications) or
an application window (for new applications) to complete the OAuth
authorization.
To use this driver, first add the following files to your extension.
* the [receiver script](https://github.com/dropbox/dropbox-js/blob/master/test/src/chrome_oauth_receiver.coffee);
the file is both valid JavaScript and valid CoffeeScript
* the [receiver page](https://github.com/dropbox/dropbox-js/blob/master/test/html/chrome_oauth_receiver.html);
change the page to reflect the paths to `dropbox.js` and to the receiver script
file
Point the driver constructor to the receiver page:
```javascript
client.authDriver(new Dropbox.Drivers.Chrome({
receiverPath: "path/to/chrome_oauth_receiver.html"}));
```
This driver caches the user's credentials so that users don't have to authorize
applications / extensions on every browser launch. Applications and extensions'
UI should include a method for the user to sign out of Dropbox, which can be
implemented by calling the `signOut` instance method of `Dropbox.Client`.
### Dropbox.Drivers.NodeServer

View file

@ -42,6 +42,13 @@ version, optimized for browser apps.
## Test
Install the CoffeeScript npm package globally, so you can type `cake` instead
of `node_modules/coffee-script/bin/cake`.
```bash
npm install -g coffee-script
```
First, you will need to obtain a couple of Dropbox tokens that will be used by
the automated tests.
@ -51,12 +58,13 @@ 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.
Once you have Dropbox tokens, you can run the test suite in node.js, in your
default browser, or as a Chrome application.
```bash
cake test
cake webtest
cake chrometest
```
The library is automatically re-built when running tests, so you don't need to
@ -68,11 +76,46 @@ The tests store all their data in folders named along the lines of
folders yourself.
## Testing Chrome Extension
### Solving Browser Issues
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.
An easy method to test a browser in a virtual machine is to skip the automated
browser opening.
```bash
BROWSER=false cake webtest
```
A similar method can be used to launch a specific browser.
```bash
BROWSER=firefox cake webtest
```
When fighting a bug, it can be useful to keep the server process running after
the test suite completes, so tests can be re-started with a browser refresh.
```bash
BROWSER=false NO_EXIT=1 cake webtest
```
[Mocha's exclusive tests](http://visionmedia.github.com/mocha/#exclusive-tests)
(`it.only` and `describe.only`) are very useful for quickly iterating while
figuring out a bug.
### Chrome Application / Extension Testing
The tests for Chrome apps / extensions require manual intervention right now.
The `cake chrometest` command will open a Google Chrome instance. The
`dropbox.js Test Suite` application must be clicked.
### Fully Automated Tests
The test suite opens up the Dropbox authorization page a few times, and also
pops up a page that cannot close itself. dropbox.js ships with a Google Chrome
extension that can fully automate the testing process on Chrome / Chromium.
The extension is written in CoffeeScript, so you will have to compile it.
@ -84,6 +127,69 @@ 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.
The extension performs some checks to prevent against attacks. However, you
should still disable the automation (by clicking on the extension icon) when
you're not testing dropbox.js, just in case the extension code has bugs.
## Release Process
1. At the very least, test in node.js and in a browser before releasing.
```bash
cake test
cake webtest
```
1. Bump the version in `package.json`.
1. Publish a new npm package.
```bash
npm publish
```
1. Commit and tag the version bump on GitHub.
```bash
git add package.json
git commit -m "Release X.Y.Z."
git tag -a -m "Release X.Y.Z" vX.Y.Z
git push
git push --tags
```
1. If you haven't already, go to the
[cdnjs GitHub page](https://github.com/cdnjs/cdnjs) and fork it.
1. If you haven't already, set up cdnjs on your machine.
```bash
cd ..
git clone git@github.com:you/cdnjs.git
cd cdnjs
git remote add up git://github.com/cdnjs/cdnjs.git
cd ../dropbox-js
```
1. Add the new release to your cdnjs fork.
```bash
cd ../cdnjs
git checkout master
git pull up master
npm install
git checkout -b dbXYZ
mkdir ajax/libs/dropbox.js/X.Y.Z
cp ../dropbox-js/lib/dropbox.min.js ajax/libs/dropbox.js/X.Y.Z/
vim ajax/libs/dropbox.js/package.json # Replace "version"'s value with "X.Y.Z"
npm test
git add -A
git commit -m "Added dropbox.js X.Y.Z"
git push origin dbXYZ
```
1. Go to your cdnjs for on GitHub and open a pull request. Use these examples
of accepted
[major release pull request](https://github.com/cdnjs/cdnjs/pull/735) and
[minor release pull request](https://github.com/cdnjs/cdnjs/pull/753).

View file

@ -12,7 +12,7 @@ This section describes how to get the library hooked up into your application.
To get started right away, place this snippet in your page's `<head>`.
```html
<script src="//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.7.1/dropbox.min.js">
<script src="//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.8.1/dropbox.min.js">
</script>
```
@ -152,24 +152,20 @@ client.authenticate(function(error, 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.
When Dropbox API calls fail, dropbox.js methods pass a `Dropbox.ApiError`
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
`Dropbox.ApiError` 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
@ -202,6 +198,18 @@ var showError = function(error) {
};
```
`Dropbox.Client` also supports a DOM event-like API for receiving all errors.
This can be used to log API errors, or to upload them to your server for
further analysis.
```javascript
client.onError.addListener(function(error) {
if (window.console) { // Skip the "if" in node.js code.
console.error(error);
}
});
```
## The Fun Part

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"name": "dropbox",
"version": "0.7.2",
"version": "0.8.1",
"description": "Client library for the Dropbox API",
"keywords": ["dropbox", "filesystem", "storage"],
"homepage": "http://github.com/dropbox/dropbox-js",
@ -23,14 +23,16 @@
"devDependencies": {
"async": ">= 0.1.22",
"chai": ">= 1.4.0",
"codo": ">= 1.5.2",
"codo": ">= 1.5.4",
"coffee-script": ">= 1.4.0",
"express": ">= 3.0.4",
"express": ">= 3.0.6",
"glob": ">= 3.1.14",
"mocha": ">= 1.7.4",
"open": "https://github.com/pwnall/node-open/tarball/master",
"remove": ">= 0.1.5",
"sinon": ">= 1.5.2",
"sinon-chai": ">= 2.2.0",
"uglify-js": ">= 2.2.2"
"sinon-chai": ">= 2.3.0",
"uglify-js": ">= 2.2.3"
},
"main": "lib/dropbox.js",
"directories": {
@ -40,7 +42,7 @@
"test": "test"
},
"scripts": {
"prepublish": "cake build",
"test": "cake test"
"prepublish": "node_modules/coffee-script/bin/cake build",
"test": "node_modules/coffee-script/bin/cake test"
}
}

View file

@ -0,0 +1,67 @@
# Checkbox, a dropbox.js Sample Application
This application demonstrates the use of the JavaScript client library for the
Dropbox API to implement a Dropbox-backed To Do list application.
In 70 lines of HTML, and 250 lines of commented CoffeeScript, Checkbox lets you
store your To Do list in your Dropbox! Just don't expect award winning design
or usability from a sample application.
See this sample in action
[here](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/checkbox.js/public/index.html).
## Dropbox Integration
This proof-of-concept application uses the "App folder" Dropbox access level,
so Dropbox automatically creates a directory for its app data in the users'
Dropboxes. The data model optimizes for ease of development and debugging.
Each task is stored as a file whose name is the tasks description. Tasks are
grouped under two folders, active and done.
The main advantage of this data model is that operations on tasks cleanly map
to file operations in Dropbox. At initialization time, the application creates
its two folders, active and done. A task is created by writing an empty string
to a file in the active folder, marked as completed by moving the file to the
done folder, and removed by deleting the associated file.
The lists of tasks are obtained by listing the contents of the active and done
folders. The data model can be easily extended, by storing JSON-encoded
information, such as deadlines, in the task files.
This sample uses the following `Dropbox.Client` methods:
* authenticate
* signOff
* getUserInfo
* mkdir
* readdir
* writeFile
* move
* remove
## Building
This sample does not require building. Follow the steps below to get your own
copy of the sample that you can hack on.
1. [Create a powered_by.js app in your Dropbox](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/powered_by.js/public/index.html).
1. [Get your own API key](https://www.dropbox.com/developers/apps).
1. [Encode your API key](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/api_keys.js/public/index.html).
1. Copy the source code to `/Apps/Static Web Apps/powered_by.js` in your Dropbox
## Dependencies
The application uses the following JavaScript libraries.
* [dropbox.js](https://github.com/dropbox/dropbox-js) for Dropbox integration
* [less](http://lesscss.org/) for CSS conciseness
* [CoffeeScript](http://coffeescript.org/) for JavaScript conciseness
* [jQuery](http://jquery.com/) for cross-browser compatibitility
The icons used in the application are all from
[the noun project](http://thenounproject.com/).
The application follows a good practice of packaging its dependencies, and not
hot-linking them.

View file

@ -0,0 +1,249 @@
# vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab :
# Controller/View for the application.
class Checkbox
# @param {Dropbox.Client} dbClient a non-authenticated Dropbox client
# @param {DOMElement} root the app's main UI element
constructor: (@dbClient, root) ->
@$root = $ root
@taskTemplate = $('#task-template').text()
@$activeList = $('#active-task-list')
@$doneList = $('#done-task-list')
$('#signout-button').click (event) => @onSignOut event
@dbClient.authenticate (error, data) =>
return @showError(error) if error
@dbClient.getUserInfo (error, userInfo) =>
return @showError(error) if error
$('#user-name').text userInfo.name
@tasks = new Tasks @, @dbClient
@tasks.load =>
@wire()
@render()
@$root.removeClass 'hidden'
# Re-renders all the data.
render: ->
@$activeList.empty()
@$doneList.empty()
@renderTask(task) for task in @tasks.active
@renderTask(task) for task in @tasks.done
# Renders a task into the
renderTask: (task) ->
$list = if task.done then @$doneList else @$activeList
$list.append @$taskDom(task)
# Renders the list element representing a task.
#
# @param {Task} task the task to be rendered
# @return {jQuery<li>} jQuery wrapper for the DOM representing the task
$taskDom: (task) ->
$task = $ @taskTemplate
$('.task-name', $task).text task.name
$('.task-remove-button', $task).click (event) => @onRemoveTask event, task
if task.done
$('.task-done-button', $task).addClass 'hidden'
$('.task-active-button', $task).click (event) =>
@onActiveTask event, task
else
$('.task-active-button', $task).addClass 'hidden'
$('.task-done-button', $task).click (event) => @onDoneTask event, task
$task
# Called when the user wants to create a new task.
onNewTask: (event) ->
event.preventDefault()
name = $('#new-task-name').val()
if @tasks.findByName name
alert "You already have this task on your list!"
else
$('#new-task-button').attr 'disabled', 'disabled'
$('#new-task-name').attr 'disabled', 'disabled'
task = new Task()
task.name = name
@tasks.addTask task, =>
$('#new-task-name').removeAttr('disabled').val ''
$('#new-task-button').removeAttr 'disabled'
@renderTask task
# Called when the user wants to mark a task as done.
onDoneTask: (event, task) ->
$task = @$taskElement event.target
$('button', $task).attr 'disabled', 'disabled'
@tasks.setTaskDone task, true, =>
$task.remove()
@renderTask task
# Called when the user wants to mark a task as active.
onActiveTask: (event, task) ->
$task = @$taskElement event.target
$('button', $task).attr 'disabled', 'disabled'
@tasks.setTaskDone task, false, =>
$task.remove()
@renderTask task
# Called when the user wants to permanently remove a task.
onRemoveTask: (event, task) ->
$task = @$taskElement event.target
$('button', $task).attr 'disabled', 'disabled'
@tasks.removeTask task, ->
$task.remove()
# Called when the user wants to sign out of the application.
onSignOut: (event, task) ->
@dbClient.signOut (error) =>
return @showError(error) if error
window.location.reload()
# Finds the DOM element representing a task.
#
# @param {DOMElement} element any element inside the task element
# @return {jQuery<DOMElement>} a jQuery wrapper around the DOM element
# representing a task
$taskElement: (element) ->
$(element).closest 'li.task'
# Sets up listeners for the relevant DOM events.
wire: ->
$('#new-task-form').submit (event) => @onNewTask event
# Updates the UI to show that an error has occurred.
showError: (error) ->
$('#error-notice').removeClass 'hidden'
console.log error if window.console
# Model that wraps all a user's tasks.
class Tasks
# @param {Checkbox} controller the application controller
constructor: (@controller) ->
@dbClient = @controller.dbClient
[@active, @done] = [[], []]
# Reads all the from a user's Dropbox.
#
# @param {function()} done called when all the tasks are read from the user's
# Dropbox, and the active and done properties are set
load: (done) ->
# We read the done tasks and the active tasks in parallel. The variables
# below tell us when we're done with both.
readActive = readDone = false
@dbClient.mkdir '/active', (error, stat) =>
# Issued mkdir so we always have a directory to read from.
# In most cases, this will fail, so don't bother checking for errors.
@dbClient.readdir '/active', (error, entries, dir_stat, entry_stats) =>
return @showError(error) if error
@active = ((new Task()).fromStat(stat) for stat in entry_stats)
readActive = true
done() if readActive and readDone
@dbClient.mkdir '/done', (error, stat) =>
@dbClient.readdir '/done', (error, entries, dir_stat, entry_stats) =>
return @showError(error) if error
@done = ((new Task()).fromStat(stat) for stat in entry_stats)
readDone = true
done() if readActive and readDone
@
# Adds a new task to the user's set of tasks.
#
# @param {Task} task the task to be added
# @param {function()} done called when the task is saved to the user's
# Dropbox
addTask: (task, done) ->
task.cleanupName()
@dbClient.writeFile task.path(), '', (error, stat) =>
return @showError(error) if error
@addTaskToModel task
done()
# Returns a task with the given name, if it exists.
#
# @param {String} name the name to search for
# @return {?Task} task the task with the given name, or null if no such task
# exists
findByName: (name) ->
for tasks in [@active, @done]
for task in tasks
return task if task.name is name
null
# Removes a task from the list of tasks.
#
# @param {Task} task the task to be removed
# @param {function()} done called when the task is removed from the user's
# Dropbox
removeTask: (task, done) ->
@dbClient.remove task.path(), (error, stat) =>
return @showError(error) if error
@removeTaskFromModel task
done()
# Marks a active task as done, or a done task as active.
#
# @param {Task} the task to be changed
setTaskDone: (task, newDoneValue, done) ->
[oldDoneValue, task.done] = [task.done, newDoneValue]
newPath = task.path()
task.done = oldDoneValue
@dbClient.move task.path(), newPath, (error, stat) =>
return @showError(error) if error
@removeTaskFromModel task
task.done = newDoneValue
@addTaskToModel task
done()
# Adds a task to the in-memory model. Should not be called directly.
addTaskToModel: (task) ->
@taskArray(task).push task
# Remove a task from the in-memory model. Should not be called directly.
removeTaskFromModel: (task) ->
taskArray = @taskArray task
for _task, index in taskArray
if _task is task
taskArray.splice index, 1
break
# @param {Task} the task whose containing array should be returned
# @return {Array<Task>} the array that should contain the given task
taskArray: (task) ->
if task.done then @done else @active
# Updates the UI to show that an error has occurred.
showError: (error) ->
@controller.showError error
# Model for a single user task.
class Task
# Creates a task with default values.
constructor: ->
@name = null
@done = false
# Reads data about a task from the stat of is file in a user's Dropbox.
#
# @param {Dropbox.Stat} entry the directory entry representing the task
fromStat: (entry) ->
@name = entry.name
@done = entry.path.split('/', 3)[1] is 'done'
@
# Cleans up the task name so that it's valid Dropbox file name.
cleanupName: (name) ->
# English-only hack that removes slashes from the task name.
@name = @name.replace(/\ \/\ /g, ' or ').replace(/\//g, ' or ')
@
# Path to the file representing the task in the user's Dropbox.
# @return {String} fully-qualified path
path: ->
(if @done then '/done/' else '/active/') + @name
# Start up the code when the DOM is fully loaded.
$ ->
client = new Dropbox.Client(
key: '/Fahm0FLioA|ZxKxLxy5irfHqsCRs+Ceo8bwJjVPu8xZlfjgGzeCjQ', sandbox: true)
client.authDriver new Dropbox.Drivers.Redirect(rememberUser: true)
new Checkbox client, '#app-ui'

View file

@ -0,0 +1,298 @@
// vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab :
.linear-gradient (@top: #ffffff, @bottom: #000000) {
background: @top;
filter: ~"progid:DXImageTransform.Microsoft.gradient(startColorstr='@{top}', endColorstr='@{bottom}')";
background: -webkit-linear-gradient(top, @top 0%, @bottom 100%);
background: -moz-linear-gradient(top, @top 0%, @bottom 100%);
background: -ms-linear-gradient(top, @top 0%, @bottom 100%);
background: linear-gradient(to bottom, @top 0%, @bottom 100%);
}
.border-radius (@radius) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
border-radius: @radius;
}
.box-sizing (@sizing) {
-webkit-box-sizing: @sizing;
-moz-box-sizing: @sizing;
box-sizing: @sizing;
}
.box-shadow (@offset-x, @offset-y, @blur, @spread, @color) {
-webkit-box-shadow: @offset-x @offset-y @blur @spread @color;
-moz-box-shadow: @offset-x @offset-y @blur @spread @color;
box-shadow: @offset-x @offset-y @blur @spread @color;
}
.font-verdana () {
font-family: Verdana, Geneva, sans-serif;
}
.font-helvetica () {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
html, body {
.linear-gradient (#ffffff, #f2f7fc);
background-attachment: fixed;
height: 100%;
margin: 0;
padding: 0;
}
#error-notice {
display: block;
position: absolute;
width: 100%;
.box-sizing(border-box);
color: hsl(0, 75%, 33%);
font-size: 12pt;
.font-verdana();
line-height: 1.5;
text-align: center;
margin: 0;
padding: 0.2em 0 0.1em 0;
#error-refresh-button {
margin: 0 1em;
padding: 0 1em;
}
&.hidden {
display: none;
}
}
#app-ui {
width: 700px;
margin: 0 auto;
h1 {
.font-verdana();
font-size: 24px;
font-weight: 300;
line-height: 2;
text-transform: lowercase;
color: #bfbfbf;
margin: 0;
padding: 2em 0 0.25em 0;
small {
.font-verdana();
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
color: #888888;
a {
font-style: normal;
color: #649cd1;
text-decoration: none;
}
}
}
#notebook-page {
border: 1px solid #aaaaaa;
-webkit-border-top-left-radius: 8px;
-webkit-border-top-right-radius: 8px;
-moz-border-radius-topleft: 8px;
-moz-border-radius-topright: 8px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
.box-shadow(0, 0, 10px, 5px, rgba(0, 0, 0, 0.05));
background-color: #fffddb;
margin: 0;
padding: 24px 0 60px 0;
}
#notebook-page, .task {
background-image: -webkit-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
-webkit-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
background-image: -moz-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
-moz-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
background-image: -ms-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
-ms-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
background-image: linear-gradient(to bottom, #f3aaaa 0%, #f3aaaa 100%),
linear-gradient(to bottom, #f3aaaa 0%, #f3aaaa 100%);
background-position: 70px 0px, 76px 0px;
background-size: 1px 100%, 1px 100%;
background-repeat: no-repeat, no-repeat;
}
#user-info {
margin: 0;
padding: 0 16px 8px 88px;
color: #555555;
.font-helvetica();
font-size: 16px;
line-height: 32px;
font-weight: 200;
text-align: right;
#user-name {
display: inline-block;
padding: 0 0 0 8px;
}
#signout-button {
visibility: hidden;
}
&:hover {
color: #000000;
#signout-button {
visibility: visible;
}
}
}
#new-task-form, h2, .task, .empty-task {
border-bottom: 1px solid #c9e4f2;
.font-helvetica();
font-size: 18px;
font-weight: 300;
line-height: 36px;
color: #555555;
margin: 0;
padding: 6px 10px 6px 88px;
}
#new-task-form {
margin: 0;
#new-task-name {
font-family: inherit;
font-size: inherit;
background: rgb(255, 254, 236);
}
.task-actions {
visibility: visible;
}
}
h2 {
font-size: 20px;
font-weight: 400;
color: #000000;
}
.task-list {
list-style: none;
margin: 0;
padding: 0;
}
.task {
span.task-name {
display: inline-block;
}
&:hover {
background-color: #fffcaf;
.task-actions {
visibility: visible;
}
}
}
.task-name {
width: 373px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
vertical-align: middle;
}
.task-actions {
width: 220px;
display: inline-block;
vertical-align: middle;
line-height: 1;
visibility: hidden;
}
}
input[type=text] {
display: inline-block;
margin: 0;
padding: 0 0.25em 0 0.25em;
line-height: 30px;
.box-sizing(border-box);
position: relative;
left: -4px;
height: 32px;
border: 1px solid #d2cd70;
.border-radius(4px);
-webkit-box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
button {
display: inline-block;
vertical-align: middle;
height: 32px;
margin: 0;
padding: 0;
min-width: 104px;
cursor: pointer;
border: 1px solid;
.border-radius(4px);
.box-shadow(0, 2px, -1px, 0, rgba(0, 0, 0, 0.1));
.font-verdana();
font-size: 13px;
font-weight: 400;
text-align: center;
text-transform: uppercase;
color: #ffffff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
filter: dropshadow(color=rgba(0, 0, 0, 0.5), offx=1, offy=1);
&:hover {
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
inset 0 0 4px rgba(255, 255, 255, 0.6);
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
inset 0 0 4px rgba(255, 255, 255, 0.6);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
inset 0 0 4px rgba(255, 255, 255, 0.6);
}
&:focus {
.box-shadow(0, 0, 3px, 1px, #33a0e8);
}
&:active {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
img {
display: inline-block;
vertical-align: -4px;
}
&.task-done-button, &#new-task-button, &.task-active-button {
border-color: #448c42;
.linear-gradient(#8ed66b, #58ba6d);
&:active {
.linear-gradient(#58ba6d, #8ed66b);
}
}
&.task-remove-button, &#error-refresh-button, &#signout-button {
border-color: #a73030;
.linear-gradient(#f67f73, #bb5757);
&:active {
.linear-gradient(#bb5757, #f67f73);
}
}
}
.hidden {
display: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="100px" height="87.5px" viewBox="0 0 100 87.5" style="enable-background:new 0 0 100 87.5;" xml:space="preserve">
<path style="fill:#010101;" d="M12.5,6.25c0,3.455-2.795,6.25-6.25,6.25C2.795,12.5,0,9.705,0,6.25C0,2.795,2.795,0,6.25,0 C9.705,0,12.5,2.795,12.5,6.25z"/>
<rect x="25" style="fill:#010101;" width="75" height="12.5"/>
<path style="fill:#010101;" d="M12.5,56.25c0,3.455-2.795,6.25-6.25,6.25C2.795,62.5,0,59.705,0,56.25S2.795,50,6.25,50 C9.705,50,12.5,52.795,12.5,56.25z"/>
<rect x="25" y="50" style="fill:#010101;" width="75" height="12.5"/>
<path style="fill:#010101;" d="M12.5,31.25c0,3.455-2.795,6.25-6.25,6.25C2.795,37.5,0,34.705,0,31.25S2.795,25,6.25,25 C9.705,25,12.5,27.795,12.5,31.25z"/>
<path style="fill:#010101;" d="M12.5,81.25c0,3.455-2.795,6.25-6.25,6.25C2.795,87.5,0,84.705,0,81.25S2.795,75,6.25,75 C9.705,75,12.5,77.795,12.5,81.25z"/>
<rect x="25" y="25" style="fill:#010101;" width="75" height="12.5"/>
<rect x="25" y="75" style="fill:#010101;" width="75" height="12.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<!-- vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab : -->
<html lang="en">
<head>
<title>Checkbox - dropbox.js Sample Application</title>
<link rel="icon" type="image/png" href="images/icon16.png" />
<link rel="stylesheet/less" type="text/css" href="./checkbox.less" />
<script type="text/javascript" src="lib/coffee-script.js"></script>
<script type="text/javascript" src="lib/dropbox.js"></script>
<script type="text/javascript" src="lib/jquery.js"></script>
<script type="text/javascript" src="lib/less.js"></script>
<script type="text/coffeescript" src="./checkbox.coffee"></script>
</head>
<body>
<aside id="error-notice" class="hidden">
<form action="#" method="GET">
Something went wrong :(
<button type="submit" id="error-refresh-button">
<img src="images/not_done.png" alt="" /> reload the app
</button>
</form>
</aside>
<article id="app-ui" class="hidden">
<h1>
checkbox
<small>powered by
<a href="https://www.dropbox.com/developers">dropbox</a>
</small>
</h1>
<div id="notebook-page">
<aside id="user-info">
<button type="button" id="signout-button">
<img src="images/remove.png" alt="" /> Sign out
</button>
<span id="user-name" />
</aside>
<h2 id="active-task-heading">Active</h2>
<ol class="task-list" id="active-task-list"></ol>
<form action="" method="GET" id="new-task-form">
<input type="text" id="new-task-name" class="task-name"
required="required" placeholder="e.g., buy milk" />
<button type="submit" id="new-task-button">
<img src="images/add.png" alt="" /> Add
</button>
</form>
<div class="empty-task">&nbsp;</div>
<h2 id="done-task-heading">Done</h2>
<ol class="task-list" id="done-task-list"></ol>
</div>
</article>
<script type="text/html" id="task-template">
<li class="task">
<span class="task-name" />
<span class="task-actions">
<button type="button" class="task-done-button">
<img src="images/done.png" alt="" /> Done
</button>
<button type="button" class="task-active-button">
<img src="images/not_done.png" alt="" /> Undo
</button>
<button type="button" class="task-remove-button">
<img src="images/remove.png" alt="" /> Delete
</button>
</span>
</li>
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -27,9 +27,19 @@ class Dropbox.ApiError
constructor: (xhr, @method, @url) ->
@status = xhr.status
if xhr.responseType
text = xhr.response or xhr.responseText
try
text = xhr.response or xhr.responseText
catch e
try
text = xhr.responseText
catch e
text = null
else
text = xhr.responseText
try
text = xhr.responseText
catch e
text = null
if text
try
@responseText = text.toString()

View file

@ -29,6 +29,10 @@ class Dropbox.Client
@fileServer = options.fileServer or @defaultFileServer()
@downloadServer = options.downloadServer or @defaultDownloadServer()
@onXhr = new Dropbox.EventSource cancelable: true
@onError = new Dropbox.EventSource
@onAuthStateChange = new Dropbox.EventSource
@oauth = new Dropbox.Oauth options
@driver = null
@filter = null
@ -51,20 +55,24 @@ class Dropbox.Client
@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
@
# @property {Dropbox.EventSource<Dropbox.Xhr>} cancelable event fired every
# time when a network request to the Dropbox API server is about to be
# sent; if the event is canceled by returning a falsey value from a
# listener, the network request is silently discarded; whenever possible,
# listeners should restrict themselves to using the xhr property of the
# Dropbox.Xhr instance passed to them; everything else in the Dropbox.Xhr
# API is in flux
onXhr: null
# @property {Dropbox.EventSource<Dropbox.ApiError>} non-cancelable event
# fired every time when a network request to the Dropbox API server results
# in an error
onError: null
# @property {Dropbox.EventSource<Dropbox.Client>} non-cancelable event fired
# every time the authState property changes; this can be used to update UI
# state
onAuthStateChange: null
# The authenticated user's Dropbx user ID.
#
@ -94,11 +102,16 @@ class Dropbox.Client
authenticate: (callback) ->
oldAuthState = null
unless @driver or @authState is DropboxClient.DONE
throw new Error "Call authDriver to set an authentication driver"
# Advances the authentication FSM by one step.
_fsmStep = =>
if oldAuthState isnt @authState
if oldAuthState isnt null
@onAuthStateChange.dispatch @
oldAuthState = @authState
if @driver.onAuthStateChange
if @driver and @driver.onAuthStateChange
return @driver.onAuthStateChange(@, _fsmStep)
switch @authState
@ -138,7 +151,9 @@ class Dropbox.Client
when DropboxClient.DONE # We have an access token.
callback null, @
when Dropbox.SIGNED_OFF # The user signed off, restart the flow.
when DropboxClient.SIGNED_OFF # The user signed off, restart the flow.
# The authState change makes reset() not trigger onAuthStateChange.
@authState = DropboxClient.RESET
@reset()
_fsmStep()
@ -148,6 +163,10 @@ class Dropbox.Client
_fsmStep() # Start up the state machine.
@
# @return {Boolean} true if this client is authenticated, false otherwise
isAuthenticated: ->
@authState is DropboxClient.DONE
# Revokes the user's Dropbox credentials.
#
# This should be called when the user explictly signs off from your
@ -164,8 +183,11 @@ class Dropbox.Client
@dispatchXhr xhr, (error) =>
return callback(error) if error
# The authState change makes reset() not trigger onAuthStateChange.
@authState = DropboxClient.RESET
@reset()
@authState = DropboxClient.SIGNED_OFF
@onAuthStateChange.dispatch @
if @driver.onAuthStateChange
@driver.onAuthStateChange @, ->
callback error
@ -178,15 +200,29 @@ class Dropbox.Client
# Retrieves information about the logged in user.
#
# @param {?Object} options the advanced settings below; for the default
# settings, skip the argument or pass null
# @option options {Boolean} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @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) ->
getUserInfo: (options, callback) ->
if (not callback) and (typeof options is 'function')
callback = options
options = null
httpCache = false
if options and options.httpCache
httpCache = true
xhr = new Dropbox.Xhr 'GET', @urls.accountInfo
xhr.signWithOauth @oauth
xhr.signWithOauth @oauth, httpCache
@dispatchXhr xhr, (error, userData) ->
callback error, Dropbox.UserInfo.parse(userData), userData
@ -205,17 +241,17 @@ class Dropbox.Client
# @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
# passed to the callback in an ArrayBuffer; this is the recommended method
# of reading non-UTF8 data such as images, as it is well supported across
# modern browsers; 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
# hacks and should not be used if the environment supports XHR Level 2 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
@ -224,6 +260,10 @@ class Dropbox.Client
# 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
# @option options {Boolean} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @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
@ -238,6 +278,7 @@ class Dropbox.Client
params = {}
responseType = null
rangeHeader = null
httpCache = false
if options
if options.versionTag
params.rev = options.versionTag
@ -262,8 +303,11 @@ class Dropbox.Client
else if options.start?
rangeHeader = "bytes=#{options.start}-"
httpCache = true if options.httpCache
xhr = new Dropbox.Xhr 'GET', "#{@urls.getFile}/#{@urlEncodePath(path)}"
xhr.setParams(params).signWithOauth(@oauth).setResponseType(responseType)
xhr.setParams(params).signWithOauth(@oauth, httpCache)
xhr.setResponseType(responseType)
xhr.setHeader 'Range', rangeHeader if rangeHeader
@dispatchXhr xhr, (error, data, metadata) ->
callback error, data, Dropbox.Stat.parse(metadata)
@ -359,6 +403,78 @@ class Dropbox.Client
@dispatchXhr xhr, (error, metadata) ->
callback error, Dropbox.Stat.parse(metadata)
# Atomic step in a resumable file upload.
#
# @param {String, ArrayBuffer, ArrayBufferView, Blob, File} data the file
# contents fragment to be uploaded; if a File is passed, its name is
# ignored
# @param {?Dropbox.UploadCursor} cursor the cursor that tracks the state of
# the resumable file upload; the cursor information will not be updated
# when the API call completes
# @param {function(?Dropbox.ApiError, ?Dropbox.UploadCursor)} callback called
# with the result of the /chunked_upload HTTP request; the second paramter
# is a Dropbox.UploadCursor instance describing the progress of the upload
# operation, and the first parameter is null if things go well
# @return {XMLHttpRequest} the XHR object used for this API call
resumableUploadStep: (data, cursor, callback) ->
if cursor
params = { offset: cursor.offset }
params.upload_id = cursor.tag if cursor.tag
else
params = { offset: 0 }
xhr = new Dropbox.Xhr 'POST', @urls.chunkedUpload
xhr.setBody(data).setParams(params).signWithOauth(@oauth)
@dispatchXhr xhr, (error, cursor) ->
if error and error.status is 400 and
error.response.upload_id and error.response.offset
callback null, Dropbox.UploadCursor.parse(error.response)
else
callback error, Dropbox.UploadCursor.parse(cursor)
# Finishes a resumable file upload.
#
# @param {String} path the path of the file to be created, 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} 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
resumableUploadFinish: (path, cursor, options, callback) ->
if (not callback) and (typeof options is 'function')
callback = options
options = null
params = { upload_id: cursor.tag }
if options
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
if options.noOverwrite
params.autorename = true
# TODO: locale support would edit the params here
xhr = new Dropbox.Xhr 'POST',
"#{@urls.commitChunkedUpload}/#{@urlEncodePath(path)}"
xhr.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
@ -384,6 +500,10 @@ class Dropbox.Client
# 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
# @option options {Boolean} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @param {function(?Dropbox.ApiError, ?Dropbox.Stat, ?Array<Dropbox.Stat>)}
# callback called with the result of the /metadata HTTP request; if the
# call succeeds, the second parameter is a Dropbox.Stat instance
@ -398,6 +518,7 @@ class Dropbox.Client
options = null
params = {}
httpCache = false
if options
if options.version?
params.rev = options.version
@ -409,11 +530,13 @@ class Dropbox.Client
params.file_limit = options.readDir.toString()
if options.cacheHash
params.hash = options.cacheHash
if options.httpCache
httpCache = true
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
xhr.setParams(params).signWithOauth @oauth, httpCache
@dispatchXhr xhr, (error, metadata) ->
stat = Dropbox.Stat.parse metadata
if metadata?.contents
@ -443,6 +566,10 @@ class Dropbox.Client
# instead of returning the contents; a folder's version identifier can be
# obtained from the versionTag attribute of a Dropbox.Stat instance
# describing it
# @option options {Boolean} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @param {function(?Dropbox.ApiError, ?Array<String>, ?Dropbox.Stat,
# ?Array<Dropbox.Stat>)} callback called with the result of the /metadata
# HTTP request; if the call succeeds, the second parameter is an array
@ -462,6 +589,10 @@ class Dropbox.Client
statOptions.readDir = options.limit
if options.versionTag
statOptions.versionTag = options.versionTag
if options.removed or options.deleted
statOptions.removed = options.removed or options.deleted
if options.httpCache
statOptions.httpCache = options.httpCache
@stat path, statOptions, (error, stat, entry_stats) ->
if entry_stats
entries = (entry_stat.name for entry_stat in entry_stats)
@ -541,6 +672,10 @@ class Dropbox.Client
# 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} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @param {function(?Dropbox.ApiError, ?Array<Dropbox.Stat>)} 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
@ -552,11 +687,15 @@ class Dropbox.Client
options = null
params = {}
if options and options.limit?
params.rev_limit = options.limit
httpCache = false
if options
if options.limit?
params.rev_limit = options.limit
if options.httpCache
httpCache = true
xhr = new Dropbox.Xhr 'GET', "#{@urls.revisions}/#{@urlEncodePath(path)}"
xhr.setParams(params).signWithOauth(@oauth)
xhr.setParams(params).signWithOauth @oauth, httpCache
@dispatchXhr xhr, (error, versions) ->
if versions
stats = (Dropbox.Stat.parse(metadata) for metadata in versions)
@ -696,6 +835,10 @@ class Dropbox.Client
# @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} httpCache if true, the API request will be set to
# allow HTTP caching to work; by default, requests are set up to avoid
# CORS preflights; setting this option can make sense when making the same
# request repeatedly (polling?)
# @param {function(?Dropbox.ApiError, ?Array<Dropbox.Stat>)} 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
@ -707,14 +850,17 @@ class Dropbox.Client
options = null
params = { query: namePattern }
httpCache = false
if options
if options.limit?
params.file_limit = options.limit
if options.removed or options.deleted
params.include_deleted = true
if options.httpCache
httpCache = true
xhr = new Dropbox.Xhr 'GET', "#{@urls.search}/#{@urlEncodePath(path)}"
xhr.setParams(params).signWithOauth(@oauth)
xhr.setParams(params).signWithOauth @oauth, httpCache
@dispatchXhr xhr, (error, results) ->
if results
stats = (Dropbox.Stat.parse(metadata) for metadata in results)
@ -753,10 +899,11 @@ class Dropbox.Client
# 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
# @param {Dropbox.PulledChanges, String} cursor 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
# should either be the Dropbox.PulledChanges obtained from a previous call
# to pullChanges, the return value of Dropbox.PulledChanges#cursor, or 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
@ -896,7 +1043,10 @@ class Dropbox.Client
reset: ->
@uid = null
@oauth.setToken null, ''
oldAuthState = @authState
@authState = DropboxClient.RESET
if oldAuthState isnt @authState
@onAuthStateChange.dispatch @
@authError = null
@_credentials = null
@
@ -906,6 +1056,7 @@ class Dropbox.Client
# @param {?Object} the result of a prior call to credentials()
# @return {Dropbox.Client} this, for easy call chaining
setCredentials: (credentials) ->
oldAuthState = @authState
@oauth.reset credentials
@uid = credentials.uid or null
if credentials.authState
@ -917,6 +1068,8 @@ class Dropbox.Client
@authState = DropboxClient.RESET
@authError = null
@_credentials = null
if oldAuthState isnt @authState
@onAuthStateChange.dispatch @
@
# @return {String} a string that uniquely identifies the Dropbox application
@ -955,6 +1108,9 @@ class Dropbox.Client
media: "#{@apiServer}/1/media/#{@fileRoot}"
copyRef: "#{@apiServer}/1/copy_ref/#{@fileRoot}"
thumbnails: "#{@fileServer}/1/thumbnails/#{@fileRoot}"
chunkedUpload: "#{@fileServer}/1/chunked_upload"
commitChunkedUpload:
"#{@fileServer}/1/commit_chunked_upload/#{@fileRoot}"
# File operations.
fileopsCopy: "#{@apiServer}/1/fileops/copy"
@ -962,22 +1118,27 @@ class Dropbox.Client
fileopsDelete: "#{@apiServer}/1/fileops/delete"
fileopsMove: "#{@apiServer}/1/fileops/move"
# authState value for a client that experienced an authentication error.
# @property {Number} the client's progress in the authentication process;
# Dropbox.Client#isAuthenticated should be called instead whenever
# possible; this attribute was intended to be used by OAuth drivers
authState: null
# authState value for a client that experienced an authentication error
@ERROR: 0
# authState value for a properly initialized client with no user credentials.
# 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.
# 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.
# authState value for a client whose request token was authorized
@AUTHORIZED: 3
# authState value for a client that has an access token.
# authState value for a client that has an access token
@DONE: 4
# authState value for a client that voluntarily invalidated its access token.
# 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.
@ -1039,17 +1200,22 @@ class Dropbox.Client
xhr = new Dropbox.Xhr('POST', @urls.accessToken).signWithOauth(@oauth)
@dispatchXhr xhr, callback
# Prepares an XHR before it is sent to the server.
# Prepares and sends an XHR to the Dropbox API server.
#
# @private
# This is a low-level method called by other client methods.
#
# @param {Dropbox.Xhr} xhr wrapper for the XHR to be sent
# @param {function(?Dropbox.ApiError, ?Object)} callback called with the
# outcome of the XHR
# @return {XMLHttpRequest} the native XHR object used to make the request
dispatchXhr: (xhr, callback) ->
xhr.setCallback callback
xhr.onError = @onError
xhr.prepare()
nativeXhr = xhr.xhr
if @filter
return nativeXhr unless @filter(nativeXhr, xhr)
xhr.send()
if @onXhr.dispatch xhr
xhr.send()
nativeXhr
# @private

View file

@ -0,0 +1,342 @@
# 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'
@storageKey = null
# 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 storeCredentials.
#
# @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
# Communicates with the driver from the OAuth receiver page.
@oauthReceiver: ->
window.addEventListener 'load', ->
opener = window.opener
if window.parent isnt window.top
opener or= window.parent
if opener
try
opener.postMessage window.location.href, '*'
catch e
# IE 9 doesn't support opener.postMessage for popup windows.
window.close()

View file

@ -0,0 +1,202 @@
DropboxChromeOnMessage = null
DropboxChromeSendMessage = null
if chrome?
# v2 manifest APIs.
if chrome.runtime
if chrome.runtime.onMessage
DropboxChromeOnMessage = chrome.runtime.onMessage
if chrome.runtime.sendMessage
DropboxChromeSendMessage = (m) -> chrome.runtime.sendMessage m
# v1 manifest APIs.
if chrome.extension
if chrome.extension.onMessage
DropboxChromeOnMessage or= chrome.extension.onMessage
if chrome.extension.sendMessage
DropboxChromeSendMessage or= (m) -> chrome.extension.sendMessage m
# Apps that use the v2 manifest don't get messenging in Chrome 25.
unless DropboxChromeOnMessage
do ->
pageHack = (page) ->
if page.Dropbox
Dropbox.Drivers.Chrome::onMessage =
page.Dropbox.Drivers.Chrome.onMessage
Dropbox.Drivers.Chrome::sendMessage =
page.Dropbox.Drivers.Chrome.sendMessage
else
page.Dropbox = Dropbox
Dropbox.Drivers.Chrome::onMessage = new Dropbox.EventSource
Dropbox.Drivers.Chrome::sendMessage =
(m) -> Dropbox.Drivers.Chrome::onMessage.dispatch m
if chrome.extension and chrome.extension.getBackgroundPage
if page = chrome.extension.getBackgroundPage()
return pageHack(page)
if chrome.runtime and chrome.runtime.getBackgroundPage
return chrome.runtime.getBackgroundPage (page) -> pageHack page
# OAuth driver specialized for Chrome apps and extensions.
class Dropbox.Drivers.Chrome
# @property {Chrome.Event<>, Dropbox.EventSource<>} fires non-cancelable
# events when Dropbox.Drivers.Chrome#sendMessage is called
onMessage: DropboxChromeOnMessage
# Sends a message across the Chrome extension / application.
#
# When a message is sent, the listeners registered to
#
# @param {Object} message the message to be sent
sendMessage: DropboxChromeSendMessage
# Expans an URL relative to the Chrome extension / application root.
#
# @param {String} url a resource URL relative to the extension root
# @return {String} the absolute resource URL
expandUrl: (url) ->
if chrome.runtime and chrome.runtime.getURL
return chrome.runtime.getURL(url)
if chrome.extension and chrome.extension.getURL
return chrome.extension.getURL(url)
url
# @param {?Object} options the settings below
# @option {String} receiverPath the path of page that receives the /authorize
# redirect and performs the postMessage; the path should be relative to the
# extension folder; by default, is 'chrome_oauth_receiver.html'
constructor: (options) ->
receiverPath = (options and options.receiverPath) or
'chrome_oauth_receiver.html'
@receiverUrl = @expandUrl receiverPath
@tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)"
scope = (options and options.scope) or 'default'
@storageKey = "dropbox_js_#{scope}_credentials"
# Saves token information when appropriate.
onAuthStateChange: (client, callback) ->
switch client.authState
when Dropbox.Client.RESET
@loadCredentials (credentials) =>
if credentials
if credentials.authState
# Stuck authentication process, reset.
return @forgetCredentials(callback)
client.setCredentials credentials
callback()
when Dropbox.Client.DONE
@storeCredentials client.credentials(), callback
when Dropbox.Client.SIGNED_OFF
@forgetCredentials callback
when Dropbox.Client.ERROR
@forgetCredentials callback
else
callback()
# Shows the authorization URL in a pop-up, waits for it to send a message.
doAuthorize: (authUrl, token, tokenSecret, callback) ->
window = handle: null
@listenForMessage token, window, callback
@openWindow authUrl, (handle) -> window.handle = handle
# Creates a popup window.
#
# @param {String} url the URL that will be loaded in the popup window
# @param {function(Object)} callback called with a handle that can be passed
# to Dropbox.Driver.Chrome#closeWindow
# @return {Dropbox.Driver.Chrome} this
openWindow: (url, callback) ->
if chrome.tabs and chrome.tabs.create
chrome.tabs.create url: url, active: true, pinned: false, (tab) ->
callback tab
return @
if chrome.app and chrome.app.window and chrome.app.window.create
chrome.app.window.create url, frame: 'none', id: 'dropbox-auth',
(window) -> callback window
return @
@
# Closes a window that was previously opened with openWindow.
#
# @param {Object} handle the object passed to an openWindow callback
closeWindow: (handle) ->
if chrome.tabs and chrome.tabs.remove and handle.id
chrome.tabs.remove handle.id
return @
if chrome.app and chrome.app.window and handle.close
handle.close()
return @
@
# URL of the redirect receiver page that messages the app / extension.
url: ->
@receiverUrl
# Listens for a postMessage from a previously opened tab.
#
# @param {String} token the token string that must be received from the tab
# @param {Object} window a JavaScript object whose "handle" property is a
# window handle passed to the callback of a
# Dropbox.Driver.Chrome#openWindow call
# @param {function()} called when the received message matches the token
listenForMessage: (token, window, callback) ->
listener = (message, sender) =>
# Reject messages not coming from the OAuth receiver window.
if sender and sender.tab
unless sender.tab.url.substring(0, @receiverUrl.length) is @receiverUrl
return
match = @tokenRe.exec message.dropbox_oauth_receiver_href or ''
if match and decodeURIComponent(match[2]) is token
@closeWindow window.handle if window.handle
@onMessage.removeListener listener
callback()
@onMessage.addListener listener
# Stores a Dropbox.Client's credentials to local storage.
#
# @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) ->
items= {}
items[@storageKey] = credentials
chrome.storage.local.set items, 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) ->
chrome.storage.local.get @storageKey, (items) =>
callback items[@storageKey] or null
@
# Deletes information previously stored by a call to storeCredentials.
#
# @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) ->
chrome.storage.local.remove @storageKey, callback
@
# Communicates with the driver from the OAuth receiver page.
@oauthReceiver: ->
window.addEventListener 'load', ->
driver = new Dropbox.Drivers.Chrome()
driver.sendMessage dropbox_oauth_receiver_href: window.location.href
window.close() if window.close

View file

@ -0,0 +1,87 @@
# 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 = """
<!doctype html>
<script type="text/javascript">window.close();</script>
<p>Please close this window.</p>
"""
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

View file

@ -57,421 +57,3 @@ class Dropbox.AuthDriver
# 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 = """
<!doctype html>
<script type="text/javascript">window.close();</script>
<p>Please close this window.</p>
"""
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

View file

@ -0,0 +1,71 @@
# Event dispatch following a publisher-subscriber (PubSub) model.
class Dropbox.EventSource
# Sets up an event source (publisher).
#
# @param {?Object} options one or more of the options below
# @option options {Boolean} cancelable if true,
constructor: (options) ->
@_cancelable = options and options.cancelable
@_listeners = []
# Registers a listener (subscriber) to events coming from this source.
#
# This is a simplified version of the addEventListener DOM API. Listeners
# must be functions, and they can be removed by calling removeListener.
#
# This method is idempotent, so a function will not be added to the list of
# listeners if was previously added.
#
# @param {function(Object)} listener called every time an event is fired; if
# the event is cancelable, the function can return false to cancel the
# event, or any other value to allow it to propagate; the return value is
# ignored for non-cancelable events
# @return {Dropbox.EventSource} this, for easy call chaining
addListener: (listener) ->
unless typeof listener is 'function'
throw new TypeError 'Invalid listener type; expected function'
unless listener in @_listeners
@_listeners.push listener
@
# Un-registers a listener (subscriber) previously added by addListener.
#
# This is a simplified version of the removeEventListener DOM API. The
# listener must be exactly the same object supplied to addListener.
#
# This method is idempotent, so it will fail silently if the given listener
# is not registered as a subscriber.
#
# @param {function(Object)} listener function that was previously passed in
# an addListener call
# @return {Dropbox.EventSource} this, for easy call chaining
removeListener: (listener) ->
if @_listeners.indexOf
# IE9+
index = @_listeners.indexOf listener
@_listeners.splice index, 1 if index isnt -1
else
# IE8 doesn't implement Array#indexOf in ES5.
for subscriber, i in @_listeners
if subscriber is listener
@_listeners.splice i, 1
break
@
# Informs the listeners (subscribers) that an event occurred.
#
# Event sources configured for non-cancelable events call all listeners in an
# unspecified order. Sources configured for cancelable events stop calling
# listeners as soon as one listener returns false value.
#
# @param {Object} event passed to all the registered listeners
# @return {Boolean} sources of cancelable events return false if the event
# was canceled and true otherwise; sources of non-cancelable events always
# return true
dispatch: (event) ->
for listener in @_listeners
returnValue = listener event
if @_cancelable and returnValue is false
return false
true

View file

@ -37,13 +37,19 @@ class Dropbox.PulledChanges
# least 5 miuntes before issuing another pullChanges request
shouldBackOff: undefined
# Serializable representation of the pull cursor inside this object.
#
# @return {String} an ASCII string that can be passed to pullChanges instead
# of this PulledChanges instance
cursor: -> @cursorTag
# 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
# @param {Object} deltaInfo the parsed JSON of a /delta API call result
constructor: (deltaInfo) ->
@blankSlate = deltaInfo.reset or false
@cursorTag = deltaInfo.cursor
@ -88,7 +94,7 @@ class Dropbox.PullChange
# 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
# @param {Object} entry the parsed JSON of a single entry in a /delta API
# call result
constructor: (entry) ->
@path = entry[0]

View file

@ -2,8 +2,8 @@
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
# @param {?Object, ?String} 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
@ -15,19 +15,28 @@ class Dropbox.PublicUrl
urlData
# @property {String} the public URL
url: undefined
url: null
# @property {Date} after this time, the URL is not usable
expiresAt: undefined
expiresAt: null
# @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
isDirect: null
# @property {Boolean} true if this is URL points to a file's preview page in
# Dropbox, false for direct links
isPreview: undefined
isPreview: null
# JSON representation of this file / folder's metadata
#
# @return {Object} conforms to the JSON restrictions; can be passed to
# Dropbox.PublicUrl#parse to obtain an identical PublicUrl instance
json: ->
# HACK: this can break if the Dropbox API ever decides to use 'direct' in
# its link info
@_json ||= url: @url, expires: @expiresAt.toString(), direct: @isDirect
# Creates a PublicUrl instance from a raw API response.
#
@ -47,14 +56,25 @@ class Dropbox.PublicUrl
else if isDirect is false
@isDirect = false
else
@isDirect = Date.now() - @expiresAt <= 86400000 # 1 day
# HACK: this can break if the Dropbox API ever decides to use 'direct' in
# its link info; unfortunately, there's no elegant way to guess
# between direct download URLs and preview URLs
if 'direct' of urlData
@isDirect = urlData.direct
else
@isDirect = Date.now() - @expiresAt <= 86400000 # 1 day
@isPreview = !@isDirect
# The JSON representation is created on-demand, to avoid unnecessary object
# creation.
# We can't use the original JSON object because we add a 'direct' field.
@_json = null
# 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
# @param {?Object, ?String} refData the parsed JSON describing a copy
# reference, or the reference string
@parse: (refData) ->
if refData and (typeof refData is 'object' or typeof refData is 'string')
@ -63,10 +83,20 @@ class Dropbox.CopyReference
refData
# @property {String} the raw reference, for use with Dropbox APIs
tag: undefined
tag: null
# @property {Date} deadline for using the reference in a copy operation
expiresAt: undefined
expiresAt: null
# JSON representation of this file / folder's metadata
#
# @return {Object} conforms to the JSON restrictions; can be passed to
# Dropbox.CopyReference#parse to obtain an identical CopyReference instance
json: ->
# NOTE: the assignment only occurs if the CopyReference was built around a
# string; CopyReferences parsed from API responses hold onto the
# original JSON
@_json ||= copy_ref: @tag, expires: @expiresAt.toString()
# Creates a CopyReference instance from a raw reference or API response.
#
@ -74,13 +104,16 @@ class Dropbox.CopyReference
# This constructor is used by Dropbox.CopyReference.parse, and should not be
# called directly.
#
# @param {?Object, ?String} refData the parsed JSON descring a copy
# @param {Object, String} refData the parsed JSON describing 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)
@_json = refData
else
@tag = refData
@expiresAt = new Date()
@expiresAt = new Date Math.ceil(Date.now() / 1000) * 1000
# The JSON representation is created on-demand, to avoid unnecessary
# object creation.
@_json = null

View file

@ -48,15 +48,15 @@ class Dropbox.Stat
# folder contents twice
versionTag: null
# @property {String} a guess of the MIME type representing the file or folder's
# contents
# @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
# @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
@ -73,6 +73,13 @@ class Dropbox.Stat
# does not report any time
clientModifiedAt: null
# JSON representation of this file / folder's metadata
#
# @return {Object} conforms to the JSON restrictions; can be passed to
# Dropbox.Stat#parse to obtain an identical Stat instance
json: ->
@_json
# Creates a Stat instance from a raw "metadata" response.
#
# @private
@ -82,6 +89,7 @@ class Dropbox.Stat
# @param {Object} metadata the result of parsing JSON API responses that are
# called "metadata" in the API documentation
constructor: (metadata) ->
@_json = metadata
@path = metadata.path
# Ensure there is a trailing /, to make path processing reliable.
@path = '/' + @path if @path.substring(0, 1) isnt '/'

View file

@ -0,0 +1,59 @@
# Tracks the progress of a resumable upload.
class Dropbox.UploadCursor
# Creates an UploadCursor instance from an API response.
#
# @param {?Object, ?String} cursorData the parsed JSON describing the status
# of a partial upload, or the upload ID string
@parse: (cursorData) ->
if cursorData and (typeof cursorData is 'object' or
typeof cursorData is 'string')
new Dropbox.UploadCursor cursorData
else
cursorData
# @property {String} the server-generated ID for this upload
tag: null
# @property {Number} number of bytes that have already been uploaded
offset: null
# @property {Date} deadline for finishing the upload
expiresAt: null
# JSON representation of this cursor.
#
# @return {Object} conforms to the JSON restrictions; can be passed to
# Dropbox.UploadCursor#parse to obtain an identical UploadCursor instance
json: ->
# NOTE: the assignment only occurs if
@_json ||= upload_id: @tag, offset: @offset, expires: @expiresAt.toString()
# Creates an UploadCursor instance from a raw reference or API response.
#
# This constructor should only be called directly to obtain a cursor for a
# new file upload. Dropbox.UploadCursor#parse should be called instead
#
# @param {?Object, ?String} cursorData the parsed JSON describing a copy
# reference, or the reference string
constructor: (cursorData) ->
@replace cursorData
# Replaces the current
#
# @private Called by Dropbox.Client#resumableUploadStep.
#
# @param {?Object, ?String} cursorData the parsed JSON describing a copy
# reference, or the reference string
# @return {Dropbox.UploadCursor} this
replace: (cursorData) ->
if typeof cursorData is 'object'
@tag = cursorData.upload_id or null
@offset = cursorData.offset or 0
@expiresAt = new Date(Date.parse(cursorData.expires) or Date.now())
@_json = cursorData
else
@tag = cursorData or null
@offset = 0
@expiresAt = new Date Math.floor(Date.now() / 1000) * 1000
@_json = null
@

View file

@ -27,7 +27,8 @@ class Dropbox.UserInfo
# returned by the authentication process
uid: null
# @property {String}
# @property {String} the user's referral link; the user might benefit if
# others use the link to sign up for Dropbox
referralUrl: null
# Specific to applications whose access type is "public app folder".
@ -49,15 +50,23 @@ class Dropbox.UserInfo
# shared with other users
sharedBytes: null
# JSON representation of this user's information.
#
# @return {Object} conforms to the JSON restrictions; can be passed to
# Dropbox.UserInfo#parse to obtain an identical UserInfo instance
json: ->
@_json
# 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
# @param {Object} userInfo the result of parsing a JSON API response that
# describes a user
constructor: (userInfo) ->
@_json = userInfo
@name = userInfo.display_name
@email = userInfo.email
@countryCode = userInfo.country or null

View file

@ -30,28 +30,41 @@ else
# https://code.google.com/p/chromium/issues/detail?id=60449
if typeof Uint8Array is 'undefined'
DropboxXhrArrayBufferView = null
DropboxXhrSendArrayBufferView = false
else
DropboxXhrArrayBufferView =
(new Uint8Array(0)).__proto__.__proto__.constructor
if Object.getPrototypeOf
DropboxXhrArrayBufferView = Object.getPrototypeOf(
Object.getPrototypeOf(new Uint8Array(0))).constructor
else if Object.__proto__
DropboxXhrArrayBufferView =
(new Uint8Array(0)).__proto__.__proto__.constructor
# Browsers that haven't implemented XHR#send(ArrayBufferView) also don't
# have a real ArrayBufferView prototype. (Safari, Firefox)
DropboxXhrSendArrayBufferView = DropboxXhrArrayBufferView isnt Object
# 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
@ieXdr = 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
# Superclass for all ArrayBufferView objects.
@ArrayBufferView = DropboxXhrArrayBufferView
# Set to true if we think we can send ArrayBufferView objects via XHR.
@sendArrayBufferView = DropboxXhrSendArrayBufferView
# Sets up an AJAX request.
#
# @param {String} method the HTTP method used to make the request ('GET',
# 'POST', 'PUT', etc.)
# '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
# be modified, e.g. by appending parameters for GET requests
constructor: (@method, baseUrl) ->
@isGet = @method is 'GET'
@url = baseUrl
@ -63,6 +76,17 @@ class Dropbox.Xhr
@responseType = null
@callback = null
@xhr = null
@onError = null
# @property {?XMLHttpRequest} the raw XMLHttpRequest object used to make the
# request; null until Dropbox.Xhr#prepare is called
xhr: null
# @property {?Dropbox.EventSource<Dropbox.ApiError>} if the XHR fails and
# this property is set, the Dropbox.ApiError instance that will be passed
# to the callback will be dispatched through the Dropbox.EventSource; the
# EventSource should be configured for non-cancelable events
onError: null
# Sets the parameters (form field values) that will be sent with the request.
#
@ -100,11 +124,22 @@ class Dropbox.Xhr
# 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))
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
# used to sign the request
# @param {Boolean} cacheFriendly if true, the signing process choice will be
# biased towards allowing the HTTP cache to work; by default, the choice
# attempts to avoid the CORS preflight request whenever possible
# @return {Dropbox.Xhr} this, for easy call chaining
signWithOauth: (oauth, cacheFriendly) ->
if Dropbox.Xhr.ieXdr
@addOauthParams oauth
else
else if @preflight or !Dropbox.Xhr.doesPreflight
@addOauthHeader oauth
else
if @isGet and cacheFriendly
@addOauthHeader oauth
else
@addOauthParams oauth
# Ammends the request parameters to include an OAuth signature.
#
@ -112,7 +147,7 @@ class Dropbox.Xhr
# the signing process.
#
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
# used to sign the request
# used to sign the request
# @return {Dropbox.Xhr} this, for easy call chaining
addOauthParams: (oauth) ->
if @signed
@ -129,7 +164,7 @@ class Dropbox.Xhr
# the signing process.
#
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
# used to sign the request
# used to sign the request
# @return {Dropbox.Xhr} this, for easy call chaining
addOauthHeader: (oauth) ->
if @signed
@ -142,7 +177,7 @@ class Dropbox.Xhr
# 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
# GET requests cannot have a body
# @return {Dropbox.Xhr} this, for easy call chaining
setBody: (body) ->
if @isGet
@ -150,9 +185,13 @@ class Dropbox.Xhr
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
if typeof body is 'string'
# Content-Type will be set automatically.
else if (typeof FormData isnt 'undefined') and (body instanceof FormData)
# Content-Type will be set automatically.
else
@headers['Content-Type'] = 'application/octet-stream'
@preflight = true
@body = body
@
@ -191,26 +230,37 @@ class Dropbox.Xhr
# Simulates having an <input type="file"> being sent with the request.
#
# @param {String} fieldName the name of the form field / parameter (not of
# the uploaded file)
# the uploaded file)
# @param {String} fileName the name of the uploaded file (not the name of the
# form field / parameter)
# 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
# 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'
throw new Error 'setFileField 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
if typeof ArrayBuffer isnt 'undefined'
if fileData instanceof ArrayBuffer
# Convert ArrayBuffer -> ArrayBufferView on standard-compliant
# browsers, to avoid warnings from the Blob constructor.
if Dropbox.Xhr.sendArrayBufferView
fileData = new Uint8Array fileData
else
# Convert ArrayBufferView -> ArrayBuffer on older browsers, to avoid
# having a Blob that contains "[object Uint8Array]" instead of the
# actual data.
if !Dropbox.Xhr.sendArrayBufferView and fileData.byteOffset is 0 and
fileData.buffer instanceof ArrayBuffer
fileData = fileData.buffer
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
@ -236,7 +286,7 @@ class Dropbox.Xhr
# @private
# @return {String} a nonce suitable for use as a part boundary in a multipart
# MIME message
# MIME message
multipartBoundary: ->
[Date.now().toString(36), Math.random().toString(36)].join '----'
@ -276,8 +326,8 @@ class Dropbox.Xhr
#
# @return {Dropbox.Xhr} this, for easy call chaining
prepare: ->
ieMode = Dropbox.Xhr.ieMode
if @isGet or @body isnt null or ieMode
ieXdr = Dropbox.Xhr.ieXdr
if @isGet or @body isnt null or ieXdr
@paramsToUrl()
if @body isnt null and typeof @body is 'string'
@headers['Content-Type'] = 'text/plain; charset=utf8'
@ -285,14 +335,18 @@ class Dropbox.Xhr
@paramsToBody()
@xhr = new Dropbox.Xhr.Request()
if ieMode
@xhr.onload = => @onLoad()
@xhr.onerror = => @onError()
if ieXdr
@xhr.onload = => @onXdrLoad()
@xhr.onerror = => @onXdrError()
@xhr.ontimeout = => @onXdrError()
# NOTE: there are reports that XHR somtimes fails if onprogress doesn't
# have any handler
@xhr.onprogress = ->
else
@xhr.onreadystatechange = => @onReadyStateChange()
@xhr.open @method, @url, true
unless ieMode
unless ieXdr
for own header, value of @headers
@xhr.setRequestHeader header, value
@ -321,16 +375,15 @@ class Dropbox.Xhr
if @body isnt null
body = @body
# send() in XHR doesn't like naked ArrayBuffers
if Dropbox.Xhr.ArrayBufferView and body instanceof ArrayBuffer
if Dropbox.Xhr.sendArrayBufferView 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
if !Dropbox.Xhr.sendArrayBufferView and typeof Blob isnt 'undefined'
# Firefox doesn't support sending ArrayBufferViews.
body = new Blob [body], type: 'application/octet-stream'
@xhr.send body
else
@ -381,6 +434,7 @@ class Dropbox.Xhr
if @xhr.status < 200 or @xhr.status >= 300
apiError = new Dropbox.ApiError @xhr, @method, @url
@onError.dispatch apiError if @onError
@callback apiError
return true
@ -432,7 +486,7 @@ class Dropbox.Xhr
true
# Handles the XDomainRequest onload event. (IE 8, 9)
onLoad: ->
onXdrLoad: ->
text = @xhr.responseText
switch @xhr.contentType
when 'application/x-www-form-urlencoded'
@ -444,7 +498,8 @@ class Dropbox.Xhr
true
# Handles the XDomainRequest onload event. (IE 8, 9)
onError: ->
onXdrError: ->
apiError = new Dropbox.ApiError @xhr, @method, @url
@onError.dispatch apiError if @onError
@callback apiError
return true

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="94.209px" viewBox="0 0 100 94.209" enable-background="new 0 0 100 94.209" xml:space="preserve">
<path d="M51.62,0c10.969,0.871,19.611,10.069,19.611,21.262c0,11.397-8.966,20.693-20.221,21.272v4.367
c3.071,0.485,5.426,3.154,5.426,6.362c0,0.784-0.145,1.536-0.4,2.232l3.992,2.305c6.17-9.318,18.615-12.359,28.422-6.696
c9.786,5.651,13.4,17.907,8.467,27.893c6.771-13.177,2.062-29.512-10.911-37.001c-3.243-1.873-6.7-3.014-10.2-3.486
c1.339-3.264,2.081-6.836,2.081-10.583C77.887,13.059,66.268,0.886,51.62,0L51.62,0z"/>
<path d="M48.044,0.016c-14.575,0.963-26.119,13.095-26.119,27.91c0,3.641,0.696,7.119,1.962,10.306
c-3.393,0.497-6.754,1.627-9.905,3.446C1.099,49.117-3.622,65.28,2.952,78.412c-4.747-9.94-1.106-22.032,8.594-27.634
c9.87-5.698,22.402-2.581,28.53,6.876l3.853-2.223c-0.242-0.677-0.366-1.409-0.366-2.168c0-3.071,2.155-5.643,5.032-6.281v-4.457
c-11.153-0.684-20.006-9.938-20.006-21.264C28.589,10.119,37.144,0.959,48.044,0.016L48.044,0.016z"/>
<path d="M49.29,26.803c-5.63,0.129-10.819,2.046-15.079,5.165c0.34,0.503,0.706,0.991,1.091,1.458
c0.386,0.468,0.794,0.916,1.222,1.344s0.876,0.836,1.344,1.222c0.343,0.284,0.712,0.539,1.075,0.799
c3.144-2.066,6.91-3.275,10.966-3.275c4.058,0,7.813,1.209,10.957,3.275c0.363-0.26,0.731-0.515,1.075-0.799
c0.468-0.386,0.916-0.794,1.344-1.222s0.836-0.876,1.222-1.344c0.386-0.468,0.752-0.956,1.093-1.458
c-4.415-3.23-9.826-5.165-15.69-5.165c-0.14,0-0.278-0.002-0.418,0C49.428,26.804,49.357,26.8,49.29,26.803L49.29,26.803z"/>
<path d="M23.628,50.297C23.035,55.735,24.068,61.39,27,66.47c2.933,5.08,7.314,8.809,12.316,11.014
c0.269-0.548,0.508-1.11,0.717-1.678c0.214-0.57,0.398-1.143,0.555-1.728s0.292-1.18,0.391-1.777
c0.075-0.438,0.115-0.883,0.154-1.329c-3.357-1.689-6.288-4.346-8.317-7.86C30.79,59.6,29.954,55.74,30.17,51.984
c-0.407-0.185-0.812-0.373-1.23-0.53c-0.568-0.211-1.142-0.396-1.728-0.553c-0.585-0.158-1.177-0.282-1.776-0.383
C24.839,50.417,24.234,50.34,23.628,50.297L23.628,50.297z"/>
<path d="M76.368,50.615c-0.604,0.043-1.21,0.121-1.807,0.221c-0.598,0.101-1.19,0.227-1.777,0.383
c-0.582,0.157-1.165,0.343-1.732,0.554c-0.418,0.156-0.816,0.354-1.225,0.538c0.217,3.753-0.617,7.616-2.648,11.128
c-2.025,3.512-4.958,6.163-8.318,7.854c0.045,0.445,0.082,0.888,0.157,1.328c0.102,0.598,0.227,1.19,0.383,1.775
c0.157,0.585,0.342,1.167,0.556,1.734c0.209,0.57,0.448,1.125,0.717,1.673c5.003-2.208,9.384-5.929,12.315-11.007
C75.92,61.716,76.961,56.053,76.368,50.615L76.368,50.615z"/>
<path d="M45.151,57.492l-3.878,2.248c4.985,10.003,1.401,22.296-8.408,27.959c-9.759,5.635-22.142,2.671-28.342-6.541
c8.046,12.395,24.497,16.459,37.442,8.985c3.136-1.81,5.782-4.145,7.903-6.817c2.163,2.81,4.9,5.251,8.164,7.136
c12.96,7.481,29.432,3.402,37.467-9.025c-6.188,9.249-18.588,12.243-28.367,6.596c-9.869-5.697-13.438-18.109-8.311-28.146
l-4.032-2.329C53.611,58.869,51.901,59.7,50,59.7C48.068,59.7,46.331,58.841,45.151,57.492L45.151,57.492z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="94.113px" viewBox="0 0 100 94.113" enable-background="new 0 0 100 94.113" xml:space="preserve">
<path d="M74.868,86.582l-16.531-28.37c-3.038,1.997-8.752,2.129-8.752,2.129s-5.779-0.132-8.817-2.129l-16.58,28.37
c0,0,9.743,7.531,25.612,7.531C65.619,94.113,74.868,86.582,74.868,86.582L74.868,86.582L74.868,86.582z"/>
<path d="M49.881,53.471c5.417,0,9.745-4.408,9.745-9.792c0-5.417-4.328-9.827-9.745-9.827s-9.791,4.41-9.791,9.827
C40.09,49.063,44.464,53.471,49.881,53.471L49.881,53.471L49.881,53.471z"/>
<path d="M74.539,0.578L58.254,29.114c3.237,1.618,6.16,6.49,6.16,6.49s2.823,5.201,2.575,8.868l32.864-0.033
c0,0,1.666-12.121-6.244-25.877C85.651,4.822,74.539,0.578,74.539,0.578L74.539,0.578L74.539,0.578z"/>
<path d="M0.191,43.845h32.897c-0.247-3.667,2.51-8.737,2.51-8.737s2.988-4.953,6.29-6.588L25.557,0c0,0-11.378,4.658-19.32,18.397
C-1.674,32.103,0.191,43.845,0.191,43.845L0.191,43.845L0.191,43.845z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" viewBox="0 0 100 100">
<path d="M 66.177,0 H 50 33.823 v 3.724 h 4.445 V 88.047 C 38.269,94.638 43.531,100 50,100 56.469,100 61.731,94.638 61.731,88.047 V 3.724 h 4.445 V 0 z M 50,96.28 c -4.416,0 -8.012,-3.695 -8.012,-8.233 V 36.559 H 58.011 V 88.047 C 58.012,92.585 54.416,96.28 50,96.28 z" />
</svg>

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

View file

@ -0,0 +1,20 @@
{
"name": "dropbox.js Test Suite",
"version": "1.0",
"manifest_version": 2,
"description": "Test suite for Chrome applications and extensions.",
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"permissions": [
"storage",
"unlimitedStorage"
],
"app": {
"launch": {
"local_path": "test/html/browser_test.html"
}
}
}

View file

@ -0,0 +1,22 @@
{
"name": "dropbox.js Test Suite",
"version": "1.0",
"manifest_version": 2,
"description": "Test suite for Chrome applications and extensions.",
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"permissions": [
"storage",
"unlimitedStorage"
],
"app": {
"background": {
"scripts": [
"test/js/chrome_app_background.js"
]
}
}
}

View file

@ -0,0 +1,10 @@
# dropbox.js Test Automator
This is a Google Chrome extension that fully automates the dropbox.js test
suite. Read the
[dropbox.js development guide](https://github.com/dropbox/dropbox-js/tree/master/doc)
to learn how the extension fits into the testing process.
You're welcome to reuse the code to automate the testing of your own
application's integration with Dropbox.

View file

@ -0,0 +1,84 @@
# Background script orchestrating the dropbox.js testing automation.
class Automator
constructor: ->
@wired = false
chrome.storage.sync.get 'enabled', (values) =>
@lifeSwitch values.enabled is 'true'
# Activates or deactivates the extension.
# @param {Boolean} enabled if true, the extension's functionality is enabled
lifeSwitch: (enabled) ->
if enabled
chrome.browserAction.setIcon
path:
19: 'images/action_on19.png'
38: 'images/action_on38.png'
chrome.browserAction.setTitle title: '(on) dropbox.js Test Automator'
@wire()
else
chrome.browserAction.setIcon
path:
19: 'images/action_off19.png',
38: 'images/action_off38.png'
chrome.browserAction.setTitle title: '(off) dropbox.js Test Automator'
@unwire()
# Checks if Dropbox's authorization dialog should be auto-clicked.
# @param {String} url the URL of the Dropbox authorization dialog
# @return {Boolean} true if the "Authorize" button should be auto-clicked
shouldAutomateAuth: (url) ->
return false unless @wired
!!(/(\?|&)oauth_callback=https?%3A%2F%2Flocalhost%3A891[12]%2F/.exec(url))
# Checks if an OAuth receiver window should be auto-closed.
# @param {String} url the URL of the OAuth receiver window
# @return {Boolean} true if the "Authorize" button should be auto-clicked
shouldAutomateClose: (url) ->
return false unless @wired
!!(/^https?:\/\/localhost:8912\/oauth_callback\?/.exec(url))
# Sets up all the features that make dropbox.js testing easier.
wire: ->
return if @wired
chrome.contentSettings.popups.set(
primaryPattern: 'http://localhost:8911/*', setting: 'allow')
@wired = true
@
# Disables the features that automate dropbox.js testing.
unwire: ->
return unless @wired
chrome.contentSettings.popups.clear({})
@wired = false
@
# Global Automator instance.
automator = new Automator()
# Current extension id, used to validate incoming messages.
extensionId = chrome.i18n.getMessage "@@extension_id"
# Communicates with content scripts.
chrome.extension.onMessage.addListener (message, sender, sendResponse) ->
return unless sender.id is extensionId
switch message.type
when 'auth'
sendResponse automate: automator.shouldAutomateAuth(message.url)
when 'close'
if automator.shouldAutomateClose(message.url) and sender.tab
chrome.tabs.remove sender.tab.id
# Listen to pref changes and activate / deactivate the extension.
chrome.storage.onChanged.addListener (changes, namespace) ->
return unless namespace is 'sync'
for name, change of changes
continue unless name is 'enabled'
automator.lifeSwitch change.newValue is 'true'
# The browser action item flips the switch that activates the extension.
chrome.browserAction.onClicked.addListener ->
chrome.storage.sync.get 'enabled', (values) ->
enabled = values.enabled is 'true'
chrome.storage.sync.set enabled: (!enabled).toString()

View file

@ -0,0 +1,18 @@
# Content script for Dropbox authorization pages.
message = type: 'auth', url: window.location.href
chrome.extension.sendMessage message, (response) ->
return unless response.automate
button = document.querySelector('[name=allow_access]') or
document.querySelector '.freshbutton-blue'
event = document.createEvent 'MouseEvents'
clientX = button.clientWidth / 2
clientY = button.clientHeight / 2
screenX = window.screenX + button.offsetLeft + clientX
screenY = window.screenY + button.offsetTop + clientY
event.initMouseEvent 'click', true, true, window, 1,
screenX, screenY, clientX, clientY, false, false, false, false, 0, null
button.dispatchEvent event

View file

@ -0,0 +1,4 @@
# Content script for Dropbox OAuth receiver pages.
message = type: 'close', url: window.location.href
chrome.extension.sendMessage message

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="128"
height="128"
viewBox="0 -2.896 128 128"
enable-background="new 0 -2.896 100 100"
xml:space="preserve"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="icon.svg"><metadata
id="metadata19"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs17" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1564"
inkscape:window-height="1237"
id="namedview15"
showgrid="true"
inkscape:zoom="6.675088"
inkscape:cx="69.437347"
inkscape:cy="69.196458"
inkscape:window-x="2702"
inkscape:window-y="34"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1"><inkscape:grid
type="xygrid"
id="grid2996"
empspacing="4"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><path
d="M 66.073434,0.91625655 C 80.113721,2.0311339 91.174173,13.804545 91.174173,28.131549 c 0,14.588124 -11.475171,26.486975 -25.882817,27.228093 v 5.589746 c 3.930871,0.620799 6.945263,4.03839 6.945263,8.14334 0,1.003517 -0.185599,1.966075 -0.510719,2.856953 l 5.108468,2.950393 c 7.898861,-11.927011 23.827142,-15.819481 36.381352,-8.569579 12.52605,7.231982 17.15068,22.919623 10.83773,35.702935 8.66686,-16.867782 2.63935,-37.77525 -13.96604,-47.361146 -4.14975,-2.397434 -8.5747,-3.857911 -13.054691,-4.462069 1.712636,-4.17791 2.662393,-8.750059 2.662393,-13.546207 0,-19.032273 -14.872283,-34.6136742 -33.621678,-35.74775145 l 0,0 z"
id="path3"
inkscape:connector-curvature="0" /><path
d="M 61.496166,0.93673655 C 42.840212,2.1693735 28.063929,17.698295 28.063929,36.661448 c 0,4.660469 0.890877,9.112298 2.511353,13.191648 -4.343029,0.636158 -8.645098,2.082554 -12.678368,4.410869 C 1.4067145,63.785861 -4.6361505,84.47445 3.7785486,101.28335 -2.2975963,88.5602 2.3628722,73.082478 14.778841,65.911936 27.41241,58.618514 43.45333,62.608264 51.297151,74.711914 l 4.931828,-2.845433 C 55.91922,70.999923 55.7605,70.064245 55.7605,69.091448 c 0,-3.92959 2.758393,-7.223022 6.440944,-8.03966 V 55.346842 C 47.925639,54.471324 36.593827,42.626233 36.593827,28.128989 36.593827,13.868545 47.5442,2.1437736 61.496166,0.93673655 l 0,0 z"
id="path5"
inkscape:connector-curvature="0" /><path
d="m 63.091042,35.224012 c -7.206382,0.165119 -13.848286,2.618873 -19.301072,6.611183 0.435199,0.643839 0.903677,1.268477 1.396476,1.866236 0.494079,0.599038 1.016318,1.172477 1.564156,1.720315 0.547839,0.547839 1.121278,1.070078 1.720316,1.564157 0.439039,0.363519 0.911358,0.689918 1.375997,1.022717 4.02431,-2.644473 8.844778,-4.19199 14.036445,-4.19199 5.194227,0 10.000615,1.547517 14.024925,4.19199 0.463359,-0.332799 0.935679,-0.659198 1.375998,-1.022717 0.599038,-0.494079 1.172477,-1.016318 1.720315,-1.564157 0.547839,-0.547838 1.070078,-1.121277 1.564157,-1.720315 0.494078,-0.599039 0.962557,-1.223677 1.399036,-1.866236 -5.649907,-4.134389 -12.57725,-6.611183 -20.081871,-6.611183 -0.1792,0 -0.355839,-0.0026 -0.535039,0 -0.0832,0.0013 -0.174079,-0.0038 -0.259839,0 l 0,0 z"
id="path7"
inkscape:connector-curvature="0" /><path
d="m 30.243763,65.296257 c -0.759038,6.960623 0.563199,14.199005 4.316149,20.701389 3.754231,6.502384 9.361897,11.275492 15.764441,14.097884 0.34432,-0.700155 0.650239,-1.419515 0.917758,-2.147834 0.273919,-0.729598 0.509439,-1.463036 0.710398,-2.211834 0.20096,-0.748798 0.373759,-1.511676 0.500479,-2.274554 0.096,-0.560639 0.1472,-1.130238 0.19712,-1.701116 -4.29695,-2.160635 -8.04862,-5.562866 -10.645734,-10.059495 -2.593274,-4.495349 -3.663351,-9.436137 -3.386872,-14.245085 -0.520958,-0.236799 -1.039357,-0.477439 -1.574396,-0.677118 -0.727038,-0.27008 -1.461756,-0.506879 -2.211834,-0.707839 -0.748798,-0.202239 -1.506556,-0.360959 -2.273275,-0.490238 -0.764158,-0.13056 -1.538556,-0.22912 -2.314234,-0.28416 l 0,0 z"
id="path9"
inkscape:connector-curvature="0" /><path
d="m 97.750797,65.703296 c -0.773118,0.05504 -1.548796,0.1536 -2.314234,0.28288 -0.765438,0.129279 -1.521916,0.291839 -2.273275,0.490238 -0.744958,0.20096 -1.491196,0.439039 -2.216954,0.709119 -0.535039,0.199679 -1.043198,0.453118 -1.567996,0.689918 0.277759,4.802548 -0.789758,9.747176 -3.388152,14.242525 -2.591993,4.496629 -6.347504,7.8899 -10.647014,10.053095 0.0576,0.568318 0.104959,1.136637 0.199679,1.699836 0.13184,0.765438 0.291839,1.523196 0.490239,2.270714 0.202239,0.750078 0.439039,1.495036 0.712958,2.219514 0.267519,0.729599 0.572159,1.44 0.917758,2.141425 6.403825,-2.826223 12.012771,-7.589091 15.761882,-14.088915 3.75167,-6.501104 5.084147,-13.749726 4.325109,-20.710349 l 0,0 z"
id="path11"
inkscape:connector-curvature="0" /><path
d="m 57.793135,74.505834 -4.963828,2.877433 c 6.380784,12.803809 1.793276,28.538793 -10.762213,35.787413 -12.491489,7.21279 -28.34169,3.41888 -36.2776703,-8.37246 10.2988543,15.86557 31.3560823,21.06747 47.9256413,11.50078 4.01407,-2.31808 7.400942,-5.30687 10.115815,-8.72574 2.768633,3.59679 6.270704,6.72126 10.449894,9.13406 16.58876,9.57437 37.672866,4.35454 47.957646,-11.55198 -7.92062,11.83997 -23.792585,15.67228 -36.309674,8.44414 C 73.297737,106.30734 68.728148,90.420035 75.289412,77.572707 l -5.159668,-2.982393 c -1.507836,1.678076 -3.69791,2.741753 -6.129904,2.741753 -2.472954,0 -4.696309,-1.099517 -6.206705,-2.826233 l 0,0 z"
id="path13"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,44 @@
{
"name": "dropbox.js Test Automator",
"version": "1.0",
"manifest_version": 2,
"description": "Automatically clicks buttons and closes windows, so you can spend more time coding.",
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"permissions": [
"http://localhost/*",
"https://localhost/*",
"https://www.dropbox.com/1/oauth/authorize*",
"contentSettings",
"storage"
],
"browser_action": {
"default_icon": {
"19": "images/action_off19.png",
"38": "images/action_off38.png"
},
"default_title": "dropbox.js Test Automator"
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["https://www.dropbox.com/1/oauth/authorize*"],
"js": ["content_auth.js"],
"run_at": "document_idle"
},
{
"matches": [
"http://localhost/*",
"https://localhost/*"
],
"include_globs": ["http*://localhost:8912/oauth_callback*"],
"js": ["content_close.js"],
"run_at": "document_idle"
}
]
}

View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>dropbox.js browser tests</title>
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<script src="/lib/dropbox.js"></script>
<script src="/test/vendor/sinon.js"></script>
<!--[if IE]>
<script src="/test/vendor/sinon-ie.js"></script>
<![endif]-->
<script src="/test/vendor/chai.js"></script>
<script src="/node_modules/sinon-chai/lib/sinon-chai.js"></script>
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/test/js/browser_mocha_setup.js"></script>
<script src="/test/.token/token.js"></script>
<script src="/test/vendor/favicon.js"></script>
<script src="/test/js/helper.js"></script>
<script src="/test/js/base64_test.js"></script>
<script src="/test/js/client_test.js"></script>
<script src="/test/js/drivers_test.js"></script>
<script src="/test/js/event_source_test.js"></script>
<script src="/test/js/hmac_test.js"></script>
<script src="/test/js/oauth_test.js"></script>
<script src="/test/js/pulled_changes_test.js"></script>
<script src="/test/js/references_test.js"></script>
<script src="/test/js/stat_test.js"></script>
<script src="/test/js/upload_cursor_test.js"></script>
<script src="/test/js/user_info_test.js"></script>
<script src="/test/js/xhr_test.js"></script>
<script src="/test/js/browser_mocha_runner.js"></script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/lib/dropbox.js" type="text/javascript"></script>
<script src="/test/js/chrome_oauth_receiver.js" type="text/javascript">
</script>
</head>
<body>
<h1>Dropbox sign-in successful</h1>
<p>Please close this window.</p>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/lib/dropbox.js" type="text/javascript"></script>
<script type="text/javascript">
Dropbox.Drivers.Popup.oauthReceiver();
</script>
</head>
<body>
<h1>Dropbox sign-in successful</h1>
<p>Please close this window.</p>
</body>
</html>

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/lib/dropbox.js"></script>
<script src="/test/.token/token.js"></script>
<script type="text/javascript">
(function() {
var opener = window.opener;
if (!opener && window.parent != window.top) {
opener = window.parent;
}
if (opener) {
var client = new Dropbox.Client(window.testFullDropboxKeys);
client.reset();
client.authDriver(new Dropbox.Drivers.Redirect(
{scope: "redirect-integration"}));
client.authenticate(function(error, _client) {
var message = [error || null, _client && _client.credentials()];
var json = JSON.stringify(message);
try {
opener.postMessage(json, '*');
} catch(e) {
// IE doesn't support opener.postMessage for popups.
}
window.close();
});
}
})();
</script>
</head>
<body>
<p>Please close this window.</p>
</body>
</html>

View file

@ -0,0 +1,11 @@
describe 'Dropbox.atob', ->
it 'decodes an ASCII string', ->
expect(Dropbox.atob('YTFiMmMz')).to.equal 'a1b2c3'
it 'decodes a non-ASCII character', ->
expect(Dropbox.atob('/A==')).to.equal String.fromCharCode(252)
describe 'Dropbox.btoa', ->
it 'encodes an ASCII string', ->
expect(Dropbox.btoa('a1b2c3')).to.equal 'YTFiMmMz'
it 'encodes a non-ASCII character', ->
expect(Dropbox.btoa(String.fromCharCode(252))).to.equal '/A=='

View file

@ -0,0 +1,9 @@
window.addEventListener 'load', ->
runner = mocha.run ->
failures = runner.failures || 0
total = runner.total || 0
image = new Image()
image.src = "/diediedie?failed=#{failures}&total=#{total}";
image.onload = ->
null

View file

@ -0,0 +1 @@
mocha.setup ui: 'bdd', slow: 150, timeout: 10000

View file

@ -0,0 +1,3 @@
chrome.app.runtime.onLaunched.addListener ->
chrome.app.window.create 'test/html/browser_test.html',
type: 'shell', frame: 'chrome', id: 'test_suite'

View file

@ -0,0 +1 @@
Dropbox.Drivers.Chrome.oauthReceiver()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,446 @@
describe 'Dropbox.Drivers.BrowserBase', ->
beforeEach ->
@node_js = module? and module?.exports? and require?
@chrome_app = chrome? and (chrome.extension or chrome.app)
@client = new Dropbox.Client testKeys
describe 'with rememberUser: false', ->
beforeEach (done) ->
return done() if @node_js or @chrome_app
@driver = new Dropbox.Drivers.BrowserBase
@driver.setStorageKey @client
@driver.forgetCredentials done
afterEach (done) ->
return done() if @node_js or @chrome_app
@driver.forgetCredentials done
describe '#loadCredentials', ->
it 'produces the credentials passed to storeCredentials', (done) ->
return done() if @node_js or @chrome_app
goldCredentials = @client.credentials()
@driver.storeCredentials goldCredentials, =>
@driver.loadCredentials (credentials) ->
expect(credentials).to.deep.equal goldCredentials
done()
it 'produces null after forgetCredentials was called', (done) ->
return done() if @node_js or @chrome_app
@driver.storeCredentials @client.credentials(), =>
@driver.forgetCredentials =>
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
it 'produces null if a different scope is provided', (done) ->
return done() if @node_js or @chrome_app
@driver.setStorageKey @client
@driver.storeCredentials @client.credentials(), =>
@driver.forgetCredentials =>
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
describe 'Dropbox.Drivers.Redirect', ->
describe '#url', ->
beforeEach ->
@stub = sinon.stub Dropbox.Drivers.BrowserBase, 'currentLocation'
afterEach ->
@stub.restore()
it 'adds a query string to a static URL', ->
@stub.returns 'http://test/file'
driver = new Dropbox.Drivers.Redirect useQuery: true
expect(driver.url()).to.
equal 'http://test/file?_dropboxjs_scope=default'
it 'adds a fragment to a static URL', ->
@stub.returns 'http://test/file'
driver = new Dropbox.Drivers.Redirect
expect(driver.url()).to.
equal 'http://test/file#?_dropboxjs_scope=default'
it 'adds a query param to a URL with a query string', ->
@stub.returns 'http://test/file?a=true'
driver = new Dropbox.Drivers.Redirect useQuery: true
expect(driver.url()).to.
equal 'http://test/file?a=true&_dropboxjs_scope=default'
it 'adds a fragment to a URL with a query string', ->
@stub.returns 'http://test/file?a=true'
driver = new Dropbox.Drivers.Redirect
expect(driver.url()).to.
equal 'http://test/file?a=true#?_dropboxjs_scope=default'
it 'adds a query string to a static URL with a fragment', ->
@stub.returns 'http://test/file#frag'
driver = new Dropbox.Drivers.Redirect useQuery: true
expect(driver.url()).to.
equal 'http://test/file?_dropboxjs_scope=default#frag'
it 'replaces the fragment in a static URL with a fragment', ->
@stub.returns 'http://test/file#frag'
driver = new Dropbox.Drivers.Redirect
expect(driver.url()).to.
equal 'http://test/file#?_dropboxjs_scope=default'
it 'adds a query param to a URL with a query string and fragment', ->
@stub.returns 'http://test/file?a=true#frag'
driver = new Dropbox.Drivers.Redirect useQuery: true
expect(driver.url()).to.
equal 'http://test/file?a=true&_dropboxjs_scope=default#frag'
it 'replaces the fragment in a URL with a query string and fragment', ->
@stub.returns 'http://test/file?a=true#frag'
driver = new Dropbox.Drivers.Redirect
expect(driver.url()).to.
equal 'http://test/file?a=true#?_dropboxjs_scope=default'
it 'obeys the scope option', ->
@stub.returns 'http://test/file'
driver = new Dropbox.Drivers.Redirect(
scope: 'not default', useQuery: true)
expect(driver.url()).to.
equal 'http://test/file?_dropboxjs_scope=not%20default'
it 'obeys the scope option when adding a fragment', ->
@stub.returns 'http://test/file'
driver = new Dropbox.Drivers.Redirect scope: 'not default'
expect(driver.url()).to.
equal 'http://test/file#?_dropboxjs_scope=not%20default'
describe '#locationToken', ->
beforeEach ->
@stub = sinon.stub Dropbox.Drivers.BrowserBase, 'currentLocation'
afterEach ->
@stub.restore()
it 'returns null if the location does not contain the arg', ->
@stub.returns 'http://test/file?_dropboxjs_scope=default& ' +
'another_token=ab%20cd&oauth_tok=en'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal null
it 'returns null if the location fragment does not contain the arg', ->
@stub.returns 'http://test/file#?_dropboxjs_scope=default& ' +
'another_token=ab%20cd&oauth_tok=en'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal null
it "extracts the token successfully with default scope", ->
@stub.returns 'http://test/file?_dropboxjs_scope=default&' +
'oauth_token=ab%20cd&other_param=true'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal 'ab cd'
it "extracts the token successfully with set scope", ->
@stub.returns 'http://test/file?_dropboxjs_scope=not%20default&' +
'oauth_token=ab%20cd'
driver = new Dropbox.Drivers.Redirect scope: 'not default'
expect(driver.locationToken()).to.equal 'ab cd'
it "extracts the token from fragment with default scope", ->
@stub.returns 'http://test/file#?_dropboxjs_scope=default&' +
'oauth_token=ab%20cd&other_param=true'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal 'ab cd'
it "extracts the token from fragment with set scope", ->
@stub.returns 'http://test/file#?_dropboxjs_scope=not%20default&' +
'oauth_token=ab%20cd'
driver = new Dropbox.Drivers.Redirect scope: 'not default'
expect(driver.locationToken()).to.equal 'ab cd'
it "returns null if the location scope doesn't match", ->
@stub.returns 'http://test/file?_dropboxjs_scope=defaultx&oauth_token=ab'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal null
it "returns null if the location fragment scope doesn't match", ->
@stub.returns 'http://test/file#?_dropboxjs_scope=defaultx&oauth_token=a'
driver = new Dropbox.Drivers.Redirect
expect(driver.locationToken()).to.equal null
describe '#loadCredentials', ->
beforeEach ->
@node_js = module? and module.exports? and require?
@chrome_app = chrome? and (chrome.extension or chrome.app?.runtime)
return if @node_js or @chrome_app
@client = new Dropbox.Client testKeys
@driver = new Dropbox.Drivers.Redirect scope: 'some_scope'
@driver.setStorageKey @client
it 'produces the credentials passed to storeCredentials', (done) ->
return done() if @node_js or @chrome_app
goldCredentials = @client.credentials()
@driver.storeCredentials goldCredentials, =>
@driver = new Dropbox.Drivers.Redirect scope: 'some_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.deep.equal goldCredentials
done()
it 'produces null after forgetCredentials was called', (done) ->
return done() if @node_js or @chrome_app
@driver.storeCredentials @client.credentials(), =>
@driver.forgetCredentials =>
@driver = new Dropbox.Drivers.Redirect scope: 'some_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
it 'produces null if a different scope is provided', (done) ->
return done() if @node_js or @chrome_app
@driver.setStorageKey @client
@driver.storeCredentials @client.credentials(), =>
@driver = new Dropbox.Drivers.Redirect scope: 'other_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
describe 'integration', ->
beforeEach ->
@node_js = module? and module.exports? and require?
@chrome_app = chrome? and (chrome.extension or chrome.app?.runtime)
it 'should work', (done) ->
return done() if @node_js or @chrome_app
@timeout 30 * 1000 # Time-consuming because the user must click.
listener = (event) ->
expect(event.data).to.match(/^\[.*\]$/)
[error, credentials] = JSON.parse event.data
expect(error).to.equal null
expect(credentials).to.have.property 'uid'
expect(credentials.uid).to.be.a 'string'
window.removeEventListener 'message', listener
done()
window.addEventListener 'message', listener
(new Dropbox.Drivers.Popup()).openWindow(
'/test/html/redirect_driver_test.html')
describe 'Dropbox.Drivers.Popup', ->
describe '#url', ->
beforeEach ->
@stub = sinon.stub Dropbox.Drivers.BrowserBase, 'currentLocation'
@stub.returns 'http://test:123/a/path/file.htmx'
afterEach ->
@stub.restore()
it 'reflects the current page when there are no options', ->
driver = new Dropbox.Drivers.Popup
expect(driver.url()).to.equal 'http://test:123/a/path/file.htmx'
it 'replaces the current file correctly', ->
driver = new Dropbox.Drivers.Popup receiverFile: 'another.file'
expect(driver.url()).to.equal 'http://test:123/a/path/another.file#'
it 'replaces the current file without a fragment correctly', ->
driver = new Dropbox.Drivers.Popup
receiverFile: 'another.file', noFragment: true
expect(driver.url()).to.equal 'http://test:123/a/path/another.file'
it 'replaces an entire URL without a fragment correctly', ->
driver = new Dropbox.Drivers.Popup
receiverUrl: 'https://something.com/filez'
expect(driver.url()).to.equal 'https://something.com/filez#'
it 'replaces an entire URL with a fragment correctly', ->
driver = new Dropbox.Drivers.Popup
receiverUrl: 'https://something.com/filez#frag'
expect(driver.url()).to.equal 'https://something.com/filez#frag'
it 'replaces an entire URL without a fragment and useQuery correctly', ->
driver = new Dropbox.Drivers.Popup
receiverUrl: 'https://something.com/filez', noFragment: true
expect(driver.url()).to.equal 'https://something.com/filez'
describe '#loadCredentials', ->
beforeEach ->
@node_js = module? and module.exports? and require?
@chrome_app = chrome? and (chrome.extension or chrome.app?.runtime)
return if @node_js or @chrome_app
@client = new Dropbox.Client testKeys
@driver = new Dropbox.Drivers.Popup scope: 'some_scope'
@driver.setStorageKey @client
it 'produces the credentials passed to storeCredentials', (done) ->
return done() if @node_js or @chrome_app
goldCredentials = @client.credentials()
@driver.storeCredentials goldCredentials, =>
@driver = new Dropbox.Drivers.Popup scope: 'some_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.deep.equal goldCredentials
done()
it 'produces null after forgetCredentials was called', (done) ->
return done() if @node_js or @chrome_app
@driver.storeCredentials @client.credentials(), =>
@driver.forgetCredentials =>
@driver = new Dropbox.Drivers.Popup scope: 'some_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
it 'produces null if a different scope is provided', (done) ->
return done() if @node_js or @chrome_app
@driver.setStorageKey @client
@driver.storeCredentials @client.credentials(), =>
@driver = new Dropbox.Drivers.Popup scope: 'other_scope'
@driver.setStorageKey @client
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
describe 'integration', ->
beforeEach ->
@node_js = module? and module.exports? and require?
@chrome_app = chrome? and (chrome.extension or chrome.app?.runtime)
it 'should work with a query string', (done) ->
return done() if @node_js or @chrome_app
@timeout 45 * 1000 # Time-consuming because the user must click.
client = new Dropbox.Client testKeys
client.reset()
authDriver = new Dropbox.Drivers.Popup
receiverFile: 'oauth_receiver.html', noFragment: true,
scope: 'popup-integration', rememberUser: false
client.authDriver authDriver
client.authenticate (error, client) =>
expect(error).to.equal null
expect(client.authState).to.equal Dropbox.Client.DONE
# Verify that we can do API calls.
client.getUserInfo (error, userInfo) ->
expect(error).to.equal null
expect(userInfo).to.be.instanceOf Dropbox.UserInfo
# Follow-up authenticate() should restart the process.
client.reset()
authDriver.doAuthorize = (authUrl, token, tokenSecret, callback) ->
client.reset()
done()
client.authenticate ->
assert false, 'The second authenticate() should not complete.'
it 'should work with a URL fragment and rememberUser: true', (done) ->
return done() if @node_js or @chrome_app
@timeout 45 * 1000 # Time-consuming because the user must click.
client = new Dropbox.Client testKeys
client.reset()
authDriver = new Dropbox.Drivers.Popup
receiverFile: 'oauth_receiver.html', noFragment: false,
scope: 'popup-integration', rememberUser: true
client.authDriver authDriver
authDriver.setStorageKey client
authDriver.forgetCredentials ->
client.authenticate (error, client) ->
expect(error).to.equal null
expect(client.authState).to.equal Dropbox.Client.DONE
# Verify that we can do API calls.
client.getUserInfo (error, userInfo) ->
expect(error).to.equal null
expect(userInfo).to.be.instanceOf Dropbox.UserInfo
# Follow-up authenticate() should use stored credentials.
client.reset()
authDriver.doAuthorize = (authUrl, token, tokenSecret, callback) ->
assert false,
'Stored credentials not used in second authenticate()'
client.authenticate (error, client) ->
# Verify that we can do API calls.
client.getUserInfo (error, userInfo) ->
expect(error).to.equal null
expect(userInfo).to.be.instanceOf Dropbox.UserInfo
done()
describe 'Dropbox.Drivers.Chrome', ->
beforeEach ->
@chrome_app = chrome? and (chrome.extension or chrome.app?.runtime)
@client = new Dropbox.Client testKeys
describe '#url', ->
beforeEach ->
return unless @chrome_app
@path = 'test/html/redirect_driver_test.html'
@driver = new Dropbox.Drivers.Chrome receiverPath: @path
it 'produces a chrome-extension:// url', ->
return unless @chrome_app
expect(@driver.url()).to.match(/^chrome-extension:\/\//)
it 'produces an URL ending in redirectPath', ->
return unless @chrome_app
url = @driver.url()
expect(url.substring(url.length - @path.length)).to.equal @path
describe '#loadCredentials', ->
beforeEach ->
return unless @chrome_app
@client = new Dropbox.Client testKeys
@driver = new Dropbox.Drivers.Chrome scope: 'some_scope'
it 'produces the credentials passed to storeCredentials', (done) ->
return done() unless @chrome_app
goldCredentials = @client.credentials()
@driver.storeCredentials goldCredentials, =>
@driver = new Dropbox.Drivers.Chrome scope: 'some_scope'
@driver.loadCredentials (credentials) ->
expect(credentials).to.deep.equal goldCredentials
done()
it 'produces null after forgetCredentials was called', (done) ->
return done() unless @chrome_app
@driver.storeCredentials @client.credentials(), =>
@driver.forgetCredentials =>
@driver = new Dropbox.Drivers.Chrome scope: 'some_scope'
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
it 'produces null if a different scope is provided', (done) ->
return done() unless @chrome_app
@driver.storeCredentials @client.credentials(), =>
@driver = new Dropbox.Drivers.Chrome scope: 'other_scope'
@driver.loadCredentials (credentials) ->
expect(credentials).to.equal null
done()
describe 'integration', ->
it 'should work', (done) ->
return done() unless @chrome_app
@timeout 45 * 1000 # Time-consuming because the user must click.
client = new Dropbox.Client testKeys
client.reset()
authDriver = new Dropbox.Drivers.Chrome(
receiverPath: 'test/html/chrome_oauth_receiver.html',
scope: 'chrome_integration')
client.authDriver authDriver
authDriver.forgetCredentials ->
client.authenticate (error, client) ->
expect(error).to.equal null
expect(client.authState).to.equal Dropbox.Client.DONE
# Verify that we can do API calls.
client.getUserInfo (error, userInfo) ->
expect(error).to.equal null
expect(userInfo).to.be.instanceOf Dropbox.UserInfo
# Follow-up authenticate() should use stored credentials.
client.reset()
authDriver.doAuthorize = (authUrl, token, tokenSecret, callback) ->
assert false,
'Stored credentials not used in second authenticate()'
client.authenticate (error, client) ->
# Verify that we can do API calls.
client.getUserInfo (error, userInfo) ->
expect(error).to.equal null
expect(userInfo).to.be.instanceOf Dropbox.UserInfo
done()

View file

@ -0,0 +1,144 @@
describe 'Dropbox.EventSource', ->
beforeEach ->
@source = new Dropbox.EventSource
@cancelable = new Dropbox.EventSource cancelable: true
# 3 listeners, 1 and 2 are already hooked up
@event1 = null
@return1 = true
@listener1 = (event) =>
@event1 = event
@return1
@source.addListener @listener1
@cancelable.addListener @listener1
@event2 = null
@return2 = false
@listener2 = (event) =>
@event2 = event
@return2
@source.addListener @listener2
@cancelable.addListener @listener2
@event3 = null
@return3 = true
@listener3 = (event) =>
@event3 = event
@return3
describe '#addListener', ->
it 'adds a new listener', ->
@source.addListener @listener3
expect(@source._listeners).to.deep.
equal [@listener1, @listener2, @listener3]
it 'does not add an existing listener', ->
@source.addListener @listener2
expect(@source._listeners).to.deep.equal [@listener1, @listener2]
it 'is idempotent', ->
@source.addListener @listener3
@source.addListener @listener3
expect(@source._listeners).to.deep.
equal [@listener1, @listener2, @listener3]
it 'refuses to add non-functions', ->
expect(=> @source.addListener 42).to.throw(TypeError, /listener type/)
describe '#removeListener', ->
it 'does nothing for a non-existing listener', ->
@source.removeListener @listener3
expect(@source._listeners).to.deep.equal [@listener1, @listener2]
it 'removes a listener at the end of the queue', ->
@source.removeListener @listener2
expect(@source._listeners).to.deep.equal [@listener1]
it 'removes a listener at the beginning of the queue', ->
@source.removeListener @listener1
expect(@source._listeners).to.deep.equal [@listener2]
it 'removes a listener at the middle of the queue', ->
@source.addListener @listener3
@source.removeListener @listener2
expect(@source._listeners).to.deep.equal [@listener1, @listener3]
it 'removes all the listeners', ->
@source.removeListener @listener1
@source.removeListener @listener2
expect(@source._listeners).to.deep.equal []
describe 'without ES5 Array#indexOf', ->
beforeEach ->
@source._listeners.indexOf = null
afterEach ->
delete @source._listeners.indexOf
assertArraysEqual = (array1, array2) ->
expect(array1.length).to.equal(array2.length)
for i in [0...array1.length]
expect(array1[i]).to.equal(array2[i])
it 'does nothing for a non-existing listener', ->
@source.removeListener @listener3
assertArraysEqual @source._listeners, [@listener1, @listener2]
it 'removes a listener at the end of the queue', ->
@source.removeListener @listener2
assertArraysEqual @source._listeners, [@listener1]
it 'removes a listener at the beginning of the queue', ->
@source.removeListener @listener1
assertArraysEqual @source._listeners, [@listener2]
it 'removes a listener at the middle of the queue', ->
@source.addListener @listener3
@source.removeListener @listener2
assertArraysEqual @source._listeners, [@listener1, @listener3]
it 'removes all the listeners', ->
@source.removeListener @listener1
@source.removeListener @listener2
assertArraysEqual @source._listeners, []
describe '#dispatch', ->
beforeEach ->
@event = { answer: 42 }
it 'passes event to all listeners', ->
@source.dispatch @event
expect(@event1).to.equal @event
expect(@event2).to.equal @event
expect(@event3).to.equal null
describe 'on non-cancelable events', ->
beforeEach ->
@source.addListener @listener3
@returnValue = @source.dispatch @event
it 'calls all the listeners', ->
expect(@event1).to.equal @event
expect(@event2).to.equal @event
expect(@event3).to.equal @event
it 'ignores the listener return values', ->
expect(@returnValue).to.equal true
describe 'on cancelable events', ->
beforeEach ->
@cancelable.addListener @listener3
@returnValue = @cancelable.dispatch @event
it 'stops calling listeners after cancelation', ->
expect(@event1).to.equal @event
expect(@event2).to.equal @event
expect(@event3).to.equal null
it 'reports cancelation', ->
expect(@returnValue).to.equal false
it 'calls all listeners if no cancelation occurs', ->
@return2 = true
@returnValue = @cancelable.dispatch @event
expect(@returnValue).to.equal true
expect(@event3).to.equal @event

View file

@ -0,0 +1,55 @@
if global? and require? and module?
# Node.JS
exports = global
exports.Dropbox = require '../../lib/dropbox'
exports.chai = require 'chai'
exports.sinon = require 'sinon'
exports.sinonChai = require 'sinon-chai'
exports.authDriver = new Dropbox.Drivers.NodeServer port: 8912
TokenStash = require './token_stash.js'
(new TokenStash()).get (credentials) ->
exports.testKeys = credentials
(new TokenStash(fullDropbox: true)).get (credentials) ->
exports.testFullDropboxKeys = credentials
testIconPath = './test/binary/dropbox.png'
fs = require 'fs'
buffer = fs.readFileSync testIconPath
bytes = []
for i in [0...buffer.length]
bytes.push String.fromCharCode(buffer.readUInt8(i))
exports.testImageBytes = bytes.join ''
exports.testImageUrl = 'http://localhost:8913/favicon.ico'
imageServer = null
exports.testImageServerOn = ->
imageServer =
new Dropbox.Drivers.NodeServer port: 8913, favicon: testIconPath
exports.testImageServerOff = ->
imageServer.closeServer()
imageServer = null
else
if chrome? and chrome.runtime
# Chrome app
exports = window
exports.authDriver = new Dropbox.Drivers.Chrome(
receiverPath: 'test/html/chrome_oauth_receiver.html',
scope: 'helper-chrome')
# Hack-implement "rememberUser: false" in the Chrome driver.
exports.authDriver.storeCredentials = (credentials, callback) -> callback()
exports.authDriver.loadCredentials = (callback) -> callback null
else
# Browser
exports = window
exports.authDriver = new Dropbox.Drivers.Popup(
receiverFile: 'oauth_receiver.html', scope: 'helper-popup')
exports.testImageUrl = '/test/binary/dropbox.png'
exports.testImageServerOn = -> null
exports.testImageServerOff = -> null
# Shared setup.
exports.assert = exports.chai.assert
exports.expect = exports.chai.expect

View file

@ -0,0 +1,22 @@
describe 'Dropbox.hmac', ->
it 'works for an empty message with an empty key', ->
expect(Dropbox.hmac('', '')).to.equal '+9sdGxiqbAgyS31ktx+3Y3BpDh0='
it 'works for the non-empty Wikipedia example', ->
expect(Dropbox.hmac('The quick brown fox jumps over the lazy dog', 'key')).
to.equal '3nybhbi3iqa8ino29wqQcBydtNk='
it 'works for the Oauth example', ->
key = 'kd94hf93k423kf44&pfkkdhi9sl3r4s00'
string = 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal'
expect(Dropbox.hmac(string, key)).to.equal 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
describe 'Dropbox.sha1', ->
it 'works for an empty message', ->
expect(Dropbox.sha1('')).to.equal '2jmj7l5rSw0yVb/vlWAYkK/YBwk='
it 'works for the FIPS-180 Appendix A sample', ->
expect(Dropbox.sha1('abc')).to.equal 'qZk+NkcGgWq6PiVxeFDCbJzQ2J0='
it 'works for the FIPS-180 Appendix B sample', ->
string = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
expect(Dropbox.sha1(string)).to.equal 'hJg+RBw70m66rkqh+VEp5eVGcPE='

View file

@ -0,0 +1,124 @@
describe 'Dropbox.Oauth', ->
beforeEach ->
@oauth = new Dropbox.Oauth
key: 'dpf43f3p2l4k3l03',
secret: 'kd94hf93k423kf44'
@oauth.setToken 'nnch734d00sl2jdk', 'pfkkdhi9sl3r4s00'
# The example in OAuth 1.0a Appendix A.
@request =
method: 'GET',
url: 'http://photos.example.net/photos'
params:
file: 'vacation.jpg',
size: 'original'
@dateStub = sinon.stub Date, 'now'
@dateStub.returns 1191242096999
afterEach ->
@dateStub.restore()
describe '#boilerplateParams', ->
it 'issues unique nonces', ->
nonces = {}
for i in [1..100]
nonce = @oauth.boilerplateParams({}).oauth_nonce
expect(nonces).not.to.have.property nonce
nonces[nonce] = true
it 'fills all the arguments', ->
params = @oauth.boilerplateParams(@request.params)
properties = ['oauth_consumer_key', 'oauth_nonce',
'oauth_signature_method', 'oauth_timestamp',
'oauth_version']
for property in properties
expect(params).to.have.property property
describe '#signature', ->
it 'works for the OAuth 1.0a example', ->
@nonceStub = sinon.stub @oauth, 'nonce'
@nonceStub.returns 'kllo9940pd9333jh'
@oauth.boilerplateParams(@request.params)
expect(@oauth.signature(@request.method, @request.url, @request.params)).
to.equal 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
@nonceStub.restore()
it 'works with an encoded key', ->
@oauth = new Dropbox.Oauth
key: Dropbox.encodeKey(@oauth.key, @oauth.secret),
token: @oauth.token, tokenSecret: @oauth.tokenSecret
@nonceStub = sinon.stub @oauth, 'nonce'
@nonceStub.returns 'kllo9940pd9333jh'
@oauth.boilerplateParams(@request.params)
expect(@oauth.signature(@request.method, @request.url, @request.params)).
to.equal 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
@nonceStub.restore()
describe '#addAuthParams', ->
it 'matches the OAuth 1.0a example', ->
@nonceStub = sinon.stub @oauth, 'nonce'
@nonceStub.returns 'kllo9940pd9333jh'
goldenParams =
file: 'vacation.jpg'
oauth_consumer_key: 'dpf43f3p2l4k3l03'
oauth_nonce: 'kllo9940pd9333jh'
oauth_signature: 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
oauth_signature_method: 'HMAC-SHA1'
oauth_timestamp: '1191242096'
oauth_token: 'nnch734d00sl2jdk'
oauth_version: '1.0'
size: 'original'
@oauth.addAuthParams @request.method, @request.url, @request.params
expect(Dropbox.Xhr.urlEncode(@request.params)).to.
eql Dropbox.Xhr.urlEncode(goldenParams)
@nonceStub.restore()
it "doesn't leave any OAuth-related value in params", ->
@oauth.authHeader(@request.method, @request.url, @request.params)
expect(Dropbox.Xhr.urlEncode(@request.params)).to.
equal "file=vacation.jpg&size=original"
describe '#authHeader', ->
it 'matches the OAuth 1.0a example', ->
@nonceStub = sinon.stub @oauth, 'nonce'
@nonceStub.returns 'kllo9940pd9333jh'
goldenHeader = 'OAuth oauth_consumer_key="dpf43f3p2l4k3l03",oauth_nonce="kllo9940pd9333jh",oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1191242096",oauth_token="nnch734d00sl2jdk",oauth_version="1.0"'
header = @oauth.authHeader @request.method, @request.url, @request.params
expect(header).to.equal goldenHeader
@nonceStub.restore()
it "doesn't leave any OAuth-related value in params", ->
@oauth.authHeader(@request.method, @request.url, @request.params)
expect(Dropbox.Xhr.urlEncode(@request.params)).to.
equal "file=vacation.jpg&size=original"
describe '#appHash', ->
it 'is a non-trivial string', ->
expect(@oauth.appHash()).to.be.a 'string'
expect(@oauth.appHash().length).to.be.greaterThan 4
it 'is consistent', ->
oauth = new Dropbox.Oauth key: @oauth.key, secret: @oauth.secret
expect(oauth.appHash()).to.equal @oauth.appHash()
it 'depends on the app key', ->
oauth = new Dropbox.Oauth key: @oauth.key + '0', secret: @oauth.secret
expect(oauth.appHash()).not.to.equal @oauth.appHash()
expect(oauth.appHash()).to.be.a 'string'
expect(oauth.appHash().length).to.be.greaterThan 4
describe '#constructor', ->
it 'raises an Error if initialized without an API key / secret', ->
expect(-> new Dropbox.Oauth(token: '123', tokenSecret: '456')).to.
throw(Error, /no api key/i)

View file

@ -0,0 +1,128 @@
describe 'Dropbox.PulledChanges', ->
describe '.parse', ->
describe 'on a sample response', ->
beforeEach ->
deltaInfo = {
"reset": false,
"cursor": "nTZYLOcTQnyB7-Wc72M-kEAcBQdk2EjLaJIRupQWgDXmRwKWzuG5V4se2mvU7yzXn4cZSJltoW4tpbqgy0Ezxh1b1p3ygp7wy-vdaYJusujnLAyEsKdYCHPZYZdZt7sQG0BopF2ufAuD56ijYbdX5DhMKe85MFqncnFDvNxSjsodEw-IkCfNZmagDmpOZCxmLqu71hLTApwhqO9-dhm-fk6KSYs-OZwRmVwOE2JAnJbWuifNiM8KwMz5sRBZ5FMJPDqXpOW5PqPCwbkAmKQACbNXFi0k1JuxulpDlQh3zMr3lyLMs-fmaDTTU355mY5xSAXK05Zgs5rPJ6lcaBOUmEBSXcPhxFDHk5NmAdA03Shq04t2_4bupzWX-txT84FmOLNncchl7ZDBCMwyrAzD2kCYOTu1_lhui0C-fiCZgZBKU4OyP6qrkdo4gZu3",
"has_more": true,
"entries": [
[
"/Getting_Started.pdf",
{
"size": "225.4KB",
"rev": "35e97029684fe",
"thumb_exists": true, # Changed to test hasThumbnail=true code.
"bytes": 230783,
"modified": "Tue, 19 Jul 2011 21:55:38 +0000",
"client_mtime": "Mon, 18 Jul 2011 18:04:35 +0000",
"path": "/Getting_Started.pdf",
"is_dir": false,
"icon": "page_white_acrobat",
"root": "app_folder", # Changed to test app_folder code path.
"mime_type": "application/pdf",
"revision": 220823
}
],
[
"/Public",
null
]
]
}
@changes = Dropbox.PulledChanges.parse deltaInfo
it 'parses blankSlate correctly', ->
expect(@changes).to.have.property 'blankSlate'
expect(@changes.blankSlate).to.equal false
it 'parses cursorTag correctly', ->
expect(@changes).to.have.property 'cursorTag'
expect(@changes.cursorTag).to.equal 'nTZYLOcTQnyB7-Wc72M-kEAcBQdk2EjLaJIRupQWgDXmRwKWzuG5V4se2mvU7yzXn4cZSJltoW4tpbqgy0Ezxh1b1p3ygp7wy-vdaYJusujnLAyEsKdYCHPZYZdZt7sQG0BopF2ufAuD56ijYbdX5DhMKe85MFqncnFDvNxSjsodEw-IkCfNZmagDmpOZCxmLqu71hLTApwhqO9-dhm-fk6KSYs-OZwRmVwOE2JAnJbWuifNiM8KwMz5sRBZ5FMJPDqXpOW5PqPCwbkAmKQACbNXFi0k1JuxulpDlQh3zMr3lyLMs-fmaDTTU355mY5xSAXK05Zgs5rPJ6lcaBOUmEBSXcPhxFDHk5NmAdA03Shq04t2_4bupzWX-txT84FmOLNncchl7ZDBCMwyrAzD2kCYOTu1_lhui0C-fiCZgZBKU4OyP6qrkdo4gZu3'
it 'parses shouldPullAgain correctly', ->
expect(@changes).to.have.property 'shouldPullAgain'
expect(@changes.shouldPullAgain).to.equal true
it 'parses shouldBackOff correctly', ->
expect(@changes).to.have.property 'shouldBackOff'
expect(@changes.shouldBackOff).to.equal false
it 'parses changes correctly', ->
expect(@changes).to.have.property 'changes'
expect(@changes.changes).to.have.length 2
expect(@changes.changes[0]).to.be.instanceOf Dropbox.PullChange
expect(@changes.changes[0].path).to.equal '/Getting_Started.pdf'
expect(@changes.changes[1]).to.be.instanceOf Dropbox.PullChange
expect(@changes.changes[1].path).to.equal '/Public'
it 'passes null through', ->
expect(Dropbox.PulledChanges.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.PulledChanges.parse(undefined)).to.equal undefined
describe 'Dropbox.PullChange', ->
describe '.parse', ->
describe 'on a modification change', ->
beforeEach ->
entry = [
"/Getting_Started.pdf",
{
"size": "225.4KB",
"rev": "35e97029684fe",
"thumb_exists": true, # Changed to test hasThumbnail=true code.
"bytes": 230783,
"modified": "Tue, 19 Jul 2011 21:55:38 +0000",
"client_mtime": "Mon, 18 Jul 2011 18:04:35 +0000",
"path": "/Getting_Started.pdf",
"is_dir": false,
"icon": "page_white_acrobat",
"root": "app_folder", # Changed to test app_folder code path.
"mime_type": "application/pdf",
"revision": 220823
}
]
@changes = Dropbox.PullChange.parse entry
it 'parses path correctly', ->
expect(@changes).to.have.property 'path'
expect(@changes.path).to.equal '/Getting_Started.pdf'
it 'parses wasRemoved correctly', ->
expect(@changes).to.have.property 'wasRemoved'
expect(@changes.wasRemoved).to.equal false
it 'parses stat correctly', ->
expect(@changes).to.have.property 'stat'
expect(@changes.stat).to.be.instanceOf Dropbox.Stat
expect(@changes.stat.path).to.equal @changes.path
describe 'on a deletion change', ->
beforeEach ->
entry = [
"/Public",
null
]
@changes = Dropbox.PullChange.parse entry
it 'parses path correctly', ->
expect(@changes).to.have.property 'path'
expect(@changes.path).to.equal '/Public'
it 'parses wasRemoved correctly', ->
expect(@changes).to.have.property 'wasRemoved'
expect(@changes.wasRemoved).to.equal true
it 'parses stat correctly', ->
expect(@changes).to.have.property 'stat'
expect(@changes.stat).to.equal null
it 'passes null through', ->
expect(Dropbox.PullChange.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.PullChange.parse(undefined)).to.equal undefined

View file

@ -0,0 +1,92 @@
describe 'Dropbox.PublicUrl', ->
describe '.parse', ->
describe 'on the /shares API example', ->
beforeEach ->
urlData = {
"url": "http://db.tt/APqhX1",
"expires": "Tue, 01 Jan 2030 00:00:00 +0000"
}
@url = Dropbox.PublicUrl.parse urlData, false
it 'parses url correctly', ->
expect(@url).to.have.property 'url'
expect(@url.url).to.equal 'http://db.tt/APqhX1'
it 'parses expiresAt correctly', ->
expect(@url).to.have.property 'expiresAt'
expect(@url.expiresAt).to.be.instanceOf Date
expect([
'Tue, 01 Jan 2030 00:00:00 GMT', # every sane JS platform
'Tue, 1 Jan 2030 00:00:00 UTC' # Internet Explorer
]).to.contain(@url.expiresAt.toUTCString())
it 'parses isDirect correctly', ->
expect(@url).to.have.property 'isDirect'
expect(@url.isDirect).to.equal false
it 'parses isPreview correctly', ->
expect(@url).to.have.property 'isPreview'
expect(@url.isPreview).to.equal true
it 'round-trips through json / parse correctly', ->
newUrl = Dropbox.PublicUrl.parse @url.json()
newUrl.json() # Get _json populated for newUrl.
expect(newUrl).to.deep.equal @url
it 'passes null through', ->
expect(Dropbox.PublicUrl.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.PublicUrl.parse(undefined)).to.equal undefined
describe 'Dropbox.CopyReference', ->
describe '.parse', ->
describe 'on the API example', ->
beforeEach ->
refData = {
"copy_ref": "z1X6ATl6aWtzOGq0c3g5Ng",
"expires": "Fri, 31 Jan 2042 21:01:05 +0000"
}
@ref = Dropbox.CopyReference.parse refData
it 'parses tag correctly', ->
expect(@ref).to.have.property 'tag'
expect(@ref.tag).to.equal 'z1X6ATl6aWtzOGq0c3g5Ng'
it 'parses expiresAt correctly', ->
expect(@ref).to.have.property 'expiresAt'
expect(@ref.expiresAt).to.be.instanceOf Date
expect([
'Fri, 31 Jan 2042 21:01:05 GMT', # every sane JS platform
'Fri, 31 Jan 2042 21:01:05 UTC' # Internet Explorer
]).to.contain(@ref.expiresAt.toUTCString())
it 'round-trips through json / parse correctly', ->
newRef = Dropbox.CopyReference.parse @ref.json()
expect(newRef).to.deep.equal @ref
describe 'on a reference string', ->
beforeEach ->
rawRef = 'z1X6ATl6aWtzOGq0c3g5Ng'
@ref = Dropbox.CopyReference.parse rawRef
it 'parses tag correctly', ->
expect(@ref).to.have.property 'tag'
expect(@ref.tag).to.equal 'z1X6ATl6aWtzOGq0c3g5Ng'
it 'parses expiresAt correctly', ->
expect(@ref).to.have.property 'expiresAt'
expect(@ref.expiresAt).to.be.instanceOf Date
expect(@ref.expiresAt - (new Date())).to.be.below 1000
it 'round-trips through json / parse correctly', ->
newRef = Dropbox.CopyReference.parse @ref.json()
expect(newRef).to.deep.equal @ref
it 'passes null through', ->
expect(Dropbox.CopyReference.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.CopyReference.parse(undefined)).to.equal undefined

View file

@ -0,0 +1,204 @@
describe 'Dropbox.Stat', ->
describe '.parse', ->
describe 'on the API file example', ->
beforeEach ->
# File example at
# https://www.dropbox.com/developers/reference/api#metadata
metadata = {
"size": "225.4KB",
"rev": "35e97029684fe",
"thumb_exists": true, # Changed to test hasThumbnail=true code.
"bytes": 230783,
"modified": "Tue, 19 Jul 2011 21:55:38 +0000",
"client_mtime": "Mon, 18 Jul 2011 18:04:35 +0000",
"path": "/Getting_Started.pdf",
"is_dir": false,
"icon": "page_white_acrobat",
"root": "app_folder", # Changed to test app_folder code path.
"mime_type": "application/pdf",
"revision": 220823
}
@stat = Dropbox.Stat.parse metadata
it 'parses the path correctly', ->
expect(@stat).to.have.property 'path'
expect(@stat.path).to.equal '/Getting_Started.pdf'
it 'parses name correctly', ->
expect(@stat).to.have.property 'name'
expect(@stat.name).to.equal 'Getting_Started.pdf'
it 'parses inAppFolder corectly', ->
expect(@stat).to.have.property 'inAppFolder'
expect(@stat.inAppFolder).to.equal true
it 'parses isFolder correctly', ->
expect(@stat).to.have.property 'isFolder'
expect(@stat.isFolder).to.equal false
expect(@stat).to.have.property 'isFile'
expect(@stat.isFile).to.equal true
it 'parses isRemoved correctly', ->
expect(@stat).to.have.property 'isRemoved'
expect(@stat.isRemoved).to.equal false
it 'parses typeIcon correctly', ->
expect(@stat).to.have.property 'typeIcon'
expect(@stat.typeIcon).to.equal 'page_white_acrobat'
it 'parses versionTag correctly', ->
expect(@stat).to.have.property 'versionTag'
expect(@stat.versionTag).to.equal '35e97029684fe'
it 'parses mimeType correctly', ->
expect(@stat).to.have.property 'mimeType'
expect(@stat.mimeType).to.equal 'application/pdf'
it 'parses size correctly', ->
expect(@stat).to.have.property 'size'
expect(@stat.size).to.equal 230783
it 'parses humanSize correctly', ->
expect(@stat).to.have.property 'humanSize'
expect(@stat.humanSize).to.equal "225.4KB"
it 'parses hasThumbnail correctly', ->
expect(@stat).to.have.property 'hasThumbnail'
expect(@stat.hasThumbnail).to.equal true
it 'parses modifiedAt correctly', ->
expect(@stat).to.have.property 'modifiedAt'
expect(@stat.modifiedAt).to.be.instanceOf Date
expect([
'Tue, 19 Jul 2011 21:55:38 GMT', # every sane JS platform
'Tue, 19 Jul 2011 21:55:38 UTC' # Internet Explorer
]).to.contain(@stat.modifiedAt.toUTCString())
it 'parses clientModifiedAt correctly', ->
expect(@stat).to.have.property 'clientModifiedAt'
expect(@stat.clientModifiedAt).to.be.instanceOf Date
expect([
'Mon, 18 Jul 2011 18:04:35 GMT', # every sane JS platform
'Mon, 18 Jul 2011 18:04:35 UTC' # Internet Explorer
]).to.contain(@stat.clientModifiedAt.toUTCString())
it 'round-trips through json / parse correctly', ->
newStat = Dropbox.Stat.parse @stat.json()
expect(newStat).to.deep.equal @stat
describe 'on the API directory example', ->
beforeEach ->
# Folder example at
# https://www.dropbox.com/developers/reference/api#metadata
metadata = {
"size": "0 bytes",
"hash": "37eb1ba1849d4b0fb0b28caf7ef3af52",
"bytes": 0,
"thumb_exists": false,
"rev": "714f029684fe",
"modified": "Wed, 27 Apr 2011 22:18:51 +0000",
"path": "/Public",
"is_dir": true,
"is_deleted": true, # Added to test isRemoved=true code path.
"icon": "folder_public",
"root": "dropbox",
"revision": 29007
}
@stat = Dropbox.Stat.parse metadata
it 'parses path correctly', ->
expect(@stat).to.have.property 'path'
expect(@stat.path).to.equal '/Public'
it 'parses name correctly', ->
expect(@stat).to.have.property 'name'
expect(@stat.name).to.equal 'Public'
it 'parses inAppFolder corectly', ->
expect(@stat).to.have.property 'inAppFolder'
expect(@stat.inAppFolder).to.equal false
it 'parses isFolder correctly', ->
expect(@stat).to.have.property 'isFolder'
expect(@stat.isFolder).to.equal true
expect(@stat).to.have.property 'isFile'
expect(@stat.isFile).to.equal false
it 'parses isRemoved correctly', ->
expect(@stat).to.have.property 'isRemoved'
expect(@stat.isRemoved).to.equal true
it 'parses typeIcon correctly', ->
expect(@stat).to.have.property 'typeIcon'
expect(@stat.typeIcon).to.equal 'folder_public'
it 'parses versionTag correctly', ->
expect(@stat).to.have.property 'versionTag'
expect(@stat.versionTag).to.equal '37eb1ba1849d4b0fb0b28caf7ef3af52'
it 'parses mimeType correctly', ->
expect(@stat).to.have.property 'mimeType'
expect(@stat.mimeType).to.equal 'inode/directory'
it 'parses size correctly', ->
expect(@stat).to.have.property 'size'
expect(@stat.size).to.equal 0
it 'parses humanSize correctly', ->
expect(@stat).to.have.property 'humanSize'
expect(@stat.humanSize).to.equal '0 bytes'
it 'parses hasThumbnail correctly', ->
expect(@stat).to.have.property 'hasThumbnail'
expect(@stat.hasThumbnail).to.equal false
it 'parses modifiedAt correctly', ->
expect(@stat).to.have.property 'modifiedAt'
expect(@stat.modifiedAt).to.be.instanceOf Date
expect([
'Wed, 27 Apr 2011 22:18:51 GMT', # every sane JS platform
'Wed, 27 Apr 2011 22:18:51 UTC' # Internet Explorer
]).to.contain(@stat.modifiedAt.toUTCString())
it 'parses missing clientModifiedAt correctly', ->
expect(@stat).to.have.property 'clientModifiedAt'
expect(@stat.clientModifiedAt).to.equal null
it 'round-trips through json / parse correctly', ->
newStat = Dropbox.Stat.parse @stat.json()
expect(newStat).to.deep.equal @stat
it 'passes null through', ->
expect(Dropbox.Stat.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.Stat.parse(undefined)).to.equal undefined
describe 'on a contrived file/path example', ->
beforeEach ->
metadata = {
"size": "225.4KB",
"rev": "35e97029684fe",
"thumb_exists": true, # Changed to test hasThumbnail=true code.
"bytes": 230783,
"modified": "Tue, 19 Jul 2011 21:55:38 +0000",
"client_mtime": "Mon, 18 Jul 2011 18:04:35 +0000",
"path": "path/to/a/file/named/Getting_Started.pdf/",
"is_dir": false,
"icon": "page_white_acrobat",
"root": "app_folder", # Changed to test app_folder code path.
"mime_type": "application/pdf",
"revision": 220823
}
@stat = Dropbox.Stat.parse metadata
it 'parses the path correctly', ->
expect(@stat).to.have.property 'path'
expect(@stat.path).to.equal '/path/to/a/file/named/Getting_Started.pdf'
it 'parses name correctly', ->
expect(@stat).to.have.property 'name'
expect(@stat.name).to.equal 'Getting_Started.pdf'

View file

@ -0,0 +1,105 @@
# Stashes Dropbox access credentials.
class TokenStash
# @param {Object} options the advanced options below
# @option options {Boolean} fullDropbox if true, the returned credentials
# will be good for full Dropbox access; otherwise, the credentials will
# work for Folder access
constructor: (options) ->
@fs = require 'fs'
# Node 0.6 hack.
unless @fs.existsSync
path = require 'path'
@fs.existsSync = (filePath) -> path.existsSync filePath
@getCache = null
@sandbox = !options?.fullDropbox
@setupFs()
# Calls the supplied method with the Dropbox access credentials.
get: (callback) ->
@getCache or= @readStash()
if @getCache
callback @getCache
return null
@liveLogin (fullCredentials, sandboxCredentials) =>
unless fullCredentials and sandboxCredentials
throw new Error('Dropbox API authorization failed')
@writeStash fullCredentials, sandboxCredentials
@getCache = @readStash()
callback @getCache
# Obtains credentials by doing a login on the live site.
liveLogin: (callback) ->
Dropbox = require '../../lib/dropbox'
sandboxClient = new Dropbox.Client @clientOptions().sandbox
fullClient = new Dropbox.Client @clientOptions().full
@setupAuth()
sandboxClient.authDriver @authDriver
sandboxClient.authenticate (error, data) =>
if error
@killAuth()
callback null
return
fullClient.authDriver @authDriver
fullClient.authenticate (error, data) =>
@killAuth()
if error
callback null
return
credentials = @clientOptions()
callback fullClient.credentials(), sandboxClient.credentials()
# Returns the options used to create a Dropbox Client.
clientOptions: ->
{
sandbox:
sandbox: true
key: 'gWJAiHNbmDA=|MJ3xAk3nLeByuOckISnHib+h+1zTCG3TKTOEFvAAZw=='
full:
key: 'OYaTsqx6IXA=|z8mRqmTRoSdCqvkTvHTZq4ZZNxa7I5wM8X5E33IwCA=='
}
# Reads the file containing the access credentials, if it is available.
#
# @return {Object?} parsed access credentials, or null if they haven't been
# stashed
readStash: ->
unless @fs.existsSync @jsonPath
return null
stash = JSON.parse @fs.readFileSync @jsonPath
if @sandbox then stash.sandbox else stash.full
# Stashes the access credentials for future test use.
writeStash: (fullCredentials, sandboxCredentials) ->
json = JSON.stringify full: fullCredentials, sandbox: sandboxCredentials
@fs.writeFileSync @jsonPath, json
js = "window.testKeys = #{JSON.stringify sandboxCredentials};" +
"window.testFullDropboxKeys = #{JSON.stringify fullCredentials};"
@fs.writeFileSync @jsPath, js
# Sets up a node.js server-based authentication driver.
setupAuth: ->
return if @authDriver
Dropbox = require '../../lib/dropbox'
@authDriver = new Dropbox.Drivers.NodeServer
# Shuts down the node.js server behind the authentication server.
killAuth: ->
return unless @authDriver
@authDriver.closeServer()
@authDriver = null
# Sets up the directory structure for the credential stash.
setupFs: ->
@dirPath = 'test/.token'
@jsonPath = 'test/.token/token.json'
@jsPath = 'test/.token/token.js'
unless @fs.existsSync @dirPath
@fs.mkdirSync @dirPath
module.exports = TokenStash

View file

@ -0,0 +1,114 @@
describe 'Dropbox.UploadCursor', ->
describe '.parse', ->
describe 'on the API example', ->
beforeEach ->
cursorData = {
"upload_id": "v0k84B0AT9fYkfMUp0sBTA",
"offset": 31337,
"expires": "Tue, 19 Jul 2011 21:55:38 +0000"
}
@cursor = Dropbox.UploadCursor.parse cursorData
it 'parses tag correctly', ->
expect(@cursor).to.have.property 'tag'
expect(@cursor.tag).to.equal 'v0k84B0AT9fYkfMUp0sBTA'
it 'parses offset correctly', ->
expect(@cursor).to.have.property 'offset'
expect(@cursor.offset).to.equal 31337
it 'parses expiresAt correctly', ->
expect(@cursor).to.have.property 'expiresAt'
expect(@cursor.expiresAt).to.be.instanceOf Date
expect([
'Tue, 19 Jul 2011 21:55:38 GMT', # every sane JS platform
'Tue, 19 Jul 2011 21:55:38 UTC' # Internet Explorer
]).to.contain(@cursor.expiresAt.toUTCString())
it 'round-trips through json / parse correctly', ->
newCursor = Dropbox.UploadCursor.parse @cursor.json()
expect(newCursor).to.deep.equal @cursor
describe 'on a reference string', ->
beforeEach ->
rawRef = 'v0k84B0AT9fYkfMUp0sBTA'
@cursor = Dropbox.UploadCursor.parse rawRef
it 'parses tag correctly', ->
expect(@cursor).to.have.property 'tag'
expect(@cursor.tag).to.equal 'v0k84B0AT9fYkfMUp0sBTA'
it 'parses offset correctly', ->
expect(@cursor).to.have.property 'offset'
expect(@cursor.offset).to.equal 0
it 'parses expiresAt correctly', ->
expect(@cursor).to.have.property 'expiresAt'
expect(@cursor.expiresAt).to.be.instanceOf Date
expect(@cursor.expiresAt - (new Date())).to.be.below 1000
it 'round-trips through json / parse correctly', ->
newCursor = Dropbox.UploadCursor.parse @cursor.json()
newCursor.json() # Get _json populated for newCursor.
expect(newCursor).to.deep.equal @cursor
it 'passes null through', ->
expect(Dropbox.CopyReference.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.CopyReference.parse(undefined)).to.equal undefined
describe '.constructor', ->
describe 'with no arguments', ->
beforeEach ->
@cursor = new Dropbox.UploadCursor
it 'sets up tag correctly', ->
expect(@cursor).to.have.property 'tag'
expect(@cursor.tag).to.equal null
it 'parses offset correctly', ->
expect(@cursor).to.have.property 'offset'
expect(@cursor.offset).to.equal 0
it 'parses expiresAt correctly', ->
expect(@cursor).to.have.property 'expiresAt'
expect(@cursor.expiresAt - (new Date())).to.be.below 1000
it 'round-trips through json / parse correctly', ->
newCursor = Dropbox.UploadCursor.parse @cursor.json()
newCursor.json() # Get _json populated for newCursor.
expect(newCursor).to.deep.equal @cursor
describe '.replace', ->
beforeEach ->
@cursor = new Dropbox.UploadCursor
describe 'on the API example', ->
beforeEach ->
cursorData = {
"upload_id": "v0k84B0AT9fYkfMUp0sBTA",
"offset": 31337,
"expires": "Tue, 19 Jul 2011 21:55:38 +0000"
}
@cursor.replace cursorData
it 'parses tag correctly', ->
expect(@cursor).to.have.property 'tag'
expect(@cursor.tag).to.equal 'v0k84B0AT9fYkfMUp0sBTA'
it 'parses offset correctly', ->
expect(@cursor).to.have.property 'offset'
expect(@cursor.offset).to.equal 31337
it 'parses expiresAt correctly', ->
expect(@cursor).to.have.property 'expiresAt'
expect(@cursor.expiresAt).to.be.instanceOf Date
expect([
'Tue, 19 Jul 2011 21:55:38 GMT', # every sane JS platform
'Tue, 19 Jul 2011 21:55:38 UTC' # Internet Explorer
]).to.contain(@cursor.expiresAt.toUTCString())
it 'round-trips through json / parse correctly', ->
newCursor = Dropbox.UploadCursor.parse @cursor.json()
expect(newCursor).to.deep.equal @cursor

View file

@ -0,0 +1,95 @@
describe 'Dropbox.UserInfo', ->
describe '.parse', ->
describe 'on the API example', ->
beforeEach ->
userData = {
"referral_link": "https://www.dropbox.com/referrals/r1a2n3d4m5s6t7",
"display_name": "John P. User",
"uid": 12345678,
"country": "US",
"quota_info": {
"shared": 253738410565,
"quota": 107374182400000,
"normal": 680031877871
},
"email": "johnpuser@company.com" # Added to reflect real responses.
}
@userInfo = Dropbox.UserInfo.parse userData
it 'parses name correctly', ->
expect(@userInfo).to.have.property 'name'
expect(@userInfo.name).to.equal 'John P. User'
it 'parses email correctly', ->
expect(@userInfo).to.have.property 'email'
expect(@userInfo.email).to.equal 'johnpuser@company.com'
it 'parses countryCode correctly', ->
expect(@userInfo).to.have.property 'countryCode'
expect(@userInfo.countryCode).to.equal 'US'
it 'parses uid correctly', ->
expect(@userInfo).to.have.property 'uid'
expect(@userInfo.uid).to.equal '12345678'
it 'parses referralUrl correctly', ->
expect(@userInfo).to.have.property 'referralUrl'
expect(@userInfo.referralUrl).to.
equal 'https://www.dropbox.com/referrals/r1a2n3d4m5s6t7'
it 'parses quota correctly', ->
expect(@userInfo).to.have.property 'quota'
expect(@userInfo.quota).to.equal 107374182400000
it 'parses usedQuota correctly', ->
expect(@userInfo).to.have.property 'usedQuota'
expect(@userInfo.usedQuota).to.equal 933770288436
it 'parses privateBytes correctly', ->
expect(@userInfo).to.have.property 'privateBytes'
expect(@userInfo.privateBytes).to.equal 680031877871
it 'parses sharedBytes correctly', ->
expect(@userInfo).to.have.property 'usedQuota'
expect(@userInfo.sharedBytes).to.equal 253738410565
it 'parses publicAppUrl correctly', ->
expect(@userInfo.publicAppUrl).to.equal null
it 'round-trips through json / parse correctly', ->
newInfo = Dropbox.UserInfo.parse @userInfo.json()
expect(newInfo).to.deep.equal @userInfo
it 'passes null through', ->
expect(Dropbox.UserInfo.parse(null)).to.equal null
it 'passes undefined through', ->
expect(Dropbox.UserInfo.parse(undefined)).to.equal undefined
describe 'on real data from a "public app folder" application', ->
beforeEach ->
userData = {
"referral_link": "https://www.dropbox.com/referrals/NTM1OTg4MTA5",
"display_name": "Victor Costan",
"uid": 87654321, # Anonymized.
"public_app_url": "https://dl-web.dropbox.com/spa/90vw6zlu4268jh4/",
"country": "US",
"quota_info": {
"shared": 6074393565,
"quota": 73201090560,
"normal": 4684642723
},
"email": "spam@gmail.com" # Anonymized.
}
@userInfo = Dropbox.UserInfo.parse userData
it 'parses publicAppUrl correctly', ->
expect(@userInfo.publicAppUrl).to.
equal 'https://dl-web.dropbox.com/spa/90vw6zlu4268jh4'
it 'round-trips through json / parse correctly', ->
newInfo = Dropbox.UserInfo.parse @userInfo.json()
expect(newInfo).to.deep.equal @userInfo

View file

@ -0,0 +1,54 @@
express = require 'express'
fs = require 'fs'
https = require 'https'
open = require 'open'
# Tiny express.js server for the Web files.
class WebFileServer
# Starts up a HTTP server.
constructor: (@port = 8911) ->
@createApp()
# Opens the test URL in a browser.
openBrowser: (appName) ->
open @testUrl(), appName
# The URL that should be used to start the tests.
testUrl: ->
"https://localhost:#{@port}/test/html/browser_test.html"
# The server code.
createApp: ->
@app = express()
@app.get '/diediedie', (request, response) =>
if 'failed' of request.query
failed = parseInt request.query['failed']
else
failed = 1
total = parseInt request.query['total'] || 0
passed = total - failed
exitCode = if failed == 0 then 0 else 1
console.log "#{passed} passed, #{failed} failed"
response.header 'Content-Type', 'image/png'
response.header 'Content-Length', '0'
response.end ''
unless 'NO_EXIT' of process.env
@server.close()
process.exit exitCode
@app.use (request, response, next) ->
response.header 'Access-Control-Allow-Origin', '*'
response.header 'Access-Control-Allow-Methods', 'DELETE,GET,POST,PUT'
response.header 'Access-Control-Allow-Headers',
'Content-Type, Authorization'
next()
@app.use express.static(fs.realpathSync(__dirname + '/../../'),
{ hidden: true })
options = key: fs.readFileSync 'test/ssl/cert.pem'
options.cert = options.key
@server = https.createServer(options, @app)
@server.listen @port
module.exports = new WebFileServer

View file

@ -0,0 +1,644 @@
describe 'Dropbox.Xhr', ->
beforeEach ->
@node_js = module? and module?.exports? and require?
@oauth = new Dropbox.Oauth testKeys
describe 'with a GET', ->
beforeEach ->
@xhr = new Dropbox.Xhr 'GET', 'https://request.url'
it 'initializes correctly', ->
expect(@xhr.isGet).to.equal true
expect(@xhr.method).to.equal 'GET'
expect(@xhr.url).to.equal 'https://request.url'
expect(@xhr.preflight).to.equal false
describe '#setHeader', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
it 'adds a HTTP header header', ->
expect(@xhr.headers).to.have.property 'Range'
expect(@xhr.headers['Range']).to.equal 'bytes=0-1000'
it 'does not work twice for the same header', ->
expect(=> @xhr.setHeader('Range', 'bytes=0-1000')).to.throw Error
it 'flags the Xhr as needing preflight', ->
expect(@xhr.preflight).to.equal true
it 'rejects Content-Type', ->
expect(=> @xhr.setHeader('Content-Type', 'text/plain')).to.throw Error
describe '#setParams', ->
beforeEach ->
@xhr.setParams 'param 1': true, 'answer': 42
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
it 'does not work twice', ->
expect(=> @xhr.setParams 'answer': 43).to.throw Error
describe '#paramsToUrl', ->
beforeEach ->
@xhr.paramsToUrl()
it 'changes the url', ->
expect(@xhr.url).to.
equal 'https://request.url?answer=42&param%201=true'
it 'sets params to null', ->
expect(@xhr.params).to.equal null
describe '#paramsToBody', ->
it 'throws an error', ->
expect(=> @xhr.paramsToBody()).to.throw Error
describe '#addOauthParams', ->
beforeEach ->
@xhr.addOauthParams @oauth
it 'keeps existing params', ->
expect(@xhr.params).to.have.property 'answer'
expect(@xhr.params.answer).to.equal 42
it 'adds an oauth_signature param', ->
expect(@xhr.params).to.have.property 'oauth_signature'
it 'does not add an Authorization header', ->
expect(@xhr.headers).not.to.have.property 'Authorization'
it 'does not work twice', ->
expect(=> @xhr.addOauthParams()).to.throw Error
describe '#addOauthHeader', ->
beforeEach ->
@xhr.addOauthHeader @oauth
it 'keeps existing params', ->
expect(@xhr.params).to.have.property 'answer'
expect(@xhr.params.answer).to.equal 42
it 'does not add an oauth_signature param', ->
expect(@xhr.params).not.to.have.property 'oauth_signature'
it 'adds an Authorization header', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#addOauthParams without params', ->
beforeEach ->
@xhr.addOauthParams @oauth
it 'adds an oauth_signature param', ->
expect(@xhr.params).to.have.property 'oauth_signature'
describe '#addOauthHeader without params', ->
beforeEach ->
@xhr.addOauthHeader @oauth
it 'adds an Authorization header', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#signWithOauth', ->
describe 'for a request that does not need preflight', ->
beforeEach ->
@xhr.signWithOauth @oauth
if Dropbox.Xhr.doesPreflight
it 'uses addOauthParams', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader in node.js', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'for a request that needs preflight', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
@xhr.signWithOauth @oauth
if Dropbox.Xhr.ieXdr # IE's XDR doesn't do HTTP headers.
it 'uses addOauthParams in IE', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'with cacheFriendly: true', ->
describe 'for a request that does not need preflight', ->
beforeEach ->
@xhr.signWithOauth @oauth, true
if Dropbox.Xhr.ieXdr
it 'uses addOauthParams in IE', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'for a request that needs preflight', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
@xhr.signWithOauth @oauth, true
if Dropbox.Xhr.ieXdr # IE's XDR doesn't do HTTP headers.
it 'uses addOauthParams in IE', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#setFileField', ->
it 'throws an error', ->
expect(=> @xhr.setFileField('file', 'filename.bin', '<p>File Data</p>',
'text/html')).to.throw Error
describe '#setBody', ->
it 'throws an error', ->
expect(=> @xhr.setBody('body data')).to.throw Error
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#setResponseType', ->
beforeEach ->
@xhr.setResponseType 'b'
it 'changes responseType', ->
expect(@xhr.responseType).to.equal 'b'
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#prepare with params', ->
beforeEach ->
@xhr.setParams answer: 42
@xhr.prepare()
it 'creates the native xhr', ->
expect(typeof @xhr.xhr).to.equal 'object'
it 'opens the native xhr', ->
return if Dropbox.Xhr.ieXdr # IE's XDR doesn't do readyState.
expect(@xhr.xhr.readyState).to.equal 1
it 'pushes the params in the url', ->
expect(@xhr.url).to.equal 'https://request.url?answer=42'
describe 'with a POST', ->
beforeEach ->
@xhr = new Dropbox.Xhr 'POST', 'https://request.url'
it 'initializes correctly', ->
expect(@xhr.isGet).to.equal false
expect(@xhr.method).to.equal 'POST'
expect(@xhr.url).to.equal 'https://request.url'
expect(@xhr.preflight).to.equal false
describe '#setHeader', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
it 'adds a HTTP header header', ->
expect(@xhr.headers).to.have.property 'Range'
expect(@xhr.headers['Range']).to.equal 'bytes=0-1000'
it 'does not work twice for the same header', ->
expect(=> @xhr.setHeader('Range', 'bytes=0-1000')).to.throw Error
it 'flags the Xhr as needing preflight', ->
expect(@xhr.preflight).to.equal true
it 'rejects Content-Type', ->
expect(=> @xhr.setHeader('Content-Type', 'text/plain')).to.throw Error
describe '#setParams', ->
beforeEach ->
@xhr.setParams 'param 1': true, 'answer': 42
it 'does not work twice', ->
expect(=> @xhr.setParams 'answer': 43).to.throw Error
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#paramsToUrl', ->
beforeEach ->
@xhr.paramsToUrl()
it 'changes the url', ->
expect(@xhr.url).to.
equal 'https://request.url?answer=42&param%201=true'
it 'sets params to null', ->
expect(@xhr.params).to.equal null
it 'does not set the body', ->
expect(@xhr.body).to.equal null
describe '#paramsToBody', ->
beforeEach ->
@xhr.paramsToBody()
it 'url-encodes the params', ->
expect(@xhr.body).to.equal 'answer=42&param%201=true'
it 'sets the Content-Type header', ->
expect(@xhr.headers).to.have.property 'Content-Type'
expect(@xhr.headers['Content-Type']).to.
equal 'application/x-www-form-urlencoded'
it 'does not change the url', ->
expect(@xhr.url).to.equal 'https://request.url'
it 'does not work twice', ->
@xhr.setParams answer: 43
expect(=> @xhr.paramsToBody()).to.throw Error
describe '#addOauthParams', ->
beforeEach ->
@xhr.addOauthParams @oauth
it 'keeps existing params', ->
expect(@xhr.params).to.have.property 'answer'
expect(@xhr.params.answer).to.equal 42
it 'adds an oauth_signature param', ->
expect(@xhr.params).to.have.property 'oauth_signature'
it 'does not add an Authorization header', ->
expect(@xhr.headers).not.to.have.property 'Authorization'
it 'does not work twice', ->
expect(=> @xhr.addOauthParams()).to.throw Error
describe '#addOauthHeader', ->
beforeEach ->
@xhr.addOauthHeader @oauth
it 'keeps existing params', ->
expect(@xhr.params).to.have.property 'answer'
expect(@xhr.params.answer).to.equal 42
it 'does not add an oauth_signature param', ->
expect(@xhr.params).not.to.have.property 'oauth_signature'
it 'adds an Authorization header', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#addOauthParams without params', ->
beforeEach ->
@xhr.addOauthParams @oauth
it 'adds an oauth_signature param', ->
expect(@xhr.params).to.have.property 'oauth_signature'
describe '#addOauthHeader without params', ->
beforeEach ->
@xhr.addOauthHeader @oauth
it 'adds an Authorization header', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#signWithOauth', ->
describe 'for a request that does not need preflight', ->
beforeEach ->
@xhr.signWithOauth @oauth
if Dropbox.Xhr.doesPreflight
it 'uses addOauthParams', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader in node.js', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'for a request that needs preflight', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
@xhr.signWithOauth @oauth
if Dropbox.Xhr.ieXdr # IE's XDR doesn't do HTTP headers.
it 'uses addOauthParams in IE', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'with cacheFriendly: true', ->
describe 'for a request that does not need preflight', ->
beforeEach ->
@xhr.signWithOauth @oauth, true
if Dropbox.Xhr.doesPreflight
it 'uses addOauthParams', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader in node.js', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe 'for a request that needs preflight', ->
beforeEach ->
@xhr.setHeader 'Range', 'bytes=0-1000'
@xhr.signWithOauth @oauth, true
if Dropbox.Xhr.ieXdr # IE's XDR doesn't do HTTP headers.
it 'uses addOauthParams in IE', ->
expect(@xhr.params).to.have.property 'oauth_signature'
else
it 'uses addOauthHeader', ->
expect(@xhr.headers).to.have.property 'Authorization'
describe '#setFileField with a String', ->
beforeEach ->
@nonceStub = sinon.stub @xhr, 'multipartBoundary'
@nonceStub.returns 'multipart----boundary'
@xhr.setFileField 'file', 'filename.bin', '<p>File Data</p>',
'text/html'
afterEach ->
@nonceStub.restore()
it 'sets the Content-Type header', ->
expect(@xhr.headers).to.have.property 'Content-Type'
expect(@xhr.headers['Content-Type']).to.
equal 'multipart/form-data; boundary=multipart----boundary'
it 'sets the body', ->
expect(@xhr.body).to.equal("""--multipart----boundary\r
Content-Disposition: form-data; name="file"; filename="filename.bin"\r
Content-Type: text/html\r
Content-Transfer-Encoding: binary\r
\r
<p>File Data</p>\r
--multipart----boundary--\r\n
""")
it 'does not work twice', ->
expect(=> @xhr.setFileField('file', 'filename.bin', '<p>File Data</p>',
'text/html')).to.throw Error
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#setBody with a string', ->
beforeEach ->
@xhr.setBody 'body data'
it 'sets the request body', ->
expect(@xhr.body).to.equal 'body data'
it 'does not work twice', ->
expect(=> @xhr.setBody('body data')).to.throw Error
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#setBody with FormData', ->
beforeEach ->
if FormData?
formData = new FormData()
formData.append 'name', 'value'
@xhr.setBody formData
it 'does not flag the XHR as needing preflight', ->
return unless FormData?
expect(@xhr.preflight).to.equal false
describe '#setBody with Blob', ->
beforeEach ->
if Blob?
blob = new Blob ["abcdef"], type: 'image/png'
@xhr.setBody blob
it 'flags the XHR as needing preflight', ->
return unless Blob?
expect(@xhr.preflight).to.equal true
it 'sets the Content-Type header', ->
return unless Blob?
expect(@xhr.headers).to.have.property 'Content-Type'
expect(@xhr.headers['Content-Type']).to.
equal 'application/octet-stream'
describe '#setBody with ArrayBuffer', ->
beforeEach ->
if ArrayBuffer?
buffer = new ArrayBuffer 5
@xhr.setBody buffer
it 'flags the XHR as needing preflight', ->
return unless ArrayBuffer?
expect(@xhr.preflight).to.equal true
it 'sets the Content-Type header', ->
return unless ArrayBuffer?
expect(@xhr.headers).to.have.property 'Content-Type'
expect(@xhr.headers['Content-Type']).to.
equal 'application/octet-stream'
describe '#setBody with ArrayBufferView', ->
beforeEach ->
if Uint8Array?
view = new Uint8Array 5
@xhr.setBody view
it 'flags the XHR as needing preflight', ->
return unless Uint8Array?
expect(@xhr.preflight).to.equal true
it 'sets the Content-Type header', ->
return unless Uint8Array?
expect(@xhr.headers).to.have.property 'Content-Type'
expect(@xhr.headers['Content-Type']).to.
equal 'application/octet-stream'
describe '#setResponseType', ->
beforeEach ->
@xhr.setResponseType 'b'
it 'changes responseType', ->
expect(@xhr.responseType).to.equal 'b'
it 'does not flag the XHR as needing preflight', ->
expect(@xhr.preflight).to.equal false
describe '#prepare with params', ->
beforeEach ->
@xhr.setParams answer: 42
@xhr.prepare()
it 'creates the native xhr', ->
expect(typeof @xhr.xhr).to.equal 'object'
it 'opens the native xhr', ->
return if Dropbox.Xhr.ieXdr # IE's XDR doesn't do readyState.
expect(@xhr.xhr.readyState).to.equal 1
if Dropbox.Xhr.ieXdr
it 'keeps the params in the URL in IE', ->
expect(@xhr.url).to.equal 'https://request.url?answer=42'
expect(@xhr.body).to.equal null
else
it 'pushes the params in the body', ->
expect(@xhr.body).to.equal 'answer=42'
describe 'with a PUT', ->
beforeEach ->
@xhr = new Dropbox.Xhr 'PUT', 'https://request.url'
it 'initializes correctly', ->
expect(@xhr.isGet).to.equal false
expect(@xhr.method).to.equal 'PUT'
expect(@xhr.url).to.equal 'https://request.url'
expect(@xhr.preflight).to.equal true
describe '#send', ->
it 'reports errors correctly', (done) ->
@url = 'https://api.dropbox.com/1/oauth/request_token'
@xhr = new Dropbox.Xhr 'POST', @url
@xhr.prepare().send (error, data) =>
expect(data).to.equal undefined
expect(error).to.be.instanceOf Dropbox.ApiError
expect(error).to.have.property 'url'
expect(error.url).to.equal @url
expect(error).to.have.property 'method'
expect(error.method).to.equal 'POST'
unless Dropbox.Xhr.ieXdr # IE's XDR doesn't do HTTP status codes.
expect(error).to.have.property 'status'
expect(error.status).to.equal 401 # Bad OAuth request.
expect(error).to.have.property 'responseText'
expect(error.responseText).to.be.a 'string'
unless Dropbox.Xhr.ieXdr # IE's XDR hides the HTTP body on error.
expect(error).to.have.property 'response'
expect(error.response).to.be.an 'object'
expect(error.toString()).to.match /^Dropbox API error/
expect(error.toString()).to.contain 'POST'
expect(error.toString()).to.contain @url
done()
it 'reports errors correctly when onError is set', (done) ->
@url = 'https://api.dropbox.com/1/oauth/request_token'
@xhr = new Dropbox.Xhr 'POST', @url
@xhr.onError = new Dropbox.EventSource
listenerError = null
@xhr.onError.addListener (error) -> listenerError = error
@xhr.prepare().send (error, data) =>
expect(data).to.equal undefined
expect(error).to.be.instanceOf Dropbox.ApiError
expect(error).to.have.property 'url'
expect(error.url).to.equal @url
expect(error).to.have.property 'method'
expect(error.method).to.equal 'POST'
expect(listenerError).to.equal error
done()
it 'processes data correctly', (done) ->
xhr = new Dropbox.Xhr 'POST',
'https://api.dropbox.com/1/oauth/request_token',
xhr.addOauthParams @oauth
xhr.prepare().send (error, data) ->
expect(error).to.not.be.ok
expect(data).to.have.property 'oauth_token'
expect(data).to.have.property 'oauth_token_secret'
done()
it 'processes data correctly when using setCallback', (done) ->
xhr = new Dropbox.Xhr 'POST',
'https://api.dropbox.com/1/oauth/request_token',
xhr.addOauthParams @oauth
xhr.setCallback (error, data) ->
expect(error).to.not.be.ok
expect(data).to.have.property 'oauth_token'
expect(data).to.have.property 'oauth_token_secret'
done()
xhr.prepare().send()
it 'sends Authorize headers correctly', (done) ->
return done() if Dropbox.Xhr.ieXdr # IE's XDR doesn't set headers.
xhr = new Dropbox.Xhr 'POST',
'https://api.dropbox.com/1/oauth/request_token',
xhr.addOauthHeader @oauth
xhr.prepare().send (error, data) ->
expect(error).to.equal null
expect(data).to.have.property 'oauth_token'
expect(data).to.have.property 'oauth_token_secret'
done()
describe 'with a binary response', ->
beforeEach ->
testImageServerOn()
@xhr = new Dropbox.Xhr 'GET', testImageUrl
afterEach ->
testImageServerOff()
describe 'with responseType b', ->
beforeEach ->
@xhr.setResponseType 'b'
it 'retrieves a string where each character is a byte', (done) ->
@xhr.prepare().send (error, data) ->
expect(error).to.not.be.ok
expect(data).to.be.a 'string'
expect(data).to.equal testImageBytes
done()
describe 'with responseType arraybuffer', ->
beforeEach ->
@xhr.setResponseType 'arraybuffer'
it 'retrieves a well-formed ArrayBuffer', (done) ->
# Skip this test on node.js and IE 9 and below
return done() unless ArrayBuffer?
@xhr.prepare().send (error, buffer) ->
expect(error).to.not.be.ok
expect(buffer).to.be.instanceOf ArrayBuffer
view = new Uint8Array buffer
length = buffer.byteLength
bytes = (String.fromCharCode view[i] for i in [0...length]).
join('')
expect(bytes).to.equal testImageBytes
done()
describe 'with responseType blob', ->
beforeEach ->
@xhr.setResponseType 'blob'
it 'retrieves a well-formed Blob', (done) ->
# Skip this test on node.js and IE 9 and below
return done() unless Blob?
@xhr.prepare().send (error, blob) ->
expect(error).to.not.be.ok
expect(blob).to.be.instanceOf Blob
reader = new FileReader
reader.onloadend = ->
return unless reader.readyState == FileReader.DONE
buffer = reader.result
view = new Uint8Array buffer
length = buffer.byteLength
bytes = (String.fromCharCode view[i] for i in [0...length]).
join('')
expect(bytes).to.equal testImageBytes
done()
reader.readAsArrayBuffer blob
describe '#urlEncode', ->
it 'iterates properly', ->
expect(Dropbox.Xhr.urlEncode({foo: 'bar', baz: 5})).to.
equal 'baz=5&foo=bar'
it 'percent-encodes properly', ->
expect(Dropbox.Xhr.urlEncode({'a +x()': "*b'"})).to.
equal 'a%20%2Bx%28%29=%2Ab%27'
describe '#urlDecode', ->
it 'iterates properly', ->
decoded = Dropbox.Xhr.urlDecode('baz=5&foo=bar')
expect(decoded['baz']).to.equal '5'
expect(decoded['foo']).to.equal 'bar'
it 'percent-decodes properly', ->
decoded = Dropbox.Xhr.urlDecode('a%20%2Bx%28%29=%2Ab%27')
expect(decoded['a +x()']).to.equal "*b'"