updated dropbox library to v.0.8.1.
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
45
lib/client/storage/dropbox/.npmignore
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
lib/client/storage/dropbox/samples/checkbox.js/README.md
Normal 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 task’s 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.
|
||||
|
|
@ -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'
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 597 B |
|
After Width: | Height: | Size: 124 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -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"> </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>
|
||||
2
lib/client/storage/dropbox/samples/checkbox.js/public/lib/jquery.js
vendored
Normal 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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
342
lib/client/storage/dropbox/src/drivers-browser.coffee
Normal 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()
|
||||
202
lib/client/storage/dropbox/src/drivers-chrome.coffee
Normal 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
|
||||
87
lib/client/storage/dropbox/src/drivers-node.coffee
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
71
lib/client/storage/dropbox/src/event_source.coffee
Normal 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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 '/'
|
||||
|
|
|
|||
59
lib/client/storage/dropbox/src/upload_cursor.coffee
Normal 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
|
||||
@
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
33
lib/client/storage/dropbox/test/app_icon/hazard.svg
Normal 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 |
BIN
lib/client/storage/dropbox/test/app_icon/hazard128.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
lib/client/storage/dropbox/test/app_icon/hazard16.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
lib/client/storage/dropbox/test/app_icon/hazard64.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
14
lib/client/storage/dropbox/test/app_icon/radiation.svg
Normal 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 |
BIN
lib/client/storage/dropbox/test/app_icon/radiation128.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
lib/client/storage/dropbox/test/app_icon/radiation16.png
Normal file
|
After Width: | Height: | Size: 324 B |
BIN
lib/client/storage/dropbox/test/app_icon/radiation64.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lib/client/storage/dropbox/test/binary/dropbox.png
Normal file
|
After Width: | Height: | Size: 854 B |
|
|
@ -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 |
BIN
lib/client/storage/dropbox/test/chrome_app/images/icon128.png
Normal file
|
After Width: | Height: | Size: 1,017 B |
BIN
lib/client/storage/dropbox/test/chrome_app/images/icon16.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
lib/client/storage/dropbox/test/chrome_app/images/icon48.png
Normal file
|
After Width: | Height: | Size: 421 B |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib/client/storage/dropbox/test/chrome_extension/README.md
Normal 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.
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# Content script for Dropbox OAuth receiver pages.
|
||||
|
||||
message = type: 'close', url: window.location.href
|
||||
chrome.extension.sendMessage message
|
||||
|
After Width: | Height: | Size: 611 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 745 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 5 KiB |
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
lib/client/storage/dropbox/test/html/browser_test.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
14
lib/client/storage/dropbox/test/html/oauth_receiver.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
11
lib/client/storage/dropbox/test/src/base64_test.coffee
Normal 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=='
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
mocha.setup ui: 'bdd', slow: 150, timeout: 10000
|
||||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1 @@
|
|||
Dropbox.Drivers.Chrome.oauthReceiver()
|
||||
1492
lib/client/storage/dropbox/test/src/client_test.coffee
Normal file
446
lib/client/storage/dropbox/test/src/drivers_test.coffee
Normal 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()
|
||||
144
lib/client/storage/dropbox/test/src/event_source_test.coffee
Normal 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
|
||||
55
lib/client/storage/dropbox/test/src/helper.coffee
Normal 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
|
||||
22
lib/client/storage/dropbox/test/src/hmac_test.coffee
Normal 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='
|
||||
|
||||
124
lib/client/storage/dropbox/test/src/oauth_test.coffee
Normal 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)
|
||||
|
||||
128
lib/client/storage/dropbox/test/src/pulled_changes_test.coffee
Normal 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
|
||||
|
||||
|
||||
92
lib/client/storage/dropbox/test/src/references_test.coffee
Normal 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
|
||||
|
||||
204
lib/client/storage/dropbox/test/src/stat_test.coffee
Normal 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'
|
||||
|
||||
|
||||
105
lib/client/storage/dropbox/test/src/token_stash.coffee
Normal 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
|
||||
114
lib/client/storage/dropbox/test/src/upload_cursor_test.coffee
Normal 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
|
||||
95
lib/client/storage/dropbox/test/src/user_info_test.coffee
Normal 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
|
||||
|
||||
|
||||
54
lib/client/storage/dropbox/test/src/web_file_server.coffee
Normal 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
|
||||
644
lib/client/storage/dropbox/test/src/xhr_test.coffee
Normal 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¶m%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¶m%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¶m%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'"
|
||||
|
||||