mirror of https://github.com/keeweb/keeweb.git
Dropbox V2 API endpoints
This commit is contained in:
parent
8ee596b067
commit
8cac1b8cdd
|
@ -45,7 +45,7 @@ module.exports = function(grunt) {
|
|||
const webpackConfig = {
|
||||
entry: {
|
||||
app: 'app',
|
||||
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver', 'qrcode',
|
||||
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'pikaday', 'filesaver', 'qrcode',
|
||||
'argon2-asm', 'argon2-wasm', 'argon2']
|
||||
},
|
||||
output: {
|
||||
|
@ -68,7 +68,6 @@ module.exports = function(grunt) {
|
|||
jquery: 'jquery/dist/jquery.min.js',
|
||||
hbs: path.resolve(__dirname, 'node_modules', 'handlebars/runtime.js'),
|
||||
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
|
||||
dropbox: 'dropbox/lib/dropbox.min.js',
|
||||
baron: 'baron/baron.min.js',
|
||||
pikaday: 'pikaday/pikaday.js',
|
||||
filesaver: 'FileSaver.js/FileSaver.min.js',
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
const DropboxLink = require('./dropbox-link');
|
||||
|
||||
const AuthReceiver = {
|
||||
receive: function() {
|
||||
const opener = window.opener || window.parent;
|
||||
if (location.href.indexOf('state=') >= 0) {
|
||||
DropboxLink.receive();
|
||||
} else {
|
||||
const message = this.urlArgsToMessage(window.location.href);
|
||||
opener.postMessage(message, window.location.origin);
|
||||
window.close();
|
||||
}
|
||||
const message = this.urlArgsToMessage(window.location.href);
|
||||
opener.postMessage(message, window.location.origin);
|
||||
window.close();
|
||||
},
|
||||
|
||||
urlArgsToMessage: function(url) {
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
const AppSettingsModel = require('../models/app-settings-model');
|
||||
|
||||
const ChooserAppKey = 'qp7ctun6qt5n9d6';
|
||||
|
||||
const DropboxChooser = function(callback) {
|
||||
this.cb = callback;
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.callback = function(err, res) {
|
||||
if (this.cb) {
|
||||
this.cb(err, res);
|
||||
}
|
||||
this.cb = null;
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.choose = function() {
|
||||
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
|
||||
const url = this.buildUrl();
|
||||
this.popup = window.open(url, 'dropbox', windowFeatures);
|
||||
if (!this.popup) {
|
||||
return this.callback('Failed to open window');
|
||||
}
|
||||
window.addEventListener('message', this.onMessage);
|
||||
this.closeInt = setInterval(this.checkClose.bind(this), 200);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.buildUrl = function() {
|
||||
const urlParams = {
|
||||
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
|
||||
'app_key': AppSettingsModel.instance.get('dropboxAppKey') || ChooserAppKey,
|
||||
'link_type': 'direct',
|
||||
trigger: 'js',
|
||||
multiselect: 'false',
|
||||
extensions: '',
|
||||
folderselect: 'false',
|
||||
iframe: 'false',
|
||||
version: 2
|
||||
};
|
||||
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(key => key + '=' + urlParams[key]).join('&');
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.onMessage = function(e) {
|
||||
if (e.source !== this.popup) {
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(e.data);
|
||||
switch (data.method) {
|
||||
case 'origin_request':
|
||||
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
|
||||
break;
|
||||
case 'files_selected':
|
||||
this.popup.close();
|
||||
this.success(data.params);
|
||||
break;
|
||||
case 'close_dialog':
|
||||
this.popup.close();
|
||||
break;
|
||||
case 'web_session_error':
|
||||
case 'web_session_unlinked':
|
||||
this.callback(data.method);
|
||||
break;
|
||||
case 'resize':
|
||||
this.popup.resize(data.params);
|
||||
break;
|
||||
case 'error':
|
||||
this.callback(data.params);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.checkClose = function() {
|
||||
if (this.popup.closed) {
|
||||
clearInterval(this.closeInt);
|
||||
window.removeEventListener('message', this.onMessage);
|
||||
if (!this.result) {
|
||||
this.callback('closed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.success = function(params) {
|
||||
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
|
||||
return this.callback('bad result');
|
||||
}
|
||||
this.result = params[0];
|
||||
this.readFile(this.result.link);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.readFile = function(url) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', () => {
|
||||
this.callback(null, { name: this.result.name, data: xhr.response });
|
||||
});
|
||||
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
|
||||
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
module.exports = DropboxChooser;
|
|
@ -1,331 +0,0 @@
|
|||
const Dropbox = require('dropbox');
|
||||
const Alerts = require('./alerts');
|
||||
const Launcher = require('./launcher');
|
||||
const Logger = require('../util/logger');
|
||||
const Locale = require('../util/locale');
|
||||
const UrlUtil = require('../util/url-util');
|
||||
const AppSettingsModel = require('../models/app-settings-model');
|
||||
|
||||
const logger = new Logger('dropbox');
|
||||
|
||||
const DropboxKeys = {
|
||||
AppFolder: 'qp7ctun6qt5n9d6',
|
||||
FullDropbox: 'eor7hvv6u6oslq9'
|
||||
};
|
||||
|
||||
const DropboxCustomErrors = {
|
||||
BadKey: 'bad-key'
|
||||
};
|
||||
|
||||
function getKey() {
|
||||
return AppSettingsModel.instance.get('dropboxAppKey') || DropboxKeys.AppFolder;
|
||||
}
|
||||
|
||||
const DropboxChooser = function(callback) {
|
||||
this.cb = callback;
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.callback = function(err, res) {
|
||||
if (this.cb) {
|
||||
this.cb(err, res);
|
||||
}
|
||||
this.cb = null;
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.choose = function() {
|
||||
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
|
||||
const url = this.buildUrl();
|
||||
this.popup = window.open(url, 'dropbox', windowFeatures);
|
||||
if (!this.popup) {
|
||||
return this.callback('Failed to open window');
|
||||
}
|
||||
window.addEventListener('message', this.onMessage);
|
||||
this.closeInt = setInterval(this.checkClose.bind(this), 200);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.buildUrl = function() {
|
||||
const urlParams = {
|
||||
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
|
||||
'app_key': getKey(),
|
||||
'link_type': 'direct',
|
||||
trigger: 'js',
|
||||
multiselect: 'false',
|
||||
extensions: '',
|
||||
folderselect: 'false',
|
||||
iframe: 'false',
|
||||
version: 2
|
||||
};
|
||||
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(key => {
|
||||
return key + '=' + urlParams[key];
|
||||
}).join('&');
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.onMessage = function(e) {
|
||||
if (e.source !== this.popup) {
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(e.data);
|
||||
switch (data.method) {
|
||||
case 'origin_request':
|
||||
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
|
||||
break;
|
||||
case 'files_selected':
|
||||
this.popup.close();
|
||||
this.success(data.params);
|
||||
break;
|
||||
case 'close_dialog':
|
||||
this.popup.close();
|
||||
break;
|
||||
case 'web_session_error':
|
||||
case 'web_session_unlinked':
|
||||
this.callback(data.method);
|
||||
break;
|
||||
case 'resize':
|
||||
this.popup.resize(data.params);
|
||||
break;
|
||||
case 'error':
|
||||
this.callback(data.params);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.checkClose = function() {
|
||||
if (this.popup.closed) {
|
||||
clearInterval(this.closeInt);
|
||||
window.removeEventListener('message', this.onMessage);
|
||||
if (!this.result) {
|
||||
this.callback('closed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.success = function(params) {
|
||||
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
|
||||
return this.callback('bad result');
|
||||
}
|
||||
this.result = params[0];
|
||||
this.readFile(this.result.link);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.readFile = function(url) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', () => {
|
||||
this.callback(null, { name: this.result.name, data: xhr.response });
|
||||
});
|
||||
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
|
||||
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
const DropboxLink = {
|
||||
ERROR_CONFLICT: Dropbox.ApiError.CONFLICT,
|
||||
ERROR_NOT_FOUND: Dropbox.ApiError.NOT_FOUND,
|
||||
|
||||
Keys: DropboxKeys,
|
||||
|
||||
_getClient: function(complete, overrideAppKey) {
|
||||
if (this._dropboxClient && this._dropboxClient.isAuthenticated()) {
|
||||
complete(null, this._dropboxClient);
|
||||
return;
|
||||
}
|
||||
if (!overrideAppKey && !this.isValidKey()) {
|
||||
return complete(DropboxCustomErrors.BadKey);
|
||||
}
|
||||
const client = new Dropbox.Client({key: overrideAppKey || getKey()});
|
||||
if (Launcher) {
|
||||
client.authDriver(new Dropbox.AuthDriver.Electron({ receiverUrl: location.href }));
|
||||
} else {
|
||||
client.authDriver(new Dropbox.AuthDriver.Popup({ receiverUrl: location.href }));
|
||||
}
|
||||
client.authenticate((error, client) => {
|
||||
if (!error) {
|
||||
this._dropboxClient = client;
|
||||
}
|
||||
complete(error, client);
|
||||
});
|
||||
},
|
||||
|
||||
_handleUiError: function(err, alertCallback, callback) {
|
||||
if (!alertCallback) {
|
||||
if (!Alerts.alertDisplayed) {
|
||||
alertCallback = Alerts.error.bind(Alerts);
|
||||
}
|
||||
}
|
||||
logger.error('Dropbox error', err);
|
||||
switch (err.status) {
|
||||
case Dropbox.ApiError.INVALID_TOKEN:
|
||||
if (!Alerts.alertDisplayed) {
|
||||
Alerts.yesno({
|
||||
icon: 'dropbox',
|
||||
header: Locale.dropboxLogin,
|
||||
body: Locale.dropboxLoginBody,
|
||||
buttons: [{result: 'yes', title: Locale.alertSignIn}, {result: '', title: Locale.alertCancel}],
|
||||
success: () => {
|
||||
this.authenticate(err => { callback(!err); });
|
||||
},
|
||||
cancel: () => {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case Dropbox.ApiError.NOT_FOUND:
|
||||
alertCallback({
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxNotFoundBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.OVER_QUOTA:
|
||||
alertCallback({
|
||||
header: Locale.dropboxFull,
|
||||
body: Locale.dropboxFullBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.RATE_LIMITED:
|
||||
alertCallback({
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxRateLimitedBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.NETWORK_ERROR:
|
||||
alertCallback({
|
||||
header: Locale.dropboxNetError,
|
||||
body: Locale.dropboxNetErrorBody
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.INVALID_PARAM:
|
||||
case Dropbox.ApiError.OAUTH_ERROR:
|
||||
case Dropbox.ApiError.INVALID_METHOD:
|
||||
alertCallback({
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxErrorBody + ' ' + err.status
|
||||
});
|
||||
break;
|
||||
case Dropbox.ApiError.CONFLICT:
|
||||
break;
|
||||
default:
|
||||
alertCallback({
|
||||
header: Locale.dropboxSyncError,
|
||||
body: Locale.dropboxErrorRepeatBody + ' ' + err
|
||||
});
|
||||
break;
|
||||
}
|
||||
callback(false);
|
||||
},
|
||||
|
||||
_callAndHandleError: function(callName, args, callback, errorAlertCallback) {
|
||||
this._getClient((err, client) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const ts = logger.ts();
|
||||
logger.debug('Call', callName);
|
||||
client[callName].apply(client, args.concat((...args) => {
|
||||
const [err] = args;
|
||||
logger.debug('Result', callName, logger.ts(ts), args);
|
||||
if (err) {
|
||||
this._handleUiError(err, errorAlertCallback, repeat => {
|
||||
if (repeat) {
|
||||
this._callAndHandleError(callName, args, callback, errorAlertCallback);
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(...args);
|
||||
}
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
canUseBuiltInKeys: function() {
|
||||
const isSelfHosted = !/^http(s?):\/\/localhost:8085/.test(location.href) &&
|
||||
!/http(s?):\/\/(app|beta)\.keeweb\.info/.test(location.href);
|
||||
return !!Launcher || !isSelfHosted;
|
||||
},
|
||||
|
||||
getKey: getKey,
|
||||
|
||||
isValidKey: function() {
|
||||
const key = getKey();
|
||||
const isBuiltIn = key === DropboxKeys.AppFolder || key === DropboxKeys.FullDropbox;
|
||||
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this.canUseBuiltInKeys());
|
||||
},
|
||||
|
||||
authenticate: function(complete, overrideAppKey) {
|
||||
this._getClient(err => { complete(err); }, overrideAppKey);
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
if (this._dropboxClient) {
|
||||
try {
|
||||
this._dropboxClient.signOut();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
this._dropboxClient.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resetClient: function() {
|
||||
this._dropboxClient = null;
|
||||
},
|
||||
|
||||
receive: function() {
|
||||
Dropbox.AuthDriver.Popup.oauthReceiver();
|
||||
},
|
||||
|
||||
saveFile: function(fileName, data, rev, complete, alertCallback) {
|
||||
if (rev) {
|
||||
const opts = typeof rev === 'string' ? { lastVersionTag: rev, noOverwrite: true, noAutoRename: true } : undefined;
|
||||
this._callAndHandleError('writeFile', [fileName, data, opts], complete, alertCallback);
|
||||
} else {
|
||||
const dir = UrlUtil.fileToDir(fileName);
|
||||
this.list(dir, (err, files) => {
|
||||
if (err) { return complete(err); }
|
||||
const exists = files.some(file => file.toLowerCase() === fileName.toLowerCase());
|
||||
if (exists) { return complete({ exists: true }); }
|
||||
this._callAndHandleError('writeFile', [fileName, data], complete);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
openFile: function(fileName, complete, errorAlertCallback) {
|
||||
this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback);
|
||||
},
|
||||
|
||||
stat: function(fileName, complete, errorAlertCallback) {
|
||||
this._callAndHandleError('stat', [fileName], complete, errorAlertCallback);
|
||||
},
|
||||
|
||||
list: function(dir, complete) {
|
||||
this._callAndHandleError('readdir', [dir || ''], (err, files, dirStat, filesStat) => {
|
||||
if (files) {
|
||||
files = files.filter(f => /\.kdbx$/i.test(f));
|
||||
}
|
||||
complete(err, files, dirStat, filesStat);
|
||||
});
|
||||
},
|
||||
|
||||
deleteFile: function(fileName, complete) {
|
||||
this._callAndHandleError('remove', [fileName], complete);
|
||||
},
|
||||
|
||||
mkdir: function(path, complete) {
|
||||
this._callAndHandleError('mkdir', [path], complete);
|
||||
},
|
||||
|
||||
canChooseFile: function() {
|
||||
return !Launcher;
|
||||
},
|
||||
|
||||
chooseFile: function(callback) {
|
||||
new DropboxChooser(callback).choose();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DropboxLink;
|
|
@ -154,9 +154,6 @@
|
|||
"openFailedRead": "Failed to read file",
|
||||
"openNothingFound": "Nothing found",
|
||||
"openNothingFoundBody": "No files which could be opened.",
|
||||
"openNothingFoundBodyFolder": "Files are searched inside {} folder",
|
||||
"openAppFolder": "app",
|
||||
"openRootFolder": "root",
|
||||
"openSelectFile": "Select a file",
|
||||
"openSelectFileBody": "Select a file which you would like to open",
|
||||
"openPassFor": "Password for",
|
||||
|
|
|
@ -75,8 +75,7 @@ _.extend(StorageBase.prototype, {
|
|||
});
|
||||
xhr.open(config.method || 'GET', config.url);
|
||||
if (this._oauthToken) {
|
||||
xhr.setRequestHeader('Authorization',
|
||||
this._oauthToken.tokenType + ' ' + this._oauthToken.accessToken);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + this._oauthToken.accessToken);
|
||||
}
|
||||
_.forEach(config.headers, (value, key) => {
|
||||
xhr.setRequestHeader(key, value);
|
||||
|
@ -128,9 +127,11 @@ _.extend(StorageBase.prototype, {
|
|||
callback();
|
||||
return;
|
||||
}
|
||||
const url = opts.url + '?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
|
||||
const responseType = opts.code ? 'code' : 'token';
|
||||
const url = opts.url + '?client_id={cid}&scope={scope}&response_type={type}&redirect_uri={url}'
|
||||
.replace('{cid}', encodeURIComponent(opts.clientId))
|
||||
.replace('{scope}', encodeURIComponent(opts.scope))
|
||||
.replace('{type}', encodeURIComponent(responseType))
|
||||
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
|
||||
this.logger.debug('OAuth popup opened');
|
||||
if (!this._openPopup(url, 'OAuth', opts.width, opts.height)) {
|
||||
|
@ -155,7 +156,7 @@ _.extend(StorageBase.prototype, {
|
|||
} else {
|
||||
this._oauthToken = token;
|
||||
this.runtimeData.set(this.name + 'OAuthToken', token);
|
||||
this.logger.debug('OAuth success');
|
||||
this.logger.debug('OAuth token received');
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
@ -186,10 +187,12 @@ _.extend(StorageBase.prototype, {
|
|||
_oauthRevokeToken: function(url) {
|
||||
const token = this.runtimeData.get(this.name + 'OAuthToken');
|
||||
if (token) {
|
||||
this._xhr({
|
||||
url: url.replace('{token}', token.accessToken),
|
||||
statuses: [200, 401]
|
||||
});
|
||||
if (url) {
|
||||
this._xhr({
|
||||
url: url.replace('{token}', token.accessToken),
|
||||
statuses: [200, 401]
|
||||
});
|
||||
}
|
||||
this.runtimeData.unset(this.name + 'OAuthToken');
|
||||
this._oauthToken = null;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,30 @@
|
|||
const StorageBase = require('./storage-base');
|
||||
const DropboxLink = require('../comp/dropbox-link');
|
||||
const Locale = require('../util/locale');
|
||||
const UrlUtil = require('../util/url-util');
|
||||
const Launcher = require('../comp/launcher');
|
||||
|
||||
const DropboxKeys = {
|
||||
AppFolder: 'qp7ctun6qt5n9d6',
|
||||
FullDropbox: 'eor7hvv6u6oslq9'
|
||||
};
|
||||
|
||||
const ApiError = {
|
||||
NETWORK_ERROR: 0,
|
||||
NO_CONTENT: 304,
|
||||
INVALID_PARAM: 400,
|
||||
INVALID_TOKEN: 401,
|
||||
OAUTH_ERROR: 403,
|
||||
NOT_FOUND: 404,
|
||||
INVALID_METHOD: 405,
|
||||
NOT_ACCEPTABLE: 406,
|
||||
CONFLICT: 409,
|
||||
RATE_LIMITED: 429,
|
||||
SERVER_ERROR: 503,
|
||||
OVER_QUOTA: 507
|
||||
};
|
||||
|
||||
const DropboxCustomErrors = {
|
||||
BadKey: 'bad-key'
|
||||
};
|
||||
|
||||
const StorageDropbox = StorageBase.extend({
|
||||
name: 'dropbox',
|
||||
|
@ -14,10 +37,10 @@ const StorageDropbox = StorageBase.extend({
|
|||
if (!err) {
|
||||
return err;
|
||||
}
|
||||
if (err.status === DropboxLink.ERROR_NOT_FOUND) {
|
||||
if (err.status === ApiError.NOT_FOUND) {
|
||||
err.notFound = true;
|
||||
}
|
||||
if (err.status === DropboxLink.ERROR_CONFLICT) {
|
||||
if (err.status === ApiError.CONFLICT) {
|
||||
err.revConflict = true;
|
||||
}
|
||||
return err;
|
||||
|
@ -53,8 +76,36 @@ const StorageDropbox = StorageBase.extend({
|
|||
return folder;
|
||||
},
|
||||
|
||||
_getKey: function() {
|
||||
return this.appSettings.get('dropboxAppKey') || DropboxKeys.AppFolder;
|
||||
},
|
||||
|
||||
_isValidKey: function() {
|
||||
const key = this._getKey();
|
||||
const isBuiltIn = key === DropboxKeys.AppFolder || key === DropboxKeys.FullDropbox;
|
||||
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this._canUseBuiltInKeys());
|
||||
},
|
||||
|
||||
_canUseBuiltInKeys: function() {
|
||||
const isSelfHosted = !/^http(s?):\/\/localhost:8085/.test(location.href) &&
|
||||
!/http(s?):\/\/(app|beta)\.keeweb\.info/.test(location.href);
|
||||
return !!Launcher || !isSelfHosted;
|
||||
},
|
||||
|
||||
_getOAuthConfig: function() {
|
||||
return {
|
||||
scope: '',
|
||||
url: 'https://www.dropbox.com/oauth2/authorize',
|
||||
// tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
|
||||
// code: true
|
||||
clientId: this._getKey(),
|
||||
width: 600,
|
||||
height: 400
|
||||
};
|
||||
},
|
||||
|
||||
needShowOpenConfig: function() {
|
||||
return !DropboxLink.isValidKey();
|
||||
return !this._isValidKey();
|
||||
},
|
||||
|
||||
getOpenConfig: function() {
|
||||
|
@ -69,19 +120,19 @@ const StorageDropbox = StorageBase.extend({
|
|||
|
||||
getSettingsConfig: function() {
|
||||
const fields = [];
|
||||
const appKey = DropboxLink.getKey();
|
||||
const appKey = this._getKey();
|
||||
const linkField = {id: 'link', title: 'dropboxLink', type: 'select', value: 'custom',
|
||||
options: { app: 'dropboxLinkApp', full: 'dropboxLinkFull', custom: 'dropboxLinkCustom' } };
|
||||
const keyField = {id: 'key', title: 'dropboxAppKey', desc: 'dropboxAppKeyDesc', type: 'text', required: true, pattern: '\\w+',
|
||||
value: appKey};
|
||||
const folderField = {id: 'folder', title: 'dropboxFolder', desc: 'dropboxFolderSettingsDesc', type: 'text',
|
||||
value: this.appSettings.get('dropboxFolder') || ''};
|
||||
const canUseBuiltInKeys = DropboxLink.canUseBuiltInKeys();
|
||||
const canUseBuiltInKeys = this._canUseBuiltInKeys();
|
||||
if (canUseBuiltInKeys) {
|
||||
fields.push(linkField);
|
||||
if (appKey === DropboxLink.Keys.AppFolder) {
|
||||
if (appKey === DropboxKeys.AppFolder) {
|
||||
linkField.value = 'app';
|
||||
} else if (appKey === DropboxLink.Keys.FullDropbox) {
|
||||
} else if (appKey === DropboxKeys.FullDropbox) {
|
||||
linkField.value = 'full';
|
||||
fields.push(folderField);
|
||||
} else {
|
||||
|
@ -96,19 +147,18 @@ const StorageDropbox = StorageBase.extend({
|
|||
},
|
||||
|
||||
applyConfig: function(config, callback) {
|
||||
DropboxLink.authenticate(err => {
|
||||
if (!err) {
|
||||
if (config.folder) {
|
||||
config.folder = this._fixConfigFolder(config.folder);
|
||||
}
|
||||
this.appSettings.set({
|
||||
dropboxAppKey: config.key,
|
||||
dropboxFolder: config.folder
|
||||
});
|
||||
DropboxLink.resetClient();
|
||||
}
|
||||
callback(err);
|
||||
}, config.key);
|
||||
if (config.key === DropboxKeys.AppFolder || config.key === DropboxKeys.FullDropbox) {
|
||||
return callback(DropboxCustomErrors.BadKey);
|
||||
}
|
||||
// TODO: try to connect using new key
|
||||
if (config.folder) {
|
||||
config.folder = this._fixConfigFolder(config.folder);
|
||||
}
|
||||
this.appSettings.set({
|
||||
dropboxAppKey: config.key,
|
||||
dropboxFolder: config.folder
|
||||
});
|
||||
callback();
|
||||
},
|
||||
|
||||
applySetting: function(key, value) {
|
||||
|
@ -117,10 +167,10 @@ const StorageDropbox = StorageBase.extend({
|
|||
key = 'dropboxAppKey';
|
||||
switch (value) {
|
||||
case 'app':
|
||||
value = DropboxLink.Keys.AppFolder;
|
||||
value = DropboxKeys.AppFolder;
|
||||
break;
|
||||
case 'full':
|
||||
value = DropboxLink.Keys.FullDropbox;
|
||||
value = DropboxKeys.FullDropbox;
|
||||
break;
|
||||
case 'custom':
|
||||
value = '(your app key)';
|
||||
|
@ -128,10 +178,11 @@ const StorageDropbox = StorageBase.extend({
|
|||
default:
|
||||
return;
|
||||
}
|
||||
DropboxLink.resetClient();
|
||||
this._oauthRevokeToken();
|
||||
break;
|
||||
case 'key':
|
||||
key = 'dropboxAppKey';
|
||||
this._oauthRevokeToken();
|
||||
break;
|
||||
case 'folder':
|
||||
key = 'dropboxFolder';
|
||||
|
@ -147,60 +198,130 @@ const StorageDropbox = StorageBase.extend({
|
|||
return '/' + fileName + '.kdbx';
|
||||
},
|
||||
|
||||
_apiCall: function(args) {
|
||||
this._oauthAuthorize(err => {
|
||||
if (err) {
|
||||
return args.error(err);
|
||||
}
|
||||
const host = args.host || 'api';
|
||||
let headers;
|
||||
let data = args.data;
|
||||
if (args.apiArg) {
|
||||
headers = { 'Dropbox-API-Arg': JSON.stringify(args.apiArg) };
|
||||
if (args.data) {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
}
|
||||
} else if (args.data) {
|
||||
data = JSON.stringify(data);
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
this._xhr({
|
||||
url: `https://${host}.dropboxapi.com/2/${args.method}`,
|
||||
method: 'POST',
|
||||
responseType: args.responseType || 'json',
|
||||
headers: headers,
|
||||
data: data,
|
||||
statuses: args.statuses || undefined,
|
||||
success: args.success,
|
||||
error: (e, xhr) => {
|
||||
let err = xhr.response && xhr.response.error || new Error('Network error');
|
||||
if (err && err.path && err.path['.tag'] === 'not_found') {
|
||||
err = new Error('File removed');
|
||||
err.notFound = true;
|
||||
this.logger.debug('File not found', args.method);
|
||||
} else {
|
||||
this.logger.error('API error', args.method, xhr.status, err);
|
||||
}
|
||||
err.status = xhr.status;
|
||||
args.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
load: function(path, opts, callback) {
|
||||
this.logger.debug('Load', path);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
DropboxLink.openFile(path, (err, data, stat) => {
|
||||
this.logger.debug('Loaded', path, stat ? stat.versionTag : null, this.logger.ts(ts));
|
||||
err = this._convertError(err);
|
||||
if (callback) { callback(err, data, stat ? { rev: stat.versionTag } : null); }
|
||||
}, _.noop);
|
||||
this._apiCall({
|
||||
method: 'files/download',
|
||||
host: 'content',
|
||||
apiArg: { path },
|
||||
responseType: 'arraybuffer',
|
||||
success: (response, xhr) => {
|
||||
const stat = JSON.parse(xhr.getResponseHeader('dropbox-api-result'));
|
||||
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
|
||||
callback(null, response, { rev: stat.rev });
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
stat: function(path, opts, callback) {
|
||||
this.logger.debug('Stat', path);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
DropboxLink.stat(path, (err, stat) => {
|
||||
if (stat && stat.isRemoved) {
|
||||
err = new Error('File removed');
|
||||
err.notFound = true;
|
||||
}
|
||||
this.logger.debug('Stated', path, stat ? stat.versionTag : null, this.logger.ts(ts));
|
||||
err = this._convertError(err);
|
||||
if (callback) { callback(err, stat ? { rev: stat.versionTag } : null); }
|
||||
}, _.noop);
|
||||
this._apiCall({
|
||||
method: 'files/get_metadata',
|
||||
data: { path },
|
||||
success: stat => {
|
||||
if (stat['.tag'] === 'file') {
|
||||
stat = { rev: stat.rev };
|
||||
} else if (stat['.tag'] === 'folder') {
|
||||
stat = { folder: true };
|
||||
}
|
||||
this.logger.debug('Stated', path, stat.folder ? 'folder' : stat.rev, this.logger.ts(ts));
|
||||
if (callback) { callback(null, stat); }
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
save: function(path, opts, data, callback, rev) {
|
||||
this.logger.debug('Save', path, rev);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
DropboxLink.saveFile(path, data, rev, (err, stat) => {
|
||||
this.logger.debug('Saved', path, this.logger.ts(ts));
|
||||
if (!callback) { return; }
|
||||
err = this._convertError(err);
|
||||
callback(err, stat ? { rev: stat.versionTag } : null);
|
||||
}, _.noop);
|
||||
const arg = {
|
||||
path,
|
||||
mode: rev ? { '.tag': 'update', update: rev } : { '.tag': 'overwrite' }
|
||||
};
|
||||
this._apiCall({
|
||||
method: 'files/upload',
|
||||
host: 'content',
|
||||
apiArg: arg,
|
||||
data: data,
|
||||
responseType: 'json',
|
||||
success: stat => {
|
||||
this.logger.debug('Saved', path, stat.rev, this.logger.ts(ts));
|
||||
callback(null, { rev: stat.rev });
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
list: function(callback) {
|
||||
DropboxLink.authenticate((err) => {
|
||||
if (err) { return callback(err); }
|
||||
DropboxLink.list(this._toFullPath(''), (err, files, dirStat, filesStat) => {
|
||||
if (err) { return callback(err); }
|
||||
const fileList = filesStat
|
||||
.filter(f => !f.isFolder && !f.isRemoved && UrlUtil.isKdbx(f.name))
|
||||
this.logger.debug('List');
|
||||
const ts = this.logger.ts();
|
||||
this._apiCall({
|
||||
method: 'files/list_folder',
|
||||
data: {
|
||||
path: this._toFullPath(''),
|
||||
recursive: false
|
||||
},
|
||||
success: data => {
|
||||
this.logger.debug('Listed', this.logger.ts(ts));
|
||||
const fileList = data.entries
|
||||
.filter(f => f['.tag'] === 'file' && f.rev && UrlUtil.isKdbx(f.name))
|
||||
.map(f => ({
|
||||
name: f.name,
|
||||
path: this._toRelPath(f.path),
|
||||
rev: f.versionTag
|
||||
path: this._toRelPath(f['path_display']),
|
||||
ref: f.rev
|
||||
}));
|
||||
const dir = dirStat.inAppFolder ? Locale.openAppFolder
|
||||
: (UrlUtil.trimStartSlash(dirStat.path) || Locale.openRootFolder);
|
||||
callback(null, fileList, dir);
|
||||
});
|
||||
callback(null, fileList);
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -208,28 +329,35 @@ const StorageDropbox = StorageBase.extend({
|
|||
this.logger.debug('Remove', path);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
DropboxLink.deleteFile(path, err => {
|
||||
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}, _.noop);
|
||||
this._apiCall({
|
||||
method: 'files/delete',
|
||||
data: { path },
|
||||
success: () => {
|
||||
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||
callback();
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
mkdir: function(path, callback) {
|
||||
DropboxLink.authenticate((err) => {
|
||||
if (err) { return callback(err); }
|
||||
this.logger.debug('Make dir', path);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
DropboxLink.mkdir(path, err => {
|
||||
this.logger.debug('Make dir', path);
|
||||
const ts = this.logger.ts();
|
||||
path = this._toFullPath(path);
|
||||
this._apiCall({
|
||||
method: 'files/create_folder',
|
||||
data: { path },
|
||||
success: () => {
|
||||
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}, _.noop);
|
||||
callback();
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
if (!enabled) {
|
||||
DropboxLink.logout();
|
||||
this._oauthRevokeToken();
|
||||
}
|
||||
StorageBase.prototype.setEnabled.call(this, enabled);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const UrlUtil = {
|
||||
multiSlashRegex: /\/{2,}/g,
|
||||
lastPartRegex: /[^\/\\]+$/,
|
||||
trimStartSlashRegex: /^\\/,
|
||||
lastPartRegex: /\/?[^\/\\]+$/,
|
||||
kdbxEndRegex: /\.kdbx$/i,
|
||||
|
||||
getDataFileName: function(url) {
|
||||
|
@ -22,11 +21,7 @@ const UrlUtil = {
|
|||
},
|
||||
|
||||
fileToDir: function(url) {
|
||||
return url.replace(this.lastPartRegex, '');
|
||||
},
|
||||
|
||||
trimStartSlash: function(url) {
|
||||
return url.replace(this.trimStartSlashRegex, '');
|
||||
return url.replace(this.lastPartRegex, '') || '/';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const OpenConfigView = require('./open-config-view');
|
|||
const Keys = require('../const/keys');
|
||||
const Alerts = require('../comp/alerts');
|
||||
const SecureInput = require('../comp/secure-input');
|
||||
const DropboxLink = require('../comp/dropbox-link');
|
||||
const DropboxChooser = require('../comp/dropbox-chooser');
|
||||
const FeatureDetector = require('../util/feature-detector');
|
||||
const Logger = require('../util/logger');
|
||||
const Locale = require('../util/locale');
|
||||
|
@ -78,7 +78,7 @@ const OpenView = Backbone.View.extend({
|
|||
!(this.model.settings.get('canOpenDemo') && !this.model.settings.get('demoOpened'));
|
||||
this.renderTemplate({
|
||||
lastOpenFiles: this.getLastOpenFiles(),
|
||||
canOpenKeyFromDropbox: DropboxLink.canChooseFile() && Storage.dropbox.enabled,
|
||||
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
|
||||
demoOpened: this.model.settings.get('demoOpened'),
|
||||
storageProviders: storageProviders,
|
||||
canOpen: this.model.settings.get('canOpen'),
|
||||
|
@ -304,14 +304,14 @@ const OpenView = Backbone.View.extend({
|
|||
|
||||
openKeyFileFromDropbox: function() {
|
||||
if (!this.busy) {
|
||||
DropboxLink.chooseFile((err, res) => {
|
||||
new DropboxChooser((err, res) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
this.params.keyFileData = res.data;
|
||||
this.params.keyFileName = res.name;
|
||||
this.displayOpenKeyFile();
|
||||
});
|
||||
}).choose();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -598,7 +598,7 @@ const OpenView = Backbone.View.extend({
|
|||
const icon = this.$el.find('.open__icon-storage[data-storage=' + storage.name + ']');
|
||||
this.busy = true;
|
||||
icon.toggleClass('flip3d', true);
|
||||
storage.list((err, files, dir) => {
|
||||
storage.list((err, files) => {
|
||||
icon.toggleClass('flip3d', false);
|
||||
this.busy = false;
|
||||
if (err || !files) {
|
||||
|
@ -613,13 +613,9 @@ const OpenView = Backbone.View.extend({
|
|||
allStorageFiles[file.path] = file;
|
||||
});
|
||||
if (!buttons.length) {
|
||||
let body = Locale.openNothingFoundBody;
|
||||
if (dir) {
|
||||
body += ' ' + Locale.openNothingFoundBodyFolder.replace('{}', dir);
|
||||
}
|
||||
Alerts.error({
|
||||
header: Locale.openNothingFound,
|
||||
body: body
|
||||
body: Locale.openNothingFoundBody
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
<li><a href="http://underscorejs.org/" target="_blank">underscore</a><span class="muted-color">, utility-belt library for JavaScript</span></li>
|
||||
<li><a href="https://jquery.com/" target="_blank">jQuery</a><span class="muted-color">, fast, small, and feature-rich JavaScript library</span></li>
|
||||
<li><a href="http://handlebarsjs.com/" target="_blank">handlebars</a><span class="muted-color">, semantic templates</span></li>
|
||||
<li><a href="https://github.com/dropbox/dropbox-js" target="_blank">dropbox-js</a><span class="muted-color">, unofficial JavaScript library for
|
||||
the Dropbox Core API</span></li>
|
||||
<li><a href="https://github.com/LazarSoft/jsqrcode" target="_blank">jsqrcode</a><span class="muted-color">, QR code scanner,
|
||||
<a href="{{licenseLinkApache}}" class="muted-color" target="_blank">Apache-2.0 license</a></span></li>
|
||||
</ul>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
"backbone": "1.3.3",
|
||||
"baron": "2.2.2",
|
||||
"bourbon": "4.2.7",
|
||||
"dropbox": "keeweb/dropbox-js#0ac0efdc2711eece73f6ac044459e1fd0d5e9390",
|
||||
"font-awesome": "4.7.0",
|
||||
"kdbxweb": "1.0.1",
|
||||
"normalize.css": "5.0.0",
|
||||
|
|
|
@ -3,6 +3,7 @@ Release notes
|
|||
##### v1.5.0 (TBD)
|
||||
`+` plugins
|
||||
`*` translations are available only as plugins
|
||||
`*` Dropbox API V2
|
||||
`+` mobile field editing improvements
|
||||
`+` file path hint in recent files list
|
||||
`+` cacheConfigSettings config option
|
||||
|
|
Loading…
Reference in New Issue