mirror of https://github.com/keeweb/keeweb.git
prettier
This commit is contained in:
parent
68245629e4
commit
84a23e4aea
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"extends": ["standard", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"semi": ["error", "always"],
|
||||
"one-var": "off",
|
||||
"space-before-function-paren": "off",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"quoteProps": "preserve"
|
||||
}
|
103
Gruntfile.js
103
Gruntfile.js
|
@ -144,7 +144,7 @@ module.exports = function(grunt) {
|
|||
eslint: {
|
||||
app: ['app/scripts/**/*.js'],
|
||||
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
||||
grunt: ['Gruntfile.js', 'grunt/**/*.js']
|
||||
grunt: ['Gruntfile.js', 'build/**/*.js']
|
||||
},
|
||||
inline: {
|
||||
app: {
|
||||
|
@ -182,12 +182,21 @@ module.exports = function(grunt) {
|
|||
files: { 'tmp/desktop/app/index.html': 'dist/index.html' }
|
||||
},
|
||||
'desktop-public-key': {
|
||||
options: { replacements: [{ pattern: '\'@@PUBLIC_KEY_CONTENT\'', replacement:
|
||||
'`' + fs.readFileSync('app/resources/public-key.pem', {encoding: 'utf8'}).trim() + '`' }] },
|
||||
options: {
|
||||
replacements: [
|
||||
{
|
||||
pattern: "'@@PUBLIC_KEY_CONTENT'",
|
||||
replacement:
|
||||
'`' + fs.readFileSync('app/resources/public-key.pem', { encoding: 'utf8' }).trim() + '`'
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'tmp/desktop/app/main.js': 'desktop/main.js' }
|
||||
},
|
||||
'cordova-html': {
|
||||
options: { replacements: [{ pattern: '<script', replacement: '<script src="cordova.js"></script><script' }] },
|
||||
options: {
|
||||
replacements: [{ pattern: '<script', replacement: '<script src="cordova.js"></script><script' }]
|
||||
},
|
||||
files: { 'tmp/cordova/app/index.html': 'dist/index.html' }
|
||||
}
|
||||
},
|
||||
|
@ -239,9 +248,9 @@ module.exports = function(grunt) {
|
|||
electronVersion: electronVersion,
|
||||
overwrite: true,
|
||||
asar: true,
|
||||
'appCopyright': `Copyright © ${year} Antelle`,
|
||||
'appVersion': pkg.version,
|
||||
'buildVersion': '<%= gitinfo.local.branch.current.shortSHA %>'
|
||||
appCopyright: `Copyright © ${year} Antelle`,
|
||||
appVersion: pkg.version,
|
||||
buildVersion: '<%= gitinfo.local.branch.current.shortSHA %>'
|
||||
},
|
||||
linux: {
|
||||
options: {
|
||||
|
@ -255,9 +264,9 @@ module.exports = function(grunt) {
|
|||
platform: 'darwin',
|
||||
arch: ['x64'],
|
||||
icon: 'graphics/icon.icns',
|
||||
'appBundleId': 'net.antelle.keeweb',
|
||||
'appCategoryType': 'public.app-category.productivity',
|
||||
'extendInfo': 'package/osx/extend.plist'
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
win32: {
|
||||
|
@ -265,13 +274,13 @@ module.exports = function(grunt) {
|
|||
platform: 'win32',
|
||||
arch: ['ia32', 'x64'],
|
||||
icon: 'graphics/icon.ico',
|
||||
'buildVersion': pkg.version,
|
||||
buildVersion: pkg.version,
|
||||
'version-string': {
|
||||
'CompanyName': 'KeeWeb',
|
||||
'FileDescription': pkg.description,
|
||||
'OriginalFilename': 'KeeWeb.exe',
|
||||
'ProductName': 'KeeWeb',
|
||||
'InternalName': 'KeeWeb'
|
||||
CompanyName: 'KeeWeb',
|
||||
FileDescription: pkg.description,
|
||||
OriginalFilename: 'KeeWeb.exe',
|
||||
ProductName: 'KeeWeb',
|
||||
InternalName: 'KeeWeb'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,9 +306,7 @@ module.exports = function(grunt) {
|
|||
},
|
||||
'desktop-update': {
|
||||
options: { archive: 'dist/desktop/UpdateDesktop.zip', comment: zipCommentPlaceholder },
|
||||
files: [
|
||||
{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }
|
||||
]
|
||||
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
|
||||
},
|
||||
'win32-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
||||
|
@ -311,13 +318,17 @@ module.exports = function(grunt) {
|
|||
},
|
||||
'linux-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }]
|
||||
files: [
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
|
||||
]
|
||||
},
|
||||
'linux-ia32': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.ia32.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-ia32', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }]
|
||||
files: [
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-ia32', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
appdmg: {
|
||||
|
@ -341,7 +352,9 @@ module.exports = function(grunt) {
|
|||
options: {
|
||||
vars: {
|
||||
version: pkg.version,
|
||||
rev: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); },
|
||||
rev: function() {
|
||||
return grunt.config.get('gitinfo.local.branch.current.shortSHA');
|
||||
},
|
||||
homepage: pkg.homepage
|
||||
}
|
||||
},
|
||||
|
@ -383,7 +396,9 @@ module.exports = function(grunt) {
|
|||
description: pkg.description,
|
||||
author: pkg.author,
|
||||
homepage: pkg.homepage,
|
||||
rev: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); }
|
||||
rev: function() {
|
||||
return grunt.config.get('gitinfo.local.branch.current.shortSHA');
|
||||
}
|
||||
}
|
||||
},
|
||||
'linux-x64': {
|
||||
|
@ -401,8 +416,18 @@ module.exports = function(grunt) {
|
|||
},
|
||||
files: [
|
||||
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true },
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-x64/', src: '**', dest: '/opt/keeweb-desktop', expand: true, nonull: true },
|
||||
{ src: 'graphics/128x128.png', dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png', nonull: true }
|
||||
{
|
||||
cwd: 'tmp/desktop/KeeWeb-linux-x64/',
|
||||
src: '**',
|
||||
dest: '/opt/keeweb-desktop',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
{
|
||||
src: 'graphics/128x128.png',
|
||||
dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png',
|
||||
nonull: true
|
||||
}
|
||||
]
|
||||
},
|
||||
'linux-ia32': {
|
||||
|
@ -420,8 +445,18 @@ module.exports = function(grunt) {
|
|||
},
|
||||
files: [
|
||||
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true },
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-ia32/', src: '**', dest: '/opt/keeweb-desktop', expand: true, nonull: true },
|
||||
{ src: 'graphics/128x128.png', dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png', nonull: true }
|
||||
{
|
||||
cwd: 'tmp/desktop/KeeWeb-linux-ia32/',
|
||||
src: '**',
|
||||
dest: '/opt/keeweb-desktop',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
{
|
||||
src: 'graphics/128x128.png',
|
||||
dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png',
|
||||
nonull: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -444,18 +479,14 @@ module.exports = function(grunt) {
|
|||
desktop: {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
expected: [
|
||||
'app.asar',
|
||||
'helper/darwin/KeeWebHelper',
|
||||
'helper/win32/KeeWebHelper.exe'
|
||||
],
|
||||
expected: ['app.asar', 'helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
||||
expectedCount: 7,
|
||||
publicKey: 'app/resources/public-key.pem'
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-html': {
|
||||
'app': {
|
||||
app: {
|
||||
options: {
|
||||
file: 'dist/index.html',
|
||||
skip: grunt.option('skip-sign')
|
||||
|
@ -521,7 +552,7 @@ module.exports = function(grunt) {
|
|||
}
|
||||
},
|
||||
'sign-dist': {
|
||||
'dist': {
|
||||
dist: {
|
||||
options: {
|
||||
sign: 'dist/desktop/Verify.sign.sha256'
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ const FeatureDetector = require('./util/feature-detector');
|
|||
const KdbxwebInit = require('./util/kdbxweb-init');
|
||||
const Locale = require('./util/locale');
|
||||
|
||||
const ready = Launcher && Launcher.ready || $;
|
||||
const ready = (Launcher && Launcher.ready) || $;
|
||||
|
||||
ready(() => {
|
||||
if (AuthReceiver.receive() || FeatureDetector.isFrame) {
|
||||
|
@ -51,16 +51,17 @@ ready(() => {
|
|||
}
|
||||
|
||||
function ensureCanRun() {
|
||||
return FeatureTester.test()
|
||||
.catch(e => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError + '<br/><br/>' + e,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
});
|
||||
throw 'Feature testing failed: ' + e;
|
||||
return FeatureTester.test().catch(e => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError + '<br/><br/>' + e,
|
||||
buttons: [],
|
||||
esc: false,
|
||||
enter: false,
|
||||
click: false
|
||||
});
|
||||
throw 'Feature testing failed: ' + e;
|
||||
});
|
||||
}
|
||||
|
||||
function loadConfigs() {
|
||||
|
@ -87,7 +88,9 @@ ready(() => {
|
|||
header: Locale.appSettingsError,
|
||||
body: Locale.appSettingsErrorBody,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
esc: false,
|
||||
enter: false,
|
||||
click: false
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -96,42 +99,46 @@ ready(() => {
|
|||
SettingsManager.setBySettings(appModel.settings);
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
return appModel.loadConfig(configParam).then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
}).catch(e => {
|
||||
if (!appModel.settings.get('cacheConfigSettings')) {
|
||||
showSettingsLoadError();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
return appModel
|
||||
.loadConfig(configParam)
|
||||
.then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
})
|
||||
.catch(e => {
|
||||
if (!appModel.settings.get('cacheConfigSettings')) {
|
||||
showSettingsLoadError();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
|
||||
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
|
||||
const hostIsInsecure = location.hostname !== 'localhost';
|
||||
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
|
||||
return new Promise(resolve => {
|
||||
Alerts.error({
|
||||
header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [
|
||||
{result: '', title: Locale.appSecWarnBtn, error: true}
|
||||
],
|
||||
complete: () => {
|
||||
showView();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
|
||||
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
|
||||
const hostIsInsecure = location.hostname !== 'localhost';
|
||||
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
|
||||
return new Promise(resolve => {
|
||||
Alerts.error({
|
||||
header: Locale.appSecWarn,
|
||||
icon: 'user-secret',
|
||||
esc: false,
|
||||
enter: false,
|
||||
click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
|
||||
complete: () => {
|
||||
showView();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
|
|
|
@ -17,14 +17,13 @@ AutoTypeFilter.prototype.getEntries = function() {
|
|||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter)
|
||||
.map(e => [e, this.getEntryRank(e)]);
|
||||
let entries = this.appModel.getEntriesByFilter(filter).map(e => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter(e => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) => x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]);
|
||||
entries = entries.sort((x, y) => (x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]));
|
||||
entries = entries.map(p => p[0]);
|
||||
return new EntryCollection(entries, {comparator: 'none'});
|
||||
return new EntryCollection(entries, { comparator: 'none' });
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function() {
|
||||
|
@ -40,10 +39,7 @@ AutoTypeFilter.prototype.prepareFilter = function() {
|
|||
AutoTypeFilter.prototype.getEntryRank = function(entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(
|
||||
entry.title.toLowerCase(),
|
||||
this.titleLower
|
||||
);
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts && entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
|
|
|
@ -33,8 +33,12 @@ AutoTypeObfuscator.prototype.obfuscate = function() {
|
|||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.finished = function() {
|
||||
return this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function(ch, ix) { return this.inputChars[ix].ch === ch; }, this);
|
||||
return (
|
||||
this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function(ch, ix) {
|
||||
return this.inputChars[ix].ch === ch;
|
||||
}, this)
|
||||
);
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.step = function() {
|
||||
|
@ -161,7 +165,7 @@ AutoTypeObfuscator.prototype.inputChar = function(ch) {
|
|||
|
||||
AutoTypeObfuscator.prototype.copyPaste = function(ch) {
|
||||
logger.debug('copyPaste', ch);
|
||||
this.ops.push({type: 'cmd', value: 'copyPaste', arg: ch});
|
||||
this.ops.push({ type: 'cmd', value: 'copyPaste', arg: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
|
@ -174,10 +178,10 @@ AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
|||
ops.push({ type: 'key', value: backward ? 'left' : 'right' });
|
||||
}
|
||||
if (ops.length === 1) {
|
||||
ops[0].mod = {'+': true};
|
||||
ops[0].mod = { '+': true };
|
||||
this.ops.push(ops[0]);
|
||||
} else {
|
||||
this.ops.push({type: 'group', value: ops, mod: {'+': true}});
|
||||
this.ops.push({ type: 'group', value: ops, mod: { '+': true } });
|
||||
}
|
||||
if (backward) {
|
||||
this.inputCursor -= count;
|
||||
|
|
|
@ -16,39 +16,127 @@ const AutoTypeRunner = function(ops) {
|
|||
AutoTypeRunner.PendingResolve = { pending: true };
|
||||
|
||||
AutoTypeRunner.Keys = {
|
||||
tab: 'tab', enter: 'enter', space: 'space',
|
||||
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn',
|
||||
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc',
|
||||
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6',
|
||||
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16',
|
||||
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide',
|
||||
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4',
|
||||
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9'
|
||||
tab: 'tab',
|
||||
enter: 'enter',
|
||||
space: 'space',
|
||||
up: 'up',
|
||||
down: 'down',
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
home: 'home',
|
||||
end: 'end',
|
||||
pgup: 'pgup',
|
||||
pgdn: 'pgdn',
|
||||
insert: 'ins',
|
||||
ins: 'ins',
|
||||
delete: 'del',
|
||||
del: 'del',
|
||||
backspace: 'bs',
|
||||
bs: 'bs',
|
||||
bksp: 'bs',
|
||||
esc: 'esc',
|
||||
win: 'win',
|
||||
lwin: 'win',
|
||||
rwin: 'rwin',
|
||||
f1: 'f1',
|
||||
f2: 'f2',
|
||||
f3: 'f3',
|
||||
f4: 'f4',
|
||||
f5: 'f5',
|
||||
f6: 'f6',
|
||||
f7: 'f7',
|
||||
f8: 'f8',
|
||||
f9: 'f9',
|
||||
f10: 'f10',
|
||||
f11: 'f11',
|
||||
f12: 'f12',
|
||||
f13: 'f13',
|
||||
f14: 'f14',
|
||||
f15: 'f15',
|
||||
f16: 'f16',
|
||||
add: 'add',
|
||||
subtract: 'subtract',
|
||||
multiply: 'multiply',
|
||||
divide: 'divide',
|
||||
numpad0: 'n0',
|
||||
numpad1: 'n1',
|
||||
numpad2: 'n2',
|
||||
numpad3: 'n3',
|
||||
numpad4: 'n4',
|
||||
numpad5: 'n5',
|
||||
numpad6: 'n6',
|
||||
numpad7: 'n7',
|
||||
numpad8: 'n8',
|
||||
numpad9: 'n9'
|
||||
};
|
||||
|
||||
AutoTypeRunner.Substitutions = {
|
||||
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); },
|
||||
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); },
|
||||
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); },
|
||||
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); },
|
||||
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); },
|
||||
group: function(runner) { return runner.getEntryGroupName(); },
|
||||
totp: function(runner, op) { return runner.getOtp(op); },
|
||||
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); },
|
||||
'dt_simple': function(runner) { return runner.dt('simple'); },
|
||||
'dt_year': function(runner) { return runner.dt('Y'); },
|
||||
'dt_month': function(runner) { return runner.dt('M'); },
|
||||
'dt_day': function(runner) { return runner.dt('D'); },
|
||||
'dt_hour': function(runner) { return runner.dt('h'); },
|
||||
'dt_minute': function(runner) { return runner.dt('m'); },
|
||||
'dt_second': function(runner) { return runner.dt('s'); },
|
||||
'dt_utc_simple': function(runner) { return runner.udt('simple'); },
|
||||
'dt_utc_year': function(runner) { return runner.udt('Y'); },
|
||||
'dt_utc_month': function(runner) { return runner.udt('M'); },
|
||||
'dt_utc_day': function(runner) { return runner.udt('D'); },
|
||||
'dt_utc_hour': function(runner) { return runner.udt('h'); },
|
||||
'dt_utc_minute': function(runner) { return runner.udt('m'); },
|
||||
'dt_utc_second': function(runner) { return runner.udt('s'); }
|
||||
title: function(runner, op) {
|
||||
return runner.getEntryFieldKeys('Title', op);
|
||||
},
|
||||
username: function(runner, op) {
|
||||
return runner.getEntryFieldKeys('UserName', op);
|
||||
},
|
||||
url: function(runner, op) {
|
||||
return runner.getEntryFieldKeys('URL', op);
|
||||
},
|
||||
password: function(runner, op) {
|
||||
return runner.getEntryFieldKeys('Password', op);
|
||||
},
|
||||
notes: function(runner, op) {
|
||||
return runner.getEntryFieldKeys('Notes', op);
|
||||
},
|
||||
group: function(runner) {
|
||||
return runner.getEntryGroupName();
|
||||
},
|
||||
totp: function(runner, op) {
|
||||
return runner.getOtp(op);
|
||||
},
|
||||
s: function(runner, op) {
|
||||
return runner.getEntryFieldKeys(op.arg, op);
|
||||
},
|
||||
'dt_simple': function(runner) {
|
||||
return runner.dt('simple');
|
||||
},
|
||||
'dt_year': function(runner) {
|
||||
return runner.dt('Y');
|
||||
},
|
||||
'dt_month': function(runner) {
|
||||
return runner.dt('M');
|
||||
},
|
||||
'dt_day': function(runner) {
|
||||
return runner.dt('D');
|
||||
},
|
||||
'dt_hour': function(runner) {
|
||||
return runner.dt('h');
|
||||
},
|
||||
'dt_minute': function(runner) {
|
||||
return runner.dt('m');
|
||||
},
|
||||
'dt_second': function(runner) {
|
||||
return runner.dt('s');
|
||||
},
|
||||
'dt_utc_simple': function(runner) {
|
||||
return runner.udt('simple');
|
||||
},
|
||||
'dt_utc_year': function(runner) {
|
||||
return runner.udt('Y');
|
||||
},
|
||||
'dt_utc_month': function(runner) {
|
||||
return runner.udt('M');
|
||||
},
|
||||
'dt_utc_day': function(runner) {
|
||||
return runner.udt('D');
|
||||
},
|
||||
'dt_utc_hour': function(runner) {
|
||||
return runner.udt('h');
|
||||
},
|
||||
'dt_utc_minute': function(runner) {
|
||||
return runner.udt('m');
|
||||
},
|
||||
'dt_utc_second': function(runner) {
|
||||
return runner.udt('s');
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
|
@ -103,7 +191,7 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
|||
op.value = [];
|
||||
const count = +op.arg;
|
||||
for (let i = 0; i < count; i++) {
|
||||
op.value.push({type: 'key', value: key});
|
||||
op.value.push({ type: 'key', value: key });
|
||||
}
|
||||
} else {
|
||||
// {TAB}
|
||||
|
@ -179,9 +267,9 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
|||
const ops = [];
|
||||
value.forEachChar(ch => {
|
||||
if (ch === 10 || ch === 13) {
|
||||
ops.push({type: 'key', value: 'enter'});
|
||||
ops.push({ type: 'key', value: 'enter' });
|
||||
} else {
|
||||
ops.push({type: 'text', value: String.fromCharCode(ch)});
|
||||
ops.push({ type: 'text', value: String.fromCharCode(ch) });
|
||||
}
|
||||
});
|
||||
return ops;
|
||||
|
@ -194,10 +282,10 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
|||
const partsOps = [];
|
||||
parts.forEach(part => {
|
||||
if (partsOps.length) {
|
||||
partsOps.push({type: 'key', value: 'enter'});
|
||||
partsOps.push({ type: 'key', value: 'enter' });
|
||||
}
|
||||
if (part) {
|
||||
partsOps.push({type: 'text', value: part});
|
||||
partsOps.push({ type: 'text', value: part });
|
||||
}
|
||||
});
|
||||
return partsOps;
|
||||
|
|
|
@ -3,15 +3,53 @@ const AutoTypeNativeHelper = require('../helper/auto-type-native-helper');
|
|||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
const KeyMap = {
|
||||
tab: 48, enter: 36, space: 49,
|
||||
up: 126, down: 125, left: 123, right: 124, home: 115, end: 119, pgup: 116, pgdn: 121,
|
||||
ins: 114, del: 117, bs: 51, esc: 53,
|
||||
win: 55, rwin: 55,
|
||||
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101,
|
||||
f10: 109, f11: 103, f12: 111, f13: 105, f14: 107, f15: 113, f16: 106,
|
||||
add: 69, subtract: 78, multiply: 67, divide: 75,
|
||||
n0: 82, n1: 83, n2: 84, n3: 85, n4: 86,
|
||||
n5: 87, n6: 88, n7: 89, n8: 91, n9: 92
|
||||
tab: 48,
|
||||
enter: 36,
|
||||
space: 49,
|
||||
up: 126,
|
||||
down: 125,
|
||||
left: 123,
|
||||
right: 124,
|
||||
home: 115,
|
||||
end: 119,
|
||||
pgup: 116,
|
||||
pgdn: 121,
|
||||
ins: 114,
|
||||
del: 117,
|
||||
bs: 51,
|
||||
esc: 53,
|
||||
win: 55,
|
||||
rwin: 55,
|
||||
f1: 122,
|
||||
f2: 120,
|
||||
f3: 99,
|
||||
f4: 118,
|
||||
f5: 96,
|
||||
f6: 97,
|
||||
f7: 98,
|
||||
f8: 100,
|
||||
f9: 101,
|
||||
f10: 109,
|
||||
f11: 103,
|
||||
f12: 111,
|
||||
f13: 105,
|
||||
f14: 107,
|
||||
f15: 113,
|
||||
f16: 106,
|
||||
add: 69,
|
||||
subtract: 78,
|
||||
multiply: 67,
|
||||
divide: 75,
|
||||
n0: 82,
|
||||
n1: 83,
|
||||
n2: 84,
|
||||
n3: 85,
|
||||
n4: 86,
|
||||
n5: 87,
|
||||
n6: 88,
|
||||
n7: 89,
|
||||
n8: 91,
|
||||
n9: 92
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
|
|
|
@ -3,15 +3,53 @@ const Locale = require('../../util/locale');
|
|||
|
||||
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
|
||||
const KeyMap = {
|
||||
tab: 'Tab', enter: 'KP_Enter', space: 'KP_Space',
|
||||
up: 'Up', down: 'Down', left: 'Left', right: 'Right', home: 'Home', end: 'End', pgup: 'Page_Up', pgdn: 'Page_Down',
|
||||
ins: 'Insert', del: 'Delete', bs: 'BackSpace', esc: 'Escape',
|
||||
win: 'Meta_L', rwin: 'Meta_R',
|
||||
f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
|
||||
f10: 'F10', f11: 'F11', f12: 'F12', f13: 'F13', f14: 'F14', f15: 'F15', f16: 'F16',
|
||||
add: 'KP_Add', subtract: 'KP_Subtract', multiply: 'KP_Multiply', divide: 'KP_Divide',
|
||||
n0: 'KP_0', n1: 'KP_1', n2: 'KP_2', n3: 'KP_3', n4: 'KP_4',
|
||||
n5: 'KP_5', n6: 'KP_6', n7: 'KP_7', n8: 'KP_8', n9: 'KP_9'
|
||||
tab: 'Tab',
|
||||
enter: 'KP_Enter',
|
||||
space: 'KP_Space',
|
||||
up: 'Up',
|
||||
down: 'Down',
|
||||
left: 'Left',
|
||||
right: 'Right',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'Page_Up',
|
||||
pgdn: 'Page_Down',
|
||||
ins: 'Insert',
|
||||
del: 'Delete',
|
||||
bs: 'BackSpace',
|
||||
esc: 'Escape',
|
||||
win: 'Meta_L',
|
||||
rwin: 'Meta_R',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KP_Add',
|
||||
subtract: 'KP_Subtract',
|
||||
multiply: 'KP_Multiply',
|
||||
divide: 'KP_Divide',
|
||||
n0: 'KP_0',
|
||||
n1: 'KP_1',
|
||||
n2: 'KP_2',
|
||||
n3: 'KP_3',
|
||||
n4: 'KP_4',
|
||||
n5: 'KP_5',
|
||||
n6: 'KP_6',
|
||||
n7: 'KP_7',
|
||||
n8: 'KP_8',
|
||||
n9: 'KP_9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
|
@ -69,7 +107,7 @@ AutoTypeEmitter.prototype.copyPaste = function(text) {
|
|||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('sleep ' + (time / 1000));
|
||||
this.pendingScript.push('sleep ' + time / 1000);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
|
|
|
@ -3,15 +3,53 @@ const AutoTypeNativeHelper = require('../helper/auto-type-native-helper');
|
|||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
const KeyMap = {
|
||||
tab: 0x09, enter: 0x0D, space: 0x20,
|
||||
up: 0x26, down: 0x28, left: 0x25, right: 0x27, home: 0x24, end: 0x23, pgup: 0x21, pgdn: 0x22,
|
||||
ins: 0x2D, del: 0x2E, bs: 0x08, esc: 0x1B,
|
||||
win: 0x5B, rwin: 0x5C,
|
||||
f1: 0x70, f2: 0x71, f3: 0x72, f4: 0x73, f5: 0x74, f6: 0x75, f7: 0x76, f8: 0x77, f9: 0x78,
|
||||
f10: 0x79, f11: 0x7A, f12: 0x7B, f13: 0x7C, f14: 0x7D, f15: 0x7E, f16: 0x7F,
|
||||
add: 0x6B, subtract: 0x6D, multiply: 0x6A, divide: 0x6F,
|
||||
n0: 0x30, n1: 0x31, n2: 0x32, n3: 0x33, n4: 0x34,
|
||||
n5: 0x35, n6: 0x36, n7: 0x37, n8: 0x38, n9: 0x39
|
||||
tab: 0x09,
|
||||
enter: 0x0d,
|
||||
space: 0x20,
|
||||
up: 0x26,
|
||||
down: 0x28,
|
||||
left: 0x25,
|
||||
right: 0x27,
|
||||
home: 0x24,
|
||||
end: 0x23,
|
||||
pgup: 0x21,
|
||||
pgdn: 0x22,
|
||||
ins: 0x2d,
|
||||
del: 0x2e,
|
||||
bs: 0x08,
|
||||
esc: 0x1b,
|
||||
win: 0x5b,
|
||||
rwin: 0x5c,
|
||||
f1: 0x70,
|
||||
f2: 0x71,
|
||||
f3: 0x72,
|
||||
f4: 0x73,
|
||||
f5: 0x74,
|
||||
f6: 0x75,
|
||||
f7: 0x76,
|
||||
f8: 0x77,
|
||||
f9: 0x78,
|
||||
f10: 0x79,
|
||||
f11: 0x7a,
|
||||
f12: 0x7b,
|
||||
f13: 0x7c,
|
||||
f14: 0x7d,
|
||||
f15: 0x7e,
|
||||
f16: 0x7f,
|
||||
add: 0x6b,
|
||||
subtract: 0x6d,
|
||||
multiply: 0x6a,
|
||||
divide: 0x6f,
|
||||
n0: 0x30,
|
||||
n1: 0x31,
|
||||
n2: 0x32,
|
||||
n3: 0x33,
|
||||
n4: 0x34,
|
||||
n5: 0x35,
|
||||
n6: 0x36,
|
||||
n7: 0x37,
|
||||
n8: 0x38,
|
||||
n9: 0x39
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
const Launcher = require('../../comp/launcher');
|
||||
|
||||
const ForeMostAppScript = 'tell application "System Events" to set frontApp to name of first process whose frontmost is true';
|
||||
const ChromeScript = 'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
const ForeMostAppScript =
|
||||
'tell application "System Events" to set frontApp to name of first process whose frontmost is true';
|
||||
const ChromeScript =
|
||||
'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const SafariScript = 'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
const SafariScript =
|
||||
'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const OtherAppsScript = 'tell application "System Events"\n' +
|
||||
const OtherAppsScript =
|
||||
'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
|
@ -15,12 +19,13 @@ const OtherAppsScript = 'tell application "System Events"\n' +
|
|||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
const AutoTypeHelper = function() {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const appName = out.trim();
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
|
@ -28,20 +33,26 @@ AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
|||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[1] || '').trim(), parts[0].trim());
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[1] || '').trim(), parts[0].trim());
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, out.trim());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const Launcher = require('../../comp/launcher');
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
const AutoTypeHelper = function() {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
const Launcher = require('../../comp/launcher');
|
||||
const AutoTypeNativeHelper = require('./auto-type-native-helper');
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
const AutoTypeHelper = function() {};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete: function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[0] || '').trim(),
|
||||
parts[1] ? parts[1].trim() : undefined);
|
||||
return callback(null, (parts[0] || '').trim(), parts[1] ? parts[1].trim() : undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -31,14 +31,16 @@ const AutoType = {
|
|||
},
|
||||
|
||||
handleEvent(e) {
|
||||
const entry = e && e.entry || null;
|
||||
const entry = (e && e.entry) || null;
|
||||
logger.debug('Auto type event', entry);
|
||||
if (this.running) {
|
||||
logger.debug('Already running, skipping event');
|
||||
return;
|
||||
}
|
||||
if (entry) {
|
||||
this.hideWindow(() => { this.runAndHandleResult({ entry }); });
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult({ entry });
|
||||
});
|
||||
} else {
|
||||
if (this.selectEntryView) {
|
||||
return;
|
||||
|
@ -180,7 +182,7 @@ const AutoType = {
|
|||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowTitle((e, title, url) => {
|
||||
const filter = new AutoTypeFilter({title, url}, this.appModel);
|
||||
const filter = new AutoTypeFilter({ title, url }, this.appModel);
|
||||
const evt = { filter };
|
||||
if (!this.appModel.files.hasOpenFiles()) {
|
||||
this.pendingEvent = evt;
|
||||
|
@ -224,10 +226,14 @@ const AutoType = {
|
|||
this.selectEntryView.on('show-open-files', () => {
|
||||
this.selectEntryView.hide();
|
||||
Backbone.trigger('open-file');
|
||||
Backbone.once('closed-open-view', () => {
|
||||
this.selectEntryView.show();
|
||||
this.selectEntryView.setupKeys();
|
||||
}, this);
|
||||
Backbone.once(
|
||||
'closed-open-view',
|
||||
() => {
|
||||
this.selectEntryView.show();
|
||||
this.selectEntryView.setupKeys();
|
||||
},
|
||||
this
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@ const EntryCollection = Backbone.Collection.extend({
|
|||
'-created': Comparators.dateComparator('created', false),
|
||||
'updated': Comparators.dateComparator('updated', true),
|
||||
'-updated': Comparators.dateComparator('updated', false),
|
||||
'-attachments': function(x, y) { return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y)); },
|
||||
'-attachments': function(x, y) {
|
||||
return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y));
|
||||
},
|
||||
'-rank': Comparators.rankComparator()
|
||||
},
|
||||
|
||||
|
@ -28,7 +30,7 @@ const EntryCollection = Backbone.Collection.extend({
|
|||
filter: null,
|
||||
|
||||
initialize: function(models, options) {
|
||||
const comparatorName = options && options.comparator || this.defaultComparator;
|
||||
const comparatorName = (options && options.comparator) || this.defaultComparator;
|
||||
this.comparator = this.comparators[comparatorName];
|
||||
},
|
||||
|
||||
|
|
|
@ -5,30 +5,31 @@ const SettingsStore = require('../comp/settings-store');
|
|||
const FileInfoCollection = Backbone.Collection.extend({
|
||||
model: FileInfoModel,
|
||||
|
||||
initialize: function () {
|
||||
},
|
||||
initialize: function() {},
|
||||
|
||||
load: function () {
|
||||
load: function() {
|
||||
return SettingsStore.load('file-info').then(data => {
|
||||
if (data) {
|
||||
this.reset(data, {silent: true});
|
||||
this.reset(data, { silent: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
save: function () {
|
||||
save: function() {
|
||||
SettingsStore.save('file-info', this.toJSON());
|
||||
},
|
||||
|
||||
getLast: function () {
|
||||
getLast: function() {
|
||||
return this.first();
|
||||
},
|
||||
|
||||
getMatch: function (storage, name, path) {
|
||||
getMatch: function(storage, name, path) {
|
||||
return this.find(fi => {
|
||||
return (fi.get('storage') || '') === (storage || '') &&
|
||||
return (
|
||||
(fi.get('storage') || '') === (storage || '') &&
|
||||
(fi.get('name') || '') === (name || '') &&
|
||||
(fi.get('path') || '') === (path || '');
|
||||
(fi.get('path') || '') === (path || '')
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -5,10 +5,30 @@ const Alerts = {
|
|||
alertDisplayed: false,
|
||||
|
||||
buttons: {
|
||||
ok: {result: 'yes', get title() { return Locale.alertOk; }},
|
||||
yes: {result: 'yes', get title() { return Locale.alertYes; }},
|
||||
no: {result: '', get title() { return Locale.alertNo; }},
|
||||
cancel: {result: '', get title() { return Locale.alertCancel; }}
|
||||
ok: {
|
||||
result: 'yes',
|
||||
get title() {
|
||||
return Locale.alertOk;
|
||||
}
|
||||
},
|
||||
yes: {
|
||||
result: 'yes',
|
||||
get title() {
|
||||
return Locale.alertYes;
|
||||
}
|
||||
},
|
||||
no: {
|
||||
result: '',
|
||||
get title() {
|
||||
return Locale.alertNo;
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
result: '',
|
||||
get title() {
|
||||
return Locale.alertCancel;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
alert: function(config) {
|
||||
|
@ -46,39 +66,54 @@ const Alerts = {
|
|||
},
|
||||
|
||||
info: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'info',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
}, config));
|
||||
this.alert(
|
||||
_.extend(
|
||||
{
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'info',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
},
|
||||
config
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
error: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'exclamation-circle',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
}, config));
|
||||
this.alert(
|
||||
_.extend(
|
||||
{
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'exclamation-circle',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
},
|
||||
config
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
yesno: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'question',
|
||||
buttons: [this.buttons.yes, this.buttons.no],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: 'yes'
|
||||
}, config));
|
||||
this.alert(
|
||||
_.extend(
|
||||
{
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'question',
|
||||
buttons: [this.buttons.yes, this.buttons.no],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: 'yes'
|
||||
},
|
||||
config
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -37,13 +37,13 @@ const AppRightsChecker = {
|
|||
this.alert = Alerts.alert({
|
||||
icon: 'lock',
|
||||
header: Locale.appRightsAlert,
|
||||
body: Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
|
||||
'<br/>' + Locale.appRightsAlertBody2 + `: <pre>${command}</pre>`,
|
||||
buttons: [
|
||||
{result: 'skip', title: Locale.alertDoNotAsk, error: true},
|
||||
Alerts.buttons.ok
|
||||
],
|
||||
success: (result) => {
|
||||
body:
|
||||
Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
|
||||
'<br/>' +
|
||||
Locale.appRightsAlertBody2 +
|
||||
`: <pre>${command}</pre>`,
|
||||
buttons: [{ result: 'skip', title: Locale.alertDoNotAsk, error: true }, Alerts.buttons.ok],
|
||||
success: result => {
|
||||
if (result === 'skip') {
|
||||
this.dontAskAnymore();
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ const AppRightsChecker = {
|
|||
|
||||
runInstaller() {
|
||||
Launcher.spawn({
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb\ Installer.app/Contents/MacOS/applet',
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
||||
complete: () => {
|
||||
this.needRunInstaller(needRun => {
|
||||
if (this.alert && !needRun) {
|
||||
|
|
|
@ -21,13 +21,13 @@ const CopyPaste = {
|
|||
Backbone.off('main-window-will-close', clearClipboard);
|
||||
}, clipboardSeconds * 1000);
|
||||
}
|
||||
return {success: true, seconds: clipboardSeconds};
|
||||
return { success: true, seconds: clipboardSeconds };
|
||||
} else {
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
return {success: true};
|
||||
return { success: true };
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
@ -41,8 +41,12 @@ const CopyPaste = {
|
|||
hiddenInput[0].selectionEnd = text.length;
|
||||
hiddenInput.focus();
|
||||
hiddenInput.on({
|
||||
'copy cut paste': function() { setTimeout(() => hiddenInput.blur(), 0); },
|
||||
blur: function() { hiddenInput.remove(); }
|
||||
'copy cut paste': function() {
|
||||
setTimeout(() => hiddenInput.blur(), 0);
|
||||
},
|
||||
blur: function() {
|
||||
hiddenInput.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -37,7 +37,12 @@ DropboxChooser.prototype.buildUrl = function() {
|
|||
iframe: 'false',
|
||||
version: 2
|
||||
};
|
||||
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(key => key + '=' + urlParams[key]).join('&');
|
||||
return (
|
||||
'https://www.dropbox.com/chooser?' +
|
||||
Object.keys(urlParams)
|
||||
.map(key => key + '=' + urlParams[key])
|
||||
.join('&')
|
||||
);
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.onMessage = function(e) {
|
||||
|
|
|
@ -2,9 +2,15 @@ const AppSettingsModel = require('../models/app-settings-model');
|
|||
|
||||
const ExportApi = {
|
||||
settings: {
|
||||
get: function(key) { return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON(); },
|
||||
set: function(key, value) { AppSettingsModel.instance.set(key, value); },
|
||||
del: function(key) { AppSettingsModel.instance.unset(key); }
|
||||
get: function(key) {
|
||||
return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON();
|
||||
},
|
||||
set: function(key, value) {
|
||||
AppSettingsModel.instance.set(key, value);
|
||||
},
|
||||
del: function(key) {
|
||||
AppSettingsModel.instance.unset(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -25,16 +25,23 @@ const FeatureTester = {
|
|||
const data = 'e567554429098a38d5f819115edffd39';
|
||||
const iv = '4db46dff4add42cb813b98de98e627c4';
|
||||
const exp = '46ab4c37d9ec594e5742971f76f7c1620bc29f2e0736b27832d6bcc5c1c39dc1';
|
||||
return aesCbc.importKey(kdbxweb.ByteUtils.hexToBytes(key)).then(() => {
|
||||
return aesCbc.encrypt(kdbxweb.ByteUtils.hexToBytes(data), kdbxweb.ByteUtils.hexToBytes(iv)).then(res => {
|
||||
if (kdbxweb.ByteUtils.bytesToHex(res) !== exp) {
|
||||
throw 'AES is not working properly';
|
||||
}
|
||||
if (kdbxweb.CryptoEngine.random(1).length !== 1) {
|
||||
throw 'Random is not working';
|
||||
}
|
||||
return aesCbc
|
||||
.importKey(kdbxweb.ByteUtils.hexToBytes(key))
|
||||
.then(() => {
|
||||
return aesCbc
|
||||
.encrypt(kdbxweb.ByteUtils.hexToBytes(data), kdbxweb.ByteUtils.hexToBytes(iv))
|
||||
.then(res => {
|
||||
if (kdbxweb.ByteUtils.bytesToHex(res) !== exp) {
|
||||
throw 'AES is not working properly';
|
||||
}
|
||||
if (kdbxweb.CryptoEngine.random(1).length !== 1) {
|
||||
throw 'Random is not working';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
throw 'WebCrypto is not supported: ' + e;
|
||||
});
|
||||
}).catch(e => { throw 'WebCrypto is not supported: ' + e; });
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -3,33 +3,37 @@ const Locale = require('../util/locale');
|
|||
|
||||
const GeneratorPresets = {
|
||||
get defaultPreset() {
|
||||
return { name: 'Default', title: Locale.genPresetDefault,
|
||||
length: 16, upper: true, lower: true, digits: true };
|
||||
return { name: 'Default', title: Locale.genPresetDefault, length: 16, upper: true, lower: true, digits: true };
|
||||
},
|
||||
|
||||
get builtIn() {
|
||||
return [
|
||||
this.defaultPreset,
|
||||
{ name: 'Pronounceable', title: Locale.genPresetPronounceable,
|
||||
length: 10, lower: true, upper: true },
|
||||
{ name: 'Med', title: Locale.genPresetMed,
|
||||
length: 16, upper: true, lower: true, digits: true, special: true, brackets: true, ambiguous: true },
|
||||
{ name: 'Long', title: Locale.genPresetLong,
|
||||
length: 32, upper: true, lower: true, digits: true },
|
||||
{ name: 'Pin4', title: Locale.genPresetPin4,
|
||||
length: 4, digits: true },
|
||||
{ name: 'Mac', title: Locale.genPresetMac,
|
||||
length: 17, upper: true, digits: true, special: true },
|
||||
{ name: 'Hash128', title: Locale.genPresetHash128,
|
||||
length: 32, lower: true, digits: true },
|
||||
{ name: 'Hash256', title: Locale.genPresetHash256,
|
||||
length: 64, lower: true, digits: true }
|
||||
{ name: 'Pronounceable', title: Locale.genPresetPronounceable, length: 10, lower: true, upper: true },
|
||||
{
|
||||
name: 'Med',
|
||||
title: Locale.genPresetMed,
|
||||
length: 16,
|
||||
upper: true,
|
||||
lower: true,
|
||||
digits: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
},
|
||||
{ name: 'Long', title: Locale.genPresetLong, length: 32, upper: true, lower: true, digits: true },
|
||||
{ name: 'Pin4', title: Locale.genPresetPin4, length: 4, digits: true },
|
||||
{ name: 'Mac', title: Locale.genPresetMac, length: 17, upper: true, digits: true, special: true },
|
||||
{ name: 'Hash128', title: Locale.genPresetHash128, length: 32, lower: true, digits: true },
|
||||
{ name: 'Hash256', title: Locale.genPresetHash256, length: 64, lower: true, digits: true }
|
||||
];
|
||||
},
|
||||
|
||||
get all() {
|
||||
let presets = this.builtIn;
|
||||
presets.forEach(preset => { preset.builtIn = true; });
|
||||
presets.forEach(preset => {
|
||||
preset.builtIn = true;
|
||||
});
|
||||
const setting = AppSettingsModel.instance.get('generatorPresets');
|
||||
if (setting) {
|
||||
if (setting.user) {
|
||||
|
|
|
@ -15,15 +15,22 @@ const KeyHandler = {
|
|||
$(document).bind('keypress', this.keypress.bind(this));
|
||||
$(document).bind('keydown', this.keydown.bind(this));
|
||||
|
||||
this.shortcuts[Keys.DOM_VK_A] = [{ handler: this.handleAKey, thisArg: this, shortcut: this.SHORTCUT_ACTION,
|
||||
modal: true, noPrevent: true }];
|
||||
this.shortcuts[Keys.DOM_VK_A] = [
|
||||
{ handler: this.handleAKey, thisArg: this, shortcut: this.SHORTCUT_ACTION, modal: true, noPrevent: true }
|
||||
];
|
||||
},
|
||||
onKey: function(key, handler, thisArg, shortcut, modal, noPrevent) {
|
||||
let keyShortcuts = this.shortcuts[key];
|
||||
if (!keyShortcuts) {
|
||||
this.shortcuts[key] = keyShortcuts = [];
|
||||
}
|
||||
keyShortcuts.push({ handler: handler, thisArg: thisArg, shortcut: shortcut, modal: modal, noPrevent: noPrevent });
|
||||
keyShortcuts.push({
|
||||
handler: handler,
|
||||
thisArg: thisArg,
|
||||
shortcut: shortcut,
|
||||
modal: modal,
|
||||
noPrevent: noPrevent
|
||||
});
|
||||
},
|
||||
offKey: function(key, handler, thisArg) {
|
||||
if (this.shortcuts[key]) {
|
||||
|
@ -51,16 +58,24 @@ const KeyHandler = {
|
|||
const isActionKey = this.isActionKey(e);
|
||||
switch (sh.shortcut) {
|
||||
case this.SHORTCUT_ACTION:
|
||||
if (!isActionKey) { continue; }
|
||||
if (!isActionKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case this.SHORTCUT_OPT:
|
||||
if (!e.altKey) { continue; }
|
||||
if (!e.altKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case this.SHORTCUT_ACTION + this.SHORTCUT_OPT:
|
||||
if (!e.altKey || !isActionKey) { continue; }
|
||||
if (!e.altKey || !isActionKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) { continue; }
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
sh.handler.call(sh.thisArg, e, code);
|
||||
|
@ -74,11 +89,15 @@ const KeyHandler = {
|
|||
}
|
||||
},
|
||||
keypress: function(e) {
|
||||
if (!this.modal &&
|
||||
if (
|
||||
!this.modal &&
|
||||
e.charCode !== Keys.DOM_VK_RETURN &&
|
||||
e.charCode !== Keys.DOM_VK_ESCAPE &&
|
||||
e.charCode !== Keys.DOM_VK_TAB &&
|
||||
!e.altKey && !e.ctrlKey && !e.metaKey) {
|
||||
!e.altKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey
|
||||
) {
|
||||
this.trigger('keypress', e);
|
||||
} else if (this.modal) {
|
||||
this.trigger('keypress:' + this.modal, e);
|
||||
|
|
|
@ -10,9 +10,13 @@ const Launcher = {
|
|||
clipboardSupported: false,
|
||||
ready: function(callback) {
|
||||
document.addEventListener('deviceready', callback, false);
|
||||
document.addEventListener('pause', () => {
|
||||
Backbone.trigger('app-minimized');
|
||||
}, false);
|
||||
document.addEventListener(
|
||||
'pause',
|
||||
() => {
|
||||
Backbone.trigger('app-minimized');
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
platform: function() {
|
||||
return 'cordova';
|
||||
|
@ -22,7 +26,9 @@ const Launcher = {
|
|||
},
|
||||
devTools: false,
|
||||
// openDevTools: function() { },
|
||||
getSaveFileName: function(defaultPath, callback) { /* skip in cordova */ },
|
||||
getSaveFileName: function(defaultPath, callback) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
getDataPath: function() {
|
||||
const storagePath = window.cordova.file.externalDataDirectory;
|
||||
return [storagePath].concat(Array.from(arguments)).filter(s => !!s);
|
||||
|
@ -47,9 +53,14 @@ const Launcher = {
|
|||
},
|
||||
writeFile: function(path, data, callback) {
|
||||
const createFile = filePath => {
|
||||
window.resolveLocalFileSystemURL(filePath.dir, dir => {
|
||||
dir.getFile(filePath.file, {create: true}, writeFile);
|
||||
}, callback, callback);
|
||||
window.resolveLocalFileSystemURL(
|
||||
filePath.dir,
|
||||
dir => {
|
||||
dir.getFile(filePath.file, { create: true }, writeFile);
|
||||
},
|
||||
callback,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
const writeFile = fileEntry => {
|
||||
|
@ -60,9 +71,11 @@ const Launcher = {
|
|||
}, callback);
|
||||
};
|
||||
|
||||
if (path.startsWith('cdvfile://')) { // then file exists
|
||||
if (path.startsWith('cdvfile://')) {
|
||||
// then file exists
|
||||
window.resolveLocalFileSystemURL(path, writeFile, callback, callback);
|
||||
} else { // create file on sd card
|
||||
} else {
|
||||
// create file on sd card
|
||||
const filePath = this.parsePath(path);
|
||||
this.mkdir(filePath.dir, () => {
|
||||
createFile(filePath);
|
||||
|
@ -70,55 +83,86 @@ const Launcher = {
|
|||
}
|
||||
},
|
||||
readFile: function(path, encoding, callback) {
|
||||
window.resolveLocalFileSystemURL(path, fileEntry => {
|
||||
fileEntry.file(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = callback;
|
||||
reader.onloadend = () => {
|
||||
const contents = new Uint8Array(reader.result);
|
||||
callback(encoding ? String.fromCharCode.apply(null, contents) : contents);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}, err => callback(undefined, err));
|
||||
}, err => callback(undefined, err));
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.file(
|
||||
file => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = callback;
|
||||
reader.onloadend = () => {
|
||||
const contents = new Uint8Array(reader.result);
|
||||
callback(encoding ? String.fromCharCode.apply(null, contents) : contents);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
fileExists: function(path, callback) {
|
||||
window.resolveLocalFileSystemURL(path, fileEntry => callback(true), () => callback(false));
|
||||
},
|
||||
deleteFile: function(path, callback) {
|
||||
window.resolveLocalFileSystemURL(path, fileEntry => {
|
||||
fileEntry.remove(callback, callback, callback);
|
||||
}, callback);
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.remove(callback, callback, callback);
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
statFile: function(path, callback) {
|
||||
window.resolveLocalFileSystemURL(path, fileEntry => {
|
||||
fileEntry.file(file => {
|
||||
callback({
|
||||
ctime: new Date(file.lastModified),
|
||||
mtime: new Date(file.lastModified)
|
||||
});
|
||||
}, err => callback(undefined, err));
|
||||
}, err => callback(undefined, err));
|
||||
window.resolveLocalFileSystemURL(
|
||||
path,
|
||||
fileEntry => {
|
||||
fileEntry.file(
|
||||
file => {
|
||||
callback({
|
||||
ctime: new Date(file.lastModified),
|
||||
mtime: new Date(file.lastModified)
|
||||
});
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
err => callback(undefined, err)
|
||||
);
|
||||
},
|
||||
mkdir: function(dir, callback) {
|
||||
const basePath = this.getDataPath().join('/');
|
||||
const createDir = (dirEntry, path, callback) => {
|
||||
const name = path.shift();
|
||||
dirEntry.getDirectory(name, { create: true }, dirEntry => {
|
||||
if (path.length) { // there is more to create
|
||||
createDir(dirEntry, path, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}, callback);
|
||||
dirEntry.getDirectory(
|
||||
name,
|
||||
{ create: true },
|
||||
dirEntry => {
|
||||
if (path.length) {
|
||||
// there is more to create
|
||||
createDir(dirEntry, path, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
const localPath = dir.replace(basePath, '').split('/').filter(s => !!s);
|
||||
const localPath = dir
|
||||
.replace(basePath, '')
|
||||
.split('/')
|
||||
.filter(s => !!s);
|
||||
|
||||
if (localPath.length) {
|
||||
window.resolveLocalFileSystemURL(basePath, dirEntry => {
|
||||
createDir(dirEntry, localPath, callback);
|
||||
}, callback);
|
||||
window.resolveLocalFileSystemURL(
|
||||
basePath,
|
||||
dirEntry => {
|
||||
createDir(dirEntry, localPath, callback);
|
||||
},
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
|
@ -145,11 +189,15 @@ const Launcher = {
|
|||
this.hideApp();
|
||||
},
|
||||
|
||||
requestExit: function() { /* skip in cordova */ },
|
||||
requestExit: function() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
requestRestart: function() {
|
||||
window.location.reload();
|
||||
},
|
||||
cancelRestart: function() { /* skip in cordova */ },
|
||||
cancelRestart: function() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
|
||||
setClipboardText: function(text) {},
|
||||
getClipboardText: function() {},
|
||||
|
@ -169,13 +217,21 @@ const Launcher = {
|
|||
},
|
||||
|
||||
// getMainWindow: function() { },
|
||||
resolveProxy: function(url, callback) { /* skip in cordova */ },
|
||||
openWindow: function(opts) { /* skip in cordova */ },
|
||||
hideApp: function() { /* skip in cordova */ },
|
||||
resolveProxy: function(url, callback) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
openWindow: function(opts) {
|
||||
/* skip in cordova */
|
||||
},
|
||||
hideApp: function() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
isAppFocused: function() {
|
||||
return false; /* skip in cordova */
|
||||
},
|
||||
showMainWindow: function() { /* skip in cordova */ },
|
||||
showMainWindow: function() {
|
||||
/* skip in cordova */
|
||||
},
|
||||
// spawn: function(config) { },
|
||||
openFileChooser: function(callback) {
|
||||
const onFileSelected = function(selected) {
|
||||
|
|
|
@ -28,18 +28,23 @@ const Launcher = {
|
|||
},
|
||||
devTools: true,
|
||||
openDevTools: function() {
|
||||
this.electron().remote.getCurrentWindow().openDevTools({ mode: 'bottom' });
|
||||
this.electron()
|
||||
.remote.getCurrentWindow()
|
||||
.openDevTools({ mode: 'bottom' });
|
||||
},
|
||||
getSaveFileName: function(defaultPath, callback) {
|
||||
if (defaultPath) {
|
||||
const homePath = this.remReq('electron').app.getPath('userDesktop');
|
||||
defaultPath = this.joinPath(homePath, defaultPath);
|
||||
}
|
||||
this.remReq('electron').dialog.showSaveDialog({
|
||||
title: Locale.launcherSave,
|
||||
defaultPath: defaultPath,
|
||||
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
|
||||
}, callback);
|
||||
this.remReq('electron').dialog.showSaveDialog(
|
||||
{
|
||||
title: Locale.launcherSave,
|
||||
defaultPath: defaultPath,
|
||||
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
getUserDataPath: function(fileName) {
|
||||
if (!this.userDataPath) {
|
||||
|
@ -110,9 +115,7 @@ const Launcher = {
|
|||
return callback();
|
||||
}
|
||||
|
||||
fs.mkdir(stack.shift(), err =>
|
||||
err ? callback(err) : create(stack, callback)
|
||||
);
|
||||
fs.mkdir(stack.shift(), err => (err ? callback(err) : create(stack, callback)));
|
||||
};
|
||||
|
||||
collect(dir, stack, () => create(stack, callback));
|
||||
|
@ -222,8 +225,12 @@ const Launcher = {
|
|||
[ps.stdin, ps.stdout, ps.stderr].forEach(s => s.setEncoding('utf-8'));
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
ps.stderr.on('data', d => { stderr += d.toString('utf-8'); });
|
||||
ps.stdout.on('data', d => { stdout += d.toString('utf-8'); });
|
||||
ps.stderr.on('data', d => {
|
||||
stderr += d.toString('utf-8');
|
||||
});
|
||||
ps.stdout.on('data', d => {
|
||||
stdout += d.toString('utf-8');
|
||||
});
|
||||
ps.on('close', code => {
|
||||
stdout = stdout.trim();
|
||||
stderr = stderr.trim();
|
||||
|
|
|
@ -18,21 +18,28 @@ const OtpQrReader = {
|
|||
if (screenshotKey) {
|
||||
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', '<code>' + screenshotKey + '</code>');
|
||||
}
|
||||
const pasteKey = FeatureDetector.isMobile ? ''
|
||||
: Locale.detSetupOtpAlertBodyWith.replace('{}',
|
||||
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>');
|
||||
const pasteKey = FeatureDetector.isMobile
|
||||
? ''
|
||||
: Locale.detSetupOtpAlertBodyWith.replace(
|
||||
'{}',
|
||||
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>'
|
||||
);
|
||||
OtpQrReader.startListenClipoard();
|
||||
const buttons = [{result: 'manually', title: Locale.detSetupOtpManualButton, silent: true},
|
||||
Alerts.buttons.cancel];
|
||||
const buttons = [
|
||||
{ result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
|
||||
Alerts.buttons.cancel
|
||||
];
|
||||
if (FeatureDetector.isMobile) {
|
||||
buttons.unshift({result: 'select', title: Locale.detSetupOtpScanButton});
|
||||
buttons.unshift({ result: 'select', title: Locale.detSetupOtpScanButton });
|
||||
}
|
||||
const line3 = FeatureDetector.isMobile ? Locale.detSetupOtpAlertBody3Mobile
|
||||
const line3 = FeatureDetector.isMobile
|
||||
? Locale.detSetupOtpAlertBody3Mobile
|
||||
: Locale.detSetupOtpAlertBody3.replace('{}', pasteKey || '');
|
||||
OtpQrReader.alert = Alerts.alert({
|
||||
icon: 'qrcode',
|
||||
header: Locale.detSetupOtpAlert,
|
||||
body: [Locale.detSetupOtpAlertBody,
|
||||
body: [
|
||||
Locale.detSetupOtpAlertBody,
|
||||
Locale.detSetupOtpAlertBody1,
|
||||
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
|
||||
line3,
|
||||
|
@ -127,7 +134,8 @@ const OtpQrReader = {
|
|||
logger.error('Error parsing QR code', err);
|
||||
Alerts.error({
|
||||
header: Locale.detOtpQrWrong,
|
||||
body: Locale.detOtpQrWrongBody + '<pre class="modal__pre">' + _.escape(err.toString()) + '</pre>'
|
||||
body:
|
||||
Locale.detOtpQrWrongBody + '<pre class="modal__pre">' + _.escape(err.toString()) + '</pre>'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -51,10 +51,18 @@ const PopupNotifier = {
|
|||
const parts = part.split('=');
|
||||
settingsObj[parts[0].trim()] = parts[1].trim();
|
||||
});
|
||||
if (settingsObj.width) { opts.width = +settingsObj.width; }
|
||||
if (settingsObj.height) { opts.height = +settingsObj.height; }
|
||||
if (settingsObj.top) { opts.y = +settingsObj.top; }
|
||||
if (settingsObj.left) { opts.x = +settingsObj.left; }
|
||||
if (settingsObj.width) {
|
||||
opts.width = +settingsObj.width;
|
||||
}
|
||||
if (settingsObj.height) {
|
||||
opts.height = +settingsObj.height;
|
||||
}
|
||||
if (settingsObj.top) {
|
||||
opts.y = +settingsObj.top;
|
||||
}
|
||||
if (settingsObj.left) {
|
||||
opts.x = +settingsObj.left;
|
||||
}
|
||||
}
|
||||
let win = Launcher.openWindow(opts);
|
||||
win.webContents.on('will-redirect', (e, url) => {
|
||||
|
@ -101,8 +109,7 @@ const PopupNotifier = {
|
|||
},
|
||||
|
||||
isOwnUrl(url) {
|
||||
return url.lastIndexOf(Links.WebApp, 0) === 0 ||
|
||||
url.lastIndexOf(location.origin + location.pathname, 0) === 0;
|
||||
return url.lastIndexOf(Links.WebApp, 0) === 0 || url.lastIndexOf(location.origin + location.pathname, 0) === 0;
|
||||
},
|
||||
|
||||
processReturnToApp: function(url) {
|
||||
|
|
|
@ -90,7 +90,10 @@ Object.defineProperty(SecureInput.prototype, 'value', {
|
|||
byteLength++;
|
||||
}
|
||||
}
|
||||
return new kdbxweb.ProtectedValue(valueBytes.buffer.slice(0, byteLength), saltBytes.buffer.slice(0, byteLength));
|
||||
return new kdbxweb.ProtectedValue(
|
||||
valueBytes.buffer.slice(0, byteLength),
|
||||
saltBytes.buffer.slice(0, byteLength)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -22,8 +22,7 @@ const SettingsManager = {
|
|||
hc: 'setGenThemeHc'
|
||||
},
|
||||
|
||||
customLocales: {
|
||||
},
|
||||
customLocales: {},
|
||||
|
||||
setBySettings: function(settings) {
|
||||
if (settings.get('theme')) {
|
||||
|
@ -59,7 +58,7 @@ const SettingsManager = {
|
|||
|
||||
setFontSize: function(fontSize) {
|
||||
const defaultFontSize = FeatureDetector.isMobile ? 14 : 12;
|
||||
document.documentElement.style.fontSize = (defaultFontSize + (fontSize || 0) * 2) + 'px';
|
||||
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
||||
},
|
||||
|
||||
setLocale(loc) {
|
||||
|
@ -83,7 +82,7 @@ const SettingsManager = {
|
|||
},
|
||||
|
||||
getBrowserLocale: function() {
|
||||
const language = navigator.languages && navigator.languages[0] || navigator.language;
|
||||
const language = (navigator.languages && navigator.languages[0]) || navigator.language;
|
||||
if (language && language.lastIndexOf('en', 0) === 0) {
|
||||
return 'en';
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@ const SingleInstanceChecker = {
|
|||
setKey: function(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
setTimeout(() => { localStorage.removeItem(key); }, 100);
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem(key);
|
||||
}, 100);
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,57 +27,64 @@ const Transport = {
|
|||
const opts = Launcher.req('url').parse(config.url);
|
||||
opts.headers = { 'User-Agent': navigator.userAgent };
|
||||
Launcher.resolveProxy(config.url, proxy => {
|
||||
logger.info('Request to ' + config.url + ' ' + (proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy'));
|
||||
logger.info(
|
||||
'Request to ' +
|
||||
config.url +
|
||||
' ' +
|
||||
(proxy ? 'using proxy ' + proxy.host + ':' + proxy.port : 'without proxy')
|
||||
);
|
||||
if (proxy) {
|
||||
opts.headers.Host = opts.host;
|
||||
opts.host = proxy.host;
|
||||
opts.port = proxy.port;
|
||||
opts.path = config.url;
|
||||
}
|
||||
Launcher.req(proto).get(opts, res => {
|
||||
logger.info('Response from ' + config.url + ': ' + res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
if (config.file) {
|
||||
const file = fs.createWriteStream(tmpFile);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(() => {
|
||||
config.success(tmpFile);
|
||||
Launcher.req(proto)
|
||||
.get(opts, res => {
|
||||
logger.info('Response from ' + config.url + ': ' + res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
if (config.file) {
|
||||
const file = fs.createWriteStream(tmpFile);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(() => {
|
||||
config.success(tmpFile);
|
||||
});
|
||||
});
|
||||
});
|
||||
file.on('error', err => {
|
||||
config.error(err);
|
||||
});
|
||||
file.on('error', err => {
|
||||
config.error(err);
|
||||
});
|
||||
} else {
|
||||
let data = [];
|
||||
res.on('data', chunk => {
|
||||
data.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
data = window.Buffer.concat(data);
|
||||
if (config.utf8) {
|
||||
data = data.toString('utf8');
|
||||
}
|
||||
config.success(data);
|
||||
});
|
||||
}
|
||||
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
|
||||
if (config.noRedirect) {
|
||||
return config.error('Too many redirects');
|
||||
}
|
||||
config.url = res.headers.location;
|
||||
config.noRedirect = true;
|
||||
Transport.httpGet(config);
|
||||
} else {
|
||||
let data = [];
|
||||
res.on('data', chunk => {
|
||||
data.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
data = window.Buffer.concat(data);
|
||||
if (config.utf8) {
|
||||
data = data.toString('utf8');
|
||||
}
|
||||
config.success(data);
|
||||
});
|
||||
config.error('HTTP status ' + res.statusCode);
|
||||
}
|
||||
} else if (res.headers.location && [301, 302].indexOf(res.statusCode) >= 0) {
|
||||
if (config.noRedirect) {
|
||||
return config.error('Too many redirects');
|
||||
})
|
||||
.on('error', e => {
|
||||
logger.error('Cannot GET ' + config.url, e);
|
||||
if (tmpFile) {
|
||||
fs.unlink(tmpFile, _.noop);
|
||||
}
|
||||
config.url = res.headers.location;
|
||||
config.noRedirect = true;
|
||||
Transport.httpGet(config);
|
||||
} else {
|
||||
config.error('HTTP status ' + res.statusCode);
|
||||
}
|
||||
}).on('error', e => {
|
||||
logger.error('Cannot GET ' + config.url, e);
|
||||
if (tmpFile) {
|
||||
fs.unlink(tmpFile, _.noop);
|
||||
}
|
||||
config.error(e);
|
||||
});
|
||||
config.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -32,8 +32,10 @@ const Updater = {
|
|||
},
|
||||
|
||||
updateInProgress: function() {
|
||||
return UpdateModel.instance.get('status') === 'checking' ||
|
||||
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0;
|
||||
return (
|
||||
UpdateModel.instance.get('status') === 'checking' ||
|
||||
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
||||
);
|
||||
},
|
||||
|
||||
init: function() {
|
||||
|
@ -55,7 +57,10 @@ const Updater = {
|
|||
let timeDiff = this.MinUpdateTimeout;
|
||||
const lastCheckDate = UpdateModel.instance.get('lastCheckDate');
|
||||
if (lastCheckDate) {
|
||||
timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval);
|
||||
timeDiff = Math.min(
|
||||
Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout),
|
||||
this.UpdateInterval
|
||||
);
|
||||
}
|
||||
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
|
||||
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
|
||||
|
@ -107,8 +112,10 @@ const Updater = {
|
|||
if (!this.canAutoUpdate()) {
|
||||
return;
|
||||
}
|
||||
if (prevLastVersion === UpdateModel.instance.get('lastVersion') &&
|
||||
UpdateModel.instance.get('updateStatus') === 'ready') {
|
||||
if (
|
||||
prevLastVersion === UpdateModel.instance.get('lastVersion') &&
|
||||
UpdateModel.instance.get('updateStatus') === 'ready'
|
||||
) {
|
||||
logger.info('Waiting for the user to apply downloaded update');
|
||||
return;
|
||||
}
|
||||
|
@ -238,7 +245,9 @@ const Updater = {
|
|||
|
||||
checkAppCacheUpdateReady: function() {
|
||||
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
||||
try { window.applicationCache.swapCache(); } catch (e) { }
|
||||
try {
|
||||
window.applicationCache.swapCache();
|
||||
} catch (e) {}
|
||||
UpdateModel.instance.set('updateStatus', 'ready');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,73 @@
|
|||
const IconMap = [
|
||||
'key', 'globe', 'exclamation-triangle', 'server', 'thumb-tack',
|
||||
'comments-o', 'puzzle-piece', 'pencil-square-o', 'plug', 'newspaper-o',
|
||||
'paperclip', 'camera', 'wifi', 'link', 'battery-three-quarters',
|
||||
'barcode', 'certificate', 'bullseye', 'desktop', 'envelope-o',
|
||||
'cog', 'clipboard', 'paper-plane-o', 'television', 'bolt',
|
||||
'inbox', 'floppy-o', 'hdd-o', 'dot-circle-o', 'expeditedssl',
|
||||
'terminal', 'print', 'map-signs', 'flag-checkered', 'wrench',
|
||||
'laptop', 'archive', 'credit-card', 'windows', 'clock-o',
|
||||
'search', 'flask', 'gamepad', 'trash-o', 'sticky-note-o',
|
||||
'ban', 'question-circle', 'cube', 'folder-o', 'folder-open-o',
|
||||
'database', 'unlock-alt', 'lock', 'check', 'pencil',
|
||||
'picture-o', 'book', 'list-alt', 'user-secret', 'cutlery',
|
||||
'home', 'star-o', 'linux', 'map-pin', 'apple',
|
||||
'wikipedia-w', 'usd', 'calendar', 'mobile'
|
||||
'key',
|
||||
'globe',
|
||||
'exclamation-triangle',
|
||||
'server',
|
||||
'thumb-tack',
|
||||
'comments-o',
|
||||
'puzzle-piece',
|
||||
'pencil-square-o',
|
||||
'plug',
|
||||
'newspaper-o',
|
||||
'paperclip',
|
||||
'camera',
|
||||
'wifi',
|
||||
'link',
|
||||
'battery-three-quarters',
|
||||
'barcode',
|
||||
'certificate',
|
||||
'bullseye',
|
||||
'desktop',
|
||||
'envelope-o',
|
||||
'cog',
|
||||
'clipboard',
|
||||
'paper-plane-o',
|
||||
'television',
|
||||
'bolt',
|
||||
'inbox',
|
||||
'floppy-o',
|
||||
'hdd-o',
|
||||
'dot-circle-o',
|
||||
'expeditedssl',
|
||||
'terminal',
|
||||
'print',
|
||||
'map-signs',
|
||||
'flag-checkered',
|
||||
'wrench',
|
||||
'laptop',
|
||||
'archive',
|
||||
'credit-card',
|
||||
'windows',
|
||||
'clock-o',
|
||||
'search',
|
||||
'flask',
|
||||
'gamepad',
|
||||
'trash-o',
|
||||
'sticky-note-o',
|
||||
'ban',
|
||||
'question-circle',
|
||||
'cube',
|
||||
'folder-o',
|
||||
'folder-open-o',
|
||||
'database',
|
||||
'unlock-alt',
|
||||
'lock',
|
||||
'check',
|
||||
'pencil',
|
||||
'picture-o',
|
||||
'book',
|
||||
'list-alt',
|
||||
'user-secret',
|
||||
'cutlery',
|
||||
'home',
|
||||
'star-o',
|
||||
'linux',
|
||||
'map-pin',
|
||||
'apple',
|
||||
'wikipedia-w',
|
||||
'usd',
|
||||
'calendar',
|
||||
'mobile'
|
||||
];
|
||||
|
||||
module.exports = IconMap;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
const Handlebars = require('hbs');
|
||||
|
||||
// inspired by https://stackoverflow.com/questions/22103989/adding-offset-to-index-when-looping-through-items-in-handlebars/39588001#39588001
|
||||
Handlebars.registerHelper('add', (lvalue, rvalue) => (
|
||||
parseInt(lvalue) + parseInt(rvalue)
|
||||
));
|
||||
Handlebars.registerHelper('add', (lvalue, rvalue) => parseInt(lvalue) + parseInt(rvalue));
|
||||
|
|
|
@ -12,7 +12,7 @@ Handlebars.registerHelper('res', function(key, options) {
|
|||
return value;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('Res', function(key) { // eslint-disable-line prefer-arrow-callback
|
||||
Handlebars.registerHelper('Res', key => {
|
||||
let value = Locale[key];
|
||||
if (value) {
|
||||
value = value[0].toUpperCase() + value.substr(1);
|
||||
|
|
|
@ -18,11 +18,16 @@ const Copyable = {
|
|||
this.hideFieldCopyTip();
|
||||
const fieldLabel = e.source.labelEl;
|
||||
const clipboardTime = e.copyRes.seconds;
|
||||
const msg = clipboardTime ? Locale.detFieldCopiedTime.replace('{}', clipboardTime)
|
||||
: Locale.detFieldCopied;
|
||||
const msg = clipboardTime ? Locale.detFieldCopiedTime.replace('{}', clipboardTime) : Locale.detFieldCopied;
|
||||
let tip;
|
||||
if (!this.isHidden()) {
|
||||
tip = Tip.createTip(fieldLabel[0], {title: msg, placement: 'right', fast: true, force: true, noInit: true});
|
||||
tip = Tip.createTip(fieldLabel[0], {
|
||||
title: msg,
|
||||
placement: 'right',
|
||||
fast: true,
|
||||
force: true,
|
||||
noInit: true
|
||||
});
|
||||
this.fieldCopyTip = tip;
|
||||
tip.show();
|
||||
}
|
||||
|
|
|
@ -8,10 +8,7 @@ kdbxweb.ProtectedValue.prototype.isProtected = true;
|
|||
kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
|
||||
const value = this._value;
|
||||
const salt = this._salt;
|
||||
let b,
|
||||
b1,
|
||||
b2,
|
||||
b3;
|
||||
let b, b1, b2, b3;
|
||||
for (let i = 0, len = value.length; i < len; i++) {
|
||||
b = value[i] ^ salt[i];
|
||||
if (b < 128) {
|
||||
|
@ -20,23 +17,32 @@ kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
i++; b1 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
i++;
|
||||
b1 = value[i] ^ salt[i];
|
||||
if (i === len) {
|
||||
break;
|
||||
}
|
||||
if (b >= 192 && b < 224) {
|
||||
if (fn(((b & 0x1f) << 6) | (b1 & 0x3f)) === false) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
i++; b2 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
i++;
|
||||
b2 = value[i] ^ salt[i];
|
||||
if (i === len) {
|
||||
break;
|
||||
}
|
||||
if (b >= 224 && b < 240) {
|
||||
if (fn(((b & 0xf) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)) === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
i++; b3 = value[i] ^ salt[i];
|
||||
if (i === len) { break; }
|
||||
i++;
|
||||
b3 = value[i] ^ salt[i];
|
||||
if (i === len) {
|
||||
break;
|
||||
}
|
||||
if (b >= 240 && b < 248) {
|
||||
let c = ((b & 7) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
|
||||
if (c <= 0xffff) {
|
||||
|
@ -60,7 +66,9 @@ kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
|
|||
Object.defineProperty(kdbxweb.ProtectedValue.prototype, 'textLength', {
|
||||
get: function() {
|
||||
let textLength = 0;
|
||||
this.forEachChar(() => { textLength++; });
|
||||
this.forEachChar(() => {
|
||||
textLength++;
|
||||
});
|
||||
return textLength;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ _.extend(Backbone.View.prototype, {
|
|||
if (this.scroll) {
|
||||
try {
|
||||
this.scroll.dispose();
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
Tip.hideTips(this.$el);
|
||||
this._parentRemove(arguments);
|
||||
|
|
|
@ -114,28 +114,38 @@ const AppModel = Backbone.Model.extend({
|
|||
this.fileInfos.reset();
|
||||
}
|
||||
config.files
|
||||
.filter(file => file && file.storage && file.name && file.path &&
|
||||
!this.fileInfos.getMatch(file.storage, file.name, file.path))
|
||||
.map(file => new FileInfoModel({
|
||||
id: IdGenerator.uuid(),
|
||||
name: file.name,
|
||||
storage: file.storage,
|
||||
path: file.path,
|
||||
opts: file.options
|
||||
}))
|
||||
.filter(
|
||||
file =>
|
||||
file &&
|
||||
file.storage &&
|
||||
file.name &&
|
||||
file.path &&
|
||||
!this.fileInfos.getMatch(file.storage, file.name, file.path)
|
||||
)
|
||||
.map(
|
||||
file =>
|
||||
new FileInfoModel({
|
||||
id: IdGenerator.uuid(),
|
||||
name: file.name,
|
||||
storage: file.storage,
|
||||
path: file.path,
|
||||
opts: file.options
|
||||
})
|
||||
)
|
||||
.reverse()
|
||||
.forEach(fi => this.fileInfos.unshift(fi));
|
||||
}
|
||||
if (config.plugins) {
|
||||
const pluginsPromises = config.plugins
|
||||
.map(plugin => PluginManager.installIfNew(plugin.url, plugin.manifest, true));
|
||||
const pluginsPromises = config.plugins.map(plugin =>
|
||||
PluginManager.installIfNew(plugin.url, plugin.manifest, true)
|
||||
);
|
||||
return Promise.all(pluginsPromises).then(() => {
|
||||
this.settings.set(config.settings);
|
||||
});
|
||||
}
|
||||
if (config.advancedSearch) {
|
||||
this.advancedSearch = config.advancedSearch;
|
||||
this.addFilter({advanced: this.advancedSearch});
|
||||
this.addFilter({ advanced: this.advancedSearch });
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -144,7 +154,7 @@ const AppModel = Backbone.Model.extend({
|
|||
return false;
|
||||
}
|
||||
this.files.add(file);
|
||||
file.get('groups').forEach(function (group) {
|
||||
file.get('groups').forEach(function(group) {
|
||||
this.menu.groupsSection.addItem(group);
|
||||
}, this);
|
||||
this._addTags(file);
|
||||
|
@ -184,9 +194,11 @@ const AppModel = Backbone.Model.extend({
|
|||
_tagsChanged: function() {
|
||||
if (this.tags.length) {
|
||||
this.menu.tagsSection.set('scrollable', true);
|
||||
this.menu.tagsSection.setItems(this.tags.map(tag => {
|
||||
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true};
|
||||
}));
|
||||
this.menu.tagsSection.setItems(
|
||||
this.tags.map(tag => {
|
||||
return { title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true };
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menu.tagsSection.set('scrollable', false);
|
||||
this.menu.tagsSection.removeAllItems();
|
||||
|
@ -310,8 +322,7 @@ const AppModel = Backbone.Model.extend({
|
|||
|
||||
getFirstSelectedGroup: function() {
|
||||
const selGroupId = this.filter.group;
|
||||
let file,
|
||||
group;
|
||||
let file, group;
|
||||
if (selGroupId) {
|
||||
this.files.some(f => {
|
||||
file = f;
|
||||
|
@ -410,19 +421,25 @@ const AppModel = Backbone.Model.extend({
|
|||
openFile: function(params, callback) {
|
||||
const logger = new Logger('open', params.name);
|
||||
logger.info('File open request');
|
||||
const fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path);
|
||||
const fileInfo = params.id
|
||||
? this.fileInfos.get(params.id)
|
||||
: this.fileInfos.getMatch(params.storage, params.name, params.path);
|
||||
if (!params.opts && fileInfo && fileInfo.get('opts')) {
|
||||
params.opts = fileInfo.get('opts');
|
||||
}
|
||||
if (fileInfo && fileInfo.get('modified')) {
|
||||
logger.info('Open file from cache because it is modified');
|
||||
this.openFileFromCache(params, (err, file) => {
|
||||
if (!err && file) {
|
||||
logger.info('Sync just opened modified file');
|
||||
_.defer(() => this.syncFile(file));
|
||||
}
|
||||
callback(err);
|
||||
}, fileInfo);
|
||||
this.openFileFromCache(
|
||||
params,
|
||||
(err, file) => {
|
||||
if (!err && file) {
|
||||
logger.info('Sync just opened modified file');
|
||||
_.defer(() => this.syncFile(file));
|
||||
}
|
||||
callback(err);
|
||||
},
|
||||
fileInfo
|
||||
);
|
||||
} else if (params.fileData) {
|
||||
logger.info('Open file from supplied content');
|
||||
const needSaveToCache = params.storage !== 'file';
|
||||
|
@ -430,7 +447,12 @@ const AppModel = Backbone.Model.extend({
|
|||
} else if (!params.storage) {
|
||||
logger.info('Open file from cache as main storage');
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
} else if (fileInfo && fileInfo.get('openDate') && fileInfo.get('rev') === params.rev && fileInfo.get('storage') !== 'file') {
|
||||
} else if (
|
||||
fileInfo &&
|
||||
fileInfo.get('openDate') &&
|
||||
fileInfo.get('rev') === params.rev &&
|
||||
fileInfo.get('storage') !== 'file'
|
||||
) {
|
||||
logger.info('Open file from cache because it is latest');
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
} else if (!fileInfo || !fileInfo.get('openDate') || params.storage === 'file') {
|
||||
|
@ -450,17 +472,17 @@ const AppModel = Backbone.Model.extend({
|
|||
} else {
|
||||
logger.info('Open file from content loaded from storage');
|
||||
params.fileData = data;
|
||||
params.rev = stat && stat.rev || null;
|
||||
params.rev = (stat && stat.rev) || null;
|
||||
const needSaveToCache = storage.name !== 'file';
|
||||
this.openFileWithData(params, callback, fileInfo, data, needSaveToCache);
|
||||
}
|
||||
});
|
||||
};
|
||||
const cacheRev = fileInfo && fileInfo.get('rev') || null;
|
||||
const cacheRev = (fileInfo && fileInfo.get('rev')) || null;
|
||||
if (cacheRev && storage.stat) {
|
||||
logger.info('Stat file');
|
||||
storage.stat(params.path, params.opts, (err, stat) => {
|
||||
if (fileInfo && storage.name !== 'file' && (err || stat && stat.rev === cacheRev)) {
|
||||
if (fileInfo && storage.name !== 'file' && (err || (stat && stat.rev === cacheRev))) {
|
||||
logger.info('Open file from cache because ' + (err ? 'stat error' : 'it is latest'), err);
|
||||
this.openFileFromCache(params, callback, fileInfo);
|
||||
} else if (stat) {
|
||||
|
@ -476,13 +498,17 @@ const AppModel = Backbone.Model.extend({
|
|||
}
|
||||
} else {
|
||||
logger.info('Open file from cache, will sync after load', params.storage);
|
||||
this.openFileFromCache(params, (err, file) => {
|
||||
if (!err && file) {
|
||||
logger.info('Sync just opened file');
|
||||
_.defer(() => this.syncFile(file));
|
||||
}
|
||||
callback(err);
|
||||
}, fileInfo);
|
||||
this.openFileFromCache(
|
||||
params,
|
||||
(err, file) => {
|
||||
if (!err && file) {
|
||||
logger.info('Sync just opened file');
|
||||
_.defer(() => this.syncFile(file));
|
||||
}
|
||||
callback(err);
|
||||
},
|
||||
fileInfo
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -520,8 +546,8 @@ const AppModel = Backbone.Model.extend({
|
|||
path: params.path,
|
||||
keyFileName: params.keyFileName,
|
||||
keyFilePath: params.keyFilePath,
|
||||
backup: fileInfo && fileInfo.get('backup') || null,
|
||||
fingerprint: fileInfo && fileInfo.get('fingerprint') || null
|
||||
backup: (fileInfo && fileInfo.get('backup')) || null,
|
||||
fingerprint: (fileInfo && fileInfo.get('fingerprint')) || null
|
||||
});
|
||||
const openComplete = err => {
|
||||
if (err) {
|
||||
|
@ -545,7 +571,7 @@ const AppModel = Backbone.Model.extend({
|
|||
logger.info('Save loaded file to cache');
|
||||
Storage.cache.save(file.id, null, params.fileData);
|
||||
}
|
||||
const rev = params.rev || fileInfo && fileInfo.get('rev');
|
||||
const rev = params.rev || (fileInfo && fileInfo.get('rev'));
|
||||
this.setFileOpts(file, params.opts);
|
||||
this.addToLastOpenFiles(file, rev);
|
||||
this.addFile(file);
|
||||
|
@ -590,7 +616,14 @@ const AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
addToLastOpenFiles: function(file, rev) {
|
||||
this.appLogger.debug('Add last open file', file.id, file.get('name'), file.get('storage'), file.get('path'), rev);
|
||||
this.appLogger.debug(
|
||||
'Add last open file',
|
||||
file.id,
|
||||
file.get('name'),
|
||||
file.get('storage'),
|
||||
file.get('path'),
|
||||
rev
|
||||
);
|
||||
const dt = new Date();
|
||||
const fileInfo = new FileInfoModel({
|
||||
id: file.id,
|
||||
|
@ -642,9 +675,12 @@ const AppModel = Backbone.Model.extend({
|
|||
|
||||
fileOpened: function(file, data, params) {
|
||||
if (file.get('storage') === 'file') {
|
||||
Storage.file.watch(file.get('path'), _.debounce(() => {
|
||||
this.syncFile(file);
|
||||
}, Timeouts.FileChangeSync));
|
||||
Storage.file.watch(
|
||||
file.get('path'),
|
||||
_.debounce(() => {
|
||||
this.syncFile(file);
|
||||
}, Timeouts.FileChangeSync)
|
||||
);
|
||||
}
|
||||
if (file.isKeyChangePending(true)) {
|
||||
Backbone.trigger('key-change-pending', { file: file });
|
||||
|
@ -671,8 +707,10 @@ const AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
getFileInfo: function(file) {
|
||||
return this.fileInfos.get(file.id) ||
|
||||
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'));
|
||||
return (
|
||||
this.fileInfos.get(file.id) ||
|
||||
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'))
|
||||
);
|
||||
},
|
||||
|
||||
syncFile: function(file, options, callback) {
|
||||
|
@ -713,7 +751,9 @@ const AppModel = Backbone.Model.extend({
|
|||
}
|
||||
file.setSyncProgress();
|
||||
const complete = (err, savedToCache) => {
|
||||
if (!err) { savedToCache = true; }
|
||||
if (!err) {
|
||||
savedToCache = true;
|
||||
}
|
||||
logger.info('Sync finished', err || 'no error');
|
||||
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
||||
fileInfo.set({
|
||||
|
@ -735,7 +775,9 @@ const AppModel = Backbone.Model.extend({
|
|||
this.fileInfos.unshift(fileInfo);
|
||||
}
|
||||
this.fileInfos.save();
|
||||
if (callback) { callback(err); }
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
if (!storage) {
|
||||
if (!file.get('modified') && fileInfo.id === file.id) {
|
||||
|
@ -744,8 +786,10 @@ const AppModel = Backbone.Model.extend({
|
|||
}
|
||||
logger.info('Local, save to cache');
|
||||
file.getData((data, err) => {
|
||||
if (err) { return complete(err); }
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
if (err) {
|
||||
return complete(err);
|
||||
}
|
||||
Storage.cache.save(fileInfo.id, null, data, err => {
|
||||
logger.info('Saved to cache', err || 'no error');
|
||||
complete(err);
|
||||
if (!err) {
|
||||
|
@ -763,8 +807,10 @@ const AppModel = Backbone.Model.extend({
|
|||
logger.info('Load from storage, attempt ' + loadLoops);
|
||||
Storage[storage].load(path, opts, (err, data, stat) => {
|
||||
logger.info('Load from storage', stat, err || 'no error');
|
||||
if (err) { return complete(err); }
|
||||
file.mergeOrUpdate(data, options.remoteKey, (err) => {
|
||||
if (err) {
|
||||
return complete(err);
|
||||
}
|
||||
file.mergeOrUpdate(data, options.remoteKey, err => {
|
||||
logger.info('Merge complete', err || 'no error');
|
||||
this.refresh();
|
||||
if (err) {
|
||||
|
@ -784,8 +830,10 @@ const AppModel = Backbone.Model.extend({
|
|||
saveToCacheAndStorage();
|
||||
} else if (file.get('dirty')) {
|
||||
logger.info('Saving not modified dirty file to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
if (err) { return complete(err); }
|
||||
Storage.cache.save(fileInfo.id, null, data, err => {
|
||||
if (err) {
|
||||
return complete(err);
|
||||
}
|
||||
file.set('dirty', false);
|
||||
logger.info('Complete, remove dirty flag');
|
||||
complete();
|
||||
|
@ -797,38 +845,46 @@ const AppModel = Backbone.Model.extend({
|
|||
});
|
||||
});
|
||||
};
|
||||
const saveToStorage = (data) => {
|
||||
const saveToStorage = data => {
|
||||
logger.info('Save data to storage');
|
||||
const storageRev = fileInfo.get('storage') === storage ? fileInfo.get('rev') : undefined;
|
||||
Storage[storage].save(path, opts, data, (err, stat) => {
|
||||
if (err && err.revConflict) {
|
||||
logger.info('Save rev conflict, reloading from storage');
|
||||
loadFromStorageAndMerge();
|
||||
} else if (err) {
|
||||
logger.info('Error saving data to storage');
|
||||
complete(err);
|
||||
} else {
|
||||
if (stat && stat.rev) {
|
||||
logger.info('Update rev in file info');
|
||||
fileInfo.set('rev', stat.rev);
|
||||
Storage[storage].save(
|
||||
path,
|
||||
opts,
|
||||
data,
|
||||
(err, stat) => {
|
||||
if (err && err.revConflict) {
|
||||
logger.info('Save rev conflict, reloading from storage');
|
||||
loadFromStorageAndMerge();
|
||||
} else if (err) {
|
||||
logger.info('Error saving data to storage');
|
||||
complete(err);
|
||||
} else {
|
||||
if (stat && stat.rev) {
|
||||
logger.info('Update rev in file info');
|
||||
fileInfo.set('rev', stat.rev);
|
||||
}
|
||||
if (stat && stat.path) {
|
||||
logger.info('Update path in file info', stat.path);
|
||||
file.set('path', stat.path);
|
||||
fileInfo.set('path', stat.path);
|
||||
path = stat.path;
|
||||
}
|
||||
file.set('syncDate', new Date());
|
||||
logger.info('Save to storage complete, update sync date');
|
||||
this.scheduleBackupFile(file, data);
|
||||
complete();
|
||||
}
|
||||
if (stat && stat.path) {
|
||||
logger.info('Update path in file info', stat.path);
|
||||
file.set('path', stat.path);
|
||||
fileInfo.set('path', stat.path);
|
||||
path = stat.path;
|
||||
}
|
||||
file.set('syncDate', new Date());
|
||||
logger.info('Save to storage complete, update sync date');
|
||||
this.scheduleBackupFile(file, data);
|
||||
complete();
|
||||
}
|
||||
}, storageRev);
|
||||
},
|
||||
storageRev
|
||||
);
|
||||
};
|
||||
const saveToCacheAndStorage = () => {
|
||||
logger.info('Getting file data for saving');
|
||||
file.getData((data, err) => {
|
||||
if (err) { return complete(err); }
|
||||
if (err) {
|
||||
return complete(err);
|
||||
}
|
||||
if (storage === 'file') {
|
||||
logger.info('Saving to file storage');
|
||||
saveToStorage(data);
|
||||
|
@ -837,8 +893,10 @@ const AppModel = Backbone.Model.extend({
|
|||
saveToStorage(data);
|
||||
} else {
|
||||
logger.info('Saving to cache');
|
||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||
if (err) { return complete(err); }
|
||||
Storage.cache.save(fileInfo.id, null, data, err => {
|
||||
if (err) {
|
||||
return complete(err);
|
||||
}
|
||||
file.set('dirty', false);
|
||||
logger.info('Saved to cache, saving to storage');
|
||||
saveToStorage(data);
|
||||
|
@ -854,9 +912,9 @@ const AppModel = Backbone.Model.extend({
|
|||
saveToCacheAndStorage();
|
||||
} else if (file.get('dirty')) {
|
||||
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
||||
file.getData((data) => {
|
||||
file.getData(data => {
|
||||
if (data) {
|
||||
Storage.cache.save(fileInfo.id, null, data, (e) => {
|
||||
Storage.cache.save(fileInfo.id, null, data, e => {
|
||||
if (!e) {
|
||||
file.set('dirty', false);
|
||||
}
|
||||
|
@ -896,7 +954,7 @@ const AppModel = Backbone.Model.extend({
|
|||
this.fileInfos.save();
|
||||
},
|
||||
|
||||
unsetKeyFile: function (fileId) {
|
||||
unsetKeyFile: function(fileId) {
|
||||
const fileInfo = this.fileInfos.get(fileId);
|
||||
fileInfo.set({
|
||||
keyFileName: null,
|
||||
|
@ -927,7 +985,7 @@ const AppModel = Backbone.Model.extend({
|
|||
if (Storage[backup.storage].getPathForName) {
|
||||
path = Storage[backup.storage].getPathForName(path);
|
||||
}
|
||||
Storage[backup.storage].save(path, opts, data, (err) => {
|
||||
Storage[backup.storage].save(path, opts, data, err => {
|
||||
if (err) {
|
||||
logger.error('Backup error', err);
|
||||
} else {
|
||||
|
@ -1002,10 +1060,16 @@ const AppModel = Backbone.Model.extend({
|
|||
if (dt.getTime() <= Date.now()) {
|
||||
needBackup = true;
|
||||
}
|
||||
logger.debug('Last backup time: ' + new Date(backup.lastTime) +
|
||||
', schedule: ' + backup.schedule +
|
||||
', next time: ' + dt +
|
||||
', ' + (needBackup ? 'backup now' : 'skip backup'));
|
||||
logger.debug(
|
||||
'Last backup time: ' +
|
||||
new Date(backup.lastTime) +
|
||||
', schedule: ' +
|
||||
backup.schedule +
|
||||
', next time: ' +
|
||||
dt +
|
||||
', ' +
|
||||
(needBackup ? 'backup now' : 'skip backup')
|
||||
);
|
||||
}
|
||||
if (!backup.pending) {
|
||||
backup.pending = true;
|
||||
|
|
|
@ -57,7 +57,7 @@ const AppSettingsModel = Backbone.Model.extend({
|
|||
return SettingsStore.load('app-settings').then(data => {
|
||||
if (data) {
|
||||
this.upgrade(data);
|
||||
this.set(data, {silent: true});
|
||||
this.set(data, { silent: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -3,8 +3,7 @@ const Backbone = require('backbone');
|
|||
const AttachmentModel = Backbone.Model.extend({
|
||||
defaults: {},
|
||||
|
||||
initialize: function() {
|
||||
},
|
||||
initialize: function() {},
|
||||
|
||||
setAttachment: function(att) {
|
||||
this.title = att.title;
|
||||
|
@ -21,28 +20,83 @@ const AttachmentModel = Backbone.Model.extend({
|
|||
|
||||
_getIcon: function(ext) {
|
||||
switch (ext) {
|
||||
case 'txt': case 'log': case 'rtf': case 'pem':
|
||||
case 'txt':
|
||||
case 'log':
|
||||
case 'rtf':
|
||||
case 'pem':
|
||||
return 'file-text-o';
|
||||
case 'html': case 'htm': case 'js': case 'css': case 'xml': case 'config': case 'json': case 'yaml':
|
||||
case 'cpp': case 'c': case 'h': case 'cc': case 'hpp': case 'mm': case 'cs': case 'php': case 'sh':
|
||||
case 'py': case 'java': case 'rb': case 'cfg': case 'properties': case 'yml': case 'asm': case 'bat':
|
||||
case 'html':
|
||||
case 'htm':
|
||||
case 'js':
|
||||
case 'css':
|
||||
case 'xml':
|
||||
case 'config':
|
||||
case 'json':
|
||||
case 'yaml':
|
||||
case 'cpp':
|
||||
case 'c':
|
||||
case 'h':
|
||||
case 'cc':
|
||||
case 'hpp':
|
||||
case 'mm':
|
||||
case 'cs':
|
||||
case 'php':
|
||||
case 'sh':
|
||||
case 'py':
|
||||
case 'java':
|
||||
case 'rb':
|
||||
case 'cfg':
|
||||
case 'properties':
|
||||
case 'yml':
|
||||
case 'asm':
|
||||
case 'bat':
|
||||
return 'file-code-o';
|
||||
case 'pdf':
|
||||
return 'file-pdf-o';
|
||||
case 'zip': case 'rar': case 'bz': case 'bz2': case '7z': case 'gzip': case 'gz': case 'tar':
|
||||
case 'cab': case 'ace': case 'dmg': case 'jar':
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case 'bz':
|
||||
case 'bz2':
|
||||
case '7z':
|
||||
case 'gzip':
|
||||
case 'gz':
|
||||
case 'tar':
|
||||
case 'cab':
|
||||
case 'ace':
|
||||
case 'dmg':
|
||||
case 'jar':
|
||||
return 'file-archive-o';
|
||||
case 'doc': case 'docx':
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'file-word-o';
|
||||
case 'xls': case 'xlsx':
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return 'file-excel-o';
|
||||
case 'ppt': case 'pptx':
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return 'file-powerpoint-o';
|
||||
case 'jpeg': case 'jpg': case 'png': case 'gif': case 'bmp': case 'tiff': case 'svg': case 'ico': case 'psd':
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'bmp':
|
||||
case 'tiff':
|
||||
case 'svg':
|
||||
case 'ico':
|
||||
case 'psd':
|
||||
return 'file-image-o';
|
||||
case 'avi': case 'mp4': case '3gp': case 'm4v': case 'mov': case 'mpeg': case 'mpg': case 'mpe':
|
||||
case 'avi':
|
||||
case 'mp4':
|
||||
case '3gp':
|
||||
case 'm4v':
|
||||
case 'mov':
|
||||
case 'mpeg':
|
||||
case 'mpg':
|
||||
case 'mpe':
|
||||
return 'file-video-o';
|
||||
case 'mp3': case 'wav': case 'flac':
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'flac':
|
||||
return 'file-audio-o';
|
||||
}
|
||||
return 'file-o';
|
||||
|
@ -50,14 +104,43 @@ const AttachmentModel = Backbone.Model.extend({
|
|||
|
||||
_getMimeType: function(ext) {
|
||||
switch (ext) {
|
||||
case 'txt': case 'log':
|
||||
case 'html': case 'htm': case 'js': case 'css': case 'xml': case 'config': case 'json': case 'yaml':
|
||||
case 'cpp': case 'c': case 'h': case 'cc': case 'hpp': case 'mm': case 'cs': case 'php': case 'sh':
|
||||
case 'py': case 'java': case 'rb': case 'cfg': case 'properties': case 'yml': case 'asm': case 'pem':
|
||||
case 'txt':
|
||||
case 'log':
|
||||
case 'html':
|
||||
case 'htm':
|
||||
case 'js':
|
||||
case 'css':
|
||||
case 'xml':
|
||||
case 'config':
|
||||
case 'json':
|
||||
case 'yaml':
|
||||
case 'cpp':
|
||||
case 'c':
|
||||
case 'h':
|
||||
case 'cc':
|
||||
case 'hpp':
|
||||
case 'mm':
|
||||
case 'cs':
|
||||
case 'php':
|
||||
case 'sh':
|
||||
case 'py':
|
||||
case 'java':
|
||||
case 'rb':
|
||||
case 'cfg':
|
||||
case 'properties':
|
||||
case 'yml':
|
||||
case 'asm':
|
||||
case 'pem':
|
||||
return 'text/plain';
|
||||
case 'pdf':
|
||||
return 'application/pdf';
|
||||
case 'jpeg': case 'jpg': case 'png': case 'gif': case 'bmp': case 'tiff': case 'svg':
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'bmp':
|
||||
case 'tiff':
|
||||
case 'svg':
|
||||
return 'image/' + ext;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,7 +13,16 @@ const EntryModel = Backbone.Model.extend({
|
|||
urlRegex: /^https?:\/\//i,
|
||||
fieldRefRegex: /^\{REF:([TNPAU])@I:(\w{32})}$/,
|
||||
|
||||
builtInFields: ['Title', 'Password', 'UserName', 'URL', 'Notes', 'TOTP Seed', 'TOTP Settings', '_etm_template_uuid'],
|
||||
builtInFields: [
|
||||
'Title',
|
||||
'Password',
|
||||
'UserName',
|
||||
'URL',
|
||||
'Notes',
|
||||
'TOTP Seed',
|
||||
'TOTP Settings',
|
||||
'_etm_template_uuid'
|
||||
],
|
||||
fieldRefFields: ['title', 'password', 'user', 'url', 'notes'],
|
||||
fieldRefIds: { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' },
|
||||
|
||||
|
@ -32,7 +41,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
|
||||
_fillByEntry: function() {
|
||||
const entry = this.entry;
|
||||
this.set({id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id}, {silent: true});
|
||||
this.set({ id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id }, { silent: true });
|
||||
this.fileName = this.file.get('name');
|
||||
this.groupName = this.group.get('title');
|
||||
this.title = this._getFieldString('Title');
|
||||
|
@ -120,7 +129,8 @@ const EntryModel = Backbone.Model.extend({
|
|||
|
||||
_buildAutoType: function() {
|
||||
this.autoTypeEnabled = this.entry.autoType.enabled;
|
||||
this.autoTypeObfuscation = this.entry.autoType.obfuscation === kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
|
||||
this.autoTypeObfuscation =
|
||||
this.entry.autoType.obfuscation === kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
|
||||
this.autoTypeSequence = this.entry.autoType.defaultSequence;
|
||||
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
|
||||
},
|
||||
|
@ -150,14 +160,18 @@ const EntryModel = Backbone.Model.extend({
|
|||
|
||||
_attachmentsToModel: function(binaries) {
|
||||
const att = [];
|
||||
_.forEach(binaries, (data, title) => {
|
||||
if (data && data.ref) {
|
||||
data = data.value;
|
||||
}
|
||||
if (data) {
|
||||
att.push(AttachmentModel.fromAttachment({data: data, title: title}));
|
||||
}
|
||||
}, this);
|
||||
_.forEach(
|
||||
binaries,
|
||||
(data, title) => {
|
||||
if (data && data.ref) {
|
||||
data = data.value;
|
||||
}
|
||||
if (data) {
|
||||
att.push(AttachmentModel.fromAttachment({ data: data, title: title }));
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
return att;
|
||||
},
|
||||
|
||||
|
@ -183,21 +197,25 @@ const EntryModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
matches: function(filter) {
|
||||
return !filter ||
|
||||
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
|
||||
(!filter.textLower || (filter.advanced ? this.matchesAdv(filter) : this.searchText.indexOf(filter.textLower) >= 0)) &&
|
||||
(!filter.color || filter.color === true && this.searchColor || this.searchColor === filter.color) &&
|
||||
(!filter.autoType || this.autoTypeEnabled);
|
||||
return (
|
||||
!filter ||
|
||||
((!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
|
||||
(!filter.textLower ||
|
||||
(filter.advanced ? this.matchesAdv(filter) : this.searchText.indexOf(filter.textLower) >= 0)) &&
|
||||
(!filter.color || (filter.color === true && this.searchColor) || this.searchColor === filter.color) &&
|
||||
(!filter.autoType || this.autoTypeEnabled))
|
||||
);
|
||||
},
|
||||
|
||||
matchesAdv: function(filter) {
|
||||
const adv = filter.advanced;
|
||||
let search,
|
||||
match;
|
||||
let search, match;
|
||||
if (adv.regex) {
|
||||
try {
|
||||
search = new RegExp(filter.text, adv.cs ? '' : 'i');
|
||||
} catch (e) { return false; }
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
match = this.matchRegex;
|
||||
} else if (adv.cs) {
|
||||
search = filter.text;
|
||||
|
@ -387,7 +405,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
setField: function(field, val, allowEmpty) {
|
||||
const hasValue = val && (typeof val === 'string' || val.isProtected && val.byteLength);
|
||||
const hasValue = val && (typeof val === 'string' || (val.isProtected && val.byteLength));
|
||||
if (hasValue || allowEmpty || this.builtInFields.indexOf(field) >= 0) {
|
||||
this._entryModified();
|
||||
val = this.sanitizeFieldValue(val);
|
||||
|
@ -542,8 +560,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (settings && settings.isProtected) {
|
||||
settings = settings.getText();
|
||||
}
|
||||
let period,
|
||||
digits;
|
||||
let period, digits;
|
||||
if (settings) {
|
||||
settings = settings.split(';');
|
||||
if (settings.length > 0 && settings[0] > 0) {
|
||||
|
@ -601,8 +618,9 @@ const EntryModel = Backbone.Model.extend({
|
|||
|
||||
setAutoTypeObfuscation: function(enabled) {
|
||||
this._entryModified();
|
||||
this.entry.autoType.obfuscation =
|
||||
enabled ? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard : kdbxweb.Consts.AutoTypeObfuscationOptions.None;
|
||||
this.entry.autoType.obfuscation = enabled
|
||||
? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard
|
||||
: kdbxweb.Consts.AutoTypeObfuscationOptions.None;
|
||||
this._buildAutoType();
|
||||
},
|
||||
|
||||
|
@ -674,20 +692,17 @@ const EntryModel = Backbone.Model.extend({
|
|||
|
||||
const fieldNames = Object.keys(this.fields);
|
||||
_.forEach(fieldNames, field => {
|
||||
ranking.push(
|
||||
{
|
||||
field: field,
|
||||
multiplicator: 2
|
||||
}
|
||||
);
|
||||
ranking.push({
|
||||
field: field,
|
||||
multiplicator: 2
|
||||
});
|
||||
});
|
||||
|
||||
_.forEach(ranking, rankingEntry => {
|
||||
if (this._getFieldString(rankingEntry.field).toLowerCase() !== '') {
|
||||
const calculatedRank = Ranking.getStringRank(
|
||||
searchString,
|
||||
this._getFieldString(rankingEntry.field).toLowerCase()
|
||||
) * rankingEntry.multiplicator;
|
||||
const calculatedRank =
|
||||
Ranking.getStringRank(searchString, this._getFieldString(rankingEntry.field).toLowerCase()) *
|
||||
rankingEntry.multiplicator;
|
||||
rank += calculatedRank;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,11 +19,15 @@ const FileInfoModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
initialize: function(data, options) {
|
||||
_.each(data, function(val, key) {
|
||||
if (/Date$/.test(key)) {
|
||||
this.set(key, val ? new Date(val) : null, options);
|
||||
}
|
||||
}, this);
|
||||
_.each(
|
||||
data,
|
||||
function(val, key) {
|
||||
if (/Date$/.test(key)) {
|
||||
this.set(key, val ? new Date(val) : null, options);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -57,8 +57,17 @@ const FileModel = Backbone.Model.extend({
|
|||
if (keyFileData) {
|
||||
kdbxweb.ByteUtils.zeroBuffer(keyFileData);
|
||||
}
|
||||
logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' +
|
||||
this.kdfArgsToString(db.header) + ', ' + Math.round(fileData.byteLength / 1024) + ' kB');
|
||||
logger.info(
|
||||
'Opened file ' +
|
||||
this.get('name') +
|
||||
': ' +
|
||||
logger.ts(ts) +
|
||||
', ' +
|
||||
this.kdfArgsToString(db.header) +
|
||||
', ' +
|
||||
Math.round(fileData.byteLength / 1024) +
|
||||
' kB'
|
||||
);
|
||||
callback();
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -77,13 +86,17 @@ const FileModel = Backbone.Model.extend({
|
|||
|
||||
kdfArgsToString: function(header) {
|
||||
if (header.kdfParameters) {
|
||||
return header.kdfParameters.keys().map(key => {
|
||||
const val = header.kdfParameters.get(key);
|
||||
if (val instanceof ArrayBuffer) {
|
||||
return;
|
||||
}
|
||||
return key + '=' + val;
|
||||
}).filter(p => p).join('&');
|
||||
return header.kdfParameters
|
||||
.keys()
|
||||
.map(key => {
|
||||
const val = header.kdfParameters.get(key);
|
||||
if (val instanceof ArrayBuffer) {
|
||||
return;
|
||||
}
|
||||
return key + '=' + val;
|
||||
})
|
||||
.filter(p => p)
|
||||
.join('&');
|
||||
} else if (header.keyEncryptionRounds) {
|
||||
return header.keyEncryptionRounds + ' rounds';
|
||||
} else {
|
||||
|
@ -127,14 +140,13 @@ const FileModel = Backbone.Model.extend({
|
|||
const password = kdbxweb.ProtectedValue.fromString('demo');
|
||||
const credentials = new kdbxweb.Credentials(password);
|
||||
const demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData));
|
||||
kdbxweb.Kdbx.load(demoFile, credentials)
|
||||
.then(db => {
|
||||
this.db = db;
|
||||
this.set('name', 'Demo');
|
||||
this.readModel();
|
||||
this.setOpenFile({passwordLength: 4, demo: true});
|
||||
callback();
|
||||
});
|
||||
kdbxweb.Kdbx.load(demoFile, credentials).then(db => {
|
||||
this.db = db;
|
||||
this.set('name', 'Demo');
|
||||
this.readModel();
|
||||
this.setOpenFile({ passwordLength: 4, demo: true });
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
setOpenFile: function(props) {
|
||||
|
@ -153,17 +165,20 @@ const FileModel = Backbone.Model.extend({
|
|||
|
||||
readModel: function() {
|
||||
const groups = new GroupCollection();
|
||||
this.set({
|
||||
uuid: this.db.getDefaultGroup().uuid.toString(),
|
||||
groups: groups,
|
||||
defaultUser: this.db.meta.defaultUser,
|
||||
recycleBinEnabled: this.db.meta.recycleBinEnabled,
|
||||
historyMaxItems: this.db.meta.historyMaxItems,
|
||||
historyMaxSize: this.db.meta.historyMaxSize,
|
||||
keyEncryptionRounds: this.db.header.keyEncryptionRounds,
|
||||
keyChangeForce: this.db.meta.keyChangeForce,
|
||||
kdfParameters: this.readKdfParams()
|
||||
}, { silent: true });
|
||||
this.set(
|
||||
{
|
||||
uuid: this.db.getDefaultGroup().uuid.toString(),
|
||||
groups: groups,
|
||||
defaultUser: this.db.meta.defaultUser,
|
||||
recycleBinEnabled: this.db.meta.recycleBinEnabled,
|
||||
historyMaxItems: this.db.meta.historyMaxItems,
|
||||
historyMaxSize: this.db.meta.historyMaxSize,
|
||||
keyEncryptionRounds: this.db.header.keyEncryptionRounds,
|
||||
keyChangeForce: this.db.meta.keyChangeForce,
|
||||
kdfParameters: this.readKdfParams()
|
||||
},
|
||||
{ silent: true }
|
||||
);
|
||||
this.db.groups.forEach(function(group) {
|
||||
let groupModel = this.getGroup(this.subId(group.uuid.id));
|
||||
if (groupModel) {
|
||||
|
@ -204,12 +219,15 @@ const FileModel = Backbone.Model.extend({
|
|||
buildObjectMap: function() {
|
||||
const entryMap = {};
|
||||
const groupMap = {};
|
||||
this.forEachGroup(group => {
|
||||
groupMap[group.id] = group;
|
||||
group.forEachOwnEntry(null, entry => {
|
||||
entryMap[entry.id] = entry;
|
||||
});
|
||||
}, { includeDisabled: true });
|
||||
this.forEachGroup(
|
||||
group => {
|
||||
groupMap[group.id] = group;
|
||||
group.forEachOwnEntry(null, entry => {
|
||||
entryMap[entry.id] = entry;
|
||||
});
|
||||
},
|
||||
{ includeDisabled: true }
|
||||
);
|
||||
this.entryMap = entryMap;
|
||||
this.groupMap = groupMap;
|
||||
},
|
||||
|
@ -371,7 +389,8 @@ const FileModel = Backbone.Model.extend({
|
|||
binaries: true
|
||||
});
|
||||
this.db.cleanup({ binaries: true });
|
||||
this.db.save()
|
||||
this.db
|
||||
.save()
|
||||
.then(data => {
|
||||
cb(data);
|
||||
})
|
||||
|
@ -382,8 +401,9 @@ const FileModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
getXml: function(cb) {
|
||||
this.db.saveXml()
|
||||
.then(xml => { cb(xml); });
|
||||
this.db.saveXml().then(xml => {
|
||||
cb(xml);
|
||||
});
|
||||
},
|
||||
|
||||
getKeyFileHash: function() {
|
||||
|
@ -508,7 +528,9 @@ const FileModel = Backbone.Model.extend({
|
|||
this.db.meta.name = name;
|
||||
this.db.meta.nameChanged = new Date();
|
||||
this.set('name', name);
|
||||
this.get('groups').first().setName(name);
|
||||
this.get('groups')
|
||||
.first()
|
||||
.setName(name);
|
||||
this.setModified();
|
||||
this.reload();
|
||||
},
|
||||
|
@ -571,10 +593,13 @@ const FileModel = Backbone.Model.extend({
|
|||
const trashGroup = this.getTrashGroup();
|
||||
if (trashGroup) {
|
||||
let modified = false;
|
||||
trashGroup.getOwnSubGroups().slice().forEach(function(group) {
|
||||
this.db.move(group, null);
|
||||
modified = true;
|
||||
}, this);
|
||||
trashGroup
|
||||
.getOwnSubGroups()
|
||||
.slice()
|
||||
.forEach(function(group) {
|
||||
this.db.move(group, null);
|
||||
modified = true;
|
||||
}, this);
|
||||
trashGroup.group.entries.slice().forEach(function(entry) {
|
||||
this.db.move(entry, null);
|
||||
modified = true;
|
||||
|
|
|
@ -24,28 +24,35 @@ const GroupModel = MenuItemModel.extend({
|
|||
}),
|
||||
|
||||
initialize: function() {
|
||||
if (!GroupCollection) { GroupCollection = require('../collections/group-collection'); }
|
||||
if (!EntryCollection) { EntryCollection = require('../collections/entry-collection'); }
|
||||
if (!GroupCollection) {
|
||||
GroupCollection = require('../collections/group-collection');
|
||||
}
|
||||
if (!EntryCollection) {
|
||||
EntryCollection = require('../collections/entry-collection');
|
||||
}
|
||||
},
|
||||
|
||||
setGroup: function(group, file, parentGroup) {
|
||||
const isRecycleBin = group.uuid.equals(file.db.meta.recycleBinUuid);
|
||||
const id = file.subId(group.uuid.id);
|
||||
this.set({
|
||||
id: id,
|
||||
uuid: group.uuid.id,
|
||||
expanded: group.expanded,
|
||||
visible: !isRecycleBin,
|
||||
items: new GroupCollection(),
|
||||
entries: new EntryCollection(),
|
||||
filterValue: id,
|
||||
enableSearching: group.enableSearching,
|
||||
enableAutoType: group.enableAutoType,
|
||||
autoTypeSeq: group.defaultAutoTypeSeq,
|
||||
top: !parentGroup,
|
||||
drag: !!parentGroup,
|
||||
collapsible: !!parentGroup
|
||||
}, { silent: true });
|
||||
this.set(
|
||||
{
|
||||
id: id,
|
||||
uuid: group.uuid.id,
|
||||
expanded: group.expanded,
|
||||
visible: !isRecycleBin,
|
||||
items: new GroupCollection(),
|
||||
entries: new EntryCollection(),
|
||||
filterValue: id,
|
||||
enableSearching: group.enableSearching,
|
||||
enableAutoType: group.enableAutoType,
|
||||
autoTypeSeq: group.defaultAutoTypeSeq,
|
||||
top: !parentGroup,
|
||||
drag: !!parentGroup,
|
||||
collapsible: !!parentGroup
|
||||
},
|
||||
{ silent: true }
|
||||
);
|
||||
this.group = group;
|
||||
this.file = file;
|
||||
this.parentGroup = parentGroup;
|
||||
|
@ -77,14 +84,17 @@ const GroupModel = MenuItemModel.extend({
|
|||
},
|
||||
|
||||
_fillByGroup: function(silent) {
|
||||
this.set({
|
||||
title: this.parentGroup ? this.group.name : this.file.get('name'),
|
||||
iconId: this.group.icon,
|
||||
icon: this._iconFromId(this.group.icon),
|
||||
customIcon: this._buildCustomIcon(),
|
||||
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null,
|
||||
expanded: this.group.expanded !== false
|
||||
}, { silent: silent });
|
||||
this.set(
|
||||
{
|
||||
title: this.parentGroup ? this.group.name : this.file.get('name'),
|
||||
iconId: this.group.icon,
|
||||
icon: this._iconFromId(this.group.icon),
|
||||
customIcon: this._buildCustomIcon(),
|
||||
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null,
|
||||
expanded: this.group.expanded !== false
|
||||
},
|
||||
{ silent: silent }
|
||||
);
|
||||
},
|
||||
|
||||
_iconFromId: function(id) {
|
||||
|
@ -129,10 +139,12 @@ const GroupModel = MenuItemModel.extend({
|
|||
},
|
||||
|
||||
matches: function(filter) {
|
||||
return (filter && filter.includeDisabled ||
|
||||
this.group.enableSearching !== false &&
|
||||
!this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup)
|
||||
) && (!filter || !filter.autoType || this.group.enableAutoType !== false);
|
||||
return (
|
||||
((filter && filter.includeDisabled) ||
|
||||
(this.group.enableSearching !== false &&
|
||||
!this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup))) &&
|
||||
(!filter || !filter.autoType || this.group.enableAutoType !== false)
|
||||
);
|
||||
},
|
||||
|
||||
getOwnSubGroups: function() {
|
||||
|
|
|
@ -16,22 +16,40 @@ const MenuModel = Backbone.Model.extend({
|
|||
|
||||
initialize: function() {
|
||||
this.menus = {};
|
||||
this.allItemsSection = new MenuSectionModel([{ locTitle: 'menuAllItems', icon: 'th-large', active: true,
|
||||
shortcut: Keys.DOM_VK_A, filterKey: '*' }]);
|
||||
this.allItemsSection = new MenuSectionModel([
|
||||
{ locTitle: 'menuAllItems', icon: 'th-large', active: true, shortcut: Keys.DOM_VK_A, filterKey: '*' }
|
||||
]);
|
||||
this.allItemsItem = this.allItemsSection.get('items').models[0];
|
||||
this.groupsSection = new GroupsMenuModel();
|
||||
this.colorsSection = new MenuSectionModel([{ locTitle: 'menuColors', icon: 'bookmark', shortcut: Keys.DOM_VK_C,
|
||||
cls: 'menu__item-colors', filterKey: 'color', filterValue: true }]);
|
||||
this.colorsSection = new MenuSectionModel([
|
||||
{
|
||||
locTitle: 'menuColors',
|
||||
icon: 'bookmark',
|
||||
shortcut: Keys.DOM_VK_C,
|
||||
cls: 'menu__item-colors',
|
||||
filterKey: 'color',
|
||||
filterValue: true
|
||||
}
|
||||
]);
|
||||
this.colorsItem = this.colorsSection.get('items').models[0];
|
||||
const defTags = [this._getDefaultTagItem()];
|
||||
this.tagsSection = new MenuSectionModel(defTags);
|
||||
this.tagsSection.set({ scrollable: true, drag: true });
|
||||
this.tagsSection.defaultItems = defTags;
|
||||
this.trashSection = new MenuSectionModel([{ locTitle: 'menuTrash', icon: 'trash', shortcut: Keys.DOM_VK_D,
|
||||
filterKey: 'trash', filterValue: true, drop: true }]);
|
||||
this.trashSection = new MenuSectionModel([
|
||||
{
|
||||
locTitle: 'menuTrash',
|
||||
icon: 'trash',
|
||||
shortcut: Keys.DOM_VK_D,
|
||||
filterKey: 'trash',
|
||||
filterValue: true,
|
||||
drop: true
|
||||
}
|
||||
]);
|
||||
Colors.AllColors.forEach(color => {
|
||||
this.colorsSection.get('items').models[0]
|
||||
.addOption({ cls: 'fa ' + color + '-color', value: color, filterValue: color });
|
||||
this.colorsSection
|
||||
.get('items')
|
||||
.models[0].addOption({ cls: 'fa ' + color + '-color', value: color, filterValue: color });
|
||||
});
|
||||
this.menus.app = new MenuSectionCollection([
|
||||
this.allItemsSection,
|
||||
|
@ -41,8 +59,12 @@ const MenuModel = Backbone.Model.extend({
|
|||
this.trashSection
|
||||
]);
|
||||
|
||||
this.generalSection = new MenuSectionModel([{ locTitle: 'menuSetGeneral', icon: 'cog', page: 'general', active: true }]);
|
||||
this.shortcutsSection = new MenuSectionModel([{ locTitle: 'shortcuts', icon: 'keyboard-o', page: 'shortcuts' }]);
|
||||
this.generalSection = new MenuSectionModel([
|
||||
{ locTitle: 'menuSetGeneral', icon: 'cog', page: 'general', active: true }
|
||||
]);
|
||||
this.shortcutsSection = new MenuSectionModel([
|
||||
{ locTitle: 'shortcuts', icon: 'keyboard-o', page: 'shortcuts' }
|
||||
]);
|
||||
this.pluginsSection = new MenuSectionModel([{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }]);
|
||||
this.aboutSection = new MenuSectionModel([{ locTitle: 'menuSetAbout', icon: 'info', page: 'about' }]);
|
||||
this.helpSection = new MenuSectionModel([{ locTitle: 'help', icon: 'question', page: 'help' }]);
|
||||
|
@ -66,7 +88,9 @@ const MenuModel = Backbone.Model.extend({
|
|||
|
||||
select: function(sel) {
|
||||
const sections = this.get('sections');
|
||||
sections.forEach(function(section) { this._select(section, sel.item); }, this);
|
||||
sections.forEach(function(section) {
|
||||
this._select(section, sel.item);
|
||||
}, this);
|
||||
if (sections === this.menus.app) {
|
||||
this.colorsItem.get('options').forEach(opt => opt.set('active', opt === sel.option));
|
||||
const selColor = sel.item === this.colorsItem && sel.option ? sel.option.get('value') + '-color' : '';
|
||||
|
@ -95,7 +119,7 @@ const MenuModel = Backbone.Model.extend({
|
|||
if (items) {
|
||||
items.forEach(it => {
|
||||
if (it.get('active') && previousItem) {
|
||||
this.select({item: previousItem});
|
||||
this.select({ item: previousItem });
|
||||
return false;
|
||||
}
|
||||
return processSection(it);
|
||||
|
@ -114,8 +138,8 @@ const MenuModel = Backbone.Model.extend({
|
|||
if (section.has('visible') && !section.get('visible')) {
|
||||
return true;
|
||||
}
|
||||
if (section.has('active') && activeItem && (section !== activeItem)) {
|
||||
this.select({item: section});
|
||||
if (section.has('active') && activeItem && section !== activeItem) {
|
||||
this.select({ item: section });
|
||||
activeItem = null;
|
||||
return false;
|
||||
}
|
||||
|
@ -146,18 +170,24 @@ const MenuModel = Backbone.Model.extend({
|
|||
|
||||
_setLocale: function() {
|
||||
[this.menus.app, this.menus.settings].forEach(menu => {
|
||||
menu.each(section => section.get('items').each(item => {
|
||||
if (item.get('locTitle')) {
|
||||
item.set('title', Format.capFirst(Locale[item.get('locTitle')]));
|
||||
}
|
||||
}));
|
||||
menu.each(section =>
|
||||
section.get('items').each(item => {
|
||||
if (item.get('locTitle')) {
|
||||
item.set('title', Format.capFirst(Locale[item.get('locTitle')]));
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
this.tagsSection.defaultItems[0] = this._getDefaultTagItem();
|
||||
},
|
||||
|
||||
_getDefaultTagItem: function() {
|
||||
return { title: Format.capFirst(Locale.tags), icon: 'tags', defaultItem: true,
|
||||
disabled: { header: Locale.menuAlertNoTags, body: Locale.menuAlertNoTagsBody, icon: 'tags' } };
|
||||
return {
|
||||
title: Format.capFirst(Locale.tags),
|
||||
icon: 'tags',
|
||||
defaultItem: true,
|
||||
disabled: { header: Locale.menuAlertNoTags, body: Locale.menuAlertNoTagsBody, icon: 'tags' }
|
||||
};
|
||||
},
|
||||
|
||||
setMenu: function(type) {
|
||||
|
|
|
@ -15,7 +15,7 @@ const RuntimeDataModel = Backbone.Model.extend({
|
|||
// we're not using cookies here now
|
||||
delete data.cookies;
|
||||
}
|
||||
this.set(data, {silent: true});
|
||||
this.set(data, { silent: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -15,8 +15,7 @@ const UpdateModel = Backbone.Model.extend({
|
|||
updateManual: false
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
},
|
||||
initialize: function() {},
|
||||
|
||||
load: function() {
|
||||
return SettingsStore.load('update-info').then(data => {
|
||||
|
@ -27,8 +26,9 @@ const UpdateModel = Backbone.Model.extend({
|
|||
data[key] = val ? new Date(val) : null;
|
||||
}
|
||||
});
|
||||
this.set(data, {silent: true});
|
||||
} catch (e) { /* failed to load model */
|
||||
this.set(data, { silent: true });
|
||||
} catch (e) {
|
||||
/* failed to load model */
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -50,17 +50,16 @@ const PluginGallery = {
|
|||
|
||||
verifySignature(gallery) {
|
||||
const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, '');
|
||||
return SignatureVerifier.verify(
|
||||
kdbxweb.ByteUtils.stringToBytes(dataToVerify),
|
||||
gallery.signature
|
||||
).then(isValid => {
|
||||
if (isValid) {
|
||||
return gallery;
|
||||
}
|
||||
this.logger.error('JSON signature invalid');
|
||||
}).catch(e => {
|
||||
this.logger.error('Error verifying plugins signature', e);
|
||||
});
|
||||
return SignatureVerifier.verify(kdbxweb.ByteUtils.stringToBytes(dataToVerify), gallery.signature)
|
||||
.then(isValid => {
|
||||
if (isValid) {
|
||||
return gallery;
|
||||
}
|
||||
this.logger.error('JSON signature invalid');
|
||||
})
|
||||
.catch(e => {
|
||||
this.logger.error('Error verifying plugins signature', e);
|
||||
});
|
||||
},
|
||||
|
||||
getCachedGallery() {
|
||||
|
|
|
@ -44,21 +44,23 @@ const PluginManager = Backbone.Model.extend({
|
|||
|
||||
install(url, expectedManifest, skipSignatureValidation) {
|
||||
this.trigger('change');
|
||||
return Plugin.loadFromUrl(url, expectedManifest).then(plugin => {
|
||||
return this.uninstall(plugin.id).then(() => {
|
||||
if (skipSignatureValidation) {
|
||||
plugin.set('skipSignatureValidation', true);
|
||||
}
|
||||
return plugin.install(true, false).then(() => {
|
||||
this.get('plugins').push(plugin);
|
||||
this.trigger('change');
|
||||
this.saveState();
|
||||
return Plugin.loadFromUrl(url, expectedManifest)
|
||||
.then(plugin => {
|
||||
return this.uninstall(plugin.id).then(() => {
|
||||
if (skipSignatureValidation) {
|
||||
plugin.set('skipSignatureValidation', true);
|
||||
}
|
||||
return plugin.install(true, false).then(() => {
|
||||
this.get('plugins').push(plugin);
|
||||
this.trigger('change');
|
||||
this.saveState();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.trigger('change');
|
||||
throw e;
|
||||
});
|
||||
}).catch(e => {
|
||||
this.trigger('change');
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
installIfNew(url, expectedManifest, skipSignatureValidation) {
|
||||
|
@ -112,24 +114,35 @@ const PluginManager = Backbone.Model.extend({
|
|||
update(id) {
|
||||
const plugins = this.get('plugins');
|
||||
const oldPlugin = plugins.get(id);
|
||||
const validStatuses = [Plugin.STATUS_ACTIVE, Plugin.STATUS_INACTIVE, Plugin.STATUS_NONE, Plugin.STATUS_ERROR, Plugin.STATUS_INVALID];
|
||||
const validStatuses = [
|
||||
Plugin.STATUS_ACTIVE,
|
||||
Plugin.STATUS_INACTIVE,
|
||||
Plugin.STATUS_NONE,
|
||||
Plugin.STATUS_ERROR,
|
||||
Plugin.STATUS_INVALID
|
||||
];
|
||||
if (!oldPlugin || validStatuses.indexOf(oldPlugin.get('status')) < 0) {
|
||||
return Promise.reject();
|
||||
}
|
||||
const url = oldPlugin.get('url');
|
||||
this.trigger('change');
|
||||
return Plugin.loadFromUrl(url).then(newPlugin => {
|
||||
return oldPlugin.update(newPlugin).then(() => {
|
||||
this.trigger('change');
|
||||
this.saveState();
|
||||
}).catch(e => {
|
||||
return Plugin.loadFromUrl(url)
|
||||
.then(newPlugin => {
|
||||
return oldPlugin
|
||||
.update(newPlugin)
|
||||
.then(() => {
|
||||
this.trigger('change');
|
||||
this.saveState();
|
||||
})
|
||||
.catch(e => {
|
||||
this.trigger('change');
|
||||
throw e;
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.trigger('change');
|
||||
throw e;
|
||||
});
|
||||
}).catch(e => {
|
||||
this.trigger('change');
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
setAutoUpdate(id, enabled) {
|
||||
|
@ -144,7 +157,9 @@ const PluginManager = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
runAutoUpdate() {
|
||||
const queue = this.get('plugins').filter(p => p.get('autoUpdate')).map(p => p.id);
|
||||
const queue = this.get('plugins')
|
||||
.filter(p => p.get('autoUpdate'))
|
||||
.map(p => p.id);
|
||||
if (!queue.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -163,7 +178,9 @@ const PluginManager = Backbone.Model.extend({
|
|||
const updateNext = () => {
|
||||
const pluginId = queue.shift();
|
||||
if (pluginId) {
|
||||
return this.update(pluginId).catch(() => {}).then(updateNext);
|
||||
return this.update(pluginId)
|
||||
.catch(() => {})
|
||||
.then(updateNext);
|
||||
}
|
||||
};
|
||||
return updateNext();
|
||||
|
@ -178,10 +195,13 @@ const PluginManager = Backbone.Model.extend({
|
|||
let enabled = desc.enabled;
|
||||
if (enabled) {
|
||||
const galleryPlugin = gallery ? gallery.plugins.find(pl => pl.manifest.name === desc.manifest.name) : null;
|
||||
const expectedPublicKey = galleryPlugin ? galleryPlugin.manifest.publicKey : SignatureVerifier.getPublicKey();
|
||||
const expectedPublicKey = galleryPlugin
|
||||
? galleryPlugin.manifest.publicKey
|
||||
: SignatureVerifier.getPublicKey();
|
||||
enabled = desc.manifest.publicKey === expectedPublicKey;
|
||||
}
|
||||
return plugin.install(enabled, true)
|
||||
return plugin
|
||||
.install(enabled, true)
|
||||
.then(() => plugin)
|
||||
.catch(() => plugin);
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -43,7 +43,11 @@ const ThemeVars = {
|
|||
result = result.replace(/([\w\-]+)\([^()]+\)/, fnText => {
|
||||
replaced = true;
|
||||
const [, name, argsStr] = fnText.match(/([\w\-]+)\((.*)\)/);
|
||||
const args = argsStr.trim().split(/\s*,\s*/).filter(arg => arg).map(arg => this.resolveArg(arg, cssStyle, locals));
|
||||
const args = argsStr
|
||||
.trim()
|
||||
.split(/\s*,\s*/)
|
||||
.filter(arg => arg)
|
||||
.map(arg => this.resolveArg(arg, cssStyle, locals));
|
||||
locals.push(this.fn[name](...args));
|
||||
return 'L' + (locals.length - 1);
|
||||
});
|
||||
|
|
|
@ -17,21 +17,51 @@ EntryPresenter.prototype = {
|
|||
}
|
||||
return this;
|
||||
},
|
||||
get id() { return this.entry ? this.entry.id : this.group.id; },
|
||||
get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); },
|
||||
get customIcon() { return this.entry ? this.entry.customIcon : undefined; },
|
||||
get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; },
|
||||
get title() { return this.entry ? this.entry.title : this.group.get('title'); },
|
||||
get notes() { return this.entry ? this.entry.notes : undefined; },
|
||||
get url() { return this.entry ? this.entry.displayUrl : undefined; },
|
||||
get user() { return this.entry ? this.entry.user : undefined; },
|
||||
get active() { return this.entry ? this.entry.id === this.activeEntryId : this.group.active; },
|
||||
get created() { return this.entry ? Format.dtStr(this.entry.created) : undefined; },
|
||||
get updated() { return this.entry ? Format.dtStr(this.entry.updated) : undefined; },
|
||||
get expired() { return this.entry ? this.entry.expired : false; },
|
||||
get tags() { return this.entry ? this.entry.tags : undefined; },
|
||||
get groupName() { return this.entry ? this.entry.groupName : undefined; },
|
||||
get fileName() { return this.entry ? this.entry.fileName : undefined; },
|
||||
get id() {
|
||||
return this.entry ? this.entry.id : this.group.id;
|
||||
},
|
||||
get icon() {
|
||||
return this.entry ? this.entry.icon : this.group.get('icon') || 'folder';
|
||||
},
|
||||
get customIcon() {
|
||||
return this.entry ? this.entry.customIcon : undefined;
|
||||
},
|
||||
get color() {
|
||||
return this.entry ? this.entry.color || (this.entry.customIcon ? this.noColor : undefined) : undefined;
|
||||
},
|
||||
get title() {
|
||||
return this.entry ? this.entry.title : this.group.get('title');
|
||||
},
|
||||
get notes() {
|
||||
return this.entry ? this.entry.notes : undefined;
|
||||
},
|
||||
get url() {
|
||||
return this.entry ? this.entry.displayUrl : undefined;
|
||||
},
|
||||
get user() {
|
||||
return this.entry ? this.entry.user : undefined;
|
||||
},
|
||||
get active() {
|
||||
return this.entry ? this.entry.id === this.activeEntryId : this.group.active;
|
||||
},
|
||||
get created() {
|
||||
return this.entry ? Format.dtStr(this.entry.created) : undefined;
|
||||
},
|
||||
get updated() {
|
||||
return this.entry ? Format.dtStr(this.entry.updated) : undefined;
|
||||
},
|
||||
get expired() {
|
||||
return this.entry ? this.entry.expired : false;
|
||||
},
|
||||
get tags() {
|
||||
return this.entry ? this.entry.tags : undefined;
|
||||
},
|
||||
get groupName() {
|
||||
return this.entry ? this.entry.groupName : undefined;
|
||||
},
|
||||
get fileName() {
|
||||
return this.entry ? this.entry.fileName : undefined;
|
||||
},
|
||||
get description() {
|
||||
if (!this.entry) {
|
||||
return '[' + Locale.listGroup + ']';
|
||||
|
|
|
@ -15,11 +15,15 @@ _.extend(IoBrowserCache.prototype, {
|
|||
const req = idb.open(this.cacheName);
|
||||
req.onerror = e => {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
if (callback) { callback(e); }
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
req.onsuccess = e => {
|
||||
this.db = e.target.result;
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onupgradeneeded = e => {
|
||||
const db = e.target.result;
|
||||
|
@ -27,7 +31,9 @@ _.extend(IoBrowserCache.prototype, {
|
|||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
if (callback) { callback(e); }
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -39,18 +45,27 @@ _.extend(IoBrowserCache.prototype, {
|
|||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
|
||||
const req = this.db
|
||||
.transaction(['files'], 'readwrite')
|
||||
.objectStore('files')
|
||||
.put(data, id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Saved', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error saving to cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error saving to cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -63,18 +78,27 @@ _.extend(IoBrowserCache.prototype, {
|
|||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
|
||||
const req = this.db
|
||||
.transaction(['files'], 'readonly')
|
||||
.objectStore('files')
|
||||
.get(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Loaded', id, this.logger.ts(ts));
|
||||
if (callback) { callback(null, req.result); }
|
||||
if (callback) {
|
||||
callback(null, req.result);
|
||||
}
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error loading from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error loading from cache', id, e);
|
||||
if (callback) { callback(e, null); }
|
||||
if (callback) {
|
||||
callback(e, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -87,18 +111,27 @@ _.extend(IoBrowserCache.prototype, {
|
|||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
|
||||
const req = this.db
|
||||
.transaction(['files'], 'readwrite')
|
||||
.objectStore('files')
|
||||
.delete(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Removed', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error removing from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
if (callback) {
|
||||
callback(req.error);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error removing from cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
if (callback) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -37,10 +37,14 @@ _.extend(IoFileCache.prototype, {
|
|||
Launcher.writeFile(path, data, err => {
|
||||
if (err) {
|
||||
this.logger.error('Error saving file', id, err);
|
||||
if (callback) { callback(err); }
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug('Saved', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -57,10 +61,14 @@ _.extend(IoFileCache.prototype, {
|
|||
Launcher.readFile(path, undefined, (data, err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error loading file', id, err);
|
||||
if (callback) { callback(err); }
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug('Loaded', id, this.logger.ts(ts));
|
||||
if (callback) { callback(null, data); }
|
||||
if (callback) {
|
||||
callback(null, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -77,10 +85,14 @@ _.extend(IoFileCache.prototype, {
|
|||
Launcher.deleteFile(path, err => {
|
||||
if (err) {
|
||||
this.logger.error('Error removing file', id, err);
|
||||
if (callback) { callback(err); }
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug('Removed', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,8 +7,7 @@ const FeatureDetector = require('../util/feature-detector');
|
|||
|
||||
const MaxRequestRetries = 3;
|
||||
|
||||
const StorageBase = function() {
|
||||
};
|
||||
const StorageBase = function() {};
|
||||
|
||||
_.extend(StorageBase.prototype, {
|
||||
name: null,
|
||||
|
@ -104,11 +103,19 @@ _.extend(StorageBase.prototype, {
|
|||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
|
||||
|
||||
const winWidth = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
const winHeight = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
||||
const winWidth = window.innerWidth
|
||||
? window.innerWidth
|
||||
: document.documentElement.clientWidth
|
||||
? document.documentElement.clientWidth
|
||||
: screen.width;
|
||||
const winHeight = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: screen.height;
|
||||
|
||||
const left = ((winWidth / 2) - (width / 2)) + dualScreenLeft;
|
||||
const top = ((winHeight / 2) - (height / 2)) + dualScreenTop;
|
||||
const left = winWidth / 2 - width / 2 + dualScreenLeft;
|
||||
const top = winHeight / 2 - height / 2 + dualScreenTop;
|
||||
|
||||
let settings = {
|
||||
width: width,
|
||||
|
@ -120,7 +127,9 @@ _.extend(StorageBase.prototype, {
|
|||
scrollbars: 'yes',
|
||||
location: 'yes'
|
||||
};
|
||||
settings = Object.keys(settings).map(key => key + '=' + settings[key]).join(',');
|
||||
settings = Object.keys(settings)
|
||||
.map(key => key + '=' + settings[key])
|
||||
.join(',');
|
||||
if (FeatureDetector.isStandalone) {
|
||||
sessionStorage.authStorage = this.name;
|
||||
}
|
||||
|
@ -147,10 +156,12 @@ _.extend(StorageBase.prototype, {
|
|||
this._oauthToken = oldToken;
|
||||
return callback();
|
||||
}
|
||||
const url = opts.url + '?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
|
||||
.replace('{cid}', encodeURIComponent(opts.clientId))
|
||||
.replace('{scope}', encodeURIComponent(opts.scope))
|
||||
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
|
||||
const url =
|
||||
opts.url +
|
||||
'?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
|
||||
.replace('{cid}', encodeURIComponent(opts.clientId))
|
||||
.replace('{scope}', encodeURIComponent(opts.scope))
|
||||
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
|
||||
this.logger.debug('OAuth: popup opened');
|
||||
const popupWindow = this._openPopup(url, 'OAuth', opts.width, opts.height);
|
||||
if (!popupWindow) {
|
||||
|
@ -185,8 +196,7 @@ _.extend(StorageBase.prototype, {
|
|||
window.addEventListener('message', windowMessage);
|
||||
},
|
||||
|
||||
_popupOpened(popupWindow) {
|
||||
},
|
||||
_popupOpened(popupWindow) {},
|
||||
|
||||
_oauthProcessReturn: function(message) {
|
||||
const token = this._oauthMsgToToken(message);
|
||||
|
@ -201,7 +211,7 @@ _.extend(StorageBase.prototype, {
|
|||
_oauthMsgToToken: function(data) {
|
||||
if (!data.token_type) {
|
||||
if (data.error) {
|
||||
return {error: data.error, errorDescription: data.error_description};
|
||||
return { error: data.error, errorDescription: data.error_description };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -80,8 +80,21 @@ const StorageDropbox = StorageBase.extend({
|
|||
return {
|
||||
desc: 'dropboxSetupDesc',
|
||||
fields: [
|
||||
{id: 'key', title: 'dropboxAppKey', desc: 'dropboxAppKeyDesc', type: 'text', required: true, pattern: '\\w+'},
|
||||
{id: 'folder', title: 'dropboxFolder', desc: 'dropboxFolderDesc', type: 'text', placeholder: 'dropboxFolderPlaceholder'}
|
||||
{
|
||||
id: 'key',
|
||||
title: 'dropboxAppKey',
|
||||
desc: 'dropboxAppKeyDesc',
|
||||
type: 'text',
|
||||
required: true,
|
||||
pattern: '\\w+'
|
||||
},
|
||||
{
|
||||
id: 'folder',
|
||||
title: 'dropboxFolder',
|
||||
desc: 'dropboxFolderDesc',
|
||||
type: 'text',
|
||||
placeholder: 'dropboxFolderPlaceholder'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
@ -89,12 +102,29 @@ const StorageDropbox = StorageBase.extend({
|
|||
getSettingsConfig: function() {
|
||||
const fields = [];
|
||||
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 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 = this._canUseBuiltInKeys();
|
||||
if (canUseBuiltInKeys) {
|
||||
fields.push(linkField);
|
||||
|
@ -198,7 +228,7 @@ const StorageDropbox = StorageBase.extend({
|
|||
statuses: args.statuses || undefined,
|
||||
success: args.success,
|
||||
error: (e, xhr) => {
|
||||
let err = xhr.response && xhr.response.error || new Error('Network error');
|
||||
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;
|
||||
|
@ -245,7 +275,9 @@ const StorageDropbox = StorageBase.extend({
|
|||
stat = { folder: true };
|
||||
}
|
||||
this.logger.debug('Stated', path, stat.folder ? 'folder' : stat.rev, this.logger.ts(ts));
|
||||
if (callback) { callback(null, stat); }
|
||||
if (callback) {
|
||||
callback(null, stat);
|
||||
}
|
||||
},
|
||||
error: callback
|
||||
});
|
||||
|
@ -284,13 +316,12 @@ const StorageDropbox = StorageBase.extend({
|
|||
},
|
||||
success: data => {
|
||||
this.logger.debug('Listed', this.logger.ts(ts));
|
||||
const fileList = data.entries
|
||||
.map(f => ({
|
||||
name: f.name,
|
||||
path: this._toRelPath(f['path_display']),
|
||||
rev: f.rev,
|
||||
dir: f['.tag'] !== 'file'
|
||||
}));
|
||||
const fileList = data.entries.map(f => ({
|
||||
name: f.name,
|
||||
path: this._toRelPath(f['path_display']),
|
||||
rev: f.rev,
|
||||
dir: f['.tag'] !== 'file'
|
||||
}));
|
||||
callback(null, fileList);
|
||||
},
|
||||
error: callback
|
||||
|
|
|
@ -19,7 +19,7 @@ const StorageFileCache = StorageBase.extend({
|
|||
|
||||
const path = Launcher.getUserDataPath('OfflineFiles');
|
||||
|
||||
const setPath = (err) => {
|
||||
const setPath = err => {
|
||||
this.path = err ? null : path;
|
||||
if (err) {
|
||||
this.logger.error('Error opening local offline storage', err);
|
||||
|
@ -49,7 +49,9 @@ const StorageFileCache = StorageBase.extend({
|
|||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Saved', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -114,10 +114,14 @@ const StorageFile = StorageBase.extend({
|
|||
Launcher.mkdir(path, err => {
|
||||
if (err) {
|
||||
this.logger.error('Error making local dir', path, err);
|
||||
if (callback) { callback('Error making local dir'); }
|
||||
if (callback) {
|
||||
callback('Error making local dir');
|
||||
}
|
||||
} else {
|
||||
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -11,7 +11,8 @@ const StorageGDrive = StorageBase.extend({
|
|||
name: 'gdrive',
|
||||
enabled: true,
|
||||
uipos: 30,
|
||||
iconSvg: '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><path d="M86.657536,76.246208 L47.768064,9 L89.111168,' +
|
||||
iconSvg:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><path d="M86.657536,76.246208 L47.768064,9 L89.111168,' +
|
||||
'9 L128,76.246208 L86.657536,76.246208 Z M25.010048,119.08 L102.690048,119.08 L123.36256,83.24 L45.68064,83.24 L25.010048,119.08 L25.010048,' +
|
||||
'119.08 Z M38.793088,9.003712 L0,76.30496 L20.671872,112.110016 L59.464704,44.808128 L38.793088,9.003712 Z"></path></svg>',
|
||||
|
||||
|
@ -24,20 +25,22 @@ const StorageGDrive = StorageBase.extend({
|
|||
|
||||
load: function(path, opts, callback) {
|
||||
this.stat(path, opts, (err, stat) => {
|
||||
if (err) { return callback && callback(err); }
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Load', path);
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + '/files/{id}/revisions/{rev}?alt=media'
|
||||
.replace('{id}', path)
|
||||
.replace('{rev}', stat.rev);
|
||||
const url =
|
||||
this._baseUrl +
|
||||
'/files/{id}/revisions/{rev}?alt=media'.replace('{id}', path).replace('{rev}', stat.rev);
|
||||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'arraybuffer',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
|
||||
return callback && callback(null, response, { rev: stat.rev });
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -55,17 +58,16 @@ const StorageGDrive = StorageBase.extend({
|
|||
}
|
||||
this.logger.debug('Stat', path);
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + '/files/{id}?fields=headRevisionId'
|
||||
.replace('{id}', path);
|
||||
const url = this._baseUrl + '/files/{id}?fields=headRevisionId'.replace('{id}', path);
|
||||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
const rev = response.headRevisionId;
|
||||
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, { rev: rev });
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Stat error', this.logger.ts(ts), err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -95,18 +97,33 @@ const StorageGDrive = StorageBase.extend({
|
|||
url = this._baseUrlUpload + '/files?uploadType=multipart&fields=id,headRevisionId';
|
||||
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
|
||||
const boundry = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
|
||||
data = new Blob([
|
||||
'--', boundry, '\r\n',
|
||||
'Content-Type: application/json; charset=UTF-8', '\r\n\r\n',
|
||||
JSON.stringify({ name: fileName }), '\r\n',
|
||||
'--', boundry, '\r\n',
|
||||
'Content-Type: application/octet-stream', '\r\n\r\n',
|
||||
data, '\r\n',
|
||||
'--', boundry, '--', '\r\n'
|
||||
], { type: 'multipart/related; boundary="' + boundry + '"' });
|
||||
data = new Blob(
|
||||
[
|
||||
'--',
|
||||
boundry,
|
||||
'\r\n',
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
'\r\n\r\n',
|
||||
JSON.stringify({ name: fileName }),
|
||||
'\r\n',
|
||||
'--',
|
||||
boundry,
|
||||
'\r\n',
|
||||
'Content-Type: application/octet-stream',
|
||||
'\r\n\r\n',
|
||||
data,
|
||||
'\r\n',
|
||||
'--',
|
||||
boundry,
|
||||
'--',
|
||||
'\r\n'
|
||||
],
|
||||
{ type: 'multipart/related; boundary="' + boundry + '"' }
|
||||
);
|
||||
} else {
|
||||
url = this._baseUrlUpload + '/files/{id}?uploadType=media&fields=headRevisionId'
|
||||
.replace('{id}', path);
|
||||
url =
|
||||
this._baseUrlUpload +
|
||||
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
|
||||
data = new Blob([data], { type: 'application/octet-stream' });
|
||||
}
|
||||
this._xhr({
|
||||
|
@ -114,7 +131,7 @@ const StorageGDrive = StorageBase.extend({
|
|||
method: isNew ? 'POST' : 'PATCH',
|
||||
responseType: 'json',
|
||||
data: data,
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
this.logger.debug('Saved', path, this.logger.ts(ts));
|
||||
const newRev = response.headRevisionId;
|
||||
if (!newRev) {
|
||||
|
@ -122,7 +139,7 @@ const StorageGDrive = StorageBase.extend({
|
|||
}
|
||||
return callback && callback(null, { rev: newRev, path: isNew ? response.id : null });
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Save error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -132,20 +149,23 @@ const StorageGDrive = StorageBase.extend({
|
|||
},
|
||||
|
||||
list: function(dir, callback) {
|
||||
this._oauthAuthorize((err) => {
|
||||
if (err) { return callback && callback(err); }
|
||||
this._oauthAuthorize(err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('List');
|
||||
let query = dir === 'shared' ? 'sharedWithMe=true'
|
||||
: dir ? `"${dir}" in parents` : '"root" in parents';
|
||||
let query = dir === 'shared' ? 'sharedWithMe=true' : dir ? `"${dir}" in parents` : '"root" in parents';
|
||||
query += ' and trashed=false';
|
||||
const url = this._baseUrl + '/files?fields={fields}&q={q}&pageSize=1000'
|
||||
.replace('{fields}', encodeURIComponent('files(id,name,mimeType,headRevisionId)'))
|
||||
.replace('{q}', encodeURIComponent(query));
|
||||
const url =
|
||||
this._baseUrl +
|
||||
'/files?fields={fields}&q={q}&pageSize=1000'
|
||||
.replace('{fields}', encodeURIComponent('files(id,name,mimeType,headRevisionId)'))
|
||||
.replace('{q}', encodeURIComponent(query));
|
||||
const ts = this.logger.ts();
|
||||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
if (!response) {
|
||||
this.logger.error('List error', this.logger.ts(ts));
|
||||
return callback && callback('list error');
|
||||
|
@ -167,7 +187,7 @@ const StorageGDrive = StorageBase.extend({
|
|||
}
|
||||
return callback && callback(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('List error', this.logger.ts(ts), err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -188,7 +208,7 @@ const StorageGDrive = StorageBase.extend({
|
|||
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Remove error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ const StorageOneDrive = StorageBase.extend({
|
|||
name: 'onedrive',
|
||||
enabled: true,
|
||||
uipos: 40,
|
||||
iconSvg: '<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="256" height="256" version="1.1" viewBox="0 0 256 256">' +
|
||||
iconSvg:
|
||||
'<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="256" height="256" version="1.1" viewBox="0 0 256 256">' +
|
||||
'<g transform="translate(296.64282,-100.61434)"><g transform="translate(222.85714,-11.428576)"><g transform="matrix(0.83394139,0,0,0.83394139,' +
|
||||
'-86.101383,10.950635)"><path d="m-419.5 365.94c-18.48-4.62-28.77-19.31-28.81-41.1-0.01-6.97 0.49-10.31 2.23-14.79 4.26-10.99 15.55-19.27 ' +
|
||||
'30.41-22.33 7.39-1.52 9.67-3.15 9.67-6.92 0-1.18 0.88-4.71 1.95-7.83 4.88-14.2 13.93-26.03 23.59-30.87 10.11-5.07 15.22-6.21 27.45-6.14 17.38 ' +
|
||||
|
@ -38,7 +39,7 @@ const StorageOneDrive = StorageBase.extend({
|
|||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
const downloadUrl = response['@microsoft.graph.downloadUrl'];
|
||||
let rev = response.eTag;
|
||||
if (!downloadUrl || !response.eTag) {
|
||||
|
@ -52,15 +53,15 @@ const StorageOneDrive = StorageBase.extend({
|
|||
success: (response, xhr) => {
|
||||
rev = xhr.getResponseHeader('ETag') || rev;
|
||||
this.logger.debug('Loaded', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, response, {rev: rev});
|
||||
return callback && callback(null, response, { rev: rev });
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Load error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -79,14 +80,14 @@ const StorageOneDrive = StorageBase.extend({
|
|||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
const rev = response.eTag;
|
||||
if (!rev) {
|
||||
this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
|
||||
return callback && callback('no eTag');
|
||||
}
|
||||
this.logger.debug('Stated', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, {rev: rev});
|
||||
return callback && callback(null, { rev: rev });
|
||||
},
|
||||
error: (err, xhr) => {
|
||||
if (xhr.status === 404) {
|
||||
|
@ -113,7 +114,7 @@ const StorageOneDrive = StorageBase.extend({
|
|||
method: 'PUT',
|
||||
responseType: 'json',
|
||||
headers: rev ? { 'If-Match': rev } : null,
|
||||
data: new Blob([data], {type: 'application/octet-stream'}),
|
||||
data: new Blob([data], { type: 'application/octet-stream' }),
|
||||
statuses: [200, 201, 412],
|
||||
success: (response, xhr) => {
|
||||
rev = response.eTag;
|
||||
|
@ -126,9 +127,9 @@ const StorageOneDrive = StorageBase.extend({
|
|||
return callback && callback({ revConflict: true }, { rev: rev });
|
||||
}
|
||||
this.logger.debug('Saved', path, rev, this.logger.ts(ts));
|
||||
return callback && callback(null, {rev: rev});
|
||||
return callback && callback(null, { rev: rev });
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Save error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -138,14 +139,16 @@ const StorageOneDrive = StorageBase.extend({
|
|||
|
||||
list: function(dir, callback) {
|
||||
this._oauthAuthorize(err => {
|
||||
if (err) { return callback && callback(err); }
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('List');
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + (dir ? `${dir}:/children` : '/drive/root/children');
|
||||
this._xhr({
|
||||
url: url,
|
||||
responseType: 'json',
|
||||
success: (response) => {
|
||||
success: response => {
|
||||
if (!response || !response.value) {
|
||||
this.logger.error('List error', this.logger.ts(ts), response);
|
||||
return callback && callback('list error');
|
||||
|
@ -161,7 +164,7 @@ const StorageOneDrive = StorageBase.extend({
|
|||
}));
|
||||
return callback && callback(null, fileList);
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('List error', this.logger.ts(ts), err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -182,7 +185,7 @@ const StorageOneDrive = StorageBase.extend({
|
|||
this.logger.debug('Removed', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Remove error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -191,7 +194,9 @@ const StorageOneDrive = StorageBase.extend({
|
|||
|
||||
mkdir: function(path, callback) {
|
||||
this._oauthAuthorize(err => {
|
||||
if (err) { return callback && callback(err); }
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
this.logger.debug('Make dir', path);
|
||||
const ts = this.logger.ts();
|
||||
const url = this._baseUrl + '/drive/root/children';
|
||||
|
@ -201,12 +206,12 @@ const StorageOneDrive = StorageBase.extend({
|
|||
method: 'POST',
|
||||
responseType: 'json',
|
||||
statuses: [200, 204],
|
||||
data: new Blob([data], {type: 'application/json'}),
|
||||
data: new Blob([data], { type: 'application/json' }),
|
||||
success: () => {
|
||||
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
},
|
||||
error: (err) => {
|
||||
error: err => {
|
||||
this.logger.error('Make dir error', path, err, this.logger.ts(ts));
|
||||
return callback && callback(err);
|
||||
}
|
||||
|
@ -216,8 +221,10 @@ const StorageOneDrive = StorageBase.extend({
|
|||
|
||||
setEnabled: function(enabled) {
|
||||
if (!enabled) {
|
||||
const url = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url}'
|
||||
.replace('{url}', this._getOauthRedirectUrl());
|
||||
const url = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url}'.replace(
|
||||
'{url}',
|
||||
this._getOauthRedirectUrl()
|
||||
);
|
||||
this._oauthRevokeToken(url);
|
||||
}
|
||||
StorageBase.prototype.setEnabled.call(this, enabled);
|
||||
|
@ -244,13 +251,12 @@ const StorageOneDrive = StorageBase.extend({
|
|||
|
||||
_popupOpened(popupWindow) {
|
||||
if (popupWindow.webContents) {
|
||||
popupWindow.webContents.on('did-finish-load', (e) => {
|
||||
popupWindow.webContents.on('did-finish-load', e => {
|
||||
const webContents = e.sender.webContents;
|
||||
const url = webContents.getURL();
|
||||
if (url && url.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')) {
|
||||
// click the login button mentioned in #821
|
||||
const script =
|
||||
`const selector = '[role="button"][aria-describedby="tileError loginHeader"]';
|
||||
const script = `const selector = '[role="button"][aria-describedby="tileError loginHeader"]';
|
||||
if (document.querySelectorAll(selector).length === 1) document.querySelector(selector).click()`;
|
||||
webContents.executeJavaScript(script).catch(() => {});
|
||||
}
|
||||
|
|
|
@ -13,9 +13,21 @@ const StorageWebDav = StorageBase.extend({
|
|||
getOpenConfig: function() {
|
||||
return {
|
||||
fields: [
|
||||
{id: 'path', title: 'openUrl', desc: 'openUrlDesc', type: 'text', required: true},
|
||||
{id: 'user', title: 'openUser', desc: 'openUserDesc', placeholder: 'openUserPlaceholder', type: 'text'},
|
||||
{id: 'password', title: 'openPass', desc: 'openPassDesc', placeholder: 'openPassPlaceholder', type: 'password'}
|
||||
{ id: 'path', title: 'openUrl', desc: 'openUrlDesc', type: 'text', required: true },
|
||||
{
|
||||
id: 'user',
|
||||
title: 'openUser',
|
||||
desc: 'openUserDesc',
|
||||
placeholder: 'openUserPlaceholder',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
title: 'openPass',
|
||||
desc: 'openPassDesc',
|
||||
placeholder: 'openPassPlaceholder',
|
||||
type: 'password'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
@ -23,9 +35,13 @@ const StorageWebDav = StorageBase.extend({
|
|||
getSettingsConfig: function() {
|
||||
return {
|
||||
fields: [
|
||||
{ id: 'webdavSaveMethod', title: 'webdavSaveMethod', type: 'select',
|
||||
{
|
||||
id: 'webdavSaveMethod',
|
||||
title: 'webdavSaveMethod',
|
||||
type: 'select',
|
||||
value: this.appSettings.get('webdavSaveMethod') || 'default',
|
||||
options: { default: 'webdavSaveMove', put: 'webdavSavePut' } }
|
||||
options: { default: 'webdavSaveMove', put: 'webdavSavePut' }
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
@ -35,27 +51,37 @@ const StorageWebDav = StorageBase.extend({
|
|||
},
|
||||
|
||||
load: function(path, opts, callback) {
|
||||
this._request({
|
||||
op: 'Load',
|
||||
method: 'GET',
|
||||
path: path,
|
||||
user: opts ? opts.user : null,
|
||||
password: opts ? opts.password : null
|
||||
}, callback ? (err, xhr, stat) => {
|
||||
callback(err, xhr.response, stat);
|
||||
} : null);
|
||||
this._request(
|
||||
{
|
||||
op: 'Load',
|
||||
method: 'GET',
|
||||
path: path,
|
||||
user: opts ? opts.user : null,
|
||||
password: opts ? opts.password : null
|
||||
},
|
||||
callback
|
||||
? (err, xhr, stat) => {
|
||||
callback(err, xhr.response, stat);
|
||||
}
|
||||
: null
|
||||
);
|
||||
},
|
||||
|
||||
stat: function(path, opts, callback) {
|
||||
this._request({
|
||||
op: 'Stat',
|
||||
method: 'HEAD',
|
||||
path: path,
|
||||
user: opts ? opts.user : null,
|
||||
password: opts ? opts.password : null
|
||||
}, callback ? (err, xhr, stat) => {
|
||||
callback(err, stat);
|
||||
} : null);
|
||||
this._request(
|
||||
{
|
||||
op: 'Stat',
|
||||
method: 'HEAD',
|
||||
path: path,
|
||||
user: opts ? opts.user : null,
|
||||
password: opts ? opts.password : null
|
||||
},
|
||||
callback
|
||||
? (err, xhr, stat) => {
|
||||
callback(err, stat);
|
||||
}
|
||||
: null
|
||||
);
|
||||
},
|
||||
|
||||
save: function(path, opts, data, callback, rev) {
|
||||
|
@ -72,76 +98,142 @@ const StorageWebDav = StorageBase.extend({
|
|||
password: opts ? opts.password : null
|
||||
};
|
||||
const that = this;
|
||||
this._request(_.defaults({
|
||||
op: 'Save:stat', method: 'HEAD'
|
||||
}, saveOpts), (err, xhr, stat) => {
|
||||
let useTmpPath = this.appSettings.get('webdavSaveMethod') !== 'put';
|
||||
if (err) {
|
||||
if (!err.notFound) {
|
||||
return cb(err);
|
||||
} else {
|
||||
that.logger.debug('Save: not found, creating');
|
||||
useTmpPath = false;
|
||||
this._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:stat',
|
||||
method: 'HEAD'
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
(err, xhr, stat) => {
|
||||
let useTmpPath = this.appSettings.get('webdavSaveMethod') !== 'put';
|
||||
if (err) {
|
||||
if (!err.notFound) {
|
||||
return cb(err);
|
||||
} else {
|
||||
that.logger.debug('Save: not found, creating');
|
||||
useTmpPath = false;
|
||||
}
|
||||
} else if (stat.rev !== rev) {
|
||||
that.logger.debug('Save error', path, 'rev conflict', stat.rev, rev);
|
||||
return cb({ revConflict: true }, xhr, stat);
|
||||
}
|
||||
} else if (stat.rev !== rev) {
|
||||
that.logger.debug('Save error', path, 'rev conflict', stat.rev, rev);
|
||||
return cb({ revConflict: true }, xhr, stat);
|
||||
}
|
||||
if (useTmpPath) {
|
||||
that._request(_.defaults({
|
||||
op: 'Save:put', method: 'PUT', path: tmpPath, data: data, nostat: true
|
||||
}, saveOpts), (err) => {
|
||||
if (err) { return cb(err); }
|
||||
that._request(_.defaults({
|
||||
op: 'Save:stat', method: 'HEAD'
|
||||
}, saveOpts), (err, xhr, stat) => {
|
||||
if (err) {
|
||||
that._request(_.defaults({op: 'Save:delete', method: 'DELETE', path: tmpPath}, saveOpts));
|
||||
return cb(err, xhr, stat);
|
||||
}
|
||||
if (stat.rev !== rev) {
|
||||
that.logger.debug('Save error', path, 'rev conflict', stat.rev, rev);
|
||||
that._request(_.defaults({op: 'Save:delete', method: 'DELETE', path: tmpPath}, saveOpts));
|
||||
return cb({revConflict: true}, xhr, stat);
|
||||
}
|
||||
let movePath = path;
|
||||
if (movePath.indexOf('://') < 0) {
|
||||
if (movePath.indexOf('/') === 0) {
|
||||
movePath = location.protocol + '//' + location.host + movePath;
|
||||
} else {
|
||||
movePath = location.href.replace(/\?(.*)/, '').replace(/[^/]*$/, movePath);
|
||||
if (useTmpPath) {
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:put',
|
||||
method: 'PUT',
|
||||
path: tmpPath,
|
||||
data: data,
|
||||
nostat: true
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
err => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:stat',
|
||||
method: 'HEAD'
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
(err, xhr, stat) => {
|
||||
if (err) {
|
||||
that._request(
|
||||
_.defaults({ op: 'Save:delete', method: 'DELETE', path: tmpPath }, saveOpts)
|
||||
);
|
||||
return cb(err, xhr, stat);
|
||||
}
|
||||
if (stat.rev !== rev) {
|
||||
that.logger.debug('Save error', path, 'rev conflict', stat.rev, rev);
|
||||
that._request(
|
||||
_.defaults({ op: 'Save:delete', method: 'DELETE', path: tmpPath }, saveOpts)
|
||||
);
|
||||
return cb({ revConflict: true }, xhr, stat);
|
||||
}
|
||||
let movePath = path;
|
||||
if (movePath.indexOf('://') < 0) {
|
||||
if (movePath.indexOf('/') === 0) {
|
||||
movePath = location.protocol + '//' + location.host + movePath;
|
||||
} else {
|
||||
movePath = location.href.replace(/\?(.*)/, '').replace(/[^/]*$/, movePath);
|
||||
}
|
||||
}
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:move',
|
||||
method: 'MOVE',
|
||||
path: tmpPath,
|
||||
nostat: true,
|
||||
headers: { Destination: movePath, 'Overwrite': 'T' }
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
err => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:stat',
|
||||
method: 'HEAD'
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
(err, xhr, stat) => {
|
||||
cb(err, xhr, stat);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
that._request(_.defaults({
|
||||
op: 'Save:move', method: 'MOVE', path: tmpPath, nostat: true,
|
||||
headers: {Destination: movePath, 'Overwrite': 'T'}
|
||||
}, saveOpts), (err) => {
|
||||
if (err) { return cb(err); }
|
||||
that._request(_.defaults({
|
||||
op: 'Save:stat', method: 'HEAD'
|
||||
}, saveOpts), (err, xhr, stat) => {
|
||||
cb(err, xhr, stat);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
that._request(_.defaults({
|
||||
op: 'Save:put', method: 'PUT', data: data, nostat: true
|
||||
}, saveOpts), (err) => {
|
||||
if (err) { return cb(err); }
|
||||
that._request(_.defaults({
|
||||
op: 'Save:stat', method: 'HEAD'
|
||||
}, saveOpts), (err, xhr, stat) => {
|
||||
cb(err, xhr, stat);
|
||||
});
|
||||
});
|
||||
);
|
||||
} else {
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:put',
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
nostat: true
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
err => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
that._request(
|
||||
_.defaults(
|
||||
{
|
||||
op: 'Save:stat',
|
||||
method: 'HEAD'
|
||||
},
|
||||
saveOpts
|
||||
),
|
||||
(err, xhr, stat) => {
|
||||
cb(err, xhr, stat);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
},
|
||||
|
||||
fileOptsToStoreOpts: function(opts, file) {
|
||||
const result = {user: opts.user, encpass: opts.encpass};
|
||||
const result = { user: opts.user, encpass: opts.encpass };
|
||||
if (opts.password) {
|
||||
const fileId = file.get('uuid');
|
||||
const password = opts.password;
|
||||
|
@ -155,7 +247,7 @@ const StorageWebDav = StorageBase.extend({
|
|||
},
|
||||
|
||||
storeOptsToFileOpts: function(opts, file) {
|
||||
const result = {user: opts.user, password: opts.password};
|
||||
const result = { user: opts.user, password: opts.password };
|
||||
if (opts.encpass) {
|
||||
const fileId = file.get('uuid');
|
||||
const encpass = atob(opts.encpass);
|
||||
|
@ -192,26 +284,41 @@ const StorageWebDav = StorageBase.extend({
|
|||
err = 'HTTP status ' + xhr.status;
|
||||
break;
|
||||
}
|
||||
if (callback) { callback(err, xhr); callback = null; }
|
||||
if (callback) {
|
||||
callback(err, xhr);
|
||||
callback = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const rev = xhr.getResponseHeader('Last-Modified');
|
||||
if (!rev && !config.nostat) {
|
||||
that.logger.debug(config.op + ' error', config.path, 'no headers', that.logger.ts(ts));
|
||||
if (callback) { callback('No Last-Modified header', xhr); callback = null; }
|
||||
if (callback) {
|
||||
callback('No Last-Modified header', xhr);
|
||||
callback = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const completedOpName = config.op + (config.op.charAt(config.op.length - 1) === 'e' ? 'd' : 'ed');
|
||||
that.logger.debug(completedOpName, config.path, rev, that.logger.ts(ts));
|
||||
if (callback) { callback(null, xhr, rev ? { rev: rev } : null); callback = null; }
|
||||
if (callback) {
|
||||
callback(null, xhr, rev ? { rev: rev } : null);
|
||||
callback = null;
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('error', () => {
|
||||
that.logger.debug(config.op + ' error', config.path, that.logger.ts(ts));
|
||||
if (callback) { callback('network error', xhr); callback = null; }
|
||||
if (callback) {
|
||||
callback('network error', xhr);
|
||||
callback = null;
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('abort', () => {
|
||||
that.logger.debug(config.op + ' error', config.path, 'aborted', that.logger.ts(ts));
|
||||
if (callback) { callback('aborted', xhr); callback = null; }
|
||||
if (callback) {
|
||||
callback('aborted', xhr);
|
||||
callback = null;
|
||||
}
|
||||
});
|
||||
xhr.open(config.method, config.path);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
|
@ -227,7 +334,7 @@ const StorageWebDav = StorageBase.extend({
|
|||
xhr.setRequestHeader('Cache-Control', 'no-cache');
|
||||
}
|
||||
if (config.data) {
|
||||
const blob = new Blob([config.data], {type: 'application/octet-stream'});
|
||||
const blob = new Blob([config.data], { type: 'application/octet-stream' });
|
||||
xhr.send(blob);
|
||||
} else {
|
||||
xhr.send();
|
||||
|
|
|
@ -52,9 +52,15 @@ Color.prototype.setHsl = function() {
|
|||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
|
|
@ -1,27 +1,38 @@
|
|||
const LastChar = String.fromCharCode(0xfffd);
|
||||
|
||||
const ciCompare = (window.Intl && window.Intl.Collator && !/Edge/.test(navigator.userAgent)) // bugged in Edge: #808
|
||||
? new Intl.Collator(undefined, { sensitivity: 'base' }).compare
|
||||
: (x, y) => x.toLocaleLowerCase().localeCompare(y.toLocaleLowerCase());
|
||||
const ciCompare =
|
||||
window.Intl && window.Intl.Collator && !/Edge/.test(navigator.userAgent) // bugged in Edge: #808
|
||||
? new Intl.Collator(undefined, { sensitivity: 'base' }).compare
|
||||
: (x, y) => x.toLocaleLowerCase().localeCompare(y.toLocaleLowerCase());
|
||||
|
||||
const Comparators = {
|
||||
stringComparator: function(field, asc) {
|
||||
if (asc) {
|
||||
return function (x, y) { return ciCompare(x[field] || LastChar, y[field] || LastChar); };
|
||||
return function(x, y) {
|
||||
return ciCompare(x[field] || LastChar, y[field] || LastChar);
|
||||
};
|
||||
} else {
|
||||
return function (x, y) { return ciCompare(y[field], x[field]); };
|
||||
return function(x, y) {
|
||||
return ciCompare(y[field], x[field]);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
rankComparator: function() {
|
||||
return function (x, y) { return y.getRank(this.filter.textLower) - x.getRank(this.filter.textLower); };
|
||||
return function(x, y) {
|
||||
return y.getRank(this.filter.textLower) - x.getRank(this.filter.textLower);
|
||||
};
|
||||
},
|
||||
|
||||
dateComparator: function(field, asc) {
|
||||
if (asc) {
|
||||
return function (x, y) { return x[field] - y[field]; };
|
||||
return function(x, y) {
|
||||
return x[field] - y[field];
|
||||
};
|
||||
} else {
|
||||
return function (x, y) { return y[field] - x[field]; };
|
||||
return function(x, y) {
|
||||
return y[field] - x[field];
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ const FeatureDetector = {
|
|||
isWindows: navigator.platform.indexOf('Win') >= 0,
|
||||
isiOS: /iPad|iPhone|iPod/i.test(navigator.userAgent),
|
||||
isMobile: MobileRegex.test(navigator.userAgent) || screen.width < MinDesktopScreenWidth,
|
||||
isPopup: !!((window.parent !== window.top) || window.opener),
|
||||
isPopup: !!(window.parent !== window.top || window.opener),
|
||||
isStandalone: !!navigator.standalone,
|
||||
isFrame: window.top !== window,
|
||||
isSelfHosted: !isDesktop && !/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
|
||||
|
@ -28,10 +28,18 @@ const FeatureDetector = {
|
|||
return !this.isMac;
|
||||
},
|
||||
screenshotToClipboardShortcut: function() {
|
||||
if (this.isiOS) { return 'Sleep+Home'; }
|
||||
if (this.isMobile) { return ''; }
|
||||
if (this.isMac) { return 'Command-Shift-Control-4'; }
|
||||
if (this.isWindows) { return 'Alt+PrintScreen'; }
|
||||
if (this.isiOS) {
|
||||
return 'Sleep+Home';
|
||||
}
|
||||
if (this.isMobile) {
|
||||
return '';
|
||||
}
|
||||
if (this.isMac) {
|
||||
return 'Command-Shift-Control-4';
|
||||
}
|
||||
if (this.isWindows) {
|
||||
return 'Alt+PrintScreen';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
supportsTitleBarStyles: function() {
|
||||
|
|
|
@ -18,8 +18,15 @@ const Format = {
|
|||
if (typeof dt === 'number') {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
return dt ? this.dStr(dt) + ' ' + this.pad(dt.getHours(), 2) + ':' + this.pad(dt.getMinutes(), 2) +
|
||||
':' + this.pad(dt.getSeconds(), 2) : '';
|
||||
return dt
|
||||
? this.dStr(dt) +
|
||||
' ' +
|
||||
this.pad(dt.getHours(), 2) +
|
||||
':' +
|
||||
this.pad(dt.getMinutes(), 2) +
|
||||
':' +
|
||||
this.pad(dt.getSeconds(), 2)
|
||||
: '';
|
||||
},
|
||||
dStr: function(dt) {
|
||||
if (typeof dt === 'number') {
|
||||
|
@ -37,8 +44,18 @@ const Format = {
|
|||
if (typeof dt === 'number') {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
return dt ? dt.getFullYear() + '-' + this.pad(dt.getMonth() + 1, 2) + '-' + this.pad(dt.getDate(), 2) + 'T' +
|
||||
this.pad(dt.getHours(), 2) + '-' + this.pad(dt.getMinutes(), 2) + '-' + this.pad(dt.getSeconds(), 2)
|
||||
return dt
|
||||
? dt.getFullYear() +
|
||||
'-' +
|
||||
this.pad(dt.getMonth() + 1, 2) +
|
||||
'-' +
|
||||
this.pad(dt.getDate(), 2) +
|
||||
'T' +
|
||||
this.pad(dt.getHours(), 2) +
|
||||
'-' +
|
||||
this.pad(dt.getMinutes(), 2) +
|
||||
'-' +
|
||||
this.pad(dt.getSeconds(), 2)
|
||||
: '';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,9 @@ const IdGenerator = {
|
|||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
},
|
||||
s4: function() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -38,22 +38,33 @@ const KdbxwebInit = {
|
|||
const GB = 1024 * MB;
|
||||
const WASM_PAGE_SIZE = 64 * 1024;
|
||||
const totalMemory = (2 * GB - 64 * KB) / 1024 / WASM_PAGE_SIZE;
|
||||
const initialMemory = Math.min(Math.max(Math.ceil(requiredMemory * 1024 / WASM_PAGE_SIZE), 256) + 256, totalMemory);
|
||||
const initialMemory = Math.min(
|
||||
Math.max(Math.ceil((requiredMemory * 1024) / WASM_PAGE_SIZE), 256) + 256,
|
||||
totalMemory
|
||||
);
|
||||
|
||||
const memoryDecl = `var wasmMemory=new WebAssembly.Memory({initial:${initialMemory},maximum:${totalMemory}});`;
|
||||
const moduleDecl = 'var Module={' +
|
||||
const moduleDecl =
|
||||
'var Module={' +
|
||||
'wasmJSMethod: "native-wasm",' +
|
||||
'wasmBinary: Uint8Array.from(atob("' + wasmBinaryBase64 + '"), c => c.charCodeAt(0)),' +
|
||||
'wasmBinary: Uint8Array.from(atob("' +
|
||||
wasmBinaryBase64 +
|
||||
'"), c => c.charCodeAt(0)),' +
|
||||
'print(...args) { postMessage({op:"log",args}) },' +
|
||||
'printErr(...args) { postMessage({op:"log",args}) },' +
|
||||
'postRun:' + this.workerPostRun.toString() + ',' +
|
||||
'calcHash:' + this.calcHash.toString() + ',' +
|
||||
'postRun:' +
|
||||
this.workerPostRun.toString() +
|
||||
',' +
|
||||
'calcHash:' +
|
||||
this.calcHash.toString() +
|
||||
',' +
|
||||
'wasmMemory:wasmMemory,' +
|
||||
'buffer:wasmMemory.buffer,' +
|
||||
'TOTAL_MEMORY:' + initialMemory * WASM_PAGE_SIZE +
|
||||
'TOTAL_MEMORY:' +
|
||||
initialMemory * WASM_PAGE_SIZE +
|
||||
'}';
|
||||
const script = argon2LoaderCode.replace(/^var Module.*?}/, memoryDecl + moduleDecl);
|
||||
const blob = new Blob([script], {type: 'application/javascript'});
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const worker = new Worker(objectUrl);
|
||||
const onMessage = e => {
|
||||
|
@ -75,7 +86,7 @@ const KdbxwebInit = {
|
|||
worker.terminate();
|
||||
KdbxwebInit.runtimeModule = null;
|
||||
if (!e.data || e.data.error || !e.data.hash) {
|
||||
const ex = e.data && e.data.error || 'unexpected error';
|
||||
const ex = (e.data && e.data.error) || 'unexpected error';
|
||||
logger.error('Worker error', ex);
|
||||
reject(ex);
|
||||
}
|
||||
|
@ -126,9 +137,21 @@ const KdbxwebInit = {
|
|||
const encodedLen = 512;
|
||||
const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL);
|
||||
|
||||
const res = Module._argon2_hash(iterations, memory, parallelism,
|
||||
password, passwordLen, salt, saltLen,
|
||||
hash, length, encoded, encodedLen, type, version);
|
||||
const res = Module._argon2_hash(
|
||||
iterations,
|
||||
memory,
|
||||
parallelism,
|
||||
password,
|
||||
passwordLen,
|
||||
salt,
|
||||
saltLen,
|
||||
hash,
|
||||
length,
|
||||
encoded,
|
||||
encodedLen,
|
||||
type,
|
||||
version
|
||||
);
|
||||
if (res) {
|
||||
throw new Error('Argon2 error ' + res);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const MaxLogsToSave = 100;
|
|||
const lastLogs = [];
|
||||
|
||||
const Logger = function(name, id) {
|
||||
this.prefix = (name ? name + (id ? ':' + id : '') : 'default');
|
||||
this.prefix = name ? name + (id ? ':' + id : '') : 'default';
|
||||
this.level = Level.All;
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const Otp = function(url, params) {
|
|||
if (params.type === 'hotp' && !params.counter) {
|
||||
throw 'Bad counter: ' + params.counter;
|
||||
}
|
||||
if (params.period && isNaN(params.period) || params.period < 1) {
|
||||
if ((params.period && isNaN(params.period)) || params.period < 1) {
|
||||
throw 'Bad period: ' + params.period;
|
||||
}
|
||||
|
||||
|
@ -70,13 +70,21 @@ Otp.prototype.next = function(callback) {
|
|||
Otp.prototype.hmac = function(data, callback) {
|
||||
const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||
const algo = { name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } };
|
||||
subtle.importKey('raw', this.key, algo, false, ['sign'])
|
||||
subtle
|
||||
.importKey('raw', this.key, algo, false, ['sign'])
|
||||
.then(key => {
|
||||
subtle.sign(algo, key, data)
|
||||
.then(sig => { callback(sig); })
|
||||
.catch(err => { callback(null, err); });
|
||||
subtle
|
||||
.sign(algo, key, data)
|
||||
.then(sig => {
|
||||
callback(sig);
|
||||
})
|
||||
.catch(err => {
|
||||
callback(null, err);
|
||||
});
|
||||
})
|
||||
.catch(err => { callback(null, err); });
|
||||
.catch(err => {
|
||||
callback(null, err);
|
||||
});
|
||||
};
|
||||
|
||||
Otp.fromBase32 = function(str) {
|
||||
|
@ -132,7 +140,12 @@ Otp.isSecret = function(str) {
|
|||
};
|
||||
|
||||
Otp.makeUrl = function(secret, period, digits) {
|
||||
return 'otpauth://totp/default?secret=' + secret + (period ? '&period=' + period : '') + (digits ? '&digits=' + digits : '');
|
||||
return (
|
||||
'otpauth://totp/default?secret=' +
|
||||
secret +
|
||||
(period ? '&period=' + period : '') +
|
||||
(digits ? '&digits=' + digits : '')
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = Otp;
|
||||
|
|
|
@ -28,7 +28,9 @@ const PasswordGenerator = {
|
|||
}
|
||||
const ranges = Object.keys(this.charRanges)
|
||||
.filter(r => opts[r])
|
||||
.map(function(r) { return this.charRanges[r]; }, this);
|
||||
.map(function(r) {
|
||||
return this.charRanges[r];
|
||||
}, this);
|
||||
if (opts.include && opts.include.length) {
|
||||
ranges.push(opts.include);
|
||||
}
|
||||
|
|
|
@ -11,20 +11,52 @@
|
|||
*/
|
||||
const PHONETIC_PRE = [
|
||||
// Simple phonetics
|
||||
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p',
|
||||
'qu', 'r', 's', 't',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'p',
|
||||
'qu',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
// Complex phonetics
|
||||
'bl',
|
||||
'ch', 'cl', 'cr',
|
||||
'ch',
|
||||
'cl',
|
||||
'cr',
|
||||
'dr',
|
||||
'fl', 'fr',
|
||||
'gl', 'gr',
|
||||
'kl', 'kr',
|
||||
'ph', 'pr', 'pl',
|
||||
'sc', 'sh', 'sl', 'sn', 'sr', 'st', 'str', 'sw',
|
||||
'th', 'tr',
|
||||
'fl',
|
||||
'fr',
|
||||
'gl',
|
||||
'gr',
|
||||
'kl',
|
||||
'kr',
|
||||
'ph',
|
||||
'pr',
|
||||
'pl',
|
||||
'sc',
|
||||
'sh',
|
||||
'sl',
|
||||
'sn',
|
||||
'sr',
|
||||
'st',
|
||||
'str',
|
||||
'sw',
|
||||
'th',
|
||||
'tr',
|
||||
'br',
|
||||
'v', 'w', 'y', 'z'
|
||||
'v',
|
||||
'w',
|
||||
'y',
|
||||
'z'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -39,9 +71,17 @@ const PHONETIC_PRE_SIMPLE_LENGTH = 16;
|
|||
*/
|
||||
const PHONETIC_MID = [
|
||||
// Simple phonetics
|
||||
'a', 'e', 'i', 'o', 'u',
|
||||
'a',
|
||||
'e',
|
||||
'i',
|
||||
'o',
|
||||
'u',
|
||||
// Complex phonetics
|
||||
'ee', 'ie', 'oo', 'ou', 'ue'
|
||||
'ee',
|
||||
'ie',
|
||||
'oo',
|
||||
'ou',
|
||||
'ue'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -56,15 +96,32 @@ const PHONETIC_MID_SIMPLE_LENGTH = 5;
|
|||
*/
|
||||
const PHONETIC_POST = [
|
||||
// Simple phonetics
|
||||
'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'y',
|
||||
'b',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'p',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'y',
|
||||
// Complex phonetics
|
||||
'ch', 'ck',
|
||||
'ch',
|
||||
'ck',
|
||||
'ln',
|
||||
'nk', 'ng',
|
||||
'nk',
|
||||
'ng',
|
||||
'rn',
|
||||
'sh', 'sk', 'st',
|
||||
'sh',
|
||||
'sk',
|
||||
'st',
|
||||
'th',
|
||||
'x', 'z'
|
||||
'x',
|
||||
'z'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -107,17 +164,14 @@ function addSyllable(wordObj) {
|
|||
const first = wordObj.word === '';
|
||||
const preOnFirst = deriv % 6 > 0;
|
||||
if ((first && preOnFirst) || wordObj.lastSkippedPost || compound) {
|
||||
wordObj.word += getNextPhonetic(PHONETIC_PRE,
|
||||
PHONETIC_PRE_SIMPLE_LENGTH, wordObj);
|
||||
wordObj.word += getNextPhonetic(PHONETIC_PRE, PHONETIC_PRE_SIMPLE_LENGTH, wordObj);
|
||||
wordObj.lastSkippedPre = false;
|
||||
} else {
|
||||
wordObj.lastSkippedPre = true;
|
||||
}
|
||||
wordObj.word += getNextPhonetic(PHONETIC_MID, PHONETIC_MID_SIMPLE_LENGTH,
|
||||
wordObj, first && wordObj.lastSkippedPre);
|
||||
wordObj.word += getNextPhonetic(PHONETIC_MID, PHONETIC_MID_SIMPLE_LENGTH, wordObj, first && wordObj.lastSkippedPre);
|
||||
if (wordObj.lastSkippedPre || compound) {
|
||||
wordObj.word += getNextPhonetic(PHONETIC_POST,
|
||||
PHONETIC_POST_SIMPLE_LENGTH, wordObj);
|
||||
wordObj.word += getNextPhonetic(PHONETIC_POST, PHONETIC_POST_SIMPLE_LENGTH, wordObj);
|
||||
wordObj.lastSkippedPost = false;
|
||||
} else {
|
||||
wordObj.lastSkippedPost = true;
|
||||
|
@ -206,7 +260,7 @@ function getNumericHash(data) {
|
|||
data += '-Phonetic';
|
||||
for (let i = 0, len = data.length; i < len; i++) {
|
||||
const chr = data.charCodeAt(i);
|
||||
numeric = ((numeric << 5) - numeric) + chr;
|
||||
numeric = (numeric << 5) - numeric + chr;
|
||||
numeric >>>= 0;
|
||||
}
|
||||
return numeric;
|
||||
|
|
|
@ -9,7 +9,7 @@ const SignatureVerifier = {
|
|||
|
||||
verify(data, signature, pk) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const algo = {name: 'RSASSA-PKCS1-v1_5', hash: {name: 'SHA-256'}};
|
||||
const algo = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } };
|
||||
try {
|
||||
if (!pk) {
|
||||
pk = this.getPublicKey();
|
||||
|
@ -18,29 +18,33 @@ const SignatureVerifier = {
|
|||
const subtle = window.crypto.subtle;
|
||||
const keyFormat = 'spki';
|
||||
pk = kdbxweb.ByteUtils.base64ToBytes(pk);
|
||||
subtle.importKey(
|
||||
keyFormat, pk,
|
||||
algo,
|
||||
false, ['verify']
|
||||
).then(cryptoKey => {
|
||||
try {
|
||||
subtle.verify(algo, cryptoKey,
|
||||
kdbxweb.ByteUtils.arrayToBuffer(signature),
|
||||
kdbxweb.ByteUtils.arrayToBuffer(data)
|
||||
).then(isValid => {
|
||||
resolve(isValid);
|
||||
}).catch(e => {
|
||||
this.logger.error('Verify error', e);
|
||||
subtle
|
||||
.importKey(keyFormat, pk, algo, false, ['verify'])
|
||||
.then(cryptoKey => {
|
||||
try {
|
||||
subtle
|
||||
.verify(
|
||||
algo,
|
||||
cryptoKey,
|
||||
kdbxweb.ByteUtils.arrayToBuffer(signature),
|
||||
kdbxweb.ByteUtils.arrayToBuffer(data)
|
||||
)
|
||||
.then(isValid => {
|
||||
resolve(isValid);
|
||||
})
|
||||
.catch(e => {
|
||||
this.logger.error('Verify error', e);
|
||||
reject();
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error('Signature verification error', e);
|
||||
reject();
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error('Signature verification error', e);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.logger.error('ImportKey error', e);
|
||||
reject();
|
||||
}
|
||||
}).catch(e => {
|
||||
this.logger.error('ImportKey error', e);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error('Signature key verification error', e);
|
||||
reject();
|
||||
|
@ -50,9 +54,7 @@ const SignatureVerifier = {
|
|||
|
||||
getPublicKey() {
|
||||
if (!this.publicKey) {
|
||||
this.publicKey = publicKey
|
||||
.match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1]
|
||||
.replace(/\s+/g, '');
|
||||
this.publicKey = publicKey.match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1].replace(/\s+/g, '');
|
||||
}
|
||||
return this.publicKey;
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ const FeatureDetector = require('./feature-detector');
|
|||
|
||||
const Tip = function(el, config) {
|
||||
this.el = el;
|
||||
this.title = config && config.title || el.attr('title');
|
||||
this.placement = config && config.placement || el.attr('tip-placement');
|
||||
this.fast = config && config.fast || false;
|
||||
this.title = (config && config.title) || el.attr('title');
|
||||
this.placement = (config && config.placement) || el.attr('tip-placement');
|
||||
this.fast = (config && config.fast) || false;
|
||||
this.tipEl = null;
|
||||
this.showTimeout = null;
|
||||
this.hideTimeout = null;
|
||||
this.force = config && config.force || false;
|
||||
this.force = (config && config.force) || false;
|
||||
this.hide = this.hide.bind(this);
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,7 @@ Tip.prototype.init = function() {
|
|||
};
|
||||
|
||||
Tip.prototype.show = function() {
|
||||
if (!Tip.enabled && !this.force || !this.title) {
|
||||
if ((!Tip.enabled && !this.force) || !this.title) {
|
||||
return;
|
||||
}
|
||||
Backbone.on('page-geometry', this.hide);
|
||||
|
@ -37,7 +37,10 @@ Tip.prototype.show = function() {
|
|||
this.hideTimeout = null;
|
||||
}
|
||||
}
|
||||
const tipEl = this.tipEl = $('<div></div>').addClass('tip').appendTo('body').html(this.title);
|
||||
const tipEl = (this.tipEl = $('<div></div>')
|
||||
.addClass('tip')
|
||||
.appendTo('body')
|
||||
.html(this.title));
|
||||
const rect = this.el[0].getBoundingClientRect();
|
||||
const tipRect = this.tipEl[0].getBoundingClientRect();
|
||||
const placement = this.placement || this.getAutoPlacement(rect, tipRect);
|
||||
|
@ -45,8 +48,7 @@ Tip.prototype.show = function() {
|
|||
if (this.fast) {
|
||||
tipEl.addClass('tip--fast');
|
||||
}
|
||||
let top,
|
||||
left;
|
||||
let top, left;
|
||||
const offset = 10;
|
||||
const sideOffset = 10;
|
||||
switch (placement) {
|
||||
|
@ -171,8 +173,7 @@ Tip.hideTip = function(el) {
|
|||
Tip.updateTip = function(el, props) {
|
||||
if (el._tip) {
|
||||
el._tip.hide();
|
||||
_.extend(el._tip, _.pick(props,
|
||||
['title', 'placement', 'fast', 'showTimeout', 'hideTimeout']));
|
||||
_.extend(el._tip, _.pick(props, ['title', 'placement', 'fast', 'showTimeout', 'hideTimeout']));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ const AppView = Backbone.View.extend({
|
|||
|
||||
titlebarStyle: 'default',
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.views = {};
|
||||
this.views.menu = new MenuView({ model: this.model.menu });
|
||||
this.views.menuDrag = new DragView('x');
|
||||
|
@ -121,16 +121,20 @@ const AppView = Backbone.View.extend({
|
|||
// https://github.com/keeweb/keeweb/issues/636#issuecomment-304225634
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5782378/
|
||||
if (FeatureDetector.needFixClicks) {
|
||||
const msEdgeScrewer = $('<input/>').appendTo(this.$el).focus();
|
||||
const msEdgeScrewer = $('<input/>')
|
||||
.appendTo(this.$el)
|
||||
.focus();
|
||||
setTimeout(() => msEdgeScrewer.remove(), 0);
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
beta: this.model.isBeta,
|
||||
titlebarStyle: this.titlebarStyle
|
||||
}));
|
||||
render: function() {
|
||||
this.$el.html(
|
||||
this.template({
|
||||
beta: this.model.isBeta,
|
||||
titlebarStyle: this.titlebarStyle
|
||||
})
|
||||
);
|
||||
this.panelEl = this.$el.find('.app__panel:first');
|
||||
this.views.listWrap.setElement(this.$el.find('.app__list-wrap')).render();
|
||||
this.views.menu.setElement(this.$el.find('.app__menu')).render();
|
||||
|
@ -158,9 +162,13 @@ const AppView = Backbone.View.extend({
|
|||
this.hideKeyChange();
|
||||
this.views.open = new OpenView({ model: this.model });
|
||||
this.views.open.setElement(this.$el.find('.app__body')).render();
|
||||
this.views.open.on('close', () => {
|
||||
Backbone.trigger('closed-open-view');
|
||||
}, this);
|
||||
this.views.open.on(
|
||||
'close',
|
||||
() => {
|
||||
Backbone.trigger('closed-open-view');
|
||||
},
|
||||
this
|
||||
);
|
||||
this.views.open.on('close', this.showEntries, this);
|
||||
},
|
||||
|
||||
|
@ -181,8 +189,7 @@ const AppView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
updateApp: function() {
|
||||
if (UpdateModel.instance.get('updateStatus') === 'ready' &&
|
||||
!Launcher && !this.model.files.hasOpenFiles()) {
|
||||
if (UpdateModel.instance.get('updateStatus') === 'ready' && !Launcher && !this.model.files.hasOpenFiles()) {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
|
@ -324,7 +331,11 @@ const AppView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
beforeUnload: function(e) {
|
||||
const exitEvent = { preventDefault() { this.prevented = true; } };
|
||||
const exitEvent = {
|
||||
preventDefault() {
|
||||
this.prevented = true;
|
||||
}
|
||||
};
|
||||
Backbone.trigger('main-window-will-close', exitEvent);
|
||||
if (exitEvent.prevented) {
|
||||
Launcher.preventExit(e);
|
||||
|
@ -344,7 +355,11 @@ const AppView = Backbone.View.extend({
|
|||
if (Launcher) {
|
||||
if (!this.exitAlertShown) {
|
||||
if (this.model.settings.get('autoSave')) {
|
||||
this.saveAndLock(result => { if (result) { exit(); } });
|
||||
this.saveAndLock(result => {
|
||||
if (result) {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
return Launcher.preventExit(e);
|
||||
}
|
||||
this.exitAlertShown = true;
|
||||
|
@ -352,13 +367,17 @@ const AppView = Backbone.View.extend({
|
|||
header: Locale.appUnsavedWarn,
|
||||
body: Locale.appUnsavedWarnBody,
|
||||
buttons: [
|
||||
{result: 'save', title: Locale.saveChanges},
|
||||
{result: 'exit', title: Locale.discardChanges, error: true},
|
||||
{result: '', title: Locale.appDontExitBtn}
|
||||
{ result: 'save', title: Locale.saveChanges },
|
||||
{ result: 'exit', title: Locale.discardChanges, error: true },
|
||||
{ result: '', title: Locale.appDontExitBtn }
|
||||
],
|
||||
success: result => {
|
||||
if (result === 'save') {
|
||||
this.saveAndLock(result => { if (result) { exit(); } });
|
||||
this.saveAndLock(result => {
|
||||
if (result) {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
exit();
|
||||
}
|
||||
|
@ -374,8 +393,13 @@ const AppView = Backbone.View.extend({
|
|||
return Launcher.preventExit(e);
|
||||
}
|
||||
return Locale.appUnsavedWarnBody;
|
||||
} else if (Launcher && !Launcher.exitRequested && !Launcher.restartPending &&
|
||||
Launcher.canMinimize() && this.model.settings.get('minimizeOnClose')) {
|
||||
} else if (
|
||||
Launcher &&
|
||||
!Launcher.exitRequested &&
|
||||
!Launcher.restartPending &&
|
||||
Launcher.canMinimize() &&
|
||||
this.model.settings.get('minimizeOnClose')
|
||||
) {
|
||||
Launcher.minimizeApp();
|
||||
return Launcher.preventExit(e);
|
||||
}
|
||||
|
@ -391,11 +415,11 @@ const AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
enterFullScreen: function () {
|
||||
enterFullScreen: function() {
|
||||
this.$el.addClass('fullscreen');
|
||||
},
|
||||
|
||||
leaveFullScreen: function () {
|
||||
leaveFullScreen: function() {
|
||||
this.$el.removeClass('fullscreen');
|
||||
},
|
||||
|
||||
|
@ -480,7 +504,7 @@ const AppView = Backbone.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
handleAutoSaveTimer: function () {
|
||||
handleAutoSaveTimer: function() {
|
||||
if (this.model.settings.get('autoSaveInterval') !== 0) {
|
||||
// trigger periodical auto save
|
||||
if (this.autoSaveTimeoutId) {
|
||||
|
@ -520,10 +544,14 @@ const AppView = Backbone.View.extend({
|
|||
body: alertBody + ' ' + errorFiles.join(', ')
|
||||
});
|
||||
}
|
||||
if (complete) { complete(true); }
|
||||
if (complete) {
|
||||
complete(true);
|
||||
}
|
||||
} else {
|
||||
that.closeAllFilesAndShowFirst();
|
||||
if (complete) { complete(true); }
|
||||
if (complete) {
|
||||
complete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -536,7 +564,11 @@ const AppView = Backbone.View.extend({
|
|||
fileToShow = this.model.fileInfos.getLast();
|
||||
}
|
||||
if (fileToShow) {
|
||||
const fileInfo = this.model.fileInfos.getMatch(fileToShow.get('storage'), fileToShow.get('name'), fileToShow.get('path'));
|
||||
const fileInfo = this.model.fileInfos.getMatch(
|
||||
fileToShow.get('storage'),
|
||||
fileToShow.get('name'),
|
||||
fileToShow.get('path')
|
||||
);
|
||||
if (fileInfo) {
|
||||
this.views.open.showOpenFileInfo(fileInfo);
|
||||
}
|
||||
|
@ -598,13 +630,13 @@ const AppView = Backbone.View.extend({
|
|||
}
|
||||
} else {
|
||||
if (menuItem) {
|
||||
this.model.menu.select({item: menuItem});
|
||||
this.model.menu.select({ item: menuItem });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.showSettings();
|
||||
if (menuItem) {
|
||||
this.model.menu.select({item: menuItem});
|
||||
this.model.menu.select({ item: menuItem });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -690,8 +722,12 @@ const AppView = Backbone.View.extend({
|
|||
showSingleInstanceAlert: function() {
|
||||
this.hideOpenFile();
|
||||
Alerts.error({
|
||||
header: Locale.appTabWarn, body: Locale.appTabWarnBody,
|
||||
esc: false, enter: false, click: false, buttons: []
|
||||
header: Locale.appTabWarn,
|
||||
body: Locale.appTabWarnBody,
|
||||
esc: false,
|
||||
enter: false,
|
||||
click: false,
|
||||
buttons: []
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const AutoTypeHintView = Backbone.View.extend({
|
|||
this.input.addEventListener('blur', this.inputBlur);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
render: function() {
|
||||
this.renderTemplate({
|
||||
cmd: FeatureDetector.isMac ? 'command' : 'ctrl',
|
||||
hasCtrl: FeatureDetector.isMac,
|
||||
|
@ -24,7 +24,9 @@ const AutoTypeHintView = Backbone.View.extend({
|
|||
});
|
||||
const rect = this.input.getBoundingClientRect();
|
||||
this.$el.appendTo(document.body).css({
|
||||
left: rect.left, top: rect.bottom + 1, width: rect.width
|
||||
left: rect.left,
|
||||
top: rect.bottom + 1,
|
||||
width: rect.width
|
||||
});
|
||||
const selfRect = this.$el[0].getBoundingClientRect();
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
|
|
|
@ -58,8 +58,10 @@ const AutoTypePopupView = Backbone.View.extend({
|
|||
render() {
|
||||
let topMessage;
|
||||
if (this.model.filter.title || this.model.filter.url) {
|
||||
topMessage = Locale.autoTypeMsgMatchedByWindow.replace('{}',
|
||||
this.model.filter.title || this.model.filter.url);
|
||||
topMessage = Locale.autoTypeMsgMatchedByWindow.replace(
|
||||
'{}',
|
||||
this.model.filter.title || this.model.filter.url
|
||||
);
|
||||
} else {
|
||||
topMessage = Locale.autoTypeMsgNoWindow;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ const DetailsAddFieldView = Backbone.View.extend({
|
|||
'click .details__field-value': 'fieldValueClick'
|
||||
},
|
||||
|
||||
render: function () {
|
||||
render: function() {
|
||||
this.renderTemplate();
|
||||
this.labelEl = this.$el.find('.details__field-label');
|
||||
return this;
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
|
||||
const Backbone = require('backbone');
|
||||
const FeatureDetector = require('../../util/feature-detector');
|
||||
|
||||
const DetailsAttachmentView = Backbone.View.extend({
|
||||
template: require('templates/details/details-attachment.hbs'),
|
||||
|
||||
events: {
|
||||
},
|
||||
events: {},
|
||||
|
||||
render: function(complete) {
|
||||
this.renderTemplate({}, true);
|
||||
const shortcut = this.$el.find('.details__attachment-preview-download-text-shortcut');
|
||||
shortcut.html(FeatureDetector.actionShortcutSymbol(false));
|
||||
const blob = new Blob([this.model.getBinary()], {type: this.model.mimeType});
|
||||
const blob = new Blob([this.model.getBinary()], { type: this.model.mimeType });
|
||||
const dataEl = this.$el.find('.details__attachment-preview-data');
|
||||
switch ((this.model.mimeType || '').split('/')[0]) {
|
||||
case 'text':
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('loadend', () => {
|
||||
$('<pre/>').text(reader.result).appendTo(dataEl);
|
||||
$('<pre/>')
|
||||
.text(reader.result)
|
||||
.appendTo(dataEl);
|
||||
complete();
|
||||
});
|
||||
reader.readAsText(blob);
|
||||
return this;
|
||||
case 'image':
|
||||
$('<img/>').attr('src', URL.createObjectURL(blob)).appendTo(dataEl);
|
||||
$('<img/>')
|
||||
.attr('src', URL.createObjectURL(blob))
|
||||
.appendTo(dataEl);
|
||||
complete();
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
const Backbone = require('backbone');
|
||||
const AutoTypeHintView = require('../auto-type-hint-view');
|
||||
const Locale = require('../../util/locale');
|
||||
|
@ -57,8 +56,10 @@ const DetailsAutoTypeView = Backbone.View.extend({
|
|||
|
||||
seqFocus: function(e) {
|
||||
if (!this.views.hint) {
|
||||
this.views.hint = new AutoTypeHintView({input: e.target}).render();
|
||||
this.views.hint.on('remove', () => { delete this.views.hint; });
|
||||
this.views.hint = new AutoTypeHintView({ input: e.target }).render();
|
||||
this.views.hint.on('remove', () => {
|
||||
delete this.views.hint;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -22,13 +22,55 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
formats: [
|
||||
{ name: 'ms', round: 1, format: function(d) { return Format.dtStr(d); } },
|
||||
{ name: 'sec', round: 1000, format: function(d) { return Format.dtStr(d); } },
|
||||
{ name: 'min', round: 1000 * 60, format: function(d) { return Format.dtStr(d).replace(':00 ', ' '); } },
|
||||
{ name: 'hour', round: 1000 * 60 * 60, format: function(d) { return Format.dtStr(d).replace(':00', ''); } },
|
||||
{ name: 'day', round: 1000 * 60 * 60 * 24, format: function(d) { return Format.dStr(d); } },
|
||||
{ name: 'month', round: 1000 * 60 * 60 * 24 * 31, format: function(d) { return Format.dStr(d); } },
|
||||
{ name: 'year', round: 1000 * 60 * 60 * 24 * 365, format: function(d) { return d.getFullYear(); } }
|
||||
{
|
||||
name: 'ms',
|
||||
round: 1,
|
||||
format: function(d) {
|
||||
return Format.dtStr(d);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sec',
|
||||
round: 1000,
|
||||
format: function(d) {
|
||||
return Format.dtStr(d);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'min',
|
||||
round: 1000 * 60,
|
||||
format: function(d) {
|
||||
return Format.dtStr(d).replace(':00 ', ' ');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'hour',
|
||||
round: 1000 * 60 * 60,
|
||||
format: function(d) {
|
||||
return Format.dtStr(d).replace(':00', '');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'day',
|
||||
round: 1000 * 60 * 60 * 24,
|
||||
format: function(d) {
|
||||
return Format.dStr(d);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'month',
|
||||
round: 1000 * 60 * 60 * 24 * 31,
|
||||
format: function(d) {
|
||||
return Format.dStr(d);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'year',
|
||||
round: 1000 * 60 * 60 * 24 * 365,
|
||||
format: function(d) {
|
||||
return d.getFullYear();
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
fieldViews: null,
|
||||
|
@ -45,14 +87,16 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
this.timelineEl = this.$el.find('.details__history-timeline');
|
||||
this.bodyEl = this.$el.find('.details__history-body');
|
||||
this.timeline.forEach(function(item, ix) {
|
||||
$('<i/>').addClass('fa fa-circle details__history-timeline-item')
|
||||
.css('left', (item.pos * 100) + '%')
|
||||
$('<i/>')
|
||||
.addClass('fa fa-circle details__history-timeline-item')
|
||||
.css('left', item.pos * 100 + '%')
|
||||
.attr('data-id', ix)
|
||||
.appendTo(this.timelineEl);
|
||||
}, this);
|
||||
this.labels.forEach(function(label) {
|
||||
$('<div/>').addClass('details__history-timeline-label')
|
||||
.css('left', (label.pos * 100) + '%')
|
||||
$('<div/>')
|
||||
.addClass('details__history-timeline-label')
|
||||
.css('left', label.pos * 100 + '%')
|
||||
.text(label.text)
|
||||
.appendTo(this.timelineEl);
|
||||
}, this);
|
||||
|
@ -78,32 +122,97 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
this.activeIx = ix;
|
||||
this.record = this.timeline[ix].rec;
|
||||
this.timelineEl.find('.details__history-timeline-item').removeClass('details__history-timeline-item--active');
|
||||
this.timelineEl.find('.details__history-timeline-item[data-id="' + ix + '"]').addClass('details__history-timeline-item--active');
|
||||
this.timelineEl
|
||||
.find('.details__history-timeline-item[data-id="' + ix + '"]')
|
||||
.addClass('details__history-timeline-item--active');
|
||||
this.removeFieldViews();
|
||||
this.bodyEl.html('');
|
||||
const colorCls = this.record.color ? this.record.color + '-color' : '';
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Rev', title: Locale.detHistoryVersion, value: ix + 1 } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detHistorySaved,
|
||||
value: Format.dtStr(this.record.updated) +
|
||||
(this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') +
|
||||
((ix === this.history.length - 1 && !this.record.unsaved) ? ' (' + Locale.detHistoryCurState + ')' : '') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnlyRaw({ model: { name: '$Title', title: Format.capFirst(Locale.title),
|
||||
value: '<i class="fa fa-' + this.record.icon + ' ' + colorCls + '"></i> ' +
|
||||
_.escape(this.record.title) || '(' + Locale.detHistoryNoTitle + ')' } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$UserName', title: Format.capFirst(Locale.user), value: this.record.user } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Password', title: Format.capFirst(Locale.password), value: this.record.password } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$URL', title: Format.capFirst(Locale.website), value: this.record.url } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Notes', title: Format.capFirst(Locale.notes), value: this.record.notes } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Tags', title: Format.capFirst(Locale.tags),
|
||||
value: this.record.tags.join(', ') } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Expires', title: Locale.detExpires,
|
||||
value: this.record.expires ? Format.dtStr(this.record.expires) : '' } }));
|
||||
_.forEach(this.record.fields, function(value, field) {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$' + field, title: field, value: value } }));
|
||||
}, this);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({ model: { name: 'Rev', title: Locale.detHistoryVersion, value: ix + 1 } })
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Updated',
|
||||
title: Locale.detHistorySaved,
|
||||
value:
|
||||
Format.dtStr(this.record.updated) +
|
||||
(this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') +
|
||||
(ix === this.history.length - 1 && !this.record.unsaved
|
||||
? ' (' + Locale.detHistoryCurState + ')'
|
||||
: '')
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnlyRaw({
|
||||
model: {
|
||||
name: '$Title',
|
||||
title: Format.capFirst(Locale.title),
|
||||
value:
|
||||
'<i class="fa fa-' +
|
||||
this.record.icon +
|
||||
' ' +
|
||||
colorCls +
|
||||
'"></i> ' +
|
||||
_.escape(this.record.title) || '(' + Locale.detHistoryNoTitle + ')'
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: { name: '$UserName', title: Format.capFirst(Locale.user), value: this.record.user }
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: { name: '$Password', title: Format.capFirst(Locale.password), value: this.record.password }
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: { name: '$URL', title: Format.capFirst(Locale.website), value: this.record.url }
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: { name: '$Notes', title: Format.capFirst(Locale.notes), value: this.record.notes }
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: { name: 'Tags', title: Format.capFirst(Locale.tags), value: this.record.tags.join(', ') }
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Expires',
|
||||
title: Locale.detExpires,
|
||||
value: this.record.expires ? Format.dtStr(this.record.expires) : ''
|
||||
}
|
||||
})
|
||||
);
|
||||
_.forEach(
|
||||
this.record.fields,
|
||||
function(value, field) {
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({ model: { name: '$' + field, title: field, value: value } })
|
||||
);
|
||||
},
|
||||
this
|
||||
);
|
||||
if (this.record.attachments.length) {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Attachments', title: Locale.detAttachments,
|
||||
value: this.record.attachments.map(att => att.title).join(', ') } }));
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Attachments',
|
||||
title: Locale.detAttachments,
|
||||
value: this.record.attachments.map(att => att.title).join(', ')
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
this.fieldViews.forEach(function(fieldView) {
|
||||
fieldView.setElement(this.bodyEl).render();
|
||||
|
@ -112,12 +221,15 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
const buttons = this.$el.find('.details__history-buttons');
|
||||
buttons.find('.details__history-button-revert').toggle(ix < this.history.length - 1);
|
||||
buttons.find('.details__history-button-delete').toggle(ix < this.history.length - 1);
|
||||
buttons.find('.details__history-button-discard').toggle(this.record.unsaved && ix === this.history.length - 1 &&
|
||||
this.history.length > 1 || false);
|
||||
buttons
|
||||
.find('.details__history-button-discard')
|
||||
.toggle((this.record.unsaved && ix === this.history.length - 1 && this.history.length > 1) || false);
|
||||
},
|
||||
|
||||
timelineItemClick: function(e) {
|
||||
const id = $(e.target).closest('.details__history-timeline-item').data('id');
|
||||
const id = $(e.target)
|
||||
.closest('.details__history-timeline-item')
|
||||
.data('id');
|
||||
this.showRecord(id);
|
||||
},
|
||||
|
||||
|
@ -142,12 +254,13 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
}));
|
||||
const period = lastRec.updated - firstRec.updated;
|
||||
const format = this.getDateFormat(period);
|
||||
this.labels = this.getLabels(firstRec.updated.getTime(), lastRec.updated.getTime(), format.round)
|
||||
.map(label => ({
|
||||
this.labels = this.getLabels(firstRec.updated.getTime(), lastRec.updated.getTime(), format.round).map(
|
||||
label => ({
|
||||
pos: (label - firstRec.updated) / (lastRec.updated - firstRec.updated),
|
||||
val: label,
|
||||
text: format.format(new Date(label))
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
getDateFormat: function(period) {
|
||||
|
@ -170,7 +283,7 @@ const DetailsHistoryView = Backbone.View.extend({
|
|||
labels.push(label);
|
||||
label += round;
|
||||
}
|
||||
if (labels.length > 1 && (labels[0] - first) / (last - first) < 0.10) {
|
||||
if (labels.length > 1 && (labels[0] - first) / (last - first) < 0.1) {
|
||||
labels.shift();
|
||||
}
|
||||
return labels;
|
||||
|
|
|
@ -61,7 +61,7 @@ const DetailsView = Backbone.View.extend({
|
|||
'contextmenu .details': 'contextMenu'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.fieldViews = [];
|
||||
this.views = {};
|
||||
this.initScroll();
|
||||
|
@ -100,7 +100,7 @@ const DetailsView = Backbone.View.extend({
|
|||
this.hideFieldCopyTip();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
render: function() {
|
||||
this.removeScroll();
|
||||
this.removeFieldViews();
|
||||
this.removeInnerViews();
|
||||
|
@ -140,45 +140,175 @@ const DetailsView = Backbone.View.extend({
|
|||
const fileNames = this.appModel.files.map(function(file) {
|
||||
return { id: file.id, value: file.get('name'), selected: file === this.model.file };
|
||||
}, this);
|
||||
this.fileEditView = new FieldViewSelect({ model: { name: '$File', title: Format.capFirst(Locale.file),
|
||||
value: function() { return fileNames; } } });
|
||||
this.fileEditView = new FieldViewSelect({
|
||||
model: {
|
||||
name: '$File',
|
||||
title: Format.capFirst(Locale.file),
|
||||
value: function() {
|
||||
return fileNames;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.fieldViews.push(this.fileEditView);
|
||||
} else {
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Format.capFirst(Locale.file),
|
||||
value: function() { return model.fileName; } } }));
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'File',
|
||||
title: Format.capFirst(Locale.file),
|
||||
value: function() {
|
||||
return model.fileName;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
this.userEditView = new FieldViewAutocomplete({ model: { name: '$UserName', title: Format.capFirst(Locale.user),
|
||||
value: function() { return model.user; }, getCompletions: this.getUserNameCompletions.bind(this) } });
|
||||
this.fieldViews.push(this.userEditView);
|
||||
this.passEditView = new FieldViewText({ model: { name: '$Password', title: Format.capFirst(Locale.password), canGen: true,
|
||||
value: function() { return model.password; } } });
|
||||
this.fieldViews.push(this.passEditView);
|
||||
this.urlEditView = new FieldViewUrl({ model: { name: '$URL', title: Format.capFirst(Locale.website),
|
||||
value: function() { return model.url; } } });
|
||||
this.fieldViews.push(this.urlEditView);
|
||||
this.fieldViews.push(new FieldViewText({ model: { name: '$Notes', title: Format.capFirst(Locale.notes), multiline: 'true',
|
||||
value: function() { return model.notes; } } }));
|
||||
this.fieldViews.push(new FieldViewTags({ model: { name: 'Tags', title: Format.capFirst(Locale.tags), tags: this.appModel.tags,
|
||||
value: function() { return model.tags; } } }));
|
||||
this.fieldViews.push(new FieldViewDate({ model: { name: 'Expires', title: Locale.detExpires, lessThanNow: '(' + Locale.detExpired + ')',
|
||||
value: function() { return model.expires; } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Group', title: Locale.detGroup,
|
||||
value: function() { return model.groupName; }, tip: function() { return model.getGroupPath().join(' / '); } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Created', title: Locale.detCreated,
|
||||
value: function() { return Format.dtStr(model.created); } } }));
|
||||
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detUpdated,
|
||||
value: function() { return Format.dtStr(model.updated); } } }));
|
||||
this.fieldViews.push(new FieldViewHistory({ model: { name: 'History', title: Format.capFirst(Locale.history),
|
||||
value: function() { return { length: model.historyLength, unsaved: model.unsaved }; } } }));
|
||||
_.forEach(model.fields, function(value, field) {
|
||||
if (field === 'otp' && this.model.otpGenerator) {
|
||||
this.fieldViews.push(new FieldViewOtp({ model: { name: '$' + field, title: field,
|
||||
value: function() { return model.otpGenerator; } } }));
|
||||
} else {
|
||||
this.fieldViews.push(new FieldViewCustom({ model: { name: '$' + field, title: field,
|
||||
value: function() { return model.fields[field]; } } }));
|
||||
this.userEditView = new FieldViewAutocomplete({
|
||||
model: {
|
||||
name: '$UserName',
|
||||
title: Format.capFirst(Locale.user),
|
||||
value: function() {
|
||||
return model.user;
|
||||
},
|
||||
getCompletions: this.getUserNameCompletions.bind(this)
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
this.fieldViews.push(this.userEditView);
|
||||
this.passEditView = new FieldViewText({
|
||||
model: {
|
||||
name: '$Password',
|
||||
title: Format.capFirst(Locale.password),
|
||||
canGen: true,
|
||||
value: function() {
|
||||
return model.password;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.fieldViews.push(this.passEditView);
|
||||
this.urlEditView = new FieldViewUrl({
|
||||
model: {
|
||||
name: '$URL',
|
||||
title: Format.capFirst(Locale.website),
|
||||
value: function() {
|
||||
return model.url;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.fieldViews.push(this.urlEditView);
|
||||
this.fieldViews.push(
|
||||
new FieldViewText({
|
||||
model: {
|
||||
name: '$Notes',
|
||||
title: Format.capFirst(Locale.notes),
|
||||
multiline: 'true',
|
||||
value: function() {
|
||||
return model.notes;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewTags({
|
||||
model: {
|
||||
name: 'Tags',
|
||||
title: Format.capFirst(Locale.tags),
|
||||
tags: this.appModel.tags,
|
||||
value: function() {
|
||||
return model.tags;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewDate({
|
||||
model: {
|
||||
name: 'Expires',
|
||||
title: Locale.detExpires,
|
||||
lessThanNow: '(' + Locale.detExpired + ')',
|
||||
value: function() {
|
||||
return model.expires;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Group',
|
||||
title: Locale.detGroup,
|
||||
value: function() {
|
||||
return model.groupName;
|
||||
},
|
||||
tip: function() {
|
||||
return model.getGroupPath().join(' / ');
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Created',
|
||||
title: Locale.detCreated,
|
||||
value: function() {
|
||||
return Format.dtStr(model.created);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewReadOnly({
|
||||
model: {
|
||||
name: 'Updated',
|
||||
title: Locale.detUpdated,
|
||||
value: function() {
|
||||
return Format.dtStr(model.updated);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.fieldViews.push(
|
||||
new FieldViewHistory({
|
||||
model: {
|
||||
name: 'History',
|
||||
title: Format.capFirst(Locale.history),
|
||||
value: function() {
|
||||
return { length: model.historyLength, unsaved: model.unsaved };
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
_.forEach(
|
||||
model.fields,
|
||||
function(value, field) {
|
||||
if (field === 'otp' && this.model.otpGenerator) {
|
||||
this.fieldViews.push(
|
||||
new FieldViewOtp({
|
||||
model: {
|
||||
name: '$' + field,
|
||||
title: field,
|
||||
value: function() {
|
||||
return model.otpGenerator;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.fieldViews.push(
|
||||
new FieldViewCustom({
|
||||
model: {
|
||||
name: '$' + field,
|
||||
title: field,
|
||||
value: function() {
|
||||
return model.fields[field];
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
const hideEmptyFields = AppSettingsModel.instance.get('hideEmptyFields');
|
||||
|
||||
|
@ -218,8 +348,16 @@ const DetailsView = Backbone.View.extend({
|
|||
}
|
||||
}
|
||||
}
|
||||
const fieldView = new FieldViewCustom({ model: { name: '$' + newFieldTitle, title: newFieldTitle, newField: newFieldTitle,
|
||||
value: function() { return ''; } } });
|
||||
const fieldView = new FieldViewCustom({
|
||||
model: {
|
||||
name: '$' + newFieldTitle,
|
||||
title: newFieldTitle,
|
||||
newField: newFieldTitle,
|
||||
value: function() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
});
|
||||
fieldView.on('change', this.fieldChanged.bind(this));
|
||||
fieldView.setElement(this.$el.find('.details__body-fields')).render();
|
||||
fieldView.edit();
|
||||
|
@ -240,24 +378,27 @@ const DetailsView = Backbone.View.extend({
|
|||
if (hideEmptyFields) {
|
||||
this.fieldViews.forEach(fieldView => {
|
||||
if (fieldView.isHidden()) {
|
||||
moreOptions.push({value: 'add:' + fieldView.model.name, icon: 'pencil',
|
||||
text: Locale.detMenuAddField.replace('{}', fieldView.model.title)});
|
||||
moreOptions.push({
|
||||
value: 'add:' + fieldView.model.name,
|
||||
icon: 'pencil',
|
||||
text: Locale.detMenuAddField.replace('{}', fieldView.model.title)
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField});
|
||||
moreOptions.push({value: 'toggle-empty', icon: 'eye', text: Locale.detMenuShowEmpty});
|
||||
moreOptions.push({ value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField });
|
||||
moreOptions.push({ value: 'toggle-empty', icon: 'eye', text: Locale.detMenuShowEmpty });
|
||||
} else {
|
||||
moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField});
|
||||
moreOptions.push({value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty});
|
||||
moreOptions.push({ value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField });
|
||||
moreOptions.push({ value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty });
|
||||
}
|
||||
moreOptions.push({value: 'otp', icon: 'clock-o', text: Locale.detSetupOtp});
|
||||
moreOptions.push({ value: 'otp', icon: 'clock-o', text: Locale.detSetupOtp });
|
||||
if (AutoType.enabled) {
|
||||
moreOptions.push({value: 'auto-type', icon: 'keyboard-o', text: Locale.detAutoTypeSettings});
|
||||
moreOptions.push({ value: 'auto-type', icon: 'keyboard-o', text: Locale.detAutoTypeSettings });
|
||||
}
|
||||
moreOptions.push({value: 'clone', icon: 'clone', text: Locale.detClone});
|
||||
moreOptions.push({ value: 'clone', icon: 'clone', text: Locale.detClone });
|
||||
const rect = this.moreView.labelEl[0].getBoundingClientRect();
|
||||
dropdownView.render({
|
||||
position: {top: rect.bottom, left: rect.left},
|
||||
position: { top: rect.bottom, left: rect.left },
|
||||
options: moreOptions
|
||||
});
|
||||
this.views.dropdownView = dropdownView;
|
||||
|
@ -301,7 +442,9 @@ const DetailsView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
setSelectedColor: function(color) {
|
||||
this.$el.find('.details__colors-popup > .details__colors-popup-item').removeClass('details__colors-popup-item--active');
|
||||
this.$el
|
||||
.find('.details__colors-popup > .details__colors-popup-item')
|
||||
.removeClass('details__colors-popup-item--active');
|
||||
const colorEl = this.$el.find('.details__header-color')[0];
|
||||
_.forEach(colorEl.classList, cls => {
|
||||
if (cls.indexOf('color') > 0 && cls.lastIndexOf('details', 0) !== 0) {
|
||||
|
@ -309,13 +452,17 @@ const DetailsView = Backbone.View.extend({
|
|||
}
|
||||
});
|
||||
if (color) {
|
||||
this.$el.find('.details__colors-popup > .' + color + '-color').addClass('details__colors-popup-item--active');
|
||||
this.$el
|
||||
.find('.details__colors-popup > .' + color + '-color')
|
||||
.addClass('details__colors-popup-item--active');
|
||||
colorEl.classList.add(color + '-color');
|
||||
}
|
||||
},
|
||||
|
||||
selectColor: function(e) {
|
||||
let color = $(e.target).closest('.details__colors-popup-item').data('color');
|
||||
let color = $(e.target)
|
||||
.closest('.details__colors-popup-item')
|
||||
.data('color');
|
||||
if (!color) {
|
||||
return;
|
||||
}
|
||||
|
@ -336,7 +483,8 @@ const DetailsView = Backbone.View.extend({
|
|||
el: this.scroller,
|
||||
model: {
|
||||
iconId: this.model.customIconId || this.model.iconId,
|
||||
url: this.model.url, file: this.model.file
|
||||
url: this.model.url,
|
||||
file: this.model.file
|
||||
}
|
||||
});
|
||||
this.listenTo(subView, 'select', this.iconSelected);
|
||||
|
@ -379,7 +527,7 @@ const DetailsView = Backbone.View.extend({
|
|||
return;
|
||||
}
|
||||
const mimeType = attachment.mimeType || 'application/octet-stream';
|
||||
const blob = new Blob([data], {type: mimeType});
|
||||
const blob = new Blob([data], { type: mimeType });
|
||||
FileSaver.saveAs(blob, attachment.title);
|
||||
},
|
||||
|
||||
|
@ -408,7 +556,9 @@ const DetailsView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
copyKeyPress: function(editView) {
|
||||
if (this.isHidden()) { return; }
|
||||
if (this.isHidden()) {
|
||||
return;
|
||||
}
|
||||
if (!window.getSelection().toString()) {
|
||||
const fieldValue = editView.value;
|
||||
const fieldText = fieldValue && fieldValue.isProtected ? fieldValue.getText() : fieldValue;
|
||||
|
@ -449,7 +599,9 @@ const DetailsView = Backbone.View.extend({
|
|||
const tip = new Tip(label, { title: Locale.detCopyHint, placement: 'right' });
|
||||
tip.show();
|
||||
this.fieldCopyTip = tip;
|
||||
setTimeout(() => { tip.hide(); }, Timeouts.AutoHideHint);
|
||||
setTimeout(() => {
|
||||
tip.hide();
|
||||
}, Timeouts.AutoHideHint);
|
||||
},
|
||||
|
||||
settingsToggled: function() {
|
||||
|
@ -500,8 +652,11 @@ const DetailsView = Backbone.View.extend({
|
|||
}
|
||||
this.entryUpdated(true);
|
||||
this.fieldViews.forEach(function(fieldView, ix) {
|
||||
if (fieldView instanceof FieldViewCustom && !fieldView.model.newField &&
|
||||
!this.model.hasField(fieldView.model.title)) {
|
||||
if (
|
||||
fieldView instanceof FieldViewCustom &&
|
||||
!fieldView.model.newField &&
|
||||
!this.model.hasField(fieldView.model.title)
|
||||
) {
|
||||
fieldView.remove();
|
||||
this.fieldViews.splice(ix, 1);
|
||||
} else {
|
||||
|
@ -584,13 +739,17 @@ const DetailsView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
addAttachedFiles: function(files) {
|
||||
_.forEach(files, function(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.addAttachment(file.name, reader.result);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}, this);
|
||||
_.forEach(
|
||||
files,
|
||||
function(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.addAttachment(file.name, reader.result);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
|
||||
addAttachment: function(name, data) {
|
||||
|
@ -676,7 +835,8 @@ const DetailsView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
focusNextField: function(config) {
|
||||
let found = false, nextFieldView;
|
||||
let found = false,
|
||||
nextFieldView;
|
||||
if (config.field === '$Title' && !config.prev) {
|
||||
found = true;
|
||||
}
|
||||
|
@ -793,10 +953,14 @@ const DetailsView = Backbone.View.extend({
|
|||
} else {
|
||||
this.moreView.remove();
|
||||
this.moreView = null;
|
||||
const fieldView = new FieldViewCustom({ model: {
|
||||
name: '$otp', title: 'otp', newField: 'otp',
|
||||
value: kdbxweb.ProtectedValue.fromString('')
|
||||
}});
|
||||
const fieldView = new FieldViewCustom({
|
||||
model: {
|
||||
name: '$otp',
|
||||
title: 'otp',
|
||||
newField: 'otp',
|
||||
value: kdbxweb.ProtectedValue.fromString('')
|
||||
}
|
||||
});
|
||||
fieldView.on('change', this.fieldChanged.bind(this));
|
||||
fieldView.setElement(this.$el.find('.details__body-fields')).render();
|
||||
fieldView.edit();
|
||||
|
|
|
@ -5,7 +5,7 @@ const DragView = Backbone.View.extend({
|
|||
'mousedown': 'mousedown'
|
||||
},
|
||||
|
||||
initialize: function (coord) {
|
||||
initialize: function(coord) {
|
||||
this.setCoord(coord);
|
||||
this.mouseDownTime = -1;
|
||||
this.mouseDownCount = 0;
|
||||
|
@ -17,7 +17,9 @@ const DragView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
$('<div/>').addClass('drag-handle__inner').appendTo(this.$el);
|
||||
$('<div/>')
|
||||
.addClass('drag-handle__inner')
|
||||
.appendTo(this.$el);
|
||||
},
|
||||
|
||||
mousedown: function(e) {
|
||||
|
@ -35,7 +37,9 @@ const DragView = Backbone.View.extend({
|
|||
}
|
||||
this.initialOffset = e[this.offsetProp];
|
||||
const cursor = this.$el.css('cursor');
|
||||
this.dragMask = $('<div/>', {'class': 'drag-mask'}).css('cursor', cursor).appendTo('body');
|
||||
this.dragMask = $('<div/>', { 'class': 'drag-mask' })
|
||||
.css('cursor', cursor)
|
||||
.appendTo('body');
|
||||
this.dragMask.on('mousemove', this.mousemove.bind(this));
|
||||
this.dragMask.on('mouseup', this.mouseup.bind(this));
|
||||
this.trigger('dragstart', { offset: this.initialOffset, coord: this.coord });
|
||||
|
|
|
@ -7,19 +7,19 @@ const DropdownView = Backbone.View.extend({
|
|||
'click .dropdown__item': 'itemClick'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.bodyClick = this.bodyClick.bind(this);
|
||||
this.listenTo(Backbone, 'show-context-menu', this.bodyClick);
|
||||
$('body').on('click contextmenu keyup', this.bodyClick);
|
||||
},
|
||||
|
||||
render: function (config) {
|
||||
render: function(config) {
|
||||
this.options = config.options;
|
||||
this.renderTemplate(config);
|
||||
this.$el.appendTo(document.body);
|
||||
const ownRect = this.$el[0].getBoundingClientRect();
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
let left = config.position.left || (config.position.right - ownRect.right + ownRect.left);
|
||||
let left = config.position.left || config.position.right - ownRect.right + ownRect.left;
|
||||
let top = config.position.top;
|
||||
if (left + ownRect.width > bodyRect.right) {
|
||||
left = Math.max(0, bodyRect.right - ownRect.width);
|
||||
|
|
|
@ -61,7 +61,8 @@ const FieldViewAutocomplete = FieldViewText.extend({
|
|||
moveAutocomplete: function(next) {
|
||||
const completions = this.model.getCompletions(this.input.val());
|
||||
if (typeof this.selectedCopmletionIx === 'number') {
|
||||
this.selectedCopmletionIx = (completions.length + this.selectedCopmletionIx + (next ? 1 : -1)) % completions.length;
|
||||
this.selectedCopmletionIx =
|
||||
(completions.length + this.selectedCopmletionIx + (next ? 1 : -1)) % completions.length;
|
||||
} else {
|
||||
this.selectedCopmletionIx = next ? 0 : completions.length - 1;
|
||||
}
|
||||
|
@ -70,10 +71,12 @@ const FieldViewAutocomplete = FieldViewText.extend({
|
|||
|
||||
updateAutocomplete: function() {
|
||||
const completions = this.model.getCompletions(this.input.val());
|
||||
const completionsHtml = completions.map((item, ix) => {
|
||||
const sel = ix === this.selectedCopmletionIx ? 'details__field-autocomplete-item--selected' : '';
|
||||
return '<div class="details__field-autocomplete-item ' + sel + '">' + _.escape(item) + '</div>';
|
||||
}).join('');
|
||||
const completionsHtml = completions
|
||||
.map((item, ix) => {
|
||||
const sel = ix === this.selectedCopmletionIx ? 'details__field-autocomplete-item--selected' : '';
|
||||
return '<div class="details__field-autocomplete-item ' + sel + '">' + _.escape(item) + '</div>';
|
||||
})
|
||||
.join('');
|
||||
this.autocomplete.html(completionsHtml);
|
||||
this.autocomplete.toggle(!!completionsHtml);
|
||||
},
|
||||
|
@ -85,7 +88,9 @@ const FieldViewAutocomplete = FieldViewText.extend({
|
|||
this.input.val(selectedItem);
|
||||
this.endEdit(selectedItem);
|
||||
} else {
|
||||
this.afterPaint(function () { this.input.focus(); });
|
||||
this.afterPaint(function() {
|
||||
this.input.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,7 +22,8 @@ const FieldViewCustom = FieldViewText.extend({
|
|||
this.isProtected = this.value instanceof kdbxweb.ProtectedValue;
|
||||
}
|
||||
this.$el.toggleClass('details__field--protected', this.isProtected);
|
||||
$('<div/>').addClass('details__field-value-btn details__field-value-btn-protect')
|
||||
$('<div/>')
|
||||
.addClass('details__field-value-btn details__field-value-btn-protect')
|
||||
.appendTo(this.valueEl)
|
||||
.mousedown(this.protectBtnClick.bind(this));
|
||||
let securityTipTitle = Locale.detLockField;
|
||||
|
@ -63,8 +64,11 @@ const FieldViewCustom = FieldViewText.extend({
|
|||
const text = emptyTitle ? '' : this.model.title || '';
|
||||
this.labelInput = $('<input/>');
|
||||
this.labelEl.html('').append(this.labelInput);
|
||||
this.labelInput.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text).focus()[0].setSelectionRange(text.length, text.length);
|
||||
this.labelInput
|
||||
.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text)
|
||||
.focus()[0]
|
||||
.setSelectionRange(text.length, text.length);
|
||||
this.labelInput.bind({
|
||||
input: this.fieldLabelInput.bind(this),
|
||||
keydown: this.fieldLabelKeydown.bind(this),
|
||||
|
@ -152,7 +156,9 @@ const FieldViewCustom = FieldViewText.extend({
|
|||
if (this.labelInput) {
|
||||
this.endEditTitle(this.labelInput.val());
|
||||
}
|
||||
this.setTimeout(function() { this.input.focus(); });
|
||||
this.setTimeout(function() {
|
||||
this.input.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -44,7 +44,9 @@ const FieldViewDate = FieldViewText.extend({
|
|||
|
||||
endEdit: function(newVal, extra) {
|
||||
if (this.picker) {
|
||||
try { this.picker.destroy(); } catch (e) {}
|
||||
try {
|
||||
this.picker.destroy();
|
||||
} catch (e) {}
|
||||
this.picker = null;
|
||||
}
|
||||
newVal = new Date(newVal);
|
||||
|
|
|
@ -4,13 +4,24 @@ const FieldViewSelect = FieldView.extend({
|
|||
readonly: true,
|
||||
|
||||
renderValue: function(value) {
|
||||
return '<select>' +
|
||||
value.map(opt => {
|
||||
return '<option ' + 'value="' + _.escape(opt.id) + '" ' + (opt.selected ? 'selected ' : '') + '>' +
|
||||
_.escape(opt.value) +
|
||||
'</option>';
|
||||
}).join('') +
|
||||
'</select>';
|
||||
return (
|
||||
'<select>' +
|
||||
value
|
||||
.map(opt => {
|
||||
return (
|
||||
'<option ' +
|
||||
'value="' +
|
||||
_.escape(opt.id) +
|
||||
'" ' +
|
||||
(opt.selected ? 'selected ' : '') +
|
||||
'>' +
|
||||
_.escape(opt.value) +
|
||||
'</option>'
|
||||
);
|
||||
})
|
||||
.join('') +
|
||||
'</select>'
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -14,9 +14,14 @@ const FieldViewTags = FieldViewText.extend({
|
|||
this.model.tags.forEach(tag => {
|
||||
allTags[tag.toLowerCase()] = tag;
|
||||
});
|
||||
return _.unique(val.split(/\s*[;,:]\s*/).filter(_.identity).map(tag => {
|
||||
return allTags[tag.toLowerCase()] || tag;
|
||||
}));
|
||||
return _.unique(
|
||||
val
|
||||
.split(/\s*[;,:]\s*/)
|
||||
.filter(_.identity)
|
||||
.map(tag => {
|
||||
return allTags[tag.toLowerCase()] || tag;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
endEdit: function(newVal, extra) {
|
||||
|
@ -60,9 +65,11 @@ const FieldViewTags = FieldViewText.extend({
|
|||
|
||||
setTags: function() {
|
||||
const availableTags = this.getAvailableTags();
|
||||
const tagsHtml = availableTags.map(tag => {
|
||||
return '<div class="details__field-autocomplete-item">' + _.escape(tag) + '</div>';
|
||||
}).join('');
|
||||
const tagsHtml = availableTags
|
||||
.map(tag => {
|
||||
return '<div class="details__field-autocomplete-item">' + _.escape(tag) + '</div>';
|
||||
})
|
||||
.join('');
|
||||
this.tagsAutocomplete.html(tagsHtml);
|
||||
this.tagsAutocomplete.toggle(!!tagsHtml);
|
||||
},
|
||||
|
@ -88,7 +95,9 @@ const FieldViewTags = FieldViewText.extend({
|
|||
this.input.focus();
|
||||
this.setTags();
|
||||
}
|
||||
this.afterPaint(function() { this.input.focus(); });
|
||||
this.afterPaint(function() {
|
||||
this.input.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ const Tip = require('../../util/tip');
|
|||
|
||||
const FieldViewText = FieldView.extend({
|
||||
renderValue: function(value) {
|
||||
return value && value.isProtected ? PasswordGenerator.present(value.textLength)
|
||||
return value && value.isProtected
|
||||
? PasswordGenerator.present(value.textLength)
|
||||
: _.escape(value || '').replace(/\n/g, '<br/>');
|
||||
},
|
||||
|
||||
|
@ -24,8 +25,11 @@ const FieldViewText = FieldView.extend({
|
|||
this.$el.toggleClass('details__field--protected', isProtected);
|
||||
this.input = $(document.createElement(this.model.multiline ? 'textarea' : 'input'));
|
||||
this.valueEl.html('').append(this.input);
|
||||
this.input.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text).focus()[0].setSelectionRange(text.length, text.length);
|
||||
this.input
|
||||
.attr({ autocomplete: 'off', spellcheck: 'false' })
|
||||
.val(text)
|
||||
.focus()[0]
|
||||
.setSelectionRange(text.length, text.length);
|
||||
this.input.bind({
|
||||
input: this.fieldValueInput.bind(this),
|
||||
keydown: this.fieldValueKeydown.bind(this),
|
||||
|
@ -42,7 +46,9 @@ const FieldViewText = FieldView.extend({
|
|||
this.createMobileControls();
|
||||
}
|
||||
if (this.model.canGen) {
|
||||
$('<div/>').addClass('details__field-value-btn details__field-value-btn-gen').appendTo(this.valueEl)
|
||||
$('<div/>')
|
||||
.addClass('details__field-value-btn details__field-value-btn-gen')
|
||||
.appendTo(this.valueEl)
|
||||
.click(this.showGeneratorClick.bind(this))
|
||||
.mousedown(this.showGenerator.bind(this));
|
||||
}
|
||||
|
@ -78,7 +84,9 @@ const FieldViewText = FieldView.extend({
|
|||
this.hideGenerator();
|
||||
} else {
|
||||
const fieldRect = this.input[0].getBoundingClientRect();
|
||||
this.gen = new GeneratorView({model: {pos: {left: fieldRect.left, top: fieldRect.bottom}, password: this.value}}).render();
|
||||
this.gen = new GeneratorView({
|
||||
model: { pos: { left: fieldRect.left, top: fieldRect.bottom }, password: this.value }
|
||||
}).render();
|
||||
this.gen.once('remove', this.generatorClosed.bind(this));
|
||||
this.gen.once('result', this.generatorResult.bind(this));
|
||||
}
|
||||
|
@ -231,8 +239,11 @@ const FieldViewText = FieldView.extend({
|
|||
mobileFieldControlTouchMove(e) {
|
||||
const touch = e.originalEvent.targetTouches[0];
|
||||
const rect = touch.target.getBoundingClientRect();
|
||||
const inside = touch.clientX >= rect.left && touch.clientX <= rect.right &&
|
||||
touch.clientY >= rect.top && touch.clientY <= rect.bottom;
|
||||
const inside =
|
||||
touch.clientX >= rect.left &&
|
||||
touch.clientX <= rect.right &&
|
||||
touch.clientY >= rect.top &&
|
||||
touch.clientY <= rect.bottom;
|
||||
if (inside) {
|
||||
this.$el.attr('active-mobile-action', $(e.target).data('action'));
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,13 @@ const FieldViewUrl = FieldViewText.extend({
|
|||
displayUrlRegex: /^http:\/\//i,
|
||||
|
||||
renderValue: function(value) {
|
||||
return value ? '<a href="' + _.escape(this.fixUrl(value)) + '" rel="noreferrer noopener" target="_blank">' + _.escape(this.displayUrl(value)) + '</a>' : '';
|
||||
return value
|
||||
? '<a href="' +
|
||||
_.escape(this.fixUrl(value)) +
|
||||
'" rel="noreferrer noopener" target="_blank">' +
|
||||
_.escape(this.displayUrl(value)) +
|
||||
'</a>'
|
||||
: '';
|
||||
},
|
||||
|
||||
fixUrl: function(url) {
|
||||
|
|
|
@ -13,8 +13,13 @@ const FieldView = Backbone.View.extend({
|
|||
|
||||
render: function() {
|
||||
this.value = typeof this.model.value === 'function' ? this.model.value() : this.model.value;
|
||||
this.renderTemplate({ editable: !this.readonly, multiline: this.model.multiline, title: this.model.title,
|
||||
canEditTitle: this.model.newField, protect: this.value && this.value.isProtected });
|
||||
this.renderTemplate({
|
||||
editable: !this.readonly,
|
||||
multiline: this.model.multiline,
|
||||
title: this.model.title,
|
||||
canEditTitle: this.model.newField,
|
||||
protect: this.value && this.value.isProtected
|
||||
});
|
||||
this.valueEl = this.$el.find('.details__field-value');
|
||||
this.valueEl.html(this.renderValue(this.value));
|
||||
this.labelEl = this.$el.find('.details__field-label');
|
||||
|
@ -38,7 +43,10 @@ const FieldView = Backbone.View.extend({
|
|||
update: function() {
|
||||
if (typeof this.model.value === 'function') {
|
||||
const newVal = this.model.value();
|
||||
if (!_.isEqual(newVal, this.value) || (this.value && newVal && this.value.toString() !== newVal.toString())) {
|
||||
if (
|
||||
!_.isEqual(newVal, this.value) ||
|
||||
(this.value && newVal && this.value.toString() !== newVal.toString())
|
||||
) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +129,9 @@ const FieldView = Backbone.View.extend({
|
|||
return;
|
||||
}
|
||||
this.editing = false;
|
||||
setTimeout(() => { this.preventCopy = false; }, 300);
|
||||
setTimeout(() => {
|
||||
this.preventCopy = false;
|
||||
}, 300);
|
||||
let textEqual;
|
||||
if (this.value && this.value.isProtected) {
|
||||
textEqual = this.value.equals(newVal);
|
||||
|
|
|
@ -16,7 +16,7 @@ const FooterView = Backbone.View.extend({
|
|||
'click .footer__btn-lock': 'lockWorkspace'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.views = {};
|
||||
|
||||
KeyHandler.onKey(Keys.DOM_VK_L, this.lockWorkspace, this, KeyHandler.SHORTCUT_ACTION, false, true);
|
||||
|
@ -31,11 +31,14 @@ const FooterView = Backbone.View.extend({
|
|||
this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.renderTemplate({
|
||||
files: this.model.files,
|
||||
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
||||
}, { plain: true });
|
||||
render: function() {
|
||||
this.renderTemplate(
|
||||
{
|
||||
files: this.model.files,
|
||||
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
||||
},
|
||||
{ plain: true }
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
|
@ -65,12 +68,16 @@ const FooterView = Backbone.View.extend({
|
|||
const right = bodyRect.right - rect.right;
|
||||
const bottom = bodyRect.bottom - rect.top;
|
||||
const generator = new GeneratorView({ model: { copy: true, pos: { right: right, bottom: bottom } } }).render();
|
||||
generator.once('remove', () => { delete this.views.gen; });
|
||||
generator.once('remove', () => {
|
||||
delete this.views.gen;
|
||||
});
|
||||
this.views.gen = generator;
|
||||
},
|
||||
|
||||
showFile: function(e) {
|
||||
const fileId = $(e.target).closest('.footer__db-item').data('file-id');
|
||||
const fileId = $(e.target)
|
||||
.closest('.footer__db-item')
|
||||
.data('file-id');
|
||||
if (fileId) {
|
||||
Backbone.trigger('show-file', { fileId: fileId });
|
||||
}
|
||||
|
|
|
@ -33,11 +33,14 @@ const GeneratorPresetsView = Backbone.View.extend({
|
|||
if (!this.selected || !this.presets.some(p => p.name === this.selected)) {
|
||||
this.selected = (this.presets.filter(p => p.default)[0] || this.presets[0]).name;
|
||||
}
|
||||
this.renderTemplate({
|
||||
presets: this.presets,
|
||||
selected: this.getPreset(this.selected),
|
||||
ranges: this.getSelectedRanges()
|
||||
}, true);
|
||||
this.renderTemplate(
|
||||
{
|
||||
presets: this.presets,
|
||||
selected: this.getPreset(this.selected),
|
||||
ranges: this.getSelectedRanges()
|
||||
},
|
||||
true
|
||||
);
|
||||
this.createScroll({
|
||||
root: this.$el.find('.gen-ps')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
|
@ -97,10 +100,15 @@ const GeneratorPresetsView = Backbone.View.extend({
|
|||
}
|
||||
const selected = this.getPreset(this.selected);
|
||||
const preset = {
|
||||
name, title,
|
||||
name,
|
||||
title,
|
||||
length: selected.length,
|
||||
upper: selected.upper, lower: selected.lower, digits: selected.digits,
|
||||
special: selected.special, brackets: selected.brackets, ambiguous: selected.ambiguous,
|
||||
upper: selected.upper,
|
||||
lower: selected.lower,
|
||||
digits: selected.digits,
|
||||
special: selected.special,
|
||||
brackets: selected.brackets,
|
||||
ambiguous: selected.ambiguous,
|
||||
include: selected.include
|
||||
};
|
||||
GeneratorPresets.add(preset);
|
||||
|
|
|
@ -29,7 +29,7 @@ const GeneratorView = Backbone.View.extend({
|
|||
presets: null,
|
||||
preset: null,
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.createPresets();
|
||||
const preset = this.preset;
|
||||
this.gen = _.clone(_.find(this.presets, pr => pr.name === preset));
|
||||
|
@ -40,7 +40,7 @@ const GeneratorView = Backbone.View.extend({
|
|||
|
||||
render: function() {
|
||||
const canCopy = document.queryCommandSupported('copy');
|
||||
const btnTitle = this.model.copy ? canCopy ? Locale.alertCopy : Locale.alertClose : Locale.alertOk;
|
||||
const btnTitle = this.model.copy ? (canCopy ? Locale.alertCopy : Locale.alertClose) : Locale.alertOk;
|
||||
this.renderTemplate({
|
||||
btnTitle: btnTitle,
|
||||
showToggleButton: this.model.copy,
|
||||
|
@ -112,8 +112,10 @@ const GeneratorView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
optionChanged: function(option) {
|
||||
if (this.preset === 'Custom' ||
|
||||
this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0) {
|
||||
if (
|
||||
this.preset === 'Custom' ||
|
||||
(this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.preset = this.gen.name = 'Custom';
|
||||
|
@ -132,7 +134,7 @@ const GeneratorView = Backbone.View.extend({
|
|||
// AppSettingsModel.instance.unset('generatorHidePassword', { silent: true });
|
||||
AppSettingsModel.instance.set('generatorHidePassword', this.hide);
|
||||
const label = this.$el.find('.gen__check-hide-label');
|
||||
Tip.updateTip(label[0], {title: this.hide ? Locale.genShowPass : Locale.genHidePass});
|
||||
Tip.updateTip(label[0], { title: this.hide ? Locale.genShowPass : Locale.genHidePass });
|
||||
this.showPassword();
|
||||
},
|
||||
|
||||
|
|
|
@ -25,17 +25,20 @@ const GrpView = Backbone.View.extend({
|
|||
render: function() {
|
||||
this.removeSubView();
|
||||
if (this.model) {
|
||||
this.renderTemplate({
|
||||
title: this.model.get('title'),
|
||||
icon: this.model.get('icon') || 'folder',
|
||||
customIcon: this.model.get('customIcon'),
|
||||
enableSearching: this.model.getEffectiveEnableSearching(),
|
||||
readonly: this.model.get('top'),
|
||||
canAutoType: AutoType.enabled,
|
||||
autoTypeSeq: this.model.get('autoTypeSeq'),
|
||||
autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
|
||||
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
|
||||
}, true);
|
||||
this.renderTemplate(
|
||||
{
|
||||
title: this.model.get('title'),
|
||||
icon: this.model.get('icon') || 'folder',
|
||||
customIcon: this.model.get('customIcon'),
|
||||
enableSearching: this.model.getEffectiveEnableSearching(),
|
||||
readonly: this.model.get('top'),
|
||||
canAutoType: AutoType.enabled,
|
||||
autoTypeSeq: this.model.get('autoTypeSeq'),
|
||||
autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
|
||||
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
|
||||
},
|
||||
true
|
||||
);
|
||||
if (!this.model.get('title')) {
|
||||
this.$el.find('#grp__field-title').focus();
|
||||
}
|
||||
|
@ -88,8 +91,10 @@ const GrpView = Backbone.View.extend({
|
|||
|
||||
focusAutoTypeSeq: function(e) {
|
||||
if (!this.views.hint) {
|
||||
this.views.hint = new AutoTypeHintView({input: e.target}).render();
|
||||
this.views.hint.on('remove', () => { delete this.views.hint; });
|
||||
this.views.hint = new AutoTypeHintView({ input: e.target }).render();
|
||||
this.views.hint.on('remove', () => {
|
||||
delete this.views.hint;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -22,12 +22,15 @@ const IconSelectView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
this.renderTemplate({
|
||||
sel: this.model.iconId,
|
||||
icons: IconMap,
|
||||
canDownloadFavicon: !!this.model.url,
|
||||
customIcons: this.model.file.getCustomIcons()
|
||||
}, true);
|
||||
this.renderTemplate(
|
||||
{
|
||||
sel: this.model.iconId,
|
||||
icons: IconMap,
|
||||
canDownloadFavicon: !!this.model.url,
|
||||
customIcons: this.model.file.getCustomIcons()
|
||||
},
|
||||
true
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
|
@ -63,13 +66,17 @@ const IconSelectView = Backbone.View.extend({
|
|||
this.setSpecialImage(img, 'download');
|
||||
this.$el.find('.icon-select__icon-download img').remove();
|
||||
this.$el.find('.icon-select__icon-download>i').removeClass('fa-spinner fa-spin');
|
||||
this.$el.find('.icon-select__icon-download').addClass('icon-select__icon--custom-selected').append(img);
|
||||
this.$el
|
||||
.find('.icon-select__icon-download')
|
||||
.addClass('icon-select__icon--custom-selected')
|
||||
.append(img);
|
||||
this.downloadingFavicon = false;
|
||||
};
|
||||
img.onerror = e => {
|
||||
logger.error('Favicon download error: ' + url, e);
|
||||
this.$el.find('.icon-select__icon-download>i').removeClass('fa-spinner fa-spin');
|
||||
this.$el.find('.icon-select__icon-download')
|
||||
this.$el
|
||||
.find('.icon-select__icon-download')
|
||||
.removeClass('icon-select__icon--custom-selected')
|
||||
.addClass('icon-select__icon--download-error');
|
||||
this.downloadingFavicon = false;
|
||||
|
@ -103,7 +110,10 @@ const IconSelectView = Backbone.View.extend({
|
|||
img.onload = () => {
|
||||
this.setSpecialImage(img, 'select');
|
||||
this.$el.find('.icon-select__icon-select img').remove();
|
||||
this.$el.find('.icon-select__icon-select').addClass('icon-select__icon--custom-selected').append(img);
|
||||
this.$el
|
||||
.find('.icon-select__icon-select')
|
||||
.addClass('icon-select__icon--custom-selected')
|
||||
.append(img);
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
|
|
|
@ -65,7 +65,10 @@ const KeyChangeView = Backbone.View.extend({
|
|||
this.keyFile = null;
|
||||
this.$el.find('.key-change__keyfile-name').html('');
|
||||
}
|
||||
this.$el.find('.key-change__file').val(null).click();
|
||||
this.$el
|
||||
.find('.key-change__file')
|
||||
.val(null)
|
||||
.click();
|
||||
this.inputEl.focus();
|
||||
},
|
||||
|
||||
|
|
|
@ -31,18 +31,58 @@ const ListSearchView = Backbone.View.extend({
|
|||
advancedSearchEnabled: false,
|
||||
advancedSearch: null,
|
||||
|
||||
initialize: function () {
|
||||
initialize: function() {
|
||||
this.sortOptions = [
|
||||
{ value: 'title', icon: 'sort-alpha-asc', loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchAZ) },
|
||||
{ value: '-title', icon: 'sort-alpha-desc', loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchZA) },
|
||||
{ value: 'website', icon: 'sort-alpha-asc', loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchAZ) },
|
||||
{ value: '-website', icon: 'sort-alpha-desc', loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchZA) },
|
||||
{ value: 'user', icon: 'sort-alpha-asc', loc: () => Format.capFirst(Locale.user) + ' ' + this.addArrow(Locale.searchAZ) },
|
||||
{ value: '-user', icon: 'sort-alpha-desc', loc: () => Format.capFirst(Locale.user) + ' ' + this.addArrow(Locale.searchZA) },
|
||||
{ value: 'created', icon: 'sort-numeric-asc', loc: () => Locale.searchCreated + ' ' + this.addArrow(Locale.searchON) },
|
||||
{ value: '-created', icon: 'sort-numeric-desc', loc: () => Locale.searchCreated + ' ' + this.addArrow(Locale.searchNO) },
|
||||
{ value: 'updated', icon: 'sort-numeric-asc', loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchON) },
|
||||
{ value: '-updated', icon: 'sort-numeric-desc', loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchNO) },
|
||||
{
|
||||
value: 'title',
|
||||
icon: 'sort-alpha-asc',
|
||||
loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchAZ)
|
||||
},
|
||||
{
|
||||
value: '-title',
|
||||
icon: 'sort-alpha-desc',
|
||||
loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchZA)
|
||||
},
|
||||
{
|
||||
value: 'website',
|
||||
icon: 'sort-alpha-asc',
|
||||
loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchAZ)
|
||||
},
|
||||
{
|
||||
value: '-website',
|
||||
icon: 'sort-alpha-desc',
|
||||
loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchZA)
|
||||
},
|
||||
{
|
||||
value: 'user',
|
||||
icon: 'sort-alpha-asc',
|
||||
loc: () => Format.capFirst(Locale.user) + ' ' + this.addArrow(Locale.searchAZ)
|
||||
},
|
||||
{
|
||||
value: '-user',
|
||||
icon: 'sort-alpha-desc',
|
||||
loc: () => Format.capFirst(Locale.user) + ' ' + this.addArrow(Locale.searchZA)
|
||||
},
|
||||
{
|
||||
value: 'created',
|
||||
icon: 'sort-numeric-asc',
|
||||
loc: () => Locale.searchCreated + ' ' + this.addArrow(Locale.searchON)
|
||||
},
|
||||
{
|
||||
value: '-created',
|
||||
icon: 'sort-numeric-desc',
|
||||
loc: () => Locale.searchCreated + ' ' + this.addArrow(Locale.searchNO)
|
||||
},
|
||||
{
|
||||
value: 'updated',
|
||||
icon: 'sort-numeric-asc',
|
||||
loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchON)
|
||||
},
|
||||
{
|
||||
value: '-updated',
|
||||
icon: 'sort-numeric-desc',
|
||||
loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchNO)
|
||||
},
|
||||
{ value: '-attachments', icon: 'sort-amount-desc', loc: () => Locale.searchAttachments },
|
||||
{ value: '-rank', icon: 'sort-numeric-desc', loc: () => Locale.searchRank }
|
||||
];
|
||||
|
@ -52,11 +92,16 @@ const ListSearchView = Backbone.View.extend({
|
|||
}, this);
|
||||
this.views = {};
|
||||
this.advancedSearch = {
|
||||
user: true, other: true,
|
||||
url: true, protect: false,
|
||||
notes: true, pass: false,
|
||||
cs: false, regex: false,
|
||||
history: false, title: true,
|
||||
user: true,
|
||||
other: true,
|
||||
url: true,
|
||||
protect: false,
|
||||
notes: true,
|
||||
pass: false,
|
||||
cs: false,
|
||||
regex: false,
|
||||
history: false,
|
||||
title: true,
|
||||
rank: true
|
||||
};
|
||||
if (this.model.advancedSearch) {
|
||||
|
@ -83,9 +128,16 @@ const ListSearchView = Backbone.View.extend({
|
|||
},
|
||||
|
||||
setLocale: function() {
|
||||
this.sortOptions.forEach(opt => { opt.text = opt.loc(); });
|
||||
const entryDesc = FeatureDetector.isMobile ? '' : (' <span class="muted-color">(' + Locale.searchShiftClickOr + ' ' +
|
||||
FeatureDetector.altShortcutSymbol(true) + 'N)</span>');
|
||||
this.sortOptions.forEach(opt => {
|
||||
opt.text = opt.loc();
|
||||
});
|
||||
const entryDesc = FeatureDetector.isMobile
|
||||
? ''
|
||||
: ' <span class="muted-color">(' +
|
||||
Locale.searchShiftClickOr +
|
||||
' ' +
|
||||
FeatureDetector.altShortcutSymbol(true) +
|
||||
'N)</span>';
|
||||
this.createOptions = [
|
||||
{ value: 'entry', icon: 'key', text: Format.capFirst(Locale.entry) + entryDesc },
|
||||
{ value: 'group', icon: 'folder', text: Format.capFirst(Locale.group) }
|
||||
|
@ -105,7 +157,7 @@ const ListSearchView = Backbone.View.extend({
|
|||
this.stopListening(KeyHandler, 'keypress', this.documentKeyPress);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
render: function() {
|
||||
let searchVal;
|
||||
if (this.inputEl) {
|
||||
searchVal = this.inputEl.val();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue