This commit is contained in:
antelle 2019-08-16 23:05:39 +02:00
parent 68245629e4
commit 84a23e4aea
179 changed files with 7485 additions and 5091 deletions

View File

@ -1,7 +1,6 @@
{ {
"extends": "standard", "extends": ["standard", "plugin:prettier/recommended"],
"rules": { "rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"semi": ["error", "always"], "semi": ["error", "always"],
"one-var": "off", "one-var": "off",
"space-before-function-paren": "off", "space-before-function-paren": "off",

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"tabWidth": 4,
"singleQuote": true,
"printWidth": 120,
"trailingComma": "none",
"quoteProps": "preserve"
}

View File

@ -144,7 +144,7 @@ module.exports = function(grunt) {
eslint: { eslint: {
app: ['app/scripts/**/*.js'], app: ['app/scripts/**/*.js'],
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'], desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
grunt: ['Gruntfile.js', 'grunt/**/*.js'] grunt: ['Gruntfile.js', 'build/**/*.js']
}, },
inline: { inline: {
app: { app: {
@ -182,12 +182,21 @@ module.exports = function(grunt) {
files: { 'tmp/desktop/app/index.html': 'dist/index.html' } files: { 'tmp/desktop/app/index.html': 'dist/index.html' }
}, },
'desktop-public-key': { 'desktop-public-key': {
options: { replacements: [{ pattern: '\'@@PUBLIC_KEY_CONTENT\'', replacement: options: {
'`' + fs.readFileSync('app/resources/public-key.pem', {encoding: 'utf8'}).trim() + '`' }] }, 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' } files: { 'tmp/desktop/app/main.js': 'desktop/main.js' }
}, },
'cordova-html': { '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' } files: { 'tmp/cordova/app/index.html': 'dist/index.html' }
} }
}, },
@ -239,9 +248,9 @@ module.exports = function(grunt) {
electronVersion: electronVersion, electronVersion: electronVersion,
overwrite: true, overwrite: true,
asar: true, asar: true,
'appCopyright': `Copyright © ${year} Antelle`, appCopyright: `Copyright © ${year} Antelle`,
'appVersion': pkg.version, appVersion: pkg.version,
'buildVersion': '<%= gitinfo.local.branch.current.shortSHA %>' buildVersion: '<%= gitinfo.local.branch.current.shortSHA %>'
}, },
linux: { linux: {
options: { options: {
@ -255,9 +264,9 @@ module.exports = function(grunt) {
platform: 'darwin', platform: 'darwin',
arch: ['x64'], arch: ['x64'],
icon: 'graphics/icon.icns', icon: 'graphics/icon.icns',
'appBundleId': 'net.antelle.keeweb', appBundleId: 'net.antelle.keeweb',
'appCategoryType': 'public.app-category.productivity', appCategoryType: 'public.app-category.productivity',
'extendInfo': 'package/osx/extend.plist' extendInfo: 'package/osx/extend.plist'
} }
}, },
win32: { win32: {
@ -265,13 +274,13 @@ module.exports = function(grunt) {
platform: 'win32', platform: 'win32',
arch: ['ia32', 'x64'], arch: ['ia32', 'x64'],
icon: 'graphics/icon.ico', icon: 'graphics/icon.ico',
'buildVersion': pkg.version, buildVersion: pkg.version,
'version-string': { 'version-string': {
'CompanyName': 'KeeWeb', CompanyName: 'KeeWeb',
'FileDescription': pkg.description, FileDescription: pkg.description,
'OriginalFilename': 'KeeWeb.exe', OriginalFilename: 'KeeWeb.exe',
'ProductName': 'KeeWeb', ProductName: 'KeeWeb',
'InternalName': 'KeeWeb' InternalName: 'KeeWeb'
} }
} }
} }
@ -297,9 +306,7 @@ module.exports = function(grunt) {
}, },
'desktop-update': { 'desktop-update': {
options: { archive: 'dist/desktop/UpdateDesktop.zip', comment: zipCommentPlaceholder }, options: { archive: 'dist/desktop/UpdateDesktop.zip', comment: zipCommentPlaceholder },
files: [ files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }
]
}, },
'win32-x64': { 'win32-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` }, options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
@ -311,13 +318,17 @@ module.exports = function(grunt) {
}, },
'linux-x64': { 'linux-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` }, options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true }, files: [
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }] { cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true },
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
]
}, },
'linux-ia32': { 'linux-ia32': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.ia32.zip` }, options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.ia32.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-ia32', src: '**', expand: true }, files: [
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }] { cwd: 'tmp/desktop/KeeWeb-linux-ia32', src: '**', expand: true },
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
]
} }
}, },
appdmg: { appdmg: {
@ -341,7 +352,9 @@ module.exports = function(grunt) {
options: { options: {
vars: { vars: {
version: pkg.version, 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 homepage: pkg.homepage
} }
}, },
@ -383,7 +396,9 @@ module.exports = function(grunt) {
description: pkg.description, description: pkg.description,
author: pkg.author, author: pkg.author,
homepage: pkg.homepage, 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': { 'linux-x64': {
@ -401,8 +416,18 @@ module.exports = function(grunt) {
}, },
files: [ files: [
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true }, { 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': { 'linux-ia32': {
@ -420,8 +445,18 @@ module.exports = function(grunt) {
}, },
files: [ files: [
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true }, { 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: { desktop: {
options: { options: {
file: 'dist/desktop/UpdateDesktop.zip', file: 'dist/desktop/UpdateDesktop.zip',
expected: [ expected: ['app.asar', 'helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
'app.asar',
'helper/darwin/KeeWebHelper',
'helper/win32/KeeWebHelper.exe'
],
expectedCount: 7, expectedCount: 7,
publicKey: 'app/resources/public-key.pem' publicKey: 'app/resources/public-key.pem'
} }
} }
}, },
'sign-html': { 'sign-html': {
'app': { app: {
options: { options: {
file: 'dist/index.html', file: 'dist/index.html',
skip: grunt.option('skip-sign') skip: grunt.option('skip-sign')
@ -521,7 +552,7 @@ module.exports = function(grunt) {
} }
}, },
'sign-dist': { 'sign-dist': {
'dist': { dist: {
options: { options: {
sign: 'dist/desktop/Verify.sign.sha256' sign: 'dist/desktop/Verify.sign.sha256'
}, },

View File

@ -24,7 +24,7 @@ const FeatureDetector = require('./util/feature-detector');
const KdbxwebInit = require('./util/kdbxweb-init'); const KdbxwebInit = require('./util/kdbxweb-init');
const Locale = require('./util/locale'); const Locale = require('./util/locale');
const ready = Launcher && Launcher.ready || $; const ready = (Launcher && Launcher.ready) || $;
ready(() => { ready(() => {
if (AuthReceiver.receive() || FeatureDetector.isFrame) { if (AuthReceiver.receive() || FeatureDetector.isFrame) {
@ -51,16 +51,17 @@ ready(() => {
} }
function ensureCanRun() { function ensureCanRun() {
return FeatureTester.test() return FeatureTester.test().catch(e => {
.catch(e => { Alerts.error({
Alerts.error({ header: Locale.appSettingsError,
header: Locale.appSettingsError, body: Locale.appNotSupportedError + '<br/><br/>' + e,
body: Locale.appNotSupportedError + '<br/><br/>' + e, buttons: [],
buttons: [], esc: false,
esc: false, enter: false, click: false enter: false,
}); click: false
throw 'Feature testing failed: ' + e;
}); });
throw 'Feature testing failed: ' + e;
});
} }
function loadConfigs() { function loadConfigs() {
@ -87,7 +88,9 @@ ready(() => {
header: Locale.appSettingsError, header: Locale.appSettingsError,
body: Locale.appSettingsErrorBody, body: Locale.appSettingsErrorBody,
buttons: [], buttons: [],
esc: false, enter: false, click: false esc: false,
enter: false,
click: false
}); });
} }
@ -96,42 +99,46 @@ ready(() => {
SettingsManager.setBySettings(appModel.settings); SettingsManager.setBySettings(appModel.settings);
const configParam = getConfigParam(); const configParam = getConfigParam();
if (configParam) { if (configParam) {
return appModel.loadConfig(configParam).then(() => { return appModel
SettingsManager.setBySettings(appModel.settings); .loadConfig(configParam)
}).catch(e => { .then(() => {
if (!appModel.settings.get('cacheConfigSettings')) { SettingsManager.setBySettings(appModel.settings);
showSettingsLoadError(); })
throw e; .catch(e => {
} if (!appModel.settings.get('cacheConfigSettings')) {
}); showSettingsLoadError();
throw e;
}
});
} }
}); });
} }
function showApp() { function showApp() {
return Promise.resolve() return Promise.resolve().then(() => {
.then(() => { const skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
const skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning'); const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0; const hostIsInsecure = location.hostname !== 'localhost';
const hostIsInsecure = location.hostname !== 'localhost'; if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) { return new Promise(resolve => {
return new Promise(resolve => { Alerts.error({
Alerts.error({ header: Locale.appSecWarn,
header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false, icon: 'user-secret',
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2, esc: false,
buttons: [ enter: false,
{result: '', title: Locale.appSecWarnBtn, error: true} click: false,
], body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
complete: () => { buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
showView(); complete: () => {
resolve(); showView();
} resolve();
}); }
}); });
} else { });
showView(); } else {
} showView();
}); }
});
} }
function postInit() { function postInit() {

View File

@ -17,14 +17,13 @@ AutoTypeFilter.prototype.getEntries = function() {
autoType: true autoType: true
}; };
this.prepareFilter(); this.prepareFilter();
let entries = this.appModel.getEntriesByFilter(filter) let entries = this.appModel.getEntriesByFilter(filter).map(e => [e, this.getEntryRank(e)]);
.map(e => [e, this.getEntryRank(e)]);
if (!this.ignoreWindowInfo) { if (!this.ignoreWindowInfo) {
entries = entries.filter(e => e[1]); 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]); entries = entries.map(p => p[0]);
return new EntryCollection(entries, {comparator: 'none'}); return new EntryCollection(entries, { comparator: 'none' });
}; };
AutoTypeFilter.prototype.hasWindowInfo = function() { AutoTypeFilter.prototype.hasWindowInfo = function() {
@ -40,10 +39,7 @@ AutoTypeFilter.prototype.prepareFilter = function() {
AutoTypeFilter.prototype.getEntryRank = function(entry) { AutoTypeFilter.prototype.getEntryRank = function(entry) {
let rank = 0; let rank = 0;
if (this.titleLower && entry.title) { if (this.titleLower && entry.title) {
rank += Ranking.getStringRank( rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
entry.title.toLowerCase(),
this.titleLower
);
} }
if (this.urlParts && entry.url) { if (this.urlParts && entry.url) {
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase()); const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());

View File

@ -33,8 +33,12 @@ AutoTypeObfuscator.prototype.obfuscate = function() {
}; };
AutoTypeObfuscator.prototype.finished = function() { AutoTypeObfuscator.prototype.finished = function() {
return this.chars.length === this.inputChars.length && return (
this.chars.every(function(ch, ix) { return this.inputChars[ix].ch === ch; }, this); this.chars.length === this.inputChars.length &&
this.chars.every(function(ch, ix) {
return this.inputChars[ix].ch === ch;
}, this)
);
}; };
AutoTypeObfuscator.prototype.step = function() { AutoTypeObfuscator.prototype.step = function() {
@ -161,7 +165,7 @@ AutoTypeObfuscator.prototype.inputChar = function(ch) {
AutoTypeObfuscator.prototype.copyPaste = function(ch) { AutoTypeObfuscator.prototype.copyPaste = function(ch) {
logger.debug('copyPaste', 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.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
this.inputCursor++; this.inputCursor++;
this.inputSel = 0; this.inputSel = 0;
@ -174,10 +178,10 @@ AutoTypeObfuscator.prototype.selectText = function(backward, count) {
ops.push({ type: 'key', value: backward ? 'left' : 'right' }); ops.push({ type: 'key', value: backward ? 'left' : 'right' });
} }
if (ops.length === 1) { if (ops.length === 1) {
ops[0].mod = {'+': true}; ops[0].mod = { '+': true };
this.ops.push(ops[0]); this.ops.push(ops[0]);
} else { } else {
this.ops.push({type: 'group', value: ops, mod: {'+': true}}); this.ops.push({ type: 'group', value: ops, mod: { '+': true } });
} }
if (backward) { if (backward) {
this.inputCursor -= count; this.inputCursor -= count;

View File

@ -16,39 +16,127 @@ const AutoTypeRunner = function(ops) {
AutoTypeRunner.PendingResolve = { pending: true }; AutoTypeRunner.PendingResolve = { pending: true };
AutoTypeRunner.Keys = { AutoTypeRunner.Keys = {
tab: 'tab', enter: 'enter', space: 'space', tab: 'tab',
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn', enter: 'enter',
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc', space: 'space',
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6', up: 'up',
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16', down: 'down',
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide', left: 'left',
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4', right: 'right',
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9' 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 = { AutoTypeRunner.Substitutions = {
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); }, title: function(runner, op) {
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); }, return runner.getEntryFieldKeys('Title', op);
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); }, },
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); }, username: function(runner, op) {
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); }, return runner.getEntryFieldKeys('UserName', op);
group: function(runner) { return runner.getEntryGroupName(); }, },
totp: function(runner, op) { return runner.getOtp(op); }, url: function(runner, op) {
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); }, return runner.getEntryFieldKeys('URL', op);
'dt_simple': function(runner) { return runner.dt('simple'); }, },
'dt_year': function(runner) { return runner.dt('Y'); }, password: function(runner, op) {
'dt_month': function(runner) { return runner.dt('M'); }, return runner.getEntryFieldKeys('Password', op);
'dt_day': function(runner) { return runner.dt('D'); }, },
'dt_hour': function(runner) { return runner.dt('h'); }, notes: function(runner, op) {
'dt_minute': function(runner) { return runner.dt('m'); }, return runner.getEntryFieldKeys('Notes', op);
'dt_second': function(runner) { return runner.dt('s'); }, },
'dt_utc_simple': function(runner) { return runner.udt('simple'); }, group: function(runner) {
'dt_utc_year': function(runner) { return runner.udt('Y'); }, return runner.getEntryGroupName();
'dt_utc_month': function(runner) { return runner.udt('M'); }, },
'dt_utc_day': function(runner) { return runner.udt('D'); }, totp: function(runner, op) {
'dt_utc_hour': function(runner) { return runner.udt('h'); }, return runner.getOtp(op);
'dt_utc_minute': function(runner) { return runner.udt('m'); }, },
'dt_utc_second': function(runner) { return runner.udt('s'); } 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) { AutoTypeRunner.prototype.resolve = function(entry, callback) {
@ -103,7 +191,7 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
op.value = []; op.value = [];
const count = +op.arg; const count = +op.arg;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
op.value.push({type: 'key', value: key}); op.value.push({ type: 'key', value: key });
} }
} else { } else {
// {TAB} // {TAB}
@ -179,9 +267,9 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
const ops = []; const ops = [];
value.forEachChar(ch => { value.forEachChar(ch => {
if (ch === 10 || ch === 13) { if (ch === 10 || ch === 13) {
ops.push({type: 'key', value: 'enter'}); ops.push({ type: 'key', value: 'enter' });
} else { } else {
ops.push({type: 'text', value: String.fromCharCode(ch)}); ops.push({ type: 'text', value: String.fromCharCode(ch) });
} }
}); });
return ops; return ops;
@ -194,10 +282,10 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
const partsOps = []; const partsOps = [];
parts.forEach(part => { parts.forEach(part => {
if (partsOps.length) { if (partsOps.length) {
partsOps.push({type: 'key', value: 'enter'}); partsOps.push({ type: 'key', value: 'enter' });
} }
if (part) { if (part) {
partsOps.push({type: 'text', value: part}); partsOps.push({ type: 'text', value: part });
} }
}); });
return partsOps; return partsOps;

View File

@ -3,15 +3,53 @@ const AutoTypeNativeHelper = require('../helper/auto-type-native-helper');
// http://eastmanreference.com/complete-list-of-applescript-key-codes/ // http://eastmanreference.com/complete-list-of-applescript-key-codes/
const KeyMap = { const KeyMap = {
tab: 48, enter: 36, space: 49, tab: 48,
up: 126, down: 125, left: 123, right: 124, home: 115, end: 119, pgup: 116, pgdn: 121, enter: 36,
ins: 114, del: 117, bs: 51, esc: 53, space: 49,
win: 55, rwin: 55, up: 126,
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101, down: 125,
f10: 109, f11: 103, f12: 111, f13: 105, f14: 107, f15: 113, f16: 106, left: 123,
add: 69, subtract: 78, multiply: 67, divide: 75, right: 124,
n0: 82, n1: 83, n2: 84, n3: 85, n4: 86, home: 115,
n5: 87, n6: 88, n7: 89, n8: 91, n9: 92 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 = { const ModMap = {

View File

@ -3,15 +3,53 @@ const Locale = require('../../util/locale');
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h // https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
const KeyMap = { const KeyMap = {
tab: 'Tab', enter: 'KP_Enter', space: 'KP_Space', tab: 'Tab',
up: 'Up', down: 'Down', left: 'Left', right: 'Right', home: 'Home', end: 'End', pgup: 'Page_Up', pgdn: 'Page_Down', enter: 'KP_Enter',
ins: 'Insert', del: 'Delete', bs: 'BackSpace', esc: 'Escape', space: 'KP_Space',
win: 'Meta_L', rwin: 'Meta_R', up: 'Up',
f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9', down: 'Down',
f10: 'F10', f11: 'F11', f12: 'F12', f13: 'F13', f14: 'F14', f15: 'F15', f16: 'F16', left: 'Left',
add: 'KP_Add', subtract: 'KP_Subtract', multiply: 'KP_Multiply', divide: 'KP_Divide', right: 'Right',
n0: 'KP_0', n1: 'KP_1', n2: 'KP_2', n3: 'KP_3', n4: 'KP_4', home: 'Home',
n5: 'KP_5', n6: 'KP_6', n7: 'KP_7', n8: 'KP_8', n9: 'KP_9' 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 = { const ModMap = {
@ -69,7 +107,7 @@ AutoTypeEmitter.prototype.copyPaste = function(text) {
}; };
AutoTypeEmitter.prototype.wait = function(time) { AutoTypeEmitter.prototype.wait = function(time) {
this.pendingScript.push('sleep ' + (time / 1000)); this.pendingScript.push('sleep ' + time / 1000);
this.callback(); this.callback();
}; };

View File

@ -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 // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
const KeyMap = { const KeyMap = {
tab: 0x09, enter: 0x0D, space: 0x20, tab: 0x09,
up: 0x26, down: 0x28, left: 0x25, right: 0x27, home: 0x24, end: 0x23, pgup: 0x21, pgdn: 0x22, enter: 0x0d,
ins: 0x2D, del: 0x2E, bs: 0x08, esc: 0x1B, space: 0x20,
win: 0x5B, rwin: 0x5C, up: 0x26,
f1: 0x70, f2: 0x71, f3: 0x72, f4: 0x73, f5: 0x74, f6: 0x75, f7: 0x76, f8: 0x77, f9: 0x78, down: 0x28,
f10: 0x79, f11: 0x7A, f12: 0x7B, f13: 0x7C, f14: 0x7D, f15: 0x7E, f16: 0x7F, left: 0x25,
add: 0x6B, subtract: 0x6D, multiply: 0x6A, divide: 0x6F, right: 0x27,
n0: 0x30, n1: 0x31, n2: 0x32, n3: 0x33, n4: 0x34, home: 0x24,
n5: 0x35, n6: 0x36, n7: 0x37, n8: 0x38, n9: 0x39 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 = { const ModMap = {

View File

@ -1,13 +1,17 @@
const Launcher = require('../../comp/launcher'); const Launcher = require('../../comp/launcher');
const ForeMostAppScript = 'tell application "System Events" to set frontApp to name of first process whose frontmost is true'; const ForeMostAppScript =
const ChromeScript = 'tell application "{}" to set appUrl to URL of active tab of front window\n' + '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' + 'tell application "{}" to set appTitle to title of active tab of front window\n' +
'return appUrl & "\n" & appTitle'; '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' + 'tell application "{}" to set appTitle to name of front document\n' +
'return appUrl & "\n" & appTitle'; 'return appUrl & "\n" & appTitle';
const OtherAppsScript = 'tell application "System Events"\n' + const OtherAppsScript =
'tell application "System Events"\n' +
' tell process "{}"\n' + ' tell process "{}"\n' +
' tell (1st window whose value of attribute "AXMain" is true)\n' + ' tell (1st window whose value of attribute "AXMain" is true)\n' +
' set windowTitle to value of attribute "AXTitle"\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\n' +
'end tell'; 'end tell';
const AutoTypeHelper = function() { const AutoTypeHelper = function() {};
};
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) { AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => { AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
if (err) { return callback(err); } if (err) {
return callback(err);
}
const appName = out.trim(); const appName = out.trim();
// getting urls and titles from Chrome or Safari: // getting urls and titles from Chrome or Safari:
// - will suit in 90% cases // - will suit in 90% cases
@ -28,20 +33,26 @@ AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
// - allows to get url // - allows to get url
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) { if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => { AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
if (err) { return callback(err); } if (err) {
return callback(err);
}
const parts = out.split('\n'); const parts = out.split('\n');
return callback(null, (parts[1] || '').trim(), parts[0].trim()); return callback(null, (parts[1] || '').trim(), parts[0].trim());
}); });
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) { } else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => { AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
if (err) { return callback(err); } if (err) {
return callback(err);
}
const parts = out.split('\n'); const parts = out.split('\n');
return callback(null, (parts[1] || '').trim(), parts[0].trim()); return callback(null, (parts[1] || '').trim(), parts[0].trim());
}); });
} else { } else {
// special cases are not available. this method may ask the user about assistive access // special cases are not available. this method may ask the user about assistive access
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => { AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
if (err) { return callback(err); } if (err) {
return callback(err);
}
return callback(null, out.trim()); return callback(null, out.trim());
}); });
} }

View File

@ -1,7 +1,6 @@
const Launcher = require('../../comp/launcher'); const Launcher = require('../../comp/launcher');
const AutoTypeHelper = function() { const AutoTypeHelper = function() {};
};
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) { AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
Launcher.spawn({ Launcher.spawn({

View File

@ -1,18 +1,18 @@
const Launcher = require('../../comp/launcher'); const Launcher = require('../../comp/launcher');
const AutoTypeNativeHelper = require('./auto-type-native-helper'); const AutoTypeNativeHelper = require('./auto-type-native-helper');
const AutoTypeHelper = function() { const AutoTypeHelper = function() {};
};
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) { AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
Launcher.spawn({ Launcher.spawn({
cmd: AutoTypeNativeHelper.getHelperPath(), cmd: AutoTypeNativeHelper.getHelperPath(),
args: ['--window-info'], args: ['--window-info'],
complete: function(err, out) { complete: function(err, out) {
if (err) { return callback(err); } if (err) {
return callback(err);
}
const parts = out.split('\n'); const parts = out.split('\n');
return callback(null, (parts[0] || '').trim(), return callback(null, (parts[0] || '').trim(), parts[1] ? parts[1].trim() : undefined);
parts[1] ? parts[1].trim() : undefined);
} }
}); });
}; };

View File

@ -31,14 +31,16 @@ const AutoType = {
}, },
handleEvent(e) { handleEvent(e) {
const entry = e && e.entry || null; const entry = (e && e.entry) || null;
logger.debug('Auto type event', entry); logger.debug('Auto type event', entry);
if (this.running) { if (this.running) {
logger.debug('Already running, skipping event'); logger.debug('Already running, skipping event');
return; return;
} }
if (entry) { if (entry) {
this.hideWindow(() => { this.runAndHandleResult({ entry }); }); this.hideWindow(() => {
this.runAndHandleResult({ entry });
});
} else { } else {
if (this.selectEntryView) { if (this.selectEntryView) {
return; return;
@ -180,7 +182,7 @@ const AutoType = {
selectEntryAndRun() { selectEntryAndRun() {
this.getActiveWindowTitle((e, title, url) => { this.getActiveWindowTitle((e, title, url) => {
const filter = new AutoTypeFilter({title, url}, this.appModel); const filter = new AutoTypeFilter({ title, url }, this.appModel);
const evt = { filter }; const evt = { filter };
if (!this.appModel.files.hasOpenFiles()) { if (!this.appModel.files.hasOpenFiles()) {
this.pendingEvent = evt; this.pendingEvent = evt;
@ -224,10 +226,14 @@ const AutoType = {
this.selectEntryView.on('show-open-files', () => { this.selectEntryView.on('show-open-files', () => {
this.selectEntryView.hide(); this.selectEntryView.hide();
Backbone.trigger('open-file'); Backbone.trigger('open-file');
Backbone.once('closed-open-view', () => { Backbone.once(
this.selectEntryView.show(); 'closed-open-view',
this.selectEntryView.setupKeys(); () => {
}, this); this.selectEntryView.show();
this.selectEntryView.setupKeys();
},
this
);
}); });
}, },

View File

@ -19,7 +19,9 @@ const EntryCollection = Backbone.Collection.extend({
'-created': Comparators.dateComparator('created', false), '-created': Comparators.dateComparator('created', false),
'updated': Comparators.dateComparator('updated', true), 'updated': Comparators.dateComparator('updated', true),
'-updated': Comparators.dateComparator('updated', false), '-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() '-rank': Comparators.rankComparator()
}, },
@ -28,7 +30,7 @@ const EntryCollection = Backbone.Collection.extend({
filter: null, filter: null,
initialize: function(models, options) { initialize: function(models, options) {
const comparatorName = options && options.comparator || this.defaultComparator; const comparatorName = (options && options.comparator) || this.defaultComparator;
this.comparator = this.comparators[comparatorName]; this.comparator = this.comparators[comparatorName];
}, },

View File

@ -5,30 +5,31 @@ const SettingsStore = require('../comp/settings-store');
const FileInfoCollection = Backbone.Collection.extend({ const FileInfoCollection = Backbone.Collection.extend({
model: FileInfoModel, model: FileInfoModel,
initialize: function () { initialize: function() {},
},
load: function () { load: function() {
return SettingsStore.load('file-info').then(data => { return SettingsStore.load('file-info').then(data => {
if (data) { if (data) {
this.reset(data, {silent: true}); this.reset(data, { silent: true });
} }
}); });
}, },
save: function () { save: function() {
SettingsStore.save('file-info', this.toJSON()); SettingsStore.save('file-info', this.toJSON());
}, },
getLast: function () { getLast: function() {
return this.first(); return this.first();
}, },
getMatch: function (storage, name, path) { getMatch: function(storage, name, path) {
return this.find(fi => { return this.find(fi => {
return (fi.get('storage') || '') === (storage || '') && return (
(fi.get('storage') || '') === (storage || '') &&
(fi.get('name') || '') === (name || '') && (fi.get('name') || '') === (name || '') &&
(fi.get('path') || '') === (path || ''); (fi.get('path') || '') === (path || '')
);
}); });
}, },

View File

@ -5,10 +5,30 @@ const Alerts = {
alertDisplayed: false, alertDisplayed: false,
buttons: { buttons: {
ok: {result: 'yes', get title() { return Locale.alertOk; }}, ok: {
yes: {result: 'yes', get title() { return Locale.alertYes; }}, result: 'yes',
no: {result: '', get title() { return Locale.alertNo; }}, get title() {
cancel: {result: '', get title() { return Locale.alertCancel; }} 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) { alert: function(config) {
@ -46,39 +66,54 @@ const Alerts = {
}, },
info: function(config) { info: function(config) {
this.alert(_.extend({ this.alert(
header: '', _.extend(
body: '', {
icon: 'info', header: '',
buttons: [this.buttons.ok], body: '',
esc: '', icon: 'info',
click: '', buttons: [this.buttons.ok],
enter: '' esc: '',
}, config)); click: '',
enter: ''
},
config
)
);
}, },
error: function(config) { error: function(config) {
this.alert(_.extend({ this.alert(
header: '', _.extend(
body: '', {
icon: 'exclamation-circle', header: '',
buttons: [this.buttons.ok], body: '',
esc: '', icon: 'exclamation-circle',
click: '', buttons: [this.buttons.ok],
enter: '' esc: '',
}, config)); click: '',
enter: ''
},
config
)
);
}, },
yesno: function(config) { yesno: function(config) {
this.alert(_.extend({ this.alert(
header: '', _.extend(
body: '', {
icon: 'question', header: '',
buttons: [this.buttons.yes, this.buttons.no], body: '',
esc: '', icon: 'question',
click: '', buttons: [this.buttons.yes, this.buttons.no],
enter: 'yes' esc: '',
}, config)); click: '',
enter: 'yes'
},
config
)
);
} }
}; };

View File

@ -37,13 +37,13 @@ const AppRightsChecker = {
this.alert = Alerts.alert({ this.alert = Alerts.alert({
icon: 'lock', icon: 'lock',
header: Locale.appRightsAlert, header: Locale.appRightsAlert,
body: Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) + body:
'<br/>' + Locale.appRightsAlertBody2 + `: <pre>${command}</pre>`, Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
buttons: [ '<br/>' +
{result: 'skip', title: Locale.alertDoNotAsk, error: true}, Locale.appRightsAlertBody2 +
Alerts.buttons.ok `: <pre>${command}</pre>`,
], buttons: [{ result: 'skip', title: Locale.alertDoNotAsk, error: true }, Alerts.buttons.ok],
success: (result) => { success: result => {
if (result === 'skip') { if (result === 'skip') {
this.dontAskAnymore(); this.dontAskAnymore();
} }
@ -54,7 +54,7 @@ const AppRightsChecker = {
runInstaller() { runInstaller() {
Launcher.spawn({ 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: () => { complete: () => {
this.needRunInstaller(needRun => { this.needRunInstaller(needRun => {
if (this.alert && !needRun) { if (this.alert && !needRun) {

View File

@ -21,13 +21,13 @@ const CopyPaste = {
Backbone.off('main-window-will-close', clearClipboard); Backbone.off('main-window-will-close', clearClipboard);
}, clipboardSeconds * 1000); }, clipboardSeconds * 1000);
} }
return {success: true, seconds: clipboardSeconds}; return { success: true, seconds: clipboardSeconds };
} else { } else {
try { try {
if (document.execCommand('copy')) { if (document.execCommand('copy')) {
return {success: true}; return { success: true };
} }
} catch (e) { } } catch (e) {}
return false; return false;
} }
}, },
@ -41,8 +41,12 @@ const CopyPaste = {
hiddenInput[0].selectionEnd = text.length; hiddenInput[0].selectionEnd = text.length;
hiddenInput.focus(); hiddenInput.focus();
hiddenInput.on({ hiddenInput.on({
'copy cut paste': function() { setTimeout(() => hiddenInput.blur(), 0); }, 'copy cut paste': function() {
blur: function() { hiddenInput.remove(); } setTimeout(() => hiddenInput.blur(), 0);
},
blur: function() {
hiddenInput.remove();
}
}); });
} }
}; };

View File

@ -37,7 +37,12 @@ DropboxChooser.prototype.buildUrl = function() {
iframe: 'false', iframe: 'false',
version: 2 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) { DropboxChooser.prototype.onMessage = function(e) {

View File

@ -2,9 +2,15 @@ const AppSettingsModel = require('../models/app-settings-model');
const ExportApi = { const ExportApi = {
settings: { settings: {
get: function(key) { return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON(); }, get: function(key) {
set: function(key, value) { AppSettingsModel.instance.set(key, value); }, return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON();
del: function(key) { AppSettingsModel.instance.unset(key); } },
set: function(key, value) {
AppSettingsModel.instance.set(key, value);
},
del: function(key) {
AppSettingsModel.instance.unset(key);
}
} }
}; };

View File

@ -25,16 +25,23 @@ const FeatureTester = {
const data = 'e567554429098a38d5f819115edffd39'; const data = 'e567554429098a38d5f819115edffd39';
const iv = '4db46dff4add42cb813b98de98e627c4'; const iv = '4db46dff4add42cb813b98de98e627c4';
const exp = '46ab4c37d9ec594e5742971f76f7c1620bc29f2e0736b27832d6bcc5c1c39dc1'; const exp = '46ab4c37d9ec594e5742971f76f7c1620bc29f2e0736b27832d6bcc5c1c39dc1';
return aesCbc.importKey(kdbxweb.ByteUtils.hexToBytes(key)).then(() => { return aesCbc
return aesCbc.encrypt(kdbxweb.ByteUtils.hexToBytes(data), kdbxweb.ByteUtils.hexToBytes(iv)).then(res => { .importKey(kdbxweb.ByteUtils.hexToBytes(key))
if (kdbxweb.ByteUtils.bytesToHex(res) !== exp) { .then(() => {
throw 'AES is not working properly'; return aesCbc
} .encrypt(kdbxweb.ByteUtils.hexToBytes(data), kdbxweb.ByteUtils.hexToBytes(iv))
if (kdbxweb.CryptoEngine.random(1).length !== 1) { .then(res => {
throw 'Random is not working'; 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; });
}); });
}, },

View File

@ -3,33 +3,37 @@ const Locale = require('../util/locale');
const GeneratorPresets = { const GeneratorPresets = {
get defaultPreset() { get defaultPreset() {
return { name: 'Default', title: Locale.genPresetDefault, return { name: 'Default', title: Locale.genPresetDefault, length: 16, upper: true, lower: true, digits: true };
length: 16, upper: true, lower: true, digits: true };
}, },
get builtIn() { get builtIn() {
return [ return [
this.defaultPreset, this.defaultPreset,
{ name: 'Pronounceable', title: Locale.genPresetPronounceable, { name: 'Pronounceable', title: Locale.genPresetPronounceable, length: 10, lower: true, upper: true },
length: 10, lower: true, upper: true }, {
{ name: 'Med', title: Locale.genPresetMed, name: 'Med',
length: 16, upper: true, lower: true, digits: true, special: true, brackets: true, ambiguous: true }, title: Locale.genPresetMed,
{ name: 'Long', title: Locale.genPresetLong, length: 16,
length: 32, upper: true, lower: true, digits: true }, upper: true,
{ name: 'Pin4', title: Locale.genPresetPin4, lower: true,
length: 4, digits: true }, digits: true,
{ name: 'Mac', title: Locale.genPresetMac, special: true,
length: 17, upper: true, digits: true, special: true }, brackets: true,
{ name: 'Hash128', title: Locale.genPresetHash128, ambiguous: true
length: 32, lower: true, digits: true }, },
{ name: 'Hash256', title: Locale.genPresetHash256, { name: 'Long', title: Locale.genPresetLong, length: 32, upper: true, lower: true, digits: true },
length: 64, 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() { get all() {
let presets = this.builtIn; let presets = this.builtIn;
presets.forEach(preset => { preset.builtIn = true; }); presets.forEach(preset => {
preset.builtIn = true;
});
const setting = AppSettingsModel.instance.get('generatorPresets'); const setting = AppSettingsModel.instance.get('generatorPresets');
if (setting) { if (setting) {
if (setting.user) { if (setting.user) {

View File

@ -15,15 +15,22 @@ const KeyHandler = {
$(document).bind('keypress', this.keypress.bind(this)); $(document).bind('keypress', this.keypress.bind(this));
$(document).bind('keydown', this.keydown.bind(this)); $(document).bind('keydown', this.keydown.bind(this));
this.shortcuts[Keys.DOM_VK_A] = [{ handler: this.handleAKey, thisArg: this, shortcut: this.SHORTCUT_ACTION, this.shortcuts[Keys.DOM_VK_A] = [
modal: true, noPrevent: true }]; { handler: this.handleAKey, thisArg: this, shortcut: this.SHORTCUT_ACTION, modal: true, noPrevent: true }
];
}, },
onKey: function(key, handler, thisArg, shortcut, modal, noPrevent) { onKey: function(key, handler, thisArg, shortcut, modal, noPrevent) {
let keyShortcuts = this.shortcuts[key]; let keyShortcuts = this.shortcuts[key];
if (!keyShortcuts) { if (!keyShortcuts) {
this.shortcuts[key] = 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) { offKey: function(key, handler, thisArg) {
if (this.shortcuts[key]) { if (this.shortcuts[key]) {
@ -51,16 +58,24 @@ const KeyHandler = {
const isActionKey = this.isActionKey(e); const isActionKey = this.isActionKey(e);
switch (sh.shortcut) { switch (sh.shortcut) {
case this.SHORTCUT_ACTION: case this.SHORTCUT_ACTION:
if (!isActionKey) { continue; } if (!isActionKey) {
continue;
}
break; break;
case this.SHORTCUT_OPT: case this.SHORTCUT_OPT:
if (!e.altKey) { continue; } if (!e.altKey) {
continue;
}
break; break;
case this.SHORTCUT_ACTION + this.SHORTCUT_OPT: case this.SHORTCUT_ACTION + this.SHORTCUT_OPT:
if (!e.altKey || !isActionKey) { continue; } if (!e.altKey || !isActionKey) {
continue;
}
break; break;
default: default:
if (e.metaKey || e.ctrlKey || e.altKey) { continue; } if (e.metaKey || e.ctrlKey || e.altKey) {
continue;
}
break; break;
} }
sh.handler.call(sh.thisArg, e, code); sh.handler.call(sh.thisArg, e, code);
@ -74,11 +89,15 @@ const KeyHandler = {
} }
}, },
keypress: function(e) { keypress: function(e) {
if (!this.modal && if (
!this.modal &&
e.charCode !== Keys.DOM_VK_RETURN && e.charCode !== Keys.DOM_VK_RETURN &&
e.charCode !== Keys.DOM_VK_ESCAPE && e.charCode !== Keys.DOM_VK_ESCAPE &&
e.charCode !== Keys.DOM_VK_TAB && e.charCode !== Keys.DOM_VK_TAB &&
!e.altKey && !e.ctrlKey && !e.metaKey) { !e.altKey &&
!e.ctrlKey &&
!e.metaKey
) {
this.trigger('keypress', e); this.trigger('keypress', e);
} else if (this.modal) { } else if (this.modal) {
this.trigger('keypress:' + this.modal, e); this.trigger('keypress:' + this.modal, e);

View File

@ -10,9 +10,13 @@ const Launcher = {
clipboardSupported: false, clipboardSupported: false,
ready: function(callback) { ready: function(callback) {
document.addEventListener('deviceready', callback, false); document.addEventListener('deviceready', callback, false);
document.addEventListener('pause', () => { document.addEventListener(
Backbone.trigger('app-minimized'); 'pause',
}, false); () => {
Backbone.trigger('app-minimized');
},
false
);
}, },
platform: function() { platform: function() {
return 'cordova'; return 'cordova';
@ -22,7 +26,9 @@ const Launcher = {
}, },
devTools: false, devTools: false,
// openDevTools: function() { }, // openDevTools: function() { },
getSaveFileName: function(defaultPath, callback) { /* skip in cordova */ }, getSaveFileName: function(defaultPath, callback) {
/* skip in cordova */
},
getDataPath: function() { getDataPath: function() {
const storagePath = window.cordova.file.externalDataDirectory; const storagePath = window.cordova.file.externalDataDirectory;
return [storagePath].concat(Array.from(arguments)).filter(s => !!s); return [storagePath].concat(Array.from(arguments)).filter(s => !!s);
@ -47,9 +53,14 @@ const Launcher = {
}, },
writeFile: function(path, data, callback) { writeFile: function(path, data, callback) {
const createFile = filePath => { const createFile = filePath => {
window.resolveLocalFileSystemURL(filePath.dir, dir => { window.resolveLocalFileSystemURL(
dir.getFile(filePath.file, {create: true}, writeFile); filePath.dir,
}, callback, callback); dir => {
dir.getFile(filePath.file, { create: true }, writeFile);
},
callback,
callback
);
}; };
const writeFile = fileEntry => { const writeFile = fileEntry => {
@ -60,9 +71,11 @@ const Launcher = {
}, callback); }, callback);
}; };
if (path.startsWith('cdvfile://')) { // then file exists if (path.startsWith('cdvfile://')) {
// then file exists
window.resolveLocalFileSystemURL(path, writeFile, callback, callback); window.resolveLocalFileSystemURL(path, writeFile, callback, callback);
} else { // create file on sd card } else {
// create file on sd card
const filePath = this.parsePath(path); const filePath = this.parsePath(path);
this.mkdir(filePath.dir, () => { this.mkdir(filePath.dir, () => {
createFile(filePath); createFile(filePath);
@ -70,55 +83,86 @@ const Launcher = {
} }
}, },
readFile: function(path, encoding, callback) { readFile: function(path, encoding, callback) {
window.resolveLocalFileSystemURL(path, fileEntry => { window.resolveLocalFileSystemURL(
fileEntry.file(file => { path,
const reader = new FileReader(); fileEntry => {
reader.onerror = callback; fileEntry.file(
reader.onloadend = () => { file => {
const contents = new Uint8Array(reader.result); const reader = new FileReader();
callback(encoding ? String.fromCharCode.apply(null, contents) : contents); reader.onerror = callback;
}; reader.onloadend = () => {
reader.readAsArrayBuffer(file); const contents = new Uint8Array(reader.result);
}, err => callback(undefined, err)); callback(encoding ? String.fromCharCode.apply(null, contents) : contents);
}, err => callback(undefined, err)); };
reader.readAsArrayBuffer(file);
},
err => callback(undefined, err)
);
},
err => callback(undefined, err)
);
}, },
fileExists: function(path, callback) { fileExists: function(path, callback) {
window.resolveLocalFileSystemURL(path, fileEntry => callback(true), () => callback(false)); window.resolveLocalFileSystemURL(path, fileEntry => callback(true), () => callback(false));
}, },
deleteFile: function(path, callback) { deleteFile: function(path, callback) {
window.resolveLocalFileSystemURL(path, fileEntry => { window.resolveLocalFileSystemURL(
fileEntry.remove(callback, callback, callback); path,
}, callback); fileEntry => {
fileEntry.remove(callback, callback, callback);
},
callback
);
}, },
statFile: function(path, callback) { statFile: function(path, callback) {
window.resolveLocalFileSystemURL(path, fileEntry => { window.resolveLocalFileSystemURL(
fileEntry.file(file => { path,
callback({ fileEntry => {
ctime: new Date(file.lastModified), fileEntry.file(
mtime: new Date(file.lastModified) file => {
}); callback({
}, err => callback(undefined, err)); ctime: new Date(file.lastModified),
}, err => callback(undefined, err)); mtime: new Date(file.lastModified)
});
},
err => callback(undefined, err)
);
},
err => callback(undefined, err)
);
}, },
mkdir: function(dir, callback) { mkdir: function(dir, callback) {
const basePath = this.getDataPath().join('/'); const basePath = this.getDataPath().join('/');
const createDir = (dirEntry, path, callback) => { const createDir = (dirEntry, path, callback) => {
const name = path.shift(); const name = path.shift();
dirEntry.getDirectory(name, { create: true }, dirEntry => { dirEntry.getDirectory(
if (path.length) { // there is more to create name,
createDir(dirEntry, path, callback); { create: true },
} else { dirEntry => {
callback(); if (path.length) {
} // there is more to create
}, callback); 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) { if (localPath.length) {
window.resolveLocalFileSystemURL(basePath, dirEntry => { window.resolveLocalFileSystemURL(
createDir(dirEntry, localPath, callback); basePath,
}, callback); dirEntry => {
createDir(dirEntry, localPath, callback);
},
callback
);
} else { } else {
callback(); callback();
} }
@ -145,11 +189,15 @@ const Launcher = {
this.hideApp(); this.hideApp();
}, },
requestExit: function() { /* skip in cordova */ }, requestExit: function() {
/* skip in cordova */
},
requestRestart: function() { requestRestart: function() {
window.location.reload(); window.location.reload();
}, },
cancelRestart: function() { /* skip in cordova */ }, cancelRestart: function() {
/* skip in cordova */
},
setClipboardText: function(text) {}, setClipboardText: function(text) {},
getClipboardText: function() {}, getClipboardText: function() {},
@ -169,13 +217,21 @@ const Launcher = {
}, },
// getMainWindow: function() { }, // getMainWindow: function() { },
resolveProxy: function(url, callback) { /* skip in cordova */ }, resolveProxy: function(url, callback) {
openWindow: function(opts) { /* skip in cordova */ }, /* skip in cordova */
hideApp: function() { /* skip in cordova */ }, },
openWindow: function(opts) {
/* skip in cordova */
},
hideApp: function() {
/* skip in cordova */
},
isAppFocused: function() { isAppFocused: function() {
return false; /* skip in cordova */ return false; /* skip in cordova */
}, },
showMainWindow: function() { /* skip in cordova */ }, showMainWindow: function() {
/* skip in cordova */
},
// spawn: function(config) { }, // spawn: function(config) { },
openFileChooser: function(callback) { openFileChooser: function(callback) {
const onFileSelected = function(selected) { const onFileSelected = function(selected) {

View File

@ -28,18 +28,23 @@ const Launcher = {
}, },
devTools: true, devTools: true,
openDevTools: function() { openDevTools: function() {
this.electron().remote.getCurrentWindow().openDevTools({ mode: 'bottom' }); this.electron()
.remote.getCurrentWindow()
.openDevTools({ mode: 'bottom' });
}, },
getSaveFileName: function(defaultPath, callback) { getSaveFileName: function(defaultPath, callback) {
if (defaultPath) { if (defaultPath) {
const homePath = this.remReq('electron').app.getPath('userDesktop'); const homePath = this.remReq('electron').app.getPath('userDesktop');
defaultPath = this.joinPath(homePath, defaultPath); defaultPath = this.joinPath(homePath, defaultPath);
} }
this.remReq('electron').dialog.showSaveDialog({ this.remReq('electron').dialog.showSaveDialog(
title: Locale.launcherSave, {
defaultPath: defaultPath, title: Locale.launcherSave,
filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }] defaultPath: defaultPath,
}, callback); filters: [{ name: Locale.launcherFileFilter, extensions: ['kdbx'] }]
},
callback
);
}, },
getUserDataPath: function(fileName) { getUserDataPath: function(fileName) {
if (!this.userDataPath) { if (!this.userDataPath) {
@ -110,9 +115,7 @@ const Launcher = {
return callback(); return callback();
} }
fs.mkdir(stack.shift(), err => fs.mkdir(stack.shift(), err => (err ? callback(err) : create(stack, callback)));
err ? callback(err) : create(stack, callback)
);
}; };
collect(dir, stack, () => 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')); [ps.stdin, ps.stdout, ps.stderr].forEach(s => s.setEncoding('utf-8'));
let stderr = ''; let stderr = '';
let stdout = ''; let stdout = '';
ps.stderr.on('data', d => { stderr += d.toString('utf-8'); }); ps.stderr.on('data', d => {
ps.stdout.on('data', d => { stdout += d.toString('utf-8'); }); stderr += d.toString('utf-8');
});
ps.stdout.on('data', d => {
stdout += d.toString('utf-8');
});
ps.on('close', code => { ps.on('close', code => {
stdout = stdout.trim(); stdout = stdout.trim();
stderr = stderr.trim(); stderr = stderr.trim();

View File

@ -18,21 +18,28 @@ const OtpQrReader = {
if (screenshotKey) { if (screenshotKey) {
screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', '<code>' + screenshotKey + '</code>'); screenshotKey = Locale.detSetupOtpAlertBodyWith.replace('{}', '<code>' + screenshotKey + '</code>');
} }
const pasteKey = FeatureDetector.isMobile ? '' const pasteKey = FeatureDetector.isMobile
: Locale.detSetupOtpAlertBodyWith.replace('{}', ? ''
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>'); : Locale.detSetupOtpAlertBodyWith.replace(
'{}',
'<code>' + FeatureDetector.actionShortcutSymbol() + 'V</code>'
);
OtpQrReader.startListenClipoard(); OtpQrReader.startListenClipoard();
const buttons = [{result: 'manually', title: Locale.detSetupOtpManualButton, silent: true}, const buttons = [
Alerts.buttons.cancel]; { result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
Alerts.buttons.cancel
];
if (FeatureDetector.isMobile) { 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 || ''); : Locale.detSetupOtpAlertBody3.replace('{}', pasteKey || '');
OtpQrReader.alert = Alerts.alert({ OtpQrReader.alert = Alerts.alert({
icon: 'qrcode', icon: 'qrcode',
header: Locale.detSetupOtpAlert, header: Locale.detSetupOtpAlert,
body: [Locale.detSetupOtpAlertBody, body: [
Locale.detSetupOtpAlertBody,
Locale.detSetupOtpAlertBody1, Locale.detSetupOtpAlertBody1,
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''), Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
line3, line3,
@ -127,7 +134,8 @@ const OtpQrReader = {
logger.error('Error parsing QR code', err); logger.error('Error parsing QR code', err);
Alerts.error({ Alerts.error({
header: Locale.detOtpQrWrong, 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) { } catch (e) {

View File

@ -51,10 +51,18 @@ const PopupNotifier = {
const parts = part.split('='); const parts = part.split('=');
settingsObj[parts[0].trim()] = parts[1].trim(); settingsObj[parts[0].trim()] = parts[1].trim();
}); });
if (settingsObj.width) { opts.width = +settingsObj.width; } if (settingsObj.width) {
if (settingsObj.height) { opts.height = +settingsObj.height; } opts.width = +settingsObj.width;
if (settingsObj.top) { opts.y = +settingsObj.top; } }
if (settingsObj.left) { opts.x = +settingsObj.left; } 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); let win = Launcher.openWindow(opts);
win.webContents.on('will-redirect', (e, url) => { win.webContents.on('will-redirect', (e, url) => {
@ -101,8 +109,7 @@ const PopupNotifier = {
}, },
isOwnUrl(url) { isOwnUrl(url) {
return url.lastIndexOf(Links.WebApp, 0) === 0 || return url.lastIndexOf(Links.WebApp, 0) === 0 || url.lastIndexOf(location.origin + location.pathname, 0) === 0;
url.lastIndexOf(location.origin + location.pathname, 0) === 0;
}, },
processReturnToApp: function(url) { processReturnToApp: function(url) {

View File

@ -90,7 +90,10 @@ Object.defineProperty(SecureInput.prototype, 'value', {
byteLength++; 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)
);
} }
}); });

View File

@ -22,8 +22,7 @@ const SettingsManager = {
hc: 'setGenThemeHc' hc: 'setGenThemeHc'
}, },
customLocales: { customLocales: {},
},
setBySettings: function(settings) { setBySettings: function(settings) {
if (settings.get('theme')) { if (settings.get('theme')) {
@ -59,7 +58,7 @@ const SettingsManager = {
setFontSize: function(fontSize) { setFontSize: function(fontSize) {
const defaultFontSize = FeatureDetector.isMobile ? 14 : 12; 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) { setLocale(loc) {
@ -83,7 +82,7 @@ const SettingsManager = {
}, },
getBrowserLocale: function() { 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) { if (language && language.lastIndexOf('en', 0) === 0) {
return 'en'; return 'en';
} }

View File

@ -30,7 +30,9 @@ const SingleInstanceChecker = {
setKey: function(key, value) { setKey: function(key, value) {
try { try {
localStorage.setItem(key, value); localStorage.setItem(key, value);
setTimeout(() => { localStorage.removeItem(key); }, 100); setTimeout(() => {
localStorage.removeItem(key);
}, 100);
} catch (e) {} } catch (e) {}
} }
}; };

View File

@ -27,57 +27,64 @@ const Transport = {
const opts = Launcher.req('url').parse(config.url); const opts = Launcher.req('url').parse(config.url);
opts.headers = { 'User-Agent': navigator.userAgent }; opts.headers = { 'User-Agent': navigator.userAgent };
Launcher.resolveProxy(config.url, proxy => { 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) { if (proxy) {
opts.headers.Host = opts.host; opts.headers.Host = opts.host;
opts.host = proxy.host; opts.host = proxy.host;
opts.port = proxy.port; opts.port = proxy.port;
opts.path = config.url; opts.path = config.url;
} }
Launcher.req(proto).get(opts, res => { Launcher.req(proto)
logger.info('Response from ' + config.url + ': ' + res.statusCode); .get(opts, res => {
if (res.statusCode === 200) { logger.info('Response from ' + config.url + ': ' + res.statusCode);
if (config.file) { if (res.statusCode === 200) {
const file = fs.createWriteStream(tmpFile); if (config.file) {
res.pipe(file); const file = fs.createWriteStream(tmpFile);
file.on('finish', () => { res.pipe(file);
file.close(() => { file.on('finish', () => {
config.success(tmpFile); file.close(() => {
config.success(tmpFile);
});
}); });
}); file.on('error', err => {
file.on('error', err => { config.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 { } else {
let data = []; config.error('HTTP status ' + res.statusCode);
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) { .on('error', e => {
return config.error('Too many redirects'); logger.error('Cannot GET ' + config.url, e);
if (tmpFile) {
fs.unlink(tmpFile, _.noop);
} }
config.url = res.headers.location; config.error(e);
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);
});
}); });
} }
}; };

View File

@ -32,8 +32,10 @@ const Updater = {
}, },
updateInProgress: function() { updateInProgress: function() {
return UpdateModel.instance.get('status') === 'checking' || return (
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0; UpdateModel.instance.get('status') === 'checking' ||
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
);
}, },
init: function() { init: function() {
@ -55,7 +57,10 @@ const Updater = {
let timeDiff = this.MinUpdateTimeout; let timeDiff = this.MinUpdateTimeout;
const lastCheckDate = UpdateModel.instance.get('lastCheckDate'); const lastCheckDate = UpdateModel.instance.get('lastCheckDate');
if (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); this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's'); logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
@ -107,8 +112,10 @@ const Updater = {
if (!this.canAutoUpdate()) { if (!this.canAutoUpdate()) {
return; return;
} }
if (prevLastVersion === UpdateModel.instance.get('lastVersion') && if (
UpdateModel.instance.get('updateStatus') === 'ready') { prevLastVersion === UpdateModel.instance.get('lastVersion') &&
UpdateModel.instance.get('updateStatus') === 'ready'
) {
logger.info('Waiting for the user to apply downloaded update'); logger.info('Waiting for the user to apply downloaded update');
return; return;
} }
@ -238,7 +245,9 @@ const Updater = {
checkAppCacheUpdateReady: function() { checkAppCacheUpdateReady: function() {
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) { if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
try { window.applicationCache.swapCache(); } catch (e) { } try {
window.applicationCache.swapCache();
} catch (e) {}
UpdateModel.instance.set('updateStatus', 'ready'); UpdateModel.instance.set('updateStatus', 'ready');
} }
} }

View File

@ -1,18 +1,73 @@
const IconMap = [ const IconMap = [
'key', 'globe', 'exclamation-triangle', 'server', 'thumb-tack', 'key',
'comments-o', 'puzzle-piece', 'pencil-square-o', 'plug', 'newspaper-o', 'globe',
'paperclip', 'camera', 'wifi', 'link', 'battery-three-quarters', 'exclamation-triangle',
'barcode', 'certificate', 'bullseye', 'desktop', 'envelope-o', 'server',
'cog', 'clipboard', 'paper-plane-o', 'television', 'bolt', 'thumb-tack',
'inbox', 'floppy-o', 'hdd-o', 'dot-circle-o', 'expeditedssl', 'comments-o',
'terminal', 'print', 'map-signs', 'flag-checkered', 'wrench', 'puzzle-piece',
'laptop', 'archive', 'credit-card', 'windows', 'clock-o', 'pencil-square-o',
'search', 'flask', 'gamepad', 'trash-o', 'sticky-note-o', 'plug',
'ban', 'question-circle', 'cube', 'folder-o', 'folder-open-o', 'newspaper-o',
'database', 'unlock-alt', 'lock', 'check', 'pencil', 'paperclip',
'picture-o', 'book', 'list-alt', 'user-secret', 'cutlery', 'camera',
'home', 'star-o', 'linux', 'map-pin', 'apple', 'wifi',
'wikipedia-w', 'usd', 'calendar', 'mobile' '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; module.exports = IconMap;

View File

@ -1,6 +1,4 @@
const Handlebars = require('hbs'); const Handlebars = require('hbs');
// inspired by https://stackoverflow.com/questions/22103989/adding-offset-to-index-when-looping-through-items-in-handlebars/39588001#39588001 // inspired by https://stackoverflow.com/questions/22103989/adding-offset-to-index-when-looping-through-items-in-handlebars/39588001#39588001
Handlebars.registerHelper('add', (lvalue, rvalue) => ( Handlebars.registerHelper('add', (lvalue, rvalue) => parseInt(lvalue) + parseInt(rvalue));
parseInt(lvalue) + parseInt(rvalue)
));

View File

@ -12,7 +12,7 @@ Handlebars.registerHelper('res', function(key, options) {
return value; return value;
}); });
Handlebars.registerHelper('Res', function(key) { // eslint-disable-line prefer-arrow-callback Handlebars.registerHelper('Res', key => {
let value = Locale[key]; let value = Locale[key];
if (value) { if (value) {
value = value[0].toUpperCase() + value.substr(1); value = value[0].toUpperCase() + value.substr(1);

View File

@ -18,11 +18,16 @@ const Copyable = {
this.hideFieldCopyTip(); this.hideFieldCopyTip();
const fieldLabel = e.source.labelEl; const fieldLabel = e.source.labelEl;
const clipboardTime = e.copyRes.seconds; const clipboardTime = e.copyRes.seconds;
const msg = clipboardTime ? Locale.detFieldCopiedTime.replace('{}', clipboardTime) const msg = clipboardTime ? Locale.detFieldCopiedTime.replace('{}', clipboardTime) : Locale.detFieldCopied;
: Locale.detFieldCopied;
let tip; let tip;
if (!this.isHidden()) { 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; this.fieldCopyTip = tip;
tip.show(); tip.show();
} }

View File

@ -8,10 +8,7 @@ kdbxweb.ProtectedValue.prototype.isProtected = true;
kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) { kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
const value = this._value; const value = this._value;
const salt = this._salt; const salt = this._salt;
let b, let b, b1, b2, b3;
b1,
b2,
b3;
for (let i = 0, len = value.length; i < len; i++) { for (let i = 0, len = value.length; i < len; i++) {
b = value[i] ^ salt[i]; b = value[i] ^ salt[i];
if (b < 128) { if (b < 128) {
@ -20,23 +17,32 @@ kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
} }
continue; continue;
} }
i++; b1 = value[i] ^ salt[i]; i++;
if (i === len) { break; } b1 = value[i] ^ salt[i];
if (i === len) {
break;
}
if (b >= 192 && b < 224) { if (b >= 192 && b < 224) {
if (fn(((b & 0x1f) << 6) | (b1 & 0x3f)) === false) { if (fn(((b & 0x1f) << 6) | (b1 & 0x3f)) === false) {
return; return;
} }
continue; continue;
} }
i++; b2 = value[i] ^ salt[i]; i++;
if (i === len) { break; } b2 = value[i] ^ salt[i];
if (i === len) {
break;
}
if (b >= 224 && b < 240) { if (b >= 224 && b < 240) {
if (fn(((b & 0xf) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)) === false) { if (fn(((b & 0xf) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)) === false) {
return; return;
} }
} }
i++; b3 = value[i] ^ salt[i]; i++;
if (i === len) { break; } b3 = value[i] ^ salt[i];
if (i === len) {
break;
}
if (b >= 240 && b < 248) { if (b >= 240 && b < 248) {
let c = ((b & 7) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f); let c = ((b & 7) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
if (c <= 0xffff) { if (c <= 0xffff) {
@ -60,7 +66,9 @@ kdbxweb.ProtectedValue.prototype.forEachChar = function(fn) {
Object.defineProperty(kdbxweb.ProtectedValue.prototype, 'textLength', { Object.defineProperty(kdbxweb.ProtectedValue.prototype, 'textLength', {
get: function() { get: function() {
let textLength = 0; let textLength = 0;
this.forEachChar(() => { textLength++; }); this.forEachChar(() => {
textLength++;
});
return textLength; return textLength;
} }
}); });

View File

@ -75,7 +75,7 @@ _.extend(Backbone.View.prototype, {
if (this.scroll) { if (this.scroll) {
try { try {
this.scroll.dispose(); this.scroll.dispose();
} catch (e) { } } catch (e) {}
} }
Tip.hideTips(this.$el); Tip.hideTips(this.$el);
this._parentRemove(arguments); this._parentRemove(arguments);

View File

@ -114,28 +114,38 @@ const AppModel = Backbone.Model.extend({
this.fileInfos.reset(); this.fileInfos.reset();
} }
config.files config.files
.filter(file => file && file.storage && file.name && file.path && .filter(
!this.fileInfos.getMatch(file.storage, file.name, file.path)) file =>
.map(file => new FileInfoModel({ file &&
id: IdGenerator.uuid(), file.storage &&
name: file.name, file.name &&
storage: file.storage, file.path &&
path: file.path, !this.fileInfos.getMatch(file.storage, file.name, file.path)
opts: file.options )
})) .map(
file =>
new FileInfoModel({
id: IdGenerator.uuid(),
name: file.name,
storage: file.storage,
path: file.path,
opts: file.options
})
)
.reverse() .reverse()
.forEach(fi => this.fileInfos.unshift(fi)); .forEach(fi => this.fileInfos.unshift(fi));
} }
if (config.plugins) { if (config.plugins) {
const pluginsPromises = config.plugins const pluginsPromises = config.plugins.map(plugin =>
.map(plugin => PluginManager.installIfNew(plugin.url, plugin.manifest, true)); PluginManager.installIfNew(plugin.url, plugin.manifest, true)
);
return Promise.all(pluginsPromises).then(() => { return Promise.all(pluginsPromises).then(() => {
this.settings.set(config.settings); this.settings.set(config.settings);
}); });
} }
if (config.advancedSearch) { if (config.advancedSearch) {
this.advancedSearch = 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; return false;
} }
this.files.add(file); this.files.add(file);
file.get('groups').forEach(function (group) { file.get('groups').forEach(function(group) {
this.menu.groupsSection.addItem(group); this.menu.groupsSection.addItem(group);
}, this); }, this);
this._addTags(file); this._addTags(file);
@ -184,9 +194,11 @@ const AppModel = Backbone.Model.extend({
_tagsChanged: function() { _tagsChanged: function() {
if (this.tags.length) { if (this.tags.length) {
this.menu.tagsSection.set('scrollable', true); this.menu.tagsSection.set('scrollable', true);
this.menu.tagsSection.setItems(this.tags.map(tag => { this.menu.tagsSection.setItems(
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true}; this.tags.map(tag => {
})); return { title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true };
})
);
} else { } else {
this.menu.tagsSection.set('scrollable', false); this.menu.tagsSection.set('scrollable', false);
this.menu.tagsSection.removeAllItems(); this.menu.tagsSection.removeAllItems();
@ -310,8 +322,7 @@ const AppModel = Backbone.Model.extend({
getFirstSelectedGroup: function() { getFirstSelectedGroup: function() {
const selGroupId = this.filter.group; const selGroupId = this.filter.group;
let file, let file, group;
group;
if (selGroupId) { if (selGroupId) {
this.files.some(f => { this.files.some(f => {
file = f; file = f;
@ -410,19 +421,25 @@ const AppModel = Backbone.Model.extend({
openFile: function(params, callback) { openFile: function(params, callback) {
const logger = new Logger('open', params.name); const logger = new Logger('open', params.name);
logger.info('File open request'); 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')) { if (!params.opts && fileInfo && fileInfo.get('opts')) {
params.opts = fileInfo.get('opts'); params.opts = fileInfo.get('opts');
} }
if (fileInfo && fileInfo.get('modified')) { if (fileInfo && fileInfo.get('modified')) {
logger.info('Open file from cache because it is modified'); logger.info('Open file from cache because it is modified');
this.openFileFromCache(params, (err, file) => { this.openFileFromCache(
if (!err && file) { params,
logger.info('Sync just opened modified file'); (err, file) => {
_.defer(() => this.syncFile(file)); if (!err && file) {
} logger.info('Sync just opened modified file');
callback(err); _.defer(() => this.syncFile(file));
}, fileInfo); }
callback(err);
},
fileInfo
);
} else if (params.fileData) { } else if (params.fileData) {
logger.info('Open file from supplied content'); logger.info('Open file from supplied content');
const needSaveToCache = params.storage !== 'file'; const needSaveToCache = params.storage !== 'file';
@ -430,7 +447,12 @@ const AppModel = Backbone.Model.extend({
} else if (!params.storage) { } else if (!params.storage) {
logger.info('Open file from cache as main storage'); logger.info('Open file from cache as main storage');
this.openFileFromCache(params, callback, fileInfo); 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'); logger.info('Open file from cache because it is latest');
this.openFileFromCache(params, callback, fileInfo); this.openFileFromCache(params, callback, fileInfo);
} else if (!fileInfo || !fileInfo.get('openDate') || params.storage === 'file') { } else if (!fileInfo || !fileInfo.get('openDate') || params.storage === 'file') {
@ -450,17 +472,17 @@ const AppModel = Backbone.Model.extend({
} else { } else {
logger.info('Open file from content loaded from storage'); logger.info('Open file from content loaded from storage');
params.fileData = data; params.fileData = data;
params.rev = stat && stat.rev || null; params.rev = (stat && stat.rev) || null;
const needSaveToCache = storage.name !== 'file'; const needSaveToCache = storage.name !== 'file';
this.openFileWithData(params, callback, fileInfo, data, needSaveToCache); 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) { if (cacheRev && storage.stat) {
logger.info('Stat file'); logger.info('Stat file');
storage.stat(params.path, params.opts, (err, stat) => { 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); logger.info('Open file from cache because ' + (err ? 'stat error' : 'it is latest'), err);
this.openFileFromCache(params, callback, fileInfo); this.openFileFromCache(params, callback, fileInfo);
} else if (stat) { } else if (stat) {
@ -476,13 +498,17 @@ const AppModel = Backbone.Model.extend({
} }
} else { } else {
logger.info('Open file from cache, will sync after load', params.storage); logger.info('Open file from cache, will sync after load', params.storage);
this.openFileFromCache(params, (err, file) => { this.openFileFromCache(
if (!err && file) { params,
logger.info('Sync just opened file'); (err, file) => {
_.defer(() => this.syncFile(file)); if (!err && file) {
} logger.info('Sync just opened file');
callback(err); _.defer(() => this.syncFile(file));
}, fileInfo); }
callback(err);
},
fileInfo
);
} }
}, },
@ -520,8 +546,8 @@ const AppModel = Backbone.Model.extend({
path: params.path, path: params.path,
keyFileName: params.keyFileName, keyFileName: params.keyFileName,
keyFilePath: params.keyFilePath, keyFilePath: params.keyFilePath,
backup: fileInfo && fileInfo.get('backup') || null, backup: (fileInfo && fileInfo.get('backup')) || null,
fingerprint: fileInfo && fileInfo.get('fingerprint') || null fingerprint: (fileInfo && fileInfo.get('fingerprint')) || null
}); });
const openComplete = err => { const openComplete = err => {
if (err) { if (err) {
@ -545,7 +571,7 @@ const AppModel = Backbone.Model.extend({
logger.info('Save loaded file to cache'); logger.info('Save loaded file to cache');
Storage.cache.save(file.id, null, params.fileData); 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.setFileOpts(file, params.opts);
this.addToLastOpenFiles(file, rev); this.addToLastOpenFiles(file, rev);
this.addFile(file); this.addFile(file);
@ -590,7 +616,14 @@ const AppModel = Backbone.Model.extend({
}, },
addToLastOpenFiles: function(file, rev) { 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 dt = new Date();
const fileInfo = new FileInfoModel({ const fileInfo = new FileInfoModel({
id: file.id, id: file.id,
@ -642,9 +675,12 @@ const AppModel = Backbone.Model.extend({
fileOpened: function(file, data, params) { fileOpened: function(file, data, params) {
if (file.get('storage') === 'file') { if (file.get('storage') === 'file') {
Storage.file.watch(file.get('path'), _.debounce(() => { Storage.file.watch(
this.syncFile(file); file.get('path'),
}, Timeouts.FileChangeSync)); _.debounce(() => {
this.syncFile(file);
}, Timeouts.FileChangeSync)
);
} }
if (file.isKeyChangePending(true)) { if (file.isKeyChangePending(true)) {
Backbone.trigger('key-change-pending', { file: file }); Backbone.trigger('key-change-pending', { file: file });
@ -671,8 +707,10 @@ const AppModel = Backbone.Model.extend({
}, },
getFileInfo: function(file) { getFileInfo: function(file) {
return this.fileInfos.get(file.id) || return (
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); this.fileInfos.get(file.id) ||
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'))
);
}, },
syncFile: function(file, options, callback) { syncFile: function(file, options, callback) {
@ -713,7 +751,9 @@ const AppModel = Backbone.Model.extend({
} }
file.setSyncProgress(); file.setSyncProgress();
const complete = (err, savedToCache) => { const complete = (err, savedToCache) => {
if (!err) { savedToCache = true; } if (!err) {
savedToCache = true;
}
logger.info('Sync finished', err || 'no error'); logger.info('Sync finished', err || 'no error');
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache); file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
fileInfo.set({ fileInfo.set({
@ -735,7 +775,9 @@ const AppModel = Backbone.Model.extend({
this.fileInfos.unshift(fileInfo); this.fileInfos.unshift(fileInfo);
} }
this.fileInfos.save(); this.fileInfos.save();
if (callback) { callback(err); } if (callback) {
callback(err);
}
}; };
if (!storage) { if (!storage) {
if (!file.get('modified') && fileInfo.id === file.id) { if (!file.get('modified') && fileInfo.id === file.id) {
@ -744,8 +786,10 @@ const AppModel = Backbone.Model.extend({
} }
logger.info('Local, save to cache'); logger.info('Local, save to cache');
file.getData((data, err) => { file.getData((data, err) => {
if (err) { return complete(err); } if (err) {
Storage.cache.save(fileInfo.id, null, data, (err) => { return complete(err);
}
Storage.cache.save(fileInfo.id, null, data, err => {
logger.info('Saved to cache', err || 'no error'); logger.info('Saved to cache', err || 'no error');
complete(err); complete(err);
if (!err) { if (!err) {
@ -763,8 +807,10 @@ const AppModel = Backbone.Model.extend({
logger.info('Load from storage, attempt ' + loadLoops); logger.info('Load from storage, attempt ' + loadLoops);
Storage[storage].load(path, opts, (err, data, stat) => { Storage[storage].load(path, opts, (err, data, stat) => {
logger.info('Load from storage', stat, err || 'no error'); logger.info('Load from storage', stat, err || 'no error');
if (err) { return complete(err); } if (err) {
file.mergeOrUpdate(data, options.remoteKey, (err) => { return complete(err);
}
file.mergeOrUpdate(data, options.remoteKey, err => {
logger.info('Merge complete', err || 'no error'); logger.info('Merge complete', err || 'no error');
this.refresh(); this.refresh();
if (err) { if (err) {
@ -784,8 +830,10 @@ const AppModel = Backbone.Model.extend({
saveToCacheAndStorage(); saveToCacheAndStorage();
} else if (file.get('dirty')) { } else if (file.get('dirty')) {
logger.info('Saving not modified dirty file to cache'); logger.info('Saving not modified dirty file to cache');
Storage.cache.save(fileInfo.id, null, data, (err) => { Storage.cache.save(fileInfo.id, null, data, err => {
if (err) { return complete(err); } if (err) {
return complete(err);
}
file.set('dirty', false); file.set('dirty', false);
logger.info('Complete, remove dirty flag'); logger.info('Complete, remove dirty flag');
complete(); complete();
@ -797,38 +845,46 @@ const AppModel = Backbone.Model.extend({
}); });
}); });
}; };
const saveToStorage = (data) => { const saveToStorage = data => {
logger.info('Save data to storage'); logger.info('Save data to storage');
const storageRev = fileInfo.get('storage') === storage ? fileInfo.get('rev') : undefined; const storageRev = fileInfo.get('storage') === storage ? fileInfo.get('rev') : undefined;
Storage[storage].save(path, opts, data, (err, stat) => { Storage[storage].save(
if (err && err.revConflict) { path,
logger.info('Save rev conflict, reloading from storage'); opts,
loadFromStorageAndMerge(); data,
} else if (err) { (err, stat) => {
logger.info('Error saving data to storage'); if (err && err.revConflict) {
complete(err); logger.info('Save rev conflict, reloading from storage');
} else { loadFromStorageAndMerge();
if (stat && stat.rev) { } else if (err) {
logger.info('Update rev in file info'); logger.info('Error saving data to storage');
fileInfo.set('rev', stat.rev); 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); storageRev
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);
}; };
const saveToCacheAndStorage = () => { const saveToCacheAndStorage = () => {
logger.info('Getting file data for saving'); logger.info('Getting file data for saving');
file.getData((data, err) => { file.getData((data, err) => {
if (err) { return complete(err); } if (err) {
return complete(err);
}
if (storage === 'file') { if (storage === 'file') {
logger.info('Saving to file storage'); logger.info('Saving to file storage');
saveToStorage(data); saveToStorage(data);
@ -837,8 +893,10 @@ const AppModel = Backbone.Model.extend({
saveToStorage(data); saveToStorage(data);
} else { } else {
logger.info('Saving to cache'); logger.info('Saving to cache');
Storage.cache.save(fileInfo.id, null, data, (err) => { Storage.cache.save(fileInfo.id, null, data, err => {
if (err) { return complete(err); } if (err) {
return complete(err);
}
file.set('dirty', false); file.set('dirty', false);
logger.info('Saved to cache, saving to storage'); logger.info('Saved to cache, saving to storage');
saveToStorage(data); saveToStorage(data);
@ -854,9 +912,9 @@ const AppModel = Backbone.Model.extend({
saveToCacheAndStorage(); saveToCacheAndStorage();
} else if (file.get('dirty')) { } else if (file.get('dirty')) {
logger.info('Stat error, dirty, save to cache', err || 'no error'); logger.info('Stat error, dirty, save to cache', err || 'no error');
file.getData((data) => { file.getData(data => {
if (data) { if (data) {
Storage.cache.save(fileInfo.id, null, data, (e) => { Storage.cache.save(fileInfo.id, null, data, e => {
if (!e) { if (!e) {
file.set('dirty', false); file.set('dirty', false);
} }
@ -896,7 +954,7 @@ const AppModel = Backbone.Model.extend({
this.fileInfos.save(); this.fileInfos.save();
}, },
unsetKeyFile: function (fileId) { unsetKeyFile: function(fileId) {
const fileInfo = this.fileInfos.get(fileId); const fileInfo = this.fileInfos.get(fileId);
fileInfo.set({ fileInfo.set({
keyFileName: null, keyFileName: null,
@ -927,7 +985,7 @@ const AppModel = Backbone.Model.extend({
if (Storage[backup.storage].getPathForName) { if (Storage[backup.storage].getPathForName) {
path = Storage[backup.storage].getPathForName(path); path = Storage[backup.storage].getPathForName(path);
} }
Storage[backup.storage].save(path, opts, data, (err) => { Storage[backup.storage].save(path, opts, data, err => {
if (err) { if (err) {
logger.error('Backup error', err); logger.error('Backup error', err);
} else { } else {
@ -1002,10 +1060,16 @@ const AppModel = Backbone.Model.extend({
if (dt.getTime() <= Date.now()) { if (dt.getTime() <= Date.now()) {
needBackup = true; needBackup = true;
} }
logger.debug('Last backup time: ' + new Date(backup.lastTime) + logger.debug(
', schedule: ' + backup.schedule + 'Last backup time: ' +
', next time: ' + dt + new Date(backup.lastTime) +
', ' + (needBackup ? 'backup now' : 'skip backup')); ', schedule: ' +
backup.schedule +
', next time: ' +
dt +
', ' +
(needBackup ? 'backup now' : 'skip backup')
);
} }
if (!backup.pending) { if (!backup.pending) {
backup.pending = true; backup.pending = true;

View File

@ -57,7 +57,7 @@ const AppSettingsModel = Backbone.Model.extend({
return SettingsStore.load('app-settings').then(data => { return SettingsStore.load('app-settings').then(data => {
if (data) { if (data) {
this.upgrade(data); this.upgrade(data);
this.set(data, {silent: true}); this.set(data, { silent: true });
} }
}); });
}, },

View File

@ -3,8 +3,7 @@ const Backbone = require('backbone');
const AttachmentModel = Backbone.Model.extend({ const AttachmentModel = Backbone.Model.extend({
defaults: {}, defaults: {},
initialize: function() { initialize: function() {},
},
setAttachment: function(att) { setAttachment: function(att) {
this.title = att.title; this.title = att.title;
@ -21,28 +20,83 @@ const AttachmentModel = Backbone.Model.extend({
_getIcon: function(ext) { _getIcon: function(ext) {
switch (ext) { switch (ext) {
case 'txt': case 'log': case 'rtf': case 'pem': case 'txt':
case 'log':
case 'rtf':
case 'pem':
return 'file-text-o'; return 'file-text-o';
case 'html': case 'htm': case 'js': case 'css': case 'xml': case 'config': case 'json': case 'yaml': case 'html':
case 'cpp': case 'c': case 'h': case 'cc': case 'hpp': case 'mm': case 'cs': case 'php': case 'sh': case 'htm':
case 'py': case 'java': case 'rb': case 'cfg': case 'properties': case 'yml': case 'asm': case 'bat': 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'; return 'file-code-o';
case 'pdf': case 'pdf':
return 'file-pdf-o'; return 'file-pdf-o';
case 'zip': case 'rar': case 'bz': case 'bz2': case '7z': case 'gzip': case 'gz': case 'tar': case 'zip':
case 'cab': case 'ace': case 'dmg': case 'jar': 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'; return 'file-archive-o';
case 'doc': case 'docx': case 'doc':
case 'docx':
return 'file-word-o'; return 'file-word-o';
case 'xls': case 'xlsx': case 'xls':
case 'xlsx':
return 'file-excel-o'; return 'file-excel-o';
case 'ppt': case 'pptx': case 'ppt':
case 'pptx':
return 'file-powerpoint-o'; 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'; 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'; return 'file-video-o';
case 'mp3': case 'wav': case 'flac': case 'mp3':
case 'wav':
case 'flac':
return 'file-audio-o'; return 'file-audio-o';
} }
return 'file-o'; return 'file-o';
@ -50,14 +104,43 @@ const AttachmentModel = Backbone.Model.extend({
_getMimeType: function(ext) { _getMimeType: function(ext) {
switch (ext) { switch (ext) {
case 'txt': case 'log': case 'txt':
case 'html': case 'htm': case 'js': case 'css': case 'xml': case 'config': case 'json': case 'yaml': case 'log':
case 'cpp': case 'c': case 'h': case 'cc': case 'hpp': case 'mm': case 'cs': case 'php': case 'sh': case 'html':
case 'py': case 'java': case 'rb': case 'cfg': case 'properties': case 'yml': case 'asm': case 'pem': 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'; return 'text/plain';
case 'pdf': case 'pdf':
return 'application/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; return 'image/' + ext;
} }
}, },

View File

@ -13,7 +13,16 @@ const EntryModel = Backbone.Model.extend({
urlRegex: /^https?:\/\//i, urlRegex: /^https?:\/\//i,
fieldRefRegex: /^\{REF:([TNPAU])@I:(\w{32})}$/, 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'], fieldRefFields: ['title', 'password', 'user', 'url', 'notes'],
fieldRefIds: { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' }, fieldRefIds: { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' },
@ -32,7 +41,7 @@ const EntryModel = Backbone.Model.extend({
_fillByEntry: function() { _fillByEntry: function() {
const entry = this.entry; 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.fileName = this.file.get('name');
this.groupName = this.group.get('title'); this.groupName = this.group.get('title');
this.title = this._getFieldString('Title'); this.title = this._getFieldString('Title');
@ -120,7 +129,8 @@ const EntryModel = Backbone.Model.extend({
_buildAutoType: function() { _buildAutoType: function() {
this.autoTypeEnabled = this.entry.autoType.enabled; 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.autoTypeSequence = this.entry.autoType.defaultSequence;
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem); this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
}, },
@ -150,14 +160,18 @@ const EntryModel = Backbone.Model.extend({
_attachmentsToModel: function(binaries) { _attachmentsToModel: function(binaries) {
const att = []; const att = [];
_.forEach(binaries, (data, title) => { _.forEach(
if (data && data.ref) { binaries,
data = data.value; (data, title) => {
} if (data && data.ref) {
if (data) { data = data.value;
att.push(AttachmentModel.fromAttachment({data: data, title: title})); }
} if (data) {
}, this); att.push(AttachmentModel.fromAttachment({ data: data, title: title }));
}
},
this
);
return att; return att;
}, },
@ -183,21 +197,25 @@ const EntryModel = Backbone.Model.extend({
}, },
matches: function(filter) { matches: function(filter) {
return !filter || return (
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) && !filter ||
(!filter.textLower || (filter.advanced ? this.matchesAdv(filter) : this.searchText.indexOf(filter.textLower) >= 0)) && ((!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
(!filter.color || filter.color === true && this.searchColor || this.searchColor === filter.color) && (!filter.textLower ||
(!filter.autoType || this.autoTypeEnabled); (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) { matchesAdv: function(filter) {
const adv = filter.advanced; const adv = filter.advanced;
let search, let search, match;
match;
if (adv.regex) { if (adv.regex) {
try { try {
search = new RegExp(filter.text, adv.cs ? '' : 'i'); search = new RegExp(filter.text, adv.cs ? '' : 'i');
} catch (e) { return false; } } catch (e) {
return false;
}
match = this.matchRegex; match = this.matchRegex;
} else if (adv.cs) { } else if (adv.cs) {
search = filter.text; search = filter.text;
@ -387,7 +405,7 @@ const EntryModel = Backbone.Model.extend({
}, },
setField: function(field, val, allowEmpty) { 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) { if (hasValue || allowEmpty || this.builtInFields.indexOf(field) >= 0) {
this._entryModified(); this._entryModified();
val = this.sanitizeFieldValue(val); val = this.sanitizeFieldValue(val);
@ -542,8 +560,7 @@ const EntryModel = Backbone.Model.extend({
if (settings && settings.isProtected) { if (settings && settings.isProtected) {
settings = settings.getText(); settings = settings.getText();
} }
let period, let period, digits;
digits;
if (settings) { if (settings) {
settings = settings.split(';'); settings = settings.split(';');
if (settings.length > 0 && settings[0] > 0) { if (settings.length > 0 && settings[0] > 0) {
@ -601,8 +618,9 @@ const EntryModel = Backbone.Model.extend({
setAutoTypeObfuscation: function(enabled) { setAutoTypeObfuscation: function(enabled) {
this._entryModified(); this._entryModified();
this.entry.autoType.obfuscation = this.entry.autoType.obfuscation = enabled
enabled ? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard : kdbxweb.Consts.AutoTypeObfuscationOptions.None; ? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard
: kdbxweb.Consts.AutoTypeObfuscationOptions.None;
this._buildAutoType(); this._buildAutoType();
}, },
@ -674,20 +692,17 @@ const EntryModel = Backbone.Model.extend({
const fieldNames = Object.keys(this.fields); const fieldNames = Object.keys(this.fields);
_.forEach(fieldNames, field => { _.forEach(fieldNames, field => {
ranking.push( ranking.push({
{ field: field,
field: field, multiplicator: 2
multiplicator: 2 });
}
);
}); });
_.forEach(ranking, rankingEntry => { _.forEach(ranking, rankingEntry => {
if (this._getFieldString(rankingEntry.field).toLowerCase() !== '') { if (this._getFieldString(rankingEntry.field).toLowerCase() !== '') {
const calculatedRank = Ranking.getStringRank( const calculatedRank =
searchString, Ranking.getStringRank(searchString, this._getFieldString(rankingEntry.field).toLowerCase()) *
this._getFieldString(rankingEntry.field).toLowerCase() rankingEntry.multiplicator;
) * rankingEntry.multiplicator;
rank += calculatedRank; rank += calculatedRank;
} }
}); });

View File

@ -19,11 +19,15 @@ const FileInfoModel = Backbone.Model.extend({
}, },
initialize: function(data, options) { initialize: function(data, options) {
_.each(data, function(val, key) { _.each(
if (/Date$/.test(key)) { data,
this.set(key, val ? new Date(val) : null, options); function(val, key) {
} if (/Date$/.test(key)) {
}, this); this.set(key, val ? new Date(val) : null, options);
}
},
this
);
} }
}); });

View File

@ -57,8 +57,17 @@ const FileModel = Backbone.Model.extend({
if (keyFileData) { if (keyFileData) {
kdbxweb.ByteUtils.zeroBuffer(keyFileData); kdbxweb.ByteUtils.zeroBuffer(keyFileData);
} }
logger.info('Opened file ' + this.get('name') + ': ' + logger.ts(ts) + ', ' + logger.info(
this.kdfArgsToString(db.header) + ', ' + Math.round(fileData.byteLength / 1024) + ' kB'); 'Opened file ' +
this.get('name') +
': ' +
logger.ts(ts) +
', ' +
this.kdfArgsToString(db.header) +
', ' +
Math.round(fileData.byteLength / 1024) +
' kB'
);
callback(); callback();
}) })
.catch(err => { .catch(err => {
@ -77,13 +86,17 @@ const FileModel = Backbone.Model.extend({
kdfArgsToString: function(header) { kdfArgsToString: function(header) {
if (header.kdfParameters) { if (header.kdfParameters) {
return header.kdfParameters.keys().map(key => { return header.kdfParameters
const val = header.kdfParameters.get(key); .keys()
if (val instanceof ArrayBuffer) { .map(key => {
return; const val = header.kdfParameters.get(key);
} if (val instanceof ArrayBuffer) {
return key + '=' + val; return;
}).filter(p => p).join('&'); }
return key + '=' + val;
})
.filter(p => p)
.join('&');
} else if (header.keyEncryptionRounds) { } else if (header.keyEncryptionRounds) {
return header.keyEncryptionRounds + ' rounds'; return header.keyEncryptionRounds + ' rounds';
} else { } else {
@ -127,14 +140,13 @@ const FileModel = Backbone.Model.extend({
const password = kdbxweb.ProtectedValue.fromString('demo'); const password = kdbxweb.ProtectedValue.fromString('demo');
const credentials = new kdbxweb.Credentials(password); const credentials = new kdbxweb.Credentials(password);
const demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData)); const demoFile = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(demoFileData));
kdbxweb.Kdbx.load(demoFile, credentials) kdbxweb.Kdbx.load(demoFile, credentials).then(db => {
.then(db => { this.db = db;
this.db = db; this.set('name', 'Demo');
this.set('name', 'Demo'); this.readModel();
this.readModel(); this.setOpenFile({ passwordLength: 4, demo: true });
this.setOpenFile({passwordLength: 4, demo: true}); callback();
callback(); });
});
}, },
setOpenFile: function(props) { setOpenFile: function(props) {
@ -153,17 +165,20 @@ const FileModel = Backbone.Model.extend({
readModel: function() { readModel: function() {
const groups = new GroupCollection(); const groups = new GroupCollection();
this.set({ this.set(
uuid: this.db.getDefaultGroup().uuid.toString(), {
groups: groups, uuid: this.db.getDefaultGroup().uuid.toString(),
defaultUser: this.db.meta.defaultUser, groups: groups,
recycleBinEnabled: this.db.meta.recycleBinEnabled, defaultUser: this.db.meta.defaultUser,
historyMaxItems: this.db.meta.historyMaxItems, recycleBinEnabled: this.db.meta.recycleBinEnabled,
historyMaxSize: this.db.meta.historyMaxSize, historyMaxItems: this.db.meta.historyMaxItems,
keyEncryptionRounds: this.db.header.keyEncryptionRounds, historyMaxSize: this.db.meta.historyMaxSize,
keyChangeForce: this.db.meta.keyChangeForce, keyEncryptionRounds: this.db.header.keyEncryptionRounds,
kdfParameters: this.readKdfParams() keyChangeForce: this.db.meta.keyChangeForce,
}, { silent: true }); kdfParameters: this.readKdfParams()
},
{ silent: true }
);
this.db.groups.forEach(function(group) { this.db.groups.forEach(function(group) {
let groupModel = this.getGroup(this.subId(group.uuid.id)); let groupModel = this.getGroup(this.subId(group.uuid.id));
if (groupModel) { if (groupModel) {
@ -204,12 +219,15 @@ const FileModel = Backbone.Model.extend({
buildObjectMap: function() { buildObjectMap: function() {
const entryMap = {}; const entryMap = {};
const groupMap = {}; const groupMap = {};
this.forEachGroup(group => { this.forEachGroup(
groupMap[group.id] = group; group => {
group.forEachOwnEntry(null, entry => { groupMap[group.id] = group;
entryMap[entry.id] = entry; group.forEachOwnEntry(null, entry => {
}); entryMap[entry.id] = entry;
}, { includeDisabled: true }); });
},
{ includeDisabled: true }
);
this.entryMap = entryMap; this.entryMap = entryMap;
this.groupMap = groupMap; this.groupMap = groupMap;
}, },
@ -371,7 +389,8 @@ const FileModel = Backbone.Model.extend({
binaries: true binaries: true
}); });
this.db.cleanup({ binaries: true }); this.db.cleanup({ binaries: true });
this.db.save() this.db
.save()
.then(data => { .then(data => {
cb(data); cb(data);
}) })
@ -382,8 +401,9 @@ const FileModel = Backbone.Model.extend({
}, },
getXml: function(cb) { getXml: function(cb) {
this.db.saveXml() this.db.saveXml().then(xml => {
.then(xml => { cb(xml); }); cb(xml);
});
}, },
getKeyFileHash: function() { getKeyFileHash: function() {
@ -508,7 +528,9 @@ const FileModel = Backbone.Model.extend({
this.db.meta.name = name; this.db.meta.name = name;
this.db.meta.nameChanged = new Date(); this.db.meta.nameChanged = new Date();
this.set('name', name); this.set('name', name);
this.get('groups').first().setName(name); this.get('groups')
.first()
.setName(name);
this.setModified(); this.setModified();
this.reload(); this.reload();
}, },
@ -571,10 +593,13 @@ const FileModel = Backbone.Model.extend({
const trashGroup = this.getTrashGroup(); const trashGroup = this.getTrashGroup();
if (trashGroup) { if (trashGroup) {
let modified = false; let modified = false;
trashGroup.getOwnSubGroups().slice().forEach(function(group) { trashGroup
this.db.move(group, null); .getOwnSubGroups()
modified = true; .slice()
}, this); .forEach(function(group) {
this.db.move(group, null);
modified = true;
}, this);
trashGroup.group.entries.slice().forEach(function(entry) { trashGroup.group.entries.slice().forEach(function(entry) {
this.db.move(entry, null); this.db.move(entry, null);
modified = true; modified = true;

View File

@ -24,28 +24,35 @@ const GroupModel = MenuItemModel.extend({
}), }),
initialize: function() { initialize: function() {
if (!GroupCollection) { GroupCollection = require('../collections/group-collection'); } if (!GroupCollection) {
if (!EntryCollection) { EntryCollection = require('../collections/entry-collection'); } GroupCollection = require('../collections/group-collection');
}
if (!EntryCollection) {
EntryCollection = require('../collections/entry-collection');
}
}, },
setGroup: function(group, file, parentGroup) { setGroup: function(group, file, parentGroup) {
const isRecycleBin = group.uuid.equals(file.db.meta.recycleBinUuid); const isRecycleBin = group.uuid.equals(file.db.meta.recycleBinUuid);
const id = file.subId(group.uuid.id); const id = file.subId(group.uuid.id);
this.set({ this.set(
id: id, {
uuid: group.uuid.id, id: id,
expanded: group.expanded, uuid: group.uuid.id,
visible: !isRecycleBin, expanded: group.expanded,
items: new GroupCollection(), visible: !isRecycleBin,
entries: new EntryCollection(), items: new GroupCollection(),
filterValue: id, entries: new EntryCollection(),
enableSearching: group.enableSearching, filterValue: id,
enableAutoType: group.enableAutoType, enableSearching: group.enableSearching,
autoTypeSeq: group.defaultAutoTypeSeq, enableAutoType: group.enableAutoType,
top: !parentGroup, autoTypeSeq: group.defaultAutoTypeSeq,
drag: !!parentGroup, top: !parentGroup,
collapsible: !!parentGroup drag: !!parentGroup,
}, { silent: true }); collapsible: !!parentGroup
},
{ silent: true }
);
this.group = group; this.group = group;
this.file = file; this.file = file;
this.parentGroup = parentGroup; this.parentGroup = parentGroup;
@ -77,14 +84,17 @@ const GroupModel = MenuItemModel.extend({
}, },
_fillByGroup: function(silent) { _fillByGroup: function(silent) {
this.set({ this.set(
title: this.parentGroup ? this.group.name : this.file.get('name'), {
iconId: this.group.icon, title: this.parentGroup ? this.group.name : this.file.get('name'),
icon: this._iconFromId(this.group.icon), iconId: this.group.icon,
customIcon: this._buildCustomIcon(), icon: this._iconFromId(this.group.icon),
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null, customIcon: this._buildCustomIcon(),
expanded: this.group.expanded !== false customIconId: this.group.customIcon ? this.group.customIcon.toString() : null,
}, { silent: silent }); expanded: this.group.expanded !== false
},
{ silent: silent }
);
}, },
_iconFromId: function(id) { _iconFromId: function(id) {
@ -129,10 +139,12 @@ const GroupModel = MenuItemModel.extend({
}, },
matches: function(filter) { matches: function(filter) {
return (filter && filter.includeDisabled || return (
this.group.enableSearching !== false && ((filter && filter.includeDisabled) ||
!this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup) (this.group.enableSearching !== false &&
) && (!filter || !filter.autoType || this.group.enableAutoType !== false); !this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup))) &&
(!filter || !filter.autoType || this.group.enableAutoType !== false)
);
}, },
getOwnSubGroups: function() { getOwnSubGroups: function() {

View File

@ -16,22 +16,40 @@ const MenuModel = Backbone.Model.extend({
initialize: function() { initialize: function() {
this.menus = {}; this.menus = {};
this.allItemsSection = new MenuSectionModel([{ locTitle: 'menuAllItems', icon: 'th-large', active: true, this.allItemsSection = new MenuSectionModel([
shortcut: Keys.DOM_VK_A, filterKey: '*' }]); { locTitle: 'menuAllItems', icon: 'th-large', active: true, shortcut: Keys.DOM_VK_A, filterKey: '*' }
]);
this.allItemsItem = this.allItemsSection.get('items').models[0]; this.allItemsItem = this.allItemsSection.get('items').models[0];
this.groupsSection = new GroupsMenuModel(); this.groupsSection = new GroupsMenuModel();
this.colorsSection = new MenuSectionModel([{ locTitle: 'menuColors', icon: 'bookmark', shortcut: Keys.DOM_VK_C, this.colorsSection = new MenuSectionModel([
cls: 'menu__item-colors', filterKey: 'color', filterValue: true }]); {
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]; this.colorsItem = this.colorsSection.get('items').models[0];
const defTags = [this._getDefaultTagItem()]; const defTags = [this._getDefaultTagItem()];
this.tagsSection = new MenuSectionModel(defTags); this.tagsSection = new MenuSectionModel(defTags);
this.tagsSection.set({ scrollable: true, drag: true }); this.tagsSection.set({ scrollable: true, drag: true });
this.tagsSection.defaultItems = defTags; this.tagsSection.defaultItems = defTags;
this.trashSection = new MenuSectionModel([{ locTitle: 'menuTrash', icon: 'trash', shortcut: Keys.DOM_VK_D, this.trashSection = new MenuSectionModel([
filterKey: 'trash', filterValue: true, drop: true }]); {
locTitle: 'menuTrash',
icon: 'trash',
shortcut: Keys.DOM_VK_D,
filterKey: 'trash',
filterValue: true,
drop: true
}
]);
Colors.AllColors.forEach(color => { Colors.AllColors.forEach(color => {
this.colorsSection.get('items').models[0] this.colorsSection
.addOption({ cls: 'fa ' + color + '-color', value: color, filterValue: color }); .get('items')
.models[0].addOption({ cls: 'fa ' + color + '-color', value: color, filterValue: color });
}); });
this.menus.app = new MenuSectionCollection([ this.menus.app = new MenuSectionCollection([
this.allItemsSection, this.allItemsSection,
@ -41,8 +59,12 @@ const MenuModel = Backbone.Model.extend({
this.trashSection this.trashSection
]); ]);
this.generalSection = new MenuSectionModel([{ locTitle: 'menuSetGeneral', icon: 'cog', page: 'general', active: true }]); this.generalSection = new MenuSectionModel([
this.shortcutsSection = new MenuSectionModel([{ locTitle: 'shortcuts', icon: 'keyboard-o', page: 'shortcuts' }]); { 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.pluginsSection = new MenuSectionModel([{ locTitle: 'plugins', icon: 'puzzle-piece', page: 'plugins' }]);
this.aboutSection = new MenuSectionModel([{ locTitle: 'menuSetAbout', icon: 'info', page: 'about' }]); this.aboutSection = new MenuSectionModel([{ locTitle: 'menuSetAbout', icon: 'info', page: 'about' }]);
this.helpSection = new MenuSectionModel([{ locTitle: 'help', icon: 'question', page: 'help' }]); this.helpSection = new MenuSectionModel([{ locTitle: 'help', icon: 'question', page: 'help' }]);
@ -66,7 +88,9 @@ const MenuModel = Backbone.Model.extend({
select: function(sel) { select: function(sel) {
const sections = this.get('sections'); 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) { if (sections === this.menus.app) {
this.colorsItem.get('options').forEach(opt => opt.set('active', opt === sel.option)); 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' : ''; const selColor = sel.item === this.colorsItem && sel.option ? sel.option.get('value') + '-color' : '';
@ -95,7 +119,7 @@ const MenuModel = Backbone.Model.extend({
if (items) { if (items) {
items.forEach(it => { items.forEach(it => {
if (it.get('active') && previousItem) { if (it.get('active') && previousItem) {
this.select({item: previousItem}); this.select({ item: previousItem });
return false; return false;
} }
return processSection(it); return processSection(it);
@ -114,8 +138,8 @@ const MenuModel = Backbone.Model.extend({
if (section.has('visible') && !section.get('visible')) { if (section.has('visible') && !section.get('visible')) {
return true; return true;
} }
if (section.has('active') && activeItem && (section !== activeItem)) { if (section.has('active') && activeItem && section !== activeItem) {
this.select({item: section}); this.select({ item: section });
activeItem = null; activeItem = null;
return false; return false;
} }
@ -146,18 +170,24 @@ const MenuModel = Backbone.Model.extend({
_setLocale: function() { _setLocale: function() {
[this.menus.app, this.menus.settings].forEach(menu => { [this.menus.app, this.menus.settings].forEach(menu => {
menu.each(section => section.get('items').each(item => { menu.each(section =>
if (item.get('locTitle')) { section.get('items').each(item => {
item.set('title', Format.capFirst(Locale[item.get('locTitle')])); if (item.get('locTitle')) {
} item.set('title', Format.capFirst(Locale[item.get('locTitle')]));
})); }
})
);
}); });
this.tagsSection.defaultItems[0] = this._getDefaultTagItem(); this.tagsSection.defaultItems[0] = this._getDefaultTagItem();
}, },
_getDefaultTagItem: function() { _getDefaultTagItem: function() {
return { title: Format.capFirst(Locale.tags), icon: 'tags', defaultItem: true, return {
disabled: { header: Locale.menuAlertNoTags, body: Locale.menuAlertNoTagsBody, icon: 'tags' } }; title: Format.capFirst(Locale.tags),
icon: 'tags',
defaultItem: true,
disabled: { header: Locale.menuAlertNoTags, body: Locale.menuAlertNoTagsBody, icon: 'tags' }
};
}, },
setMenu: function(type) { setMenu: function(type) {

View File

@ -15,7 +15,7 @@ const RuntimeDataModel = Backbone.Model.extend({
// we're not using cookies here now // we're not using cookies here now
delete data.cookies; delete data.cookies;
} }
this.set(data, {silent: true}); this.set(data, { silent: true });
} }
}); });
}, },

View File

@ -15,8 +15,7 @@ const UpdateModel = Backbone.Model.extend({
updateManual: false updateManual: false
}, },
initialize: function() { initialize: function() {},
},
load: function() { load: function() {
return SettingsStore.load('update-info').then(data => { return SettingsStore.load('update-info').then(data => {
@ -27,8 +26,9 @@ const UpdateModel = Backbone.Model.extend({
data[key] = val ? new Date(val) : null; data[key] = val ? new Date(val) : null;
} }
}); });
this.set(data, {silent: true}); this.set(data, { silent: true });
} catch (e) { /* failed to load model */ } catch (e) {
/* failed to load model */
} }
} }
}); });

View File

@ -50,17 +50,16 @@ const PluginGallery = {
verifySignature(gallery) { verifySignature(gallery) {
const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, ''); const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, '');
return SignatureVerifier.verify( return SignatureVerifier.verify(kdbxweb.ByteUtils.stringToBytes(dataToVerify), gallery.signature)
kdbxweb.ByteUtils.stringToBytes(dataToVerify), .then(isValid => {
gallery.signature if (isValid) {
).then(isValid => { return gallery;
if (isValid) { }
return gallery; this.logger.error('JSON signature invalid');
} })
this.logger.error('JSON signature invalid'); .catch(e => {
}).catch(e => { this.logger.error('Error verifying plugins signature', e);
this.logger.error('Error verifying plugins signature', e); });
});
}, },
getCachedGallery() { getCachedGallery() {

View File

@ -44,21 +44,23 @@ const PluginManager = Backbone.Model.extend({
install(url, expectedManifest, skipSignatureValidation) { install(url, expectedManifest, skipSignatureValidation) {
this.trigger('change'); this.trigger('change');
return Plugin.loadFromUrl(url, expectedManifest).then(plugin => { return Plugin.loadFromUrl(url, expectedManifest)
return this.uninstall(plugin.id).then(() => { .then(plugin => {
if (skipSignatureValidation) { return this.uninstall(plugin.id).then(() => {
plugin.set('skipSignatureValidation', true); if (skipSignatureValidation) {
} plugin.set('skipSignatureValidation', true);
return plugin.install(true, false).then(() => { }
this.get('plugins').push(plugin); return plugin.install(true, false).then(() => {
this.trigger('change'); this.get('plugins').push(plugin);
this.saveState(); this.trigger('change');
this.saveState();
});
}); });
})
.catch(e => {
this.trigger('change');
throw e;
}); });
}).catch(e => {
this.trigger('change');
throw e;
});
}, },
installIfNew(url, expectedManifest, skipSignatureValidation) { installIfNew(url, expectedManifest, skipSignatureValidation) {
@ -112,24 +114,35 @@ const PluginManager = Backbone.Model.extend({
update(id) { update(id) {
const plugins = this.get('plugins'); const plugins = this.get('plugins');
const oldPlugin = plugins.get(id); 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) { if (!oldPlugin || validStatuses.indexOf(oldPlugin.get('status')) < 0) {
return Promise.reject(); return Promise.reject();
} }
const url = oldPlugin.get('url'); const url = oldPlugin.get('url');
this.trigger('change'); this.trigger('change');
return Plugin.loadFromUrl(url).then(newPlugin => { return Plugin.loadFromUrl(url)
return oldPlugin.update(newPlugin).then(() => { .then(newPlugin => {
this.trigger('change'); return oldPlugin
this.saveState(); .update(newPlugin)
}).catch(e => { .then(() => {
this.trigger('change');
this.saveState();
})
.catch(e => {
this.trigger('change');
throw e;
});
})
.catch(e => {
this.trigger('change'); this.trigger('change');
throw e; throw e;
}); });
}).catch(e => {
this.trigger('change');
throw e;
});
}, },
setAutoUpdate(id, enabled) { setAutoUpdate(id, enabled) {
@ -144,7 +157,9 @@ const PluginManager = Backbone.Model.extend({
}, },
runAutoUpdate() { 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) { if (!queue.length) {
return Promise.resolve(); return Promise.resolve();
} }
@ -163,7 +178,9 @@ const PluginManager = Backbone.Model.extend({
const updateNext = () => { const updateNext = () => {
const pluginId = queue.shift(); const pluginId = queue.shift();
if (pluginId) { if (pluginId) {
return this.update(pluginId).catch(() => {}).then(updateNext); return this.update(pluginId)
.catch(() => {})
.then(updateNext);
} }
}; };
return updateNext(); return updateNext();
@ -178,10 +195,13 @@ const PluginManager = Backbone.Model.extend({
let enabled = desc.enabled; let enabled = desc.enabled;
if (enabled) { if (enabled) {
const galleryPlugin = gallery ? gallery.plugins.find(pl => pl.manifest.name === desc.manifest.name) : null; 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; enabled = desc.manifest.publicKey === expectedPublicKey;
} }
return plugin.install(enabled, true) return plugin
.install(enabled, true)
.then(() => plugin) .then(() => plugin)
.catch(() => plugin); .catch(() => plugin);
}, },

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,11 @@ const ThemeVars = {
result = result.replace(/([\w\-]+)\([^()]+\)/, fnText => { result = result.replace(/([\w\-]+)\([^()]+\)/, fnText => {
replaced = true; replaced = true;
const [, name, argsStr] = fnText.match(/([\w\-]+)\((.*)\)/); 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)); locals.push(this.fn[name](...args));
return 'L' + (locals.length - 1); return 'L' + (locals.length - 1);
}); });

View File

@ -17,21 +17,51 @@ EntryPresenter.prototype = {
} }
return this; return this;
}, },
get id() { return this.entry ? this.entry.id : this.group.id; }, get id() {
get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); }, return this.entry ? this.entry.id : this.group.id;
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 icon() {
get title() { return this.entry ? this.entry.title : this.group.get('title'); }, return this.entry ? this.entry.icon : this.group.get('icon') || 'folder';
get notes() { return this.entry ? this.entry.notes : undefined; }, },
get url() { return this.entry ? this.entry.displayUrl : undefined; }, get customIcon() {
get user() { return this.entry ? this.entry.user : undefined; }, return this.entry ? this.entry.customIcon : 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 color() {
get updated() { return this.entry ? Format.dtStr(this.entry.updated) : undefined; }, return this.entry ? this.entry.color || (this.entry.customIcon ? this.noColor : undefined) : undefined;
get expired() { return this.entry ? this.entry.expired : false; }, },
get tags() { return this.entry ? this.entry.tags : undefined; }, get title() {
get groupName() { return this.entry ? this.entry.groupName : undefined; }, return this.entry ? this.entry.title : this.group.get('title');
get fileName() { return this.entry ? this.entry.fileName : undefined; }, },
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() { get description() {
if (!this.entry) { if (!this.entry) {
return '[' + Locale.listGroup + ']'; return '[' + Locale.listGroup + ']';

View File

@ -15,11 +15,15 @@ _.extend(IoBrowserCache.prototype, {
const req = idb.open(this.cacheName); const req = idb.open(this.cacheName);
req.onerror = e => { req.onerror = e => {
this.logger.error('Error opening indexed db', e); this.logger.error('Error opening indexed db', e);
if (callback) { callback(e); } if (callback) {
callback(e);
}
}; };
req.onsuccess = e => { req.onsuccess = e => {
this.db = e.target.result; this.db = e.target.result;
if (callback) { callback(); } if (callback) {
callback();
}
}; };
req.onupgradeneeded = e => { req.onupgradeneeded = e => {
const db = e.target.result; const db = e.target.result;
@ -27,7 +31,9 @@ _.extend(IoBrowserCache.prototype, {
}; };
} catch (e) { } catch (e) {
this.logger.error('Error opening indexed db', 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 { try {
const ts = this.logger.ts(); 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 = () => { req.onsuccess = () => {
this.logger.debug('Saved', id, this.logger.ts(ts)); this.logger.debug('Saved', id, this.logger.ts(ts));
if (callback) { callback(); } if (callback) {
callback();
}
}; };
req.onerror = () => { req.onerror = () => {
this.logger.error('Error saving to cache', id, req.error); this.logger.error('Error saving to cache', id, req.error);
if (callback) { callback(req.error); } if (callback) {
callback(req.error);
}
}; };
} catch (e) { } catch (e) {
this.logger.error('Error saving to cache', id, 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 { try {
const ts = this.logger.ts(); 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 = () => { req.onsuccess = () => {
this.logger.debug('Loaded', id, this.logger.ts(ts)); this.logger.debug('Loaded', id, this.logger.ts(ts));
if (callback) { callback(null, req.result); } if (callback) {
callback(null, req.result);
}
}; };
req.onerror = () => { req.onerror = () => {
this.logger.error('Error loading from cache', id, req.error); this.logger.error('Error loading from cache', id, req.error);
if (callback) { callback(req.error); } if (callback) {
callback(req.error);
}
}; };
} catch (e) { } catch (e) {
this.logger.error('Error loading from cache', id, 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 { try {
const ts = this.logger.ts(); 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 = () => { req.onsuccess = () => {
this.logger.debug('Removed', id, this.logger.ts(ts)); this.logger.debug('Removed', id, this.logger.ts(ts));
if (callback) { callback(); } if (callback) {
callback();
}
}; };
req.onerror = () => { req.onerror = () => {
this.logger.error('Error removing from cache', id, req.error); this.logger.error('Error removing from cache', id, req.error);
if (callback) { callback(req.error); } if (callback) {
callback(req.error);
}
}; };
} catch (e) { } catch (e) {
this.logger.error('Error removing from cache', id, e); this.logger.error('Error removing from cache', id, e);
if (callback) { callback(e); } if (callback) {
callback(e);
}
} }
}); });
} }

View File

@ -37,10 +37,14 @@ _.extend(IoFileCache.prototype, {
Launcher.writeFile(path, data, err => { Launcher.writeFile(path, data, err => {
if (err) { if (err) {
this.logger.error('Error saving file', id, err); this.logger.error('Error saving file', id, err);
if (callback) { callback(err); } if (callback) {
callback(err);
}
} else { } else {
this.logger.debug('Saved', id, this.logger.ts(ts)); 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) => { Launcher.readFile(path, undefined, (data, err) => {
if (err) { if (err) {
this.logger.error('Error loading file', id, err); this.logger.error('Error loading file', id, err);
if (callback) { callback(err); } if (callback) {
callback(err);
}
} else { } else {
this.logger.debug('Loaded', id, this.logger.ts(ts)); 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 => { Launcher.deleteFile(path, err => {
if (err) { if (err) {
this.logger.error('Error removing file', id, err); this.logger.error('Error removing file', id, err);
if (callback) { callback(err); } if (callback) {
callback(err);
}
} else { } else {
this.logger.debug('Removed', id, this.logger.ts(ts)); this.logger.debug('Removed', id, this.logger.ts(ts));
if (callback) { callback(); } if (callback) {
callback();
}
} }
}); });
}); });

View File

@ -7,8 +7,7 @@ const FeatureDetector = require('../util/feature-detector');
const MaxRequestRetries = 3; const MaxRequestRetries = 3;
const StorageBase = function() { const StorageBase = function() {};
};
_.extend(StorageBase.prototype, { _.extend(StorageBase.prototype, {
name: null, name: null,
@ -104,11 +103,19 @@ _.extend(StorageBase.prototype, {
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left; const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top; const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
const winWidth = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; const winWidth = window.innerWidth
const winHeight = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; ? 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 left = winWidth / 2 - width / 2 + dualScreenLeft;
const top = ((winHeight / 2) - (height / 2)) + dualScreenTop; const top = winHeight / 2 - height / 2 + dualScreenTop;
let settings = { let settings = {
width: width, width: width,
@ -120,7 +127,9 @@ _.extend(StorageBase.prototype, {
scrollbars: 'yes', scrollbars: 'yes',
location: '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) { if (FeatureDetector.isStandalone) {
sessionStorage.authStorage = this.name; sessionStorage.authStorage = this.name;
} }
@ -147,10 +156,12 @@ _.extend(StorageBase.prototype, {
this._oauthToken = oldToken; this._oauthToken = oldToken;
return callback(); return callback();
} }
const url = opts.url + '?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}' const url =
.replace('{cid}', encodeURIComponent(opts.clientId)) opts.url +
.replace('{scope}', encodeURIComponent(opts.scope)) '?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl())); .replace('{cid}', encodeURIComponent(opts.clientId))
.replace('{scope}', encodeURIComponent(opts.scope))
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
this.logger.debug('OAuth: popup opened'); this.logger.debug('OAuth: popup opened');
const popupWindow = this._openPopup(url, 'OAuth', opts.width, opts.height); const popupWindow = this._openPopup(url, 'OAuth', opts.width, opts.height);
if (!popupWindow) { if (!popupWindow) {
@ -185,8 +196,7 @@ _.extend(StorageBase.prototype, {
window.addEventListener('message', windowMessage); window.addEventListener('message', windowMessage);
}, },
_popupOpened(popupWindow) { _popupOpened(popupWindow) {},
},
_oauthProcessReturn: function(message) { _oauthProcessReturn: function(message) {
const token = this._oauthMsgToToken(message); const token = this._oauthMsgToToken(message);
@ -201,7 +211,7 @@ _.extend(StorageBase.prototype, {
_oauthMsgToToken: function(data) { _oauthMsgToToken: function(data) {
if (!data.token_type) { if (!data.token_type) {
if (data.error) { if (data.error) {
return {error: data.error, errorDescription: data.error_description}; return { error: data.error, errorDescription: data.error_description };
} else { } else {
return undefined; return undefined;
} }

View File

@ -80,8 +80,21 @@ const StorageDropbox = StorageBase.extend({
return { return {
desc: 'dropboxSetupDesc', desc: 'dropboxSetupDesc',
fields: [ 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() { getSettingsConfig: function() {
const fields = []; const fields = [];
const appKey = this._getKey(); const appKey = this._getKey();
const linkField = {id: 'link', title: 'dropboxLink', type: 'select', value: 'custom', const linkField = {
options: { app: 'dropboxLinkApp', full: 'dropboxLinkFull', custom: 'dropboxLinkCustom' } }; id: 'link',
const keyField = {id: 'key', title: 'dropboxAppKey', desc: 'dropboxAppKeyDesc', type: 'text', required: true, pattern: '\\w+', title: 'dropboxLink',
value: appKey}; type: 'select',
const folderField = {id: 'folder', title: 'dropboxFolder', desc: 'dropboxFolderSettingsDesc', type: 'text', value: 'custom',
value: this.appSettings.get('dropboxFolder') || ''}; 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(); const canUseBuiltInKeys = this._canUseBuiltInKeys();
if (canUseBuiltInKeys) { if (canUseBuiltInKeys) {
fields.push(linkField); fields.push(linkField);
@ -198,7 +228,7 @@ const StorageDropbox = StorageBase.extend({
statuses: args.statuses || undefined, statuses: args.statuses || undefined,
success: args.success, success: args.success,
error: (e, xhr) => { 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') { if (err && err.path && err.path['.tag'] === 'not_found') {
err = new Error('File removed'); err = new Error('File removed');
err.notFound = true; err.notFound = true;
@ -245,7 +275,9 @@ const StorageDropbox = StorageBase.extend({
stat = { folder: true }; stat = { folder: true };
} }
this.logger.debug('Stated', path, stat.folder ? 'folder' : stat.rev, this.logger.ts(ts)); 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 error: callback
}); });
@ -284,13 +316,12 @@ const StorageDropbox = StorageBase.extend({
}, },
success: data => { success: data => {
this.logger.debug('Listed', this.logger.ts(ts)); this.logger.debug('Listed', this.logger.ts(ts));
const fileList = data.entries const fileList = data.entries.map(f => ({
.map(f => ({ name: f.name,
name: f.name, path: this._toRelPath(f['path_display']),
path: this._toRelPath(f['path_display']), rev: f.rev,
rev: f.rev, dir: f['.tag'] !== 'file'
dir: f['.tag'] !== 'file' }));
}));
callback(null, fileList); callback(null, fileList);
}, },
error: callback error: callback

View File

@ -19,7 +19,7 @@ const StorageFileCache = StorageBase.extend({
const path = Launcher.getUserDataPath('OfflineFiles'); const path = Launcher.getUserDataPath('OfflineFiles');
const setPath = (err) => { const setPath = err => {
this.path = err ? null : path; this.path = err ? null : path;
if (err) { if (err) {
this.logger.error('Error opening local offline storage', err); this.logger.error('Error opening local offline storage', err);
@ -49,7 +49,9 @@ const StorageFileCache = StorageBase.extend({
return callback && callback(err); return callback && callback(err);
} }
this.logger.debug('Saved', id, this.logger.ts(ts)); this.logger.debug('Saved', id, this.logger.ts(ts));
if (callback) { callback(); } if (callback) {
callback();
}
}); });
}); });
}, },

View File

@ -114,10 +114,14 @@ const StorageFile = StorageBase.extend({
Launcher.mkdir(path, err => { Launcher.mkdir(path, err => {
if (err) { if (err) {
this.logger.error('Error making local dir', path, 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 { } else {
this.logger.debug('Made dir', path, this.logger.ts(ts)); this.logger.debug('Made dir', path, this.logger.ts(ts));
if (callback) { callback(); } if (callback) {
callback();
}
} }
}); });
}, },

View File

@ -11,7 +11,8 @@ const StorageGDrive = StorageBase.extend({
name: 'gdrive', name: 'gdrive',
enabled: true, enabled: true,
uipos: 30, 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,' + '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>', '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) { load: function(path, opts, callback) {
this.stat(path, opts, (err, stat) => { this.stat(path, opts, (err, stat) => {
if (err) { return callback && callback(err); } if (err) {
return callback && callback(err);
}
this.logger.debug('Load', path); this.logger.debug('Load', path);
const ts = this.logger.ts(); const ts = this.logger.ts();
const url = this._baseUrl + '/files/{id}/revisions/{rev}?alt=media' const url =
.replace('{id}', path) this._baseUrl +
.replace('{rev}', stat.rev); '/files/{id}/revisions/{rev}?alt=media'.replace('{id}', path).replace('{rev}', stat.rev);
this._xhr({ this._xhr({
url: url, url: url,
responseType: 'arraybuffer', responseType: 'arraybuffer',
success: (response) => { success: response => {
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts)); this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
return callback && callback(null, response, { rev: stat.rev }); return callback && callback(null, response, { rev: stat.rev });
}, },
error: (err) => { error: err => {
this.logger.error('Load error', path, err, this.logger.ts(ts)); this.logger.error('Load error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -55,17 +58,16 @@ const StorageGDrive = StorageBase.extend({
} }
this.logger.debug('Stat', path); this.logger.debug('Stat', path);
const ts = this.logger.ts(); const ts = this.logger.ts();
const url = this._baseUrl + '/files/{id}?fields=headRevisionId' const url = this._baseUrl + '/files/{id}?fields=headRevisionId'.replace('{id}', path);
.replace('{id}', path);
this._xhr({ this._xhr({
url: url, url: url,
responseType: 'json', responseType: 'json',
success: (response) => { success: response => {
const rev = response.headRevisionId; const rev = response.headRevisionId;
this.logger.debug('Stated', path, rev, this.logger.ts(ts)); this.logger.debug('Stated', 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('Stat error', this.logger.ts(ts), err); this.logger.error('Stat error', this.logger.ts(ts), err);
return callback && callback(err); return callback && callback(err);
} }
@ -95,18 +97,33 @@ const StorageGDrive = StorageBase.extend({
url = this._baseUrlUpload + '/files?uploadType=multipart&fields=id,headRevisionId'; url = this._baseUrlUpload + '/files?uploadType=multipart&fields=id,headRevisionId';
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx'; const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
const boundry = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000); const boundry = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
data = new Blob([ data = new Blob(
'--', boundry, '\r\n', [
'Content-Type: application/json; charset=UTF-8', '\r\n\r\n', '--',
JSON.stringify({ name: fileName }), '\r\n', boundry,
'--', boundry, '\r\n', '\r\n',
'Content-Type: application/octet-stream', '\r\n\r\n', 'Content-Type: application/json; charset=UTF-8',
data, '\r\n', '\r\n\r\n',
'--', boundry, '--', '\r\n' JSON.stringify({ name: fileName }),
], { type: 'multipart/related; boundary="' + boundry + '"' }); '\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 { } else {
url = this._baseUrlUpload + '/files/{id}?uploadType=media&fields=headRevisionId' url =
.replace('{id}', path); this._baseUrlUpload +
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
data = new Blob([data], { type: 'application/octet-stream' }); data = new Blob([data], { type: 'application/octet-stream' });
} }
this._xhr({ this._xhr({
@ -114,7 +131,7 @@ const StorageGDrive = StorageBase.extend({
method: isNew ? 'POST' : 'PATCH', method: isNew ? 'POST' : 'PATCH',
responseType: 'json', responseType: 'json',
data: data, data: data,
success: (response) => { success: response => {
this.logger.debug('Saved', path, this.logger.ts(ts)); this.logger.debug('Saved', path, this.logger.ts(ts));
const newRev = response.headRevisionId; const newRev = response.headRevisionId;
if (!newRev) { if (!newRev) {
@ -122,7 +139,7 @@ const StorageGDrive = StorageBase.extend({
} }
return callback && callback(null, { rev: newRev, path: isNew ? response.id : null }); 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)); this.logger.error('Save error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -132,20 +149,23 @@ const StorageGDrive = StorageBase.extend({
}, },
list: function(dir, callback) { list: function(dir, callback) {
this._oauthAuthorize((err) => { this._oauthAuthorize(err => {
if (err) { return callback && callback(err); } if (err) {
return callback && callback(err);
}
this.logger.debug('List'); this.logger.debug('List');
let query = dir === 'shared' ? 'sharedWithMe=true' let query = dir === 'shared' ? 'sharedWithMe=true' : dir ? `"${dir}" in parents` : '"root" in parents';
: dir ? `"${dir}" in parents` : '"root" in parents';
query += ' and trashed=false'; query += ' and trashed=false';
const url = this._baseUrl + '/files?fields={fields}&q={q}&pageSize=1000' const url =
.replace('{fields}', encodeURIComponent('files(id,name,mimeType,headRevisionId)')) this._baseUrl +
.replace('{q}', encodeURIComponent(query)); '/files?fields={fields}&q={q}&pageSize=1000'
.replace('{fields}', encodeURIComponent('files(id,name,mimeType,headRevisionId)'))
.replace('{q}', encodeURIComponent(query));
const ts = this.logger.ts(); const ts = this.logger.ts();
this._xhr({ this._xhr({
url: url, url: url,
responseType: 'json', responseType: 'json',
success: (response) => { success: response => {
if (!response) { if (!response) {
this.logger.error('List error', this.logger.ts(ts)); this.logger.error('List error', this.logger.ts(ts));
return callback && callback('list error'); return callback && callback('list error');
@ -167,7 +187,7 @@ const StorageGDrive = StorageBase.extend({
} }
return callback && callback(null, fileList); return callback && callback(null, fileList);
}, },
error: (err) => { error: err => {
this.logger.error('List error', this.logger.ts(ts), err); this.logger.error('List error', this.logger.ts(ts), err);
return callback && callback(err); return callback && callback(err);
} }
@ -188,7 +208,7 @@ const StorageGDrive = StorageBase.extend({
this.logger.debug('Removed', path, this.logger.ts(ts)); this.logger.debug('Removed', path, this.logger.ts(ts));
return callback && callback(); return callback && callback();
}, },
error: (err) => { error: err => {
this.logger.error('Remove error', path, err, this.logger.ts(ts)); this.logger.error('Remove error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }

View File

@ -9,7 +9,8 @@ const StorageOneDrive = StorageBase.extend({
name: 'onedrive', name: 'onedrive',
enabled: true, enabled: true,
uipos: 40, 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,' + '<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 ' + '-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 ' + '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({ this._xhr({
url: url, url: url,
responseType: 'json', responseType: 'json',
success: (response) => { success: response => {
const downloadUrl = response['@microsoft.graph.downloadUrl']; const downloadUrl = response['@microsoft.graph.downloadUrl'];
let rev = response.eTag; let rev = response.eTag;
if (!downloadUrl || !response.eTag) { if (!downloadUrl || !response.eTag) {
@ -52,15 +53,15 @@ const StorageOneDrive = StorageBase.extend({
success: (response, xhr) => { success: (response, xhr) => {
rev = xhr.getResponseHeader('ETag') || rev; rev = xhr.getResponseHeader('ETag') || rev;
this.logger.debug('Loaded', path, rev, this.logger.ts(ts)); 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)); this.logger.error('Load error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
}); });
}, },
error: (err) => { error: err => {
this.logger.error('Load error', path, err, this.logger.ts(ts)); this.logger.error('Load error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -79,14 +80,14 @@ const StorageOneDrive = StorageBase.extend({
this._xhr({ this._xhr({
url: url, url: url,
responseType: 'json', responseType: 'json',
success: (response) => { success: response => {
const rev = response.eTag; const rev = response.eTag;
if (!rev) { if (!rev) {
this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts)); this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts));
return callback && callback('no eTag'); return callback && callback('no eTag');
} }
this.logger.debug('Stated', path, rev, this.logger.ts(ts)); 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) => { error: (err, xhr) => {
if (xhr.status === 404) { if (xhr.status === 404) {
@ -113,7 +114,7 @@ const StorageOneDrive = StorageBase.extend({
method: 'PUT', method: 'PUT',
responseType: 'json', responseType: 'json',
headers: rev ? { 'If-Match': rev } : null, 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], statuses: [200, 201, 412],
success: (response, xhr) => { success: (response, xhr) => {
rev = response.eTag; rev = response.eTag;
@ -126,9 +127,9 @@ const StorageOneDrive = StorageBase.extend({
return callback && callback({ revConflict: true }, { rev: rev }); return callback && callback({ revConflict: true }, { rev: rev });
} }
this.logger.debug('Saved', path, rev, this.logger.ts(ts)); 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)); this.logger.error('Save error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -138,14 +139,16 @@ const StorageOneDrive = StorageBase.extend({
list: function(dir, callback) { list: function(dir, callback) {
this._oauthAuthorize(err => { this._oauthAuthorize(err => {
if (err) { return callback && callback(err); } if (err) {
return callback && callback(err);
}
this.logger.debug('List'); this.logger.debug('List');
const ts = this.logger.ts(); const ts = this.logger.ts();
const url = this._baseUrl + (dir ? `${dir}:/children` : '/drive/root/children'); const url = this._baseUrl + (dir ? `${dir}:/children` : '/drive/root/children');
this._xhr({ this._xhr({
url: url, url: url,
responseType: 'json', responseType: 'json',
success: (response) => { success: response => {
if (!response || !response.value) { if (!response || !response.value) {
this.logger.error('List error', this.logger.ts(ts), response); this.logger.error('List error', this.logger.ts(ts), response);
return callback && callback('list error'); return callback && callback('list error');
@ -161,7 +164,7 @@ const StorageOneDrive = StorageBase.extend({
})); }));
return callback && callback(null, fileList); return callback && callback(null, fileList);
}, },
error: (err) => { error: err => {
this.logger.error('List error', this.logger.ts(ts), err); this.logger.error('List error', this.logger.ts(ts), err);
return callback && callback(err); return callback && callback(err);
} }
@ -182,7 +185,7 @@ const StorageOneDrive = StorageBase.extend({
this.logger.debug('Removed', path, this.logger.ts(ts)); this.logger.debug('Removed', path, this.logger.ts(ts));
return callback && callback(); return callback && callback();
}, },
error: (err) => { error: err => {
this.logger.error('Remove error', path, err, this.logger.ts(ts)); this.logger.error('Remove error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -191,7 +194,9 @@ const StorageOneDrive = StorageBase.extend({
mkdir: function(path, callback) { mkdir: function(path, callback) {
this._oauthAuthorize(err => { this._oauthAuthorize(err => {
if (err) { return callback && callback(err); } if (err) {
return callback && callback(err);
}
this.logger.debug('Make dir', path); this.logger.debug('Make dir', path);
const ts = this.logger.ts(); const ts = this.logger.ts();
const url = this._baseUrl + '/drive/root/children'; const url = this._baseUrl + '/drive/root/children';
@ -201,12 +206,12 @@ const StorageOneDrive = StorageBase.extend({
method: 'POST', method: 'POST',
responseType: 'json', responseType: 'json',
statuses: [200, 204], statuses: [200, 204],
data: new Blob([data], {type: 'application/json'}), data: new Blob([data], { type: 'application/json' }),
success: () => { success: () => {
this.logger.debug('Made dir', path, this.logger.ts(ts)); this.logger.debug('Made dir', path, this.logger.ts(ts));
return callback && callback(); return callback && callback();
}, },
error: (err) => { error: err => {
this.logger.error('Make dir error', path, err, this.logger.ts(ts)); this.logger.error('Make dir error', path, err, this.logger.ts(ts));
return callback && callback(err); return callback && callback(err);
} }
@ -216,8 +221,10 @@ const StorageOneDrive = StorageBase.extend({
setEnabled: function(enabled) { setEnabled: function(enabled) {
if (!enabled) { if (!enabled) {
const url = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url}' const url = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url}'.replace(
.replace('{url}', this._getOauthRedirectUrl()); '{url}',
this._getOauthRedirectUrl()
);
this._oauthRevokeToken(url); this._oauthRevokeToken(url);
} }
StorageBase.prototype.setEnabled.call(this, enabled); StorageBase.prototype.setEnabled.call(this, enabled);
@ -244,13 +251,12 @@ const StorageOneDrive = StorageBase.extend({
_popupOpened(popupWindow) { _popupOpened(popupWindow) {
if (popupWindow.webContents) { if (popupWindow.webContents) {
popupWindow.webContents.on('did-finish-load', (e) => { popupWindow.webContents.on('did-finish-load', e => {
const webContents = e.sender.webContents; const webContents = e.sender.webContents;
const url = webContents.getURL(); const url = webContents.getURL();
if (url && url.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')) { if (url && url.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')) {
// click the login button mentioned in #821 // click the login button mentioned in #821
const script = const script = `const selector = '[role="button"][aria-describedby="tileError loginHeader"]';
`const selector = '[role="button"][aria-describedby="tileError loginHeader"]';
if (document.querySelectorAll(selector).length === 1) document.querySelector(selector).click()`; if (document.querySelectorAll(selector).length === 1) document.querySelector(selector).click()`;
webContents.executeJavaScript(script).catch(() => {}); webContents.executeJavaScript(script).catch(() => {});
} }

View File

@ -13,9 +13,21 @@ const StorageWebDav = StorageBase.extend({
getOpenConfig: function() { getOpenConfig: function() {
return { return {
fields: [ fields: [
{id: 'path', title: 'openUrl', desc: 'openUrlDesc', type: 'text', required: true}, { 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: '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() { getSettingsConfig: function() {
return { return {
fields: [ fields: [
{ id: 'webdavSaveMethod', title: 'webdavSaveMethod', type: 'select', {
id: 'webdavSaveMethod',
title: 'webdavSaveMethod',
type: 'select',
value: this.appSettings.get('webdavSaveMethod') || 'default', 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) { load: function(path, opts, callback) {
this._request({ this._request(
op: 'Load', {
method: 'GET', op: 'Load',
path: path, method: 'GET',
user: opts ? opts.user : null, path: path,
password: opts ? opts.password : null user: opts ? opts.user : null,
}, callback ? (err, xhr, stat) => { password: opts ? opts.password : null
callback(err, xhr.response, stat); },
} : null); callback
? (err, xhr, stat) => {
callback(err, xhr.response, stat);
}
: null
);
}, },
stat: function(path, opts, callback) { stat: function(path, opts, callback) {
this._request({ this._request(
op: 'Stat', {
method: 'HEAD', op: 'Stat',
path: path, method: 'HEAD',
user: opts ? opts.user : null, path: path,
password: opts ? opts.password : null user: opts ? opts.user : null,
}, callback ? (err, xhr, stat) => { password: opts ? opts.password : null
callback(err, stat); },
} : null); callback
? (err, xhr, stat) => {
callback(err, stat);
}
: null
);
}, },
save: function(path, opts, data, callback, rev) { save: function(path, opts, data, callback, rev) {
@ -72,76 +98,142 @@ const StorageWebDav = StorageBase.extend({
password: opts ? opts.password : null password: opts ? opts.password : null
}; };
const that = this; const that = this;
this._request(_.defaults({ this._request(
op: 'Save:stat', method: 'HEAD' _.defaults(
}, saveOpts), (err, xhr, stat) => { {
let useTmpPath = this.appSettings.get('webdavSaveMethod') !== 'put'; op: 'Save:stat',
if (err) { method: 'HEAD'
if (!err.notFound) { },
return cb(err); saveOpts
} else { ),
that.logger.debug('Save: not found, creating'); (err, xhr, stat) => {
useTmpPath = false; 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) { if (useTmpPath) {
that.logger.debug('Save error', path, 'rev conflict', stat.rev, rev); that._request(
return cb({ revConflict: true }, xhr, stat); _.defaults(
} {
if (useTmpPath) { op: 'Save:put',
that._request(_.defaults({ method: 'PUT',
op: 'Save:put', method: 'PUT', path: tmpPath, data: data, nostat: true path: tmpPath,
}, saveOpts), (err) => { data: data,
if (err) { return cb(err); } nostat: true
that._request(_.defaults({ },
op: 'Save:stat', method: 'HEAD' saveOpts
}, saveOpts), (err, xhr, stat) => { ),
if (err) { err => {
that._request(_.defaults({op: 'Save:delete', method: 'DELETE', path: tmpPath}, saveOpts)); if (err) {
return cb(err, xhr, stat); return cb(err);
}
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: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, } else {
headers: {Destination: movePath, 'Overwrite': 'T'} that._request(
}, saveOpts), (err) => { _.defaults(
if (err) { return cb(err); } {
that._request(_.defaults({ op: 'Save:put',
op: 'Save:stat', method: 'HEAD' method: 'PUT',
}, saveOpts), (err, xhr, stat) => { data: data,
cb(err, xhr, stat); nostat: true
}); },
}); saveOpts
}); ),
}); err => {
} else { if (err) {
that._request(_.defaults({ return cb(err);
op: 'Save:put', method: 'PUT', data: data, nostat: true }
}, saveOpts), (err) => { that._request(
if (err) { return cb(err); } _.defaults(
that._request(_.defaults({ {
op: 'Save:stat', method: 'HEAD' op: 'Save:stat',
}, saveOpts), (err, xhr, stat) => { method: 'HEAD'
cb(err, xhr, stat); },
}); saveOpts
}); ),
(err, xhr, stat) => {
cb(err, xhr, stat);
}
);
}
);
}
} }
}); );
}, },
fileOptsToStoreOpts: function(opts, file) { fileOptsToStoreOpts: function(opts, file) {
const result = {user: opts.user, encpass: opts.encpass}; const result = { user: opts.user, encpass: opts.encpass };
if (opts.password) { if (opts.password) {
const fileId = file.get('uuid'); const fileId = file.get('uuid');
const password = opts.password; const password = opts.password;
@ -155,7 +247,7 @@ const StorageWebDav = StorageBase.extend({
}, },
storeOptsToFileOpts: function(opts, file) { storeOptsToFileOpts: function(opts, file) {
const result = {user: opts.user, password: opts.password}; const result = { user: opts.user, password: opts.password };
if (opts.encpass) { if (opts.encpass) {
const fileId = file.get('uuid'); const fileId = file.get('uuid');
const encpass = atob(opts.encpass); const encpass = atob(opts.encpass);
@ -192,26 +284,41 @@ const StorageWebDav = StorageBase.extend({
err = 'HTTP status ' + xhr.status; err = 'HTTP status ' + xhr.status;
break; break;
} }
if (callback) { callback(err, xhr); callback = null; } if (callback) {
callback(err, xhr);
callback = null;
}
return; return;
} }
const rev = xhr.getResponseHeader('Last-Modified'); const rev = xhr.getResponseHeader('Last-Modified');
if (!rev && !config.nostat) { if (!rev && !config.nostat) {
that.logger.debug(config.op + ' error', config.path, 'no headers', that.logger.ts(ts)); 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; return;
} }
const completedOpName = config.op + (config.op.charAt(config.op.length - 1) === 'e' ? 'd' : 'ed'); 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)); 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', () => { xhr.addEventListener('error', () => {
that.logger.debug(config.op + ' error', config.path, that.logger.ts(ts)); 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', () => { xhr.addEventListener('abort', () => {
that.logger.debug(config.op + ' error', config.path, 'aborted', that.logger.ts(ts)); 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.open(config.method, config.path);
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
@ -227,7 +334,7 @@ const StorageWebDav = StorageBase.extend({
xhr.setRequestHeader('Cache-Control', 'no-cache'); xhr.setRequestHeader('Cache-Control', 'no-cache');
} }
if (config.data) { 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); xhr.send(blob);
} else { } else {
xhr.send(); xhr.send();

View File

@ -52,9 +52,15 @@ Color.prototype.setHsl = function() {
const d = max - min; const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break; case r:
case g: h = (b - r) / d + 2; break; h = (g - b) / d + (g < b ? 6 : 0);
case b: h = (r - g) / d + 4; break; break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
} }
h /= 6; h /= 6;
} }

View File

@ -1,27 +1,38 @@
const LastChar = String.fromCharCode(0xfffd); const LastChar = String.fromCharCode(0xfffd);
const ciCompare = (window.Intl && window.Intl.Collator && !/Edge/.test(navigator.userAgent)) // bugged in Edge: #808 const ciCompare =
? new Intl.Collator(undefined, { sensitivity: 'base' }).compare window.Intl && window.Intl.Collator && !/Edge/.test(navigator.userAgent) // bugged in Edge: #808
: (x, y) => x.toLocaleLowerCase().localeCompare(y.toLocaleLowerCase()); ? new Intl.Collator(undefined, { sensitivity: 'base' }).compare
: (x, y) => x.toLocaleLowerCase().localeCompare(y.toLocaleLowerCase());
const Comparators = { const Comparators = {
stringComparator: function(field, asc) { stringComparator: function(field, asc) {
if (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 { } else {
return function (x, y) { return ciCompare(y[field], x[field]); }; return function(x, y) {
return ciCompare(y[field], x[field]);
};
} }
}, },
rankComparator: function() { 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) { dateComparator: function(field, asc) {
if (asc) { if (asc) {
return function (x, y) { return x[field] - y[field]; }; return function(x, y) {
return x[field] - y[field];
};
} else { } else {
return function (x, y) { return y[field] - x[field]; }; return function(x, y) {
return y[field] - x[field];
};
} }
} }
}; };

View File

@ -9,7 +9,7 @@ const FeatureDetector = {
isWindows: navigator.platform.indexOf('Win') >= 0, isWindows: navigator.platform.indexOf('Win') >= 0,
isiOS: /iPad|iPhone|iPod/i.test(navigator.userAgent), isiOS: /iPad|iPhone|iPod/i.test(navigator.userAgent),
isMobile: MobileRegex.test(navigator.userAgent) || screen.width < MinDesktopScreenWidth, 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, isStandalone: !!navigator.standalone,
isFrame: window.top !== window, isFrame: window.top !== window,
isSelfHosted: !isDesktop && !/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href), isSelfHosted: !isDesktop && !/^http(s?):\/\/((localhost:8085)|((app|beta)\.keeweb\.info))/.test(location.href),
@ -28,10 +28,18 @@ const FeatureDetector = {
return !this.isMac; return !this.isMac;
}, },
screenshotToClipboardShortcut: function() { screenshotToClipboardShortcut: function() {
if (this.isiOS) { return 'Sleep+Home'; } if (this.isiOS) {
if (this.isMobile) { return ''; } return 'Sleep+Home';
if (this.isMac) { return 'Command-Shift-Control-4'; } }
if (this.isWindows) { return 'Alt+PrintScreen'; } if (this.isMobile) {
return '';
}
if (this.isMac) {
return 'Command-Shift-Control-4';
}
if (this.isWindows) {
return 'Alt+PrintScreen';
}
return ''; return '';
}, },
supportsTitleBarStyles: function() { supportsTitleBarStyles: function() {

View File

@ -18,8 +18,15 @@ const Format = {
if (typeof dt === 'number') { if (typeof dt === 'number') {
dt = new Date(dt); dt = new Date(dt);
} }
return dt ? this.dStr(dt) + ' ' + this.pad(dt.getHours(), 2) + ':' + this.pad(dt.getMinutes(), 2) + return dt
':' + this.pad(dt.getSeconds(), 2) : ''; ? this.dStr(dt) +
' ' +
this.pad(dt.getHours(), 2) +
':' +
this.pad(dt.getMinutes(), 2) +
':' +
this.pad(dt.getSeconds(), 2)
: '';
}, },
dStr: function(dt) { dStr: function(dt) {
if (typeof dt === 'number') { if (typeof dt === 'number') {
@ -37,8 +44,18 @@ const Format = {
if (typeof dt === 'number') { if (typeof dt === 'number') {
dt = new Date(dt); dt = new Date(dt);
} }
return dt ? dt.getFullYear() + '-' + this.pad(dt.getMonth() + 1, 2) + '-' + this.pad(dt.getDate(), 2) + 'T' + return dt
this.pad(dt.getHours(), 2) + '-' + this.pad(dt.getMinutes(), 2) + '-' + this.pad(dt.getSeconds(), 2) ? 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)
: ''; : '';
} }
}; };

View File

@ -4,7 +4,9 @@ const IdGenerator = {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}, },
s4: function() { s4: function() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
} }
}; };

View File

@ -38,22 +38,33 @@ const KdbxwebInit = {
const GB = 1024 * MB; const GB = 1024 * MB;
const WASM_PAGE_SIZE = 64 * 1024; const WASM_PAGE_SIZE = 64 * 1024;
const totalMemory = (2 * GB - 64 * KB) / 1024 / WASM_PAGE_SIZE; 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 memoryDecl = `var wasmMemory=new WebAssembly.Memory({initial:${initialMemory},maximum:${totalMemory}});`;
const moduleDecl = 'var Module={' + const moduleDecl =
'var Module={' +
'wasmJSMethod: "native-wasm",' + '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}) },' + 'print(...args) { postMessage({op:"log",args}) },' +
'printErr(...args) { postMessage({op:"log",args}) },' + 'printErr(...args) { postMessage({op:"log",args}) },' +
'postRun:' + this.workerPostRun.toString() + ',' + 'postRun:' +
'calcHash:' + this.calcHash.toString() + ',' + this.workerPostRun.toString() +
',' +
'calcHash:' +
this.calcHash.toString() +
',' +
'wasmMemory:wasmMemory,' + 'wasmMemory:wasmMemory,' +
'buffer:wasmMemory.buffer,' + 'buffer:wasmMemory.buffer,' +
'TOTAL_MEMORY:' + initialMemory * WASM_PAGE_SIZE + 'TOTAL_MEMORY:' +
initialMemory * WASM_PAGE_SIZE +
'}'; '}';
const script = argon2LoaderCode.replace(/^var Module.*?}/, memoryDecl + moduleDecl); 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 objectUrl = URL.createObjectURL(blob);
const worker = new Worker(objectUrl); const worker = new Worker(objectUrl);
const onMessage = e => { const onMessage = e => {
@ -75,7 +86,7 @@ const KdbxwebInit = {
worker.terminate(); worker.terminate();
KdbxwebInit.runtimeModule = null; KdbxwebInit.runtimeModule = null;
if (!e.data || e.data.error || !e.data.hash) { 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); logger.error('Worker error', ex);
reject(ex); reject(ex);
} }
@ -126,9 +137,21 @@ const KdbxwebInit = {
const encodedLen = 512; const encodedLen = 512;
const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL); const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL);
const res = Module._argon2_hash(iterations, memory, parallelism, const res = Module._argon2_hash(
password, passwordLen, salt, saltLen, iterations,
hash, length, encoded, encodedLen, type, version); memory,
parallelism,
password,
passwordLen,
salt,
saltLen,
hash,
length,
encoded,
encodedLen,
type,
version
);
if (res) { if (res) {
throw new Error('Argon2 error ' + res); throw new Error('Argon2 error ' + res);
} }

View File

@ -12,7 +12,7 @@ const MaxLogsToSave = 100;
const lastLogs = []; const lastLogs = [];
const Logger = function(name, id) { const Logger = function(name, id) {
this.prefix = (name ? name + (id ? ':' + id : '') : 'default'); this.prefix = name ? name + (id ? ':' + id : '') : 'default';
this.level = Level.All; this.level = Level.All;
}; };

View File

@ -18,7 +18,7 @@ const Otp = function(url, params) {
if (params.type === 'hotp' && !params.counter) { if (params.type === 'hotp' && !params.counter) {
throw 'Bad counter: ' + 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; throw 'Bad period: ' + params.period;
} }
@ -70,13 +70,21 @@ Otp.prototype.next = function(callback) {
Otp.prototype.hmac = function(data, callback) { Otp.prototype.hmac = function(data, callback) {
const subtle = window.crypto.subtle || window.crypto.webkitSubtle; const subtle = window.crypto.subtle || window.crypto.webkitSubtle;
const algo = { name: 'HMAC', hash: { name: this.algorithm.replace('SHA', 'SHA-') } }; 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 => { .then(key => {
subtle.sign(algo, key, data) subtle
.then(sig => { callback(sig); }) .sign(algo, key, data)
.catch(err => { callback(null, err); }); .then(sig => {
callback(sig);
})
.catch(err => {
callback(null, err);
});
}) })
.catch(err => { callback(null, err); }); .catch(err => {
callback(null, err);
});
}; };
Otp.fromBase32 = function(str) { Otp.fromBase32 = function(str) {
@ -132,7 +140,12 @@ Otp.isSecret = function(str) {
}; };
Otp.makeUrl = function(secret, period, digits) { 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; module.exports = Otp;

View File

@ -28,7 +28,9 @@ const PasswordGenerator = {
} }
const ranges = Object.keys(this.charRanges) const ranges = Object.keys(this.charRanges)
.filter(r => opts[r]) .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) { if (opts.include && opts.include.length) {
ranges.push(opts.include); ranges.push(opts.include);
} }

View File

@ -11,20 +11,52 @@
*/ */
const PHONETIC_PRE = [ const PHONETIC_PRE = [
// Simple phonetics // Simple phonetics
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'b',
'qu', 'r', 's', 't', 'c',
'd',
'f',
'g',
'h',
'j',
'k',
'l',
'm',
'n',
'p',
'qu',
'r',
's',
't',
// Complex phonetics // Complex phonetics
'bl', 'bl',
'ch', 'cl', 'cr', 'ch',
'cl',
'cr',
'dr', 'dr',
'fl', 'fr', 'fl',
'gl', 'gr', 'fr',
'kl', 'kr', 'gl',
'ph', 'pr', 'pl', 'gr',
'sc', 'sh', 'sl', 'sn', 'sr', 'st', 'str', 'sw', 'kl',
'th', 'tr', 'kr',
'ph',
'pr',
'pl',
'sc',
'sh',
'sl',
'sn',
'sr',
'st',
'str',
'sw',
'th',
'tr',
'br', 'br',
'v', 'w', 'y', 'z' 'v',
'w',
'y',
'z'
]; ];
/** /**
@ -39,9 +71,17 @@ const PHONETIC_PRE_SIMPLE_LENGTH = 16;
*/ */
const PHONETIC_MID = [ const PHONETIC_MID = [
// Simple phonetics // Simple phonetics
'a', 'e', 'i', 'o', 'u', 'a',
'e',
'i',
'o',
'u',
// Complex phonetics // 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 = [ const PHONETIC_POST = [
// Simple phonetics // 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 // Complex phonetics
'ch', 'ck', 'ch',
'ck',
'ln', 'ln',
'nk', 'ng', 'nk',
'ng',
'rn', 'rn',
'sh', 'sk', 'st', 'sh',
'sk',
'st',
'th', 'th',
'x', 'z' 'x',
'z'
]; ];
/** /**
@ -107,17 +164,14 @@ function addSyllable(wordObj) {
const first = wordObj.word === ''; const first = wordObj.word === '';
const preOnFirst = deriv % 6 > 0; const preOnFirst = deriv % 6 > 0;
if ((first && preOnFirst) || wordObj.lastSkippedPost || compound) { if ((first && preOnFirst) || wordObj.lastSkippedPost || compound) {
wordObj.word += getNextPhonetic(PHONETIC_PRE, wordObj.word += getNextPhonetic(PHONETIC_PRE, PHONETIC_PRE_SIMPLE_LENGTH, wordObj);
PHONETIC_PRE_SIMPLE_LENGTH, wordObj);
wordObj.lastSkippedPre = false; wordObj.lastSkippedPre = false;
} else { } else {
wordObj.lastSkippedPre = true; wordObj.lastSkippedPre = true;
} }
wordObj.word += getNextPhonetic(PHONETIC_MID, PHONETIC_MID_SIMPLE_LENGTH, wordObj.word += getNextPhonetic(PHONETIC_MID, PHONETIC_MID_SIMPLE_LENGTH, wordObj, first && wordObj.lastSkippedPre);
wordObj, first && wordObj.lastSkippedPre);
if (wordObj.lastSkippedPre || compound) { if (wordObj.lastSkippedPre || compound) {
wordObj.word += getNextPhonetic(PHONETIC_POST, wordObj.word += getNextPhonetic(PHONETIC_POST, PHONETIC_POST_SIMPLE_LENGTH, wordObj);
PHONETIC_POST_SIMPLE_LENGTH, wordObj);
wordObj.lastSkippedPost = false; wordObj.lastSkippedPost = false;
} else { } else {
wordObj.lastSkippedPost = true; wordObj.lastSkippedPost = true;
@ -206,7 +260,7 @@ function getNumericHash(data) {
data += '-Phonetic'; data += '-Phonetic';
for (let i = 0, len = data.length; i < len; i++) { for (let i = 0, len = data.length; i < len; i++) {
const chr = data.charCodeAt(i); const chr = data.charCodeAt(i);
numeric = ((numeric << 5) - numeric) + chr; numeric = (numeric << 5) - numeric + chr;
numeric >>>= 0; numeric >>>= 0;
} }
return numeric; return numeric;

View File

@ -9,7 +9,7 @@ const SignatureVerifier = {
verify(data, signature, pk) { verify(data, signature, pk) {
return new Promise((resolve, reject) => { 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 { try {
if (!pk) { if (!pk) {
pk = this.getPublicKey(); pk = this.getPublicKey();
@ -18,29 +18,33 @@ const SignatureVerifier = {
const subtle = window.crypto.subtle; const subtle = window.crypto.subtle;
const keyFormat = 'spki'; const keyFormat = 'spki';
pk = kdbxweb.ByteUtils.base64ToBytes(pk); pk = kdbxweb.ByteUtils.base64ToBytes(pk);
subtle.importKey( subtle
keyFormat, pk, .importKey(keyFormat, pk, algo, false, ['verify'])
algo, .then(cryptoKey => {
false, ['verify'] try {
).then(cryptoKey => { subtle
try { .verify(
subtle.verify(algo, cryptoKey, algo,
kdbxweb.ByteUtils.arrayToBuffer(signature), cryptoKey,
kdbxweb.ByteUtils.arrayToBuffer(data) kdbxweb.ByteUtils.arrayToBuffer(signature),
).then(isValid => { kdbxweb.ByteUtils.arrayToBuffer(data)
resolve(isValid); )
}).catch(e => { .then(isValid => {
this.logger.error('Verify error', e); resolve(isValid);
})
.catch(e => {
this.logger.error('Verify error', e);
reject();
});
} catch (e) {
this.logger.error('Signature verification error', e);
reject(); reject();
}); }
} catch (e) { })
this.logger.error('Signature verification error', e); .catch(e => {
this.logger.error('ImportKey error', e);
reject(); reject();
} });
}).catch(e => {
this.logger.error('ImportKey error', e);
reject();
});
} catch (e) { } catch (e) {
this.logger.error('Signature key verification error', e); this.logger.error('Signature key verification error', e);
reject(); reject();
@ -50,9 +54,7 @@ const SignatureVerifier = {
getPublicKey() { getPublicKey() {
if (!this.publicKey) { if (!this.publicKey) {
this.publicKey = publicKey this.publicKey = publicKey.match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1].replace(/\s+/g, '');
.match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1]
.replace(/\s+/g, '');
} }
return this.publicKey; return this.publicKey;
} }

View File

@ -3,13 +3,13 @@ const FeatureDetector = require('./feature-detector');
const Tip = function(el, config) { const Tip = function(el, config) {
this.el = el; this.el = el;
this.title = config && config.title || el.attr('title'); this.title = (config && config.title) || el.attr('title');
this.placement = config && config.placement || el.attr('tip-placement'); this.placement = (config && config.placement) || el.attr('tip-placement');
this.fast = config && config.fast || false; this.fast = (config && config.fast) || false;
this.tipEl = null; this.tipEl = null;
this.showTimeout = null; this.showTimeout = null;
this.hideTimeout = null; this.hideTimeout = null;
this.force = config && config.force || false; this.force = (config && config.force) || false;
this.hide = this.hide.bind(this); this.hide = this.hide.bind(this);
}; };
@ -26,7 +26,7 @@ Tip.prototype.init = function() {
}; };
Tip.prototype.show = function() { Tip.prototype.show = function() {
if (!Tip.enabled && !this.force || !this.title) { if ((!Tip.enabled && !this.force) || !this.title) {
return; return;
} }
Backbone.on('page-geometry', this.hide); Backbone.on('page-geometry', this.hide);
@ -37,7 +37,10 @@ Tip.prototype.show = function() {
this.hideTimeout = null; 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 rect = this.el[0].getBoundingClientRect();
const tipRect = this.tipEl[0].getBoundingClientRect(); const tipRect = this.tipEl[0].getBoundingClientRect();
const placement = this.placement || this.getAutoPlacement(rect, tipRect); const placement = this.placement || this.getAutoPlacement(rect, tipRect);
@ -45,8 +48,7 @@ Tip.prototype.show = function() {
if (this.fast) { if (this.fast) {
tipEl.addClass('tip--fast'); tipEl.addClass('tip--fast');
} }
let top, let top, left;
left;
const offset = 10; const offset = 10;
const sideOffset = 10; const sideOffset = 10;
switch (placement) { switch (placement) {
@ -171,8 +173,7 @@ Tip.hideTip = function(el) {
Tip.updateTip = function(el, props) { Tip.updateTip = function(el, props) {
if (el._tip) { if (el._tip) {
el._tip.hide(); el._tip.hide();
_.extend(el._tip, _.pick(props, _.extend(el._tip, _.pick(props, ['title', 'placement', 'fast', 'showTimeout', 'hideTimeout']));
['title', 'placement', 'fast', 'showTimeout', 'hideTimeout']));
} }
}; };

View File

@ -41,7 +41,7 @@ const AppView = Backbone.View.extend({
titlebarStyle: 'default', titlebarStyle: 'default',
initialize: function () { initialize: function() {
this.views = {}; this.views = {};
this.views.menu = new MenuView({ model: this.model.menu }); this.views.menu = new MenuView({ model: this.model.menu });
this.views.menuDrag = new DragView('x'); 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://github.com/keeweb/keeweb/issues/636#issuecomment-304225634
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5782378/ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5782378/
if (FeatureDetector.needFixClicks) { if (FeatureDetector.needFixClicks) {
const msEdgeScrewer = $('<input/>').appendTo(this.$el).focus(); const msEdgeScrewer = $('<input/>')
.appendTo(this.$el)
.focus();
setTimeout(() => msEdgeScrewer.remove(), 0); setTimeout(() => msEdgeScrewer.remove(), 0);
} }
}, },
render: function () { render: function() {
this.$el.html(this.template({ this.$el.html(
beta: this.model.isBeta, this.template({
titlebarStyle: this.titlebarStyle beta: this.model.isBeta,
})); titlebarStyle: this.titlebarStyle
})
);
this.panelEl = this.$el.find('.app__panel:first'); this.panelEl = this.$el.find('.app__panel:first');
this.views.listWrap.setElement(this.$el.find('.app__list-wrap')).render(); this.views.listWrap.setElement(this.$el.find('.app__list-wrap')).render();
this.views.menu.setElement(this.$el.find('.app__menu')).render(); this.views.menu.setElement(this.$el.find('.app__menu')).render();
@ -158,9 +162,13 @@ const AppView = Backbone.View.extend({
this.hideKeyChange(); this.hideKeyChange();
this.views.open = new OpenView({ model: this.model }); this.views.open = new OpenView({ model: this.model });
this.views.open.setElement(this.$el.find('.app__body')).render(); this.views.open.setElement(this.$el.find('.app__body')).render();
this.views.open.on('close', () => { this.views.open.on(
Backbone.trigger('closed-open-view'); 'close',
}, this); () => {
Backbone.trigger('closed-open-view');
},
this
);
this.views.open.on('close', this.showEntries, this); this.views.open.on('close', this.showEntries, this);
}, },
@ -181,8 +189,7 @@ const AppView = Backbone.View.extend({
}, },
updateApp: function() { updateApp: function() {
if (UpdateModel.instance.get('updateStatus') === 'ready' && if (UpdateModel.instance.get('updateStatus') === 'ready' && !Launcher && !this.model.files.hasOpenFiles()) {
!Launcher && !this.model.files.hasOpenFiles()) {
window.location.reload(); window.location.reload();
} }
}, },
@ -324,7 +331,11 @@ const AppView = Backbone.View.extend({
}, },
beforeUnload: function(e) { beforeUnload: function(e) {
const exitEvent = { preventDefault() { this.prevented = true; } }; const exitEvent = {
preventDefault() {
this.prevented = true;
}
};
Backbone.trigger('main-window-will-close', exitEvent); Backbone.trigger('main-window-will-close', exitEvent);
if (exitEvent.prevented) { if (exitEvent.prevented) {
Launcher.preventExit(e); Launcher.preventExit(e);
@ -344,7 +355,11 @@ const AppView = Backbone.View.extend({
if (Launcher) { if (Launcher) {
if (!this.exitAlertShown) { if (!this.exitAlertShown) {
if (this.model.settings.get('autoSave')) { if (this.model.settings.get('autoSave')) {
this.saveAndLock(result => { if (result) { exit(); } }); this.saveAndLock(result => {
if (result) {
exit();
}
});
return Launcher.preventExit(e); return Launcher.preventExit(e);
} }
this.exitAlertShown = true; this.exitAlertShown = true;
@ -352,13 +367,17 @@ const AppView = Backbone.View.extend({
header: Locale.appUnsavedWarn, header: Locale.appUnsavedWarn,
body: Locale.appUnsavedWarnBody, body: Locale.appUnsavedWarnBody,
buttons: [ buttons: [
{result: 'save', title: Locale.saveChanges}, { result: 'save', title: Locale.saveChanges },
{result: 'exit', title: Locale.discardChanges, error: true}, { result: 'exit', title: Locale.discardChanges, error: true },
{result: '', title: Locale.appDontExitBtn} { result: '', title: Locale.appDontExitBtn }
], ],
success: result => { success: result => {
if (result === 'save') { if (result === 'save') {
this.saveAndLock(result => { if (result) { exit(); } }); this.saveAndLock(result => {
if (result) {
exit();
}
});
} else { } else {
exit(); exit();
} }
@ -374,8 +393,13 @@ const AppView = Backbone.View.extend({
return Launcher.preventExit(e); return Launcher.preventExit(e);
} }
return Locale.appUnsavedWarnBody; return Locale.appUnsavedWarnBody;
} else if (Launcher && !Launcher.exitRequested && !Launcher.restartPending && } else if (
Launcher.canMinimize() && this.model.settings.get('minimizeOnClose')) { Launcher &&
!Launcher.exitRequested &&
!Launcher.restartPending &&
Launcher.canMinimize() &&
this.model.settings.get('minimizeOnClose')
) {
Launcher.minimizeApp(); Launcher.minimizeApp();
return Launcher.preventExit(e); return Launcher.preventExit(e);
} }
@ -391,11 +415,11 @@ const AppView = Backbone.View.extend({
} }
}, },
enterFullScreen: function () { enterFullScreen: function() {
this.$el.addClass('fullscreen'); this.$el.addClass('fullscreen');
}, },
leaveFullScreen: function () { leaveFullScreen: function() {
this.$el.removeClass('fullscreen'); this.$el.removeClass('fullscreen');
}, },
@ -480,7 +504,7 @@ const AppView = Backbone.View.extend({
} }
}, },
handleAutoSaveTimer: function () { handleAutoSaveTimer: function() {
if (this.model.settings.get('autoSaveInterval') !== 0) { if (this.model.settings.get('autoSaveInterval') !== 0) {
// trigger periodical auto save // trigger periodical auto save
if (this.autoSaveTimeoutId) { if (this.autoSaveTimeoutId) {
@ -520,10 +544,14 @@ const AppView = Backbone.View.extend({
body: alertBody + ' ' + errorFiles.join(', ') body: alertBody + ' ' + errorFiles.join(', ')
}); });
} }
if (complete) { complete(true); } if (complete) {
complete(true);
}
} else { } else {
that.closeAllFilesAndShowFirst(); that.closeAllFilesAndShowFirst();
if (complete) { complete(true); } if (complete) {
complete(true);
}
} }
} }
} }
@ -536,7 +564,11 @@ const AppView = Backbone.View.extend({
fileToShow = this.model.fileInfos.getLast(); fileToShow = this.model.fileInfos.getLast();
} }
if (fileToShow) { 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) { if (fileInfo) {
this.views.open.showOpenFileInfo(fileInfo); this.views.open.showOpenFileInfo(fileInfo);
} }
@ -598,13 +630,13 @@ const AppView = Backbone.View.extend({
} }
} else { } else {
if (menuItem) { if (menuItem) {
this.model.menu.select({item: menuItem}); this.model.menu.select({ item: menuItem });
} }
} }
} else { } else {
this.showSettings(); this.showSettings();
if (menuItem) { 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() { showSingleInstanceAlert: function() {
this.hideOpenFile(); this.hideOpenFile();
Alerts.error({ Alerts.error({
header: Locale.appTabWarn, body: Locale.appTabWarnBody, header: Locale.appTabWarn,
esc: false, enter: false, click: false, buttons: [] body: Locale.appTabWarnBody,
esc: false,
enter: false,
click: false,
buttons: []
}); });
}, },

View File

@ -16,7 +16,7 @@ const AutoTypeHintView = Backbone.View.extend({
this.input.addEventListener('blur', this.inputBlur); this.input.addEventListener('blur', this.inputBlur);
}, },
render: function () { render: function() {
this.renderTemplate({ this.renderTemplate({
cmd: FeatureDetector.isMac ? 'command' : 'ctrl', cmd: FeatureDetector.isMac ? 'command' : 'ctrl',
hasCtrl: FeatureDetector.isMac, hasCtrl: FeatureDetector.isMac,
@ -24,7 +24,9 @@ const AutoTypeHintView = Backbone.View.extend({
}); });
const rect = this.input.getBoundingClientRect(); const rect = this.input.getBoundingClientRect();
this.$el.appendTo(document.body).css({ 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 selfRect = this.$el[0].getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect();

View File

@ -58,8 +58,10 @@ const AutoTypePopupView = Backbone.View.extend({
render() { render() {
let topMessage; let topMessage;
if (this.model.filter.title || this.model.filter.url) { if (this.model.filter.title || this.model.filter.url) {
topMessage = Locale.autoTypeMsgMatchedByWindow.replace('{}', topMessage = Locale.autoTypeMsgMatchedByWindow.replace(
this.model.filter.title || this.model.filter.url); '{}',
this.model.filter.title || this.model.filter.url
);
} else { } else {
topMessage = Locale.autoTypeMsgNoWindow; topMessage = Locale.autoTypeMsgNoWindow;
} }

View File

@ -8,7 +8,7 @@ const DetailsAddFieldView = Backbone.View.extend({
'click .details__field-value': 'fieldValueClick' 'click .details__field-value': 'fieldValueClick'
}, },
render: function () { render: function() {
this.renderTemplate(); this.renderTemplate();
this.labelEl = this.$el.find('.details__field-label'); this.labelEl = this.$el.find('.details__field-label');
return this; return this;

View File

@ -1,30 +1,32 @@
const Backbone = require('backbone'); const Backbone = require('backbone');
const FeatureDetector = require('../../util/feature-detector'); const FeatureDetector = require('../../util/feature-detector');
const DetailsAttachmentView = Backbone.View.extend({ const DetailsAttachmentView = Backbone.View.extend({
template: require('templates/details/details-attachment.hbs'), template: require('templates/details/details-attachment.hbs'),
events: { events: {},
},
render: function(complete) { render: function(complete) {
this.renderTemplate({}, true); this.renderTemplate({}, true);
const shortcut = this.$el.find('.details__attachment-preview-download-text-shortcut'); const shortcut = this.$el.find('.details__attachment-preview-download-text-shortcut');
shortcut.html(FeatureDetector.actionShortcutSymbol(false)); 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'); const dataEl = this.$el.find('.details__attachment-preview-data');
switch ((this.model.mimeType || '').split('/')[0]) { switch ((this.model.mimeType || '').split('/')[0]) {
case 'text': case 'text':
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener('loadend', () => { reader.addEventListener('loadend', () => {
$('<pre/>').text(reader.result).appendTo(dataEl); $('<pre/>')
.text(reader.result)
.appendTo(dataEl);
complete(); complete();
}); });
reader.readAsText(blob); reader.readAsText(blob);
return this; return this;
case 'image': case 'image':
$('<img/>').attr('src', URL.createObjectURL(blob)).appendTo(dataEl); $('<img/>')
.attr('src', URL.createObjectURL(blob))
.appendTo(dataEl);
complete(); complete();
return this; return this;
} }

View File

@ -1,4 +1,3 @@
const Backbone = require('backbone'); const Backbone = require('backbone');
const AutoTypeHintView = require('../auto-type-hint-view'); const AutoTypeHintView = require('../auto-type-hint-view');
const Locale = require('../../util/locale'); const Locale = require('../../util/locale');
@ -57,8 +56,10 @@ const DetailsAutoTypeView = Backbone.View.extend({
seqFocus: function(e) { seqFocus: function(e) {
if (!this.views.hint) { if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render(); this.views.hint = new AutoTypeHintView({ input: e.target }).render();
this.views.hint.on('remove', () => { delete this.views.hint; }); this.views.hint.on('remove', () => {
delete this.views.hint;
});
} }
}, },

View File

@ -22,13 +22,55 @@ const DetailsHistoryView = Backbone.View.extend({
}, },
formats: [ formats: [
{ name: 'ms', round: 1, format: function(d) { return Format.dtStr(d); } }, {
{ name: 'sec', round: 1000, format: function(d) { return Format.dtStr(d); } }, name: 'ms',
{ name: 'min', round: 1000 * 60, format: function(d) { return Format.dtStr(d).replace(':00 ', ' '); } }, round: 1,
{ name: 'hour', round: 1000 * 60 * 60, format: function(d) { return Format.dtStr(d).replace(':00', ''); } }, format: function(d) {
{ name: 'day', round: 1000 * 60 * 60 * 24, format: function(d) { return Format.dStr(d); } }, return Format.dtStr(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: '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, fieldViews: null,
@ -45,14 +87,16 @@ const DetailsHistoryView = Backbone.View.extend({
this.timelineEl = this.$el.find('.details__history-timeline'); this.timelineEl = this.$el.find('.details__history-timeline');
this.bodyEl = this.$el.find('.details__history-body'); this.bodyEl = this.$el.find('.details__history-body');
this.timeline.forEach(function(item, ix) { this.timeline.forEach(function(item, ix) {
$('<i/>').addClass('fa fa-circle details__history-timeline-item') $('<i/>')
.css('left', (item.pos * 100) + '%') .addClass('fa fa-circle details__history-timeline-item')
.css('left', item.pos * 100 + '%')
.attr('data-id', ix) .attr('data-id', ix)
.appendTo(this.timelineEl); .appendTo(this.timelineEl);
}, this); }, this);
this.labels.forEach(function(label) { this.labels.forEach(function(label) {
$('<div/>').addClass('details__history-timeline-label') $('<div/>')
.css('left', (label.pos * 100) + '%') .addClass('details__history-timeline-label')
.css('left', label.pos * 100 + '%')
.text(label.text) .text(label.text)
.appendTo(this.timelineEl); .appendTo(this.timelineEl);
}, this); }, this);
@ -78,32 +122,97 @@ const DetailsHistoryView = Backbone.View.extend({
this.activeIx = ix; this.activeIx = ix;
this.record = this.timeline[ix].rec; 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').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.removeFieldViews();
this.bodyEl.html(''); this.bodyEl.html('');
const colorCls = this.record.color ? this.record.color + '-color' : ''; 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(
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Updated', title: Locale.detHistorySaved, new FieldViewReadOnly({ model: { name: 'Rev', title: Locale.detHistoryVersion, value: ix + 1 } })
value: Format.dtStr(this.record.updated) + );
(this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') + this.fieldViews.push(
((ix === this.history.length - 1 && !this.record.unsaved) ? ' (' + Locale.detHistoryCurState + ')' : '') } })); new FieldViewReadOnly({
this.fieldViews.push(new FieldViewReadOnlyRaw({ model: { name: '$Title', title: Format.capFirst(Locale.title), model: {
value: '<i class="fa fa-' + this.record.icon + ' ' + colorCls + '"></i> ' + name: 'Updated',
_.escape(this.record.title) || '(' + Locale.detHistoryNoTitle + ')' } })); title: Locale.detHistorySaved,
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$UserName', title: Format.capFirst(Locale.user), value: this.record.user } })); value:
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Password', title: Format.capFirst(Locale.password), value: this.record.password } })); Format.dtStr(this.record.updated) +
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$URL', title: Format.capFirst(Locale.website), value: this.record.url } })); (this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') +
this.fieldViews.push(new FieldViewReadOnly({ model: { name: '$Notes', title: Format.capFirst(Locale.notes), value: this.record.notes } })); (ix === this.history.length - 1 && !this.record.unsaved
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Tags', title: Format.capFirst(Locale.tags), ? ' (' + Locale.detHistoryCurState + ')'
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.fieldViews.push(
}, this); 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) { if (this.record.attachments.length) {
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'Attachments', title: Locale.detAttachments, this.fieldViews.push(
value: this.record.attachments.map(att => att.title).join(', ') } })); new FieldViewReadOnly({
model: {
name: 'Attachments',
title: Locale.detAttachments,
value: this.record.attachments.map(att => att.title).join(', ')
}
})
);
} }
this.fieldViews.forEach(function(fieldView) { this.fieldViews.forEach(function(fieldView) {
fieldView.setElement(this.bodyEl).render(); fieldView.setElement(this.bodyEl).render();
@ -112,12 +221,15 @@ const DetailsHistoryView = Backbone.View.extend({
const buttons = this.$el.find('.details__history-buttons'); 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-revert').toggle(ix < this.history.length - 1);
buttons.find('.details__history-button-delete').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 && buttons
this.history.length > 1 || false); .find('.details__history-button-discard')
.toggle((this.record.unsaved && ix === this.history.length - 1 && this.history.length > 1) || false);
}, },
timelineItemClick: function(e) { 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); this.showRecord(id);
}, },
@ -142,12 +254,13 @@ const DetailsHistoryView = Backbone.View.extend({
})); }));
const period = lastRec.updated - firstRec.updated; const period = lastRec.updated - firstRec.updated;
const format = this.getDateFormat(period); const format = this.getDateFormat(period);
this.labels = this.getLabels(firstRec.updated.getTime(), lastRec.updated.getTime(), format.round) this.labels = this.getLabels(firstRec.updated.getTime(), lastRec.updated.getTime(), format.round).map(
.map(label => ({ label => ({
pos: (label - firstRec.updated) / (lastRec.updated - firstRec.updated), pos: (label - firstRec.updated) / (lastRec.updated - firstRec.updated),
val: label, val: label,
text: format.format(new Date(label)) text: format.format(new Date(label))
})); })
);
}, },
getDateFormat: function(period) { getDateFormat: function(period) {
@ -170,7 +283,7 @@ const DetailsHistoryView = Backbone.View.extend({
labels.push(label); labels.push(label);
label += round; 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(); labels.shift();
} }
return labels; return labels;

View File

@ -61,7 +61,7 @@ const DetailsView = Backbone.View.extend({
'contextmenu .details': 'contextMenu' 'contextmenu .details': 'contextMenu'
}, },
initialize: function () { initialize: function() {
this.fieldViews = []; this.fieldViews = [];
this.views = {}; this.views = {};
this.initScroll(); this.initScroll();
@ -100,7 +100,7 @@ const DetailsView = Backbone.View.extend({
this.hideFieldCopyTip(); this.hideFieldCopyTip();
}, },
render: function () { render: function() {
this.removeScroll(); this.removeScroll();
this.removeFieldViews(); this.removeFieldViews();
this.removeInnerViews(); this.removeInnerViews();
@ -140,45 +140,175 @@ const DetailsView = Backbone.View.extend({
const fileNames = this.appModel.files.map(function(file) { const fileNames = this.appModel.files.map(function(file) {
return { id: file.id, value: file.get('name'), selected: file === this.model.file }; return { id: file.id, value: file.get('name'), selected: file === this.model.file };
}, this); }, this);
this.fileEditView = new FieldViewSelect({ model: { name: '$File', title: Format.capFirst(Locale.file), this.fileEditView = new FieldViewSelect({
value: function() { return fileNames; } } }); model: {
name: '$File',
title: Format.capFirst(Locale.file),
value: function() {
return fileNames;
}
}
});
this.fieldViews.push(this.fileEditView); this.fieldViews.push(this.fileEditView);
} else { } else {
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Format.capFirst(Locale.file), this.fieldViews.push(
value: function() { return model.fileName; } } })); 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), this.userEditView = new FieldViewAutocomplete({
value: function() { return model.user; }, getCompletions: this.getUserNameCompletions.bind(this) } }); model: {
this.fieldViews.push(this.userEditView); name: '$UserName',
this.passEditView = new FieldViewText({ model: { name: '$Password', title: Format.capFirst(Locale.password), canGen: true, title: Format.capFirst(Locale.user),
value: function() { return model.password; } } }); value: function() {
this.fieldViews.push(this.passEditView); return model.user;
this.urlEditView = new FieldViewUrl({ model: { name: '$URL', title: Format.capFirst(Locale.website), },
value: function() { return model.url; } } }); getCompletions: this.getUserNameCompletions.bind(this)
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); });
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'); 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, const fieldView = new FieldViewCustom({
value: function() { return ''; } } }); model: {
name: '$' + newFieldTitle,
title: newFieldTitle,
newField: newFieldTitle,
value: function() {
return '';
}
}
});
fieldView.on('change', this.fieldChanged.bind(this)); fieldView.on('change', this.fieldChanged.bind(this));
fieldView.setElement(this.$el.find('.details__body-fields')).render(); fieldView.setElement(this.$el.find('.details__body-fields')).render();
fieldView.edit(); fieldView.edit();
@ -240,24 +378,27 @@ const DetailsView = Backbone.View.extend({
if (hideEmptyFields) { if (hideEmptyFields) {
this.fieldViews.forEach(fieldView => { this.fieldViews.forEach(fieldView => {
if (fieldView.isHidden()) { if (fieldView.isHidden()) {
moreOptions.push({value: 'add:' + fieldView.model.name, icon: 'pencil', moreOptions.push({
text: Locale.detMenuAddField.replace('{}', fieldView.model.title)}); value: 'add:' + fieldView.model.name,
icon: 'pencil',
text: Locale.detMenuAddField.replace('{}', fieldView.model.title)
});
} }
}, this); }, this);
moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField}); moreOptions.push({ value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField });
moreOptions.push({value: 'toggle-empty', icon: 'eye', text: Locale.detMenuShowEmpty}); moreOptions.push({ value: 'toggle-empty', icon: 'eye', text: Locale.detMenuShowEmpty });
} else { } else {
moreOptions.push({value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField}); moreOptions.push({ value: 'add-new', icon: 'plus', text: Locale.detMenuAddNewField });
moreOptions.push({value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty}); 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) { 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(); const rect = this.moreView.labelEl[0].getBoundingClientRect();
dropdownView.render({ dropdownView.render({
position: {top: rect.bottom, left: rect.left}, position: { top: rect.bottom, left: rect.left },
options: moreOptions options: moreOptions
}); });
this.views.dropdownView = dropdownView; this.views.dropdownView = dropdownView;
@ -301,7 +442,9 @@ const DetailsView = Backbone.View.extend({
}, },
setSelectedColor: function(color) { 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]; const colorEl = this.$el.find('.details__header-color')[0];
_.forEach(colorEl.classList, cls => { _.forEach(colorEl.classList, cls => {
if (cls.indexOf('color') > 0 && cls.lastIndexOf('details', 0) !== 0) { if (cls.indexOf('color') > 0 && cls.lastIndexOf('details', 0) !== 0) {
@ -309,13 +452,17 @@ const DetailsView = Backbone.View.extend({
} }
}); });
if (color) { 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'); colorEl.classList.add(color + '-color');
} }
}, },
selectColor: function(e) { 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) { if (!color) {
return; return;
} }
@ -336,7 +483,8 @@ const DetailsView = Backbone.View.extend({
el: this.scroller, el: this.scroller,
model: { model: {
iconId: this.model.customIconId || this.model.iconId, 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); this.listenTo(subView, 'select', this.iconSelected);
@ -379,7 +527,7 @@ const DetailsView = Backbone.View.extend({
return; return;
} }
const mimeType = attachment.mimeType || 'application/octet-stream'; 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); FileSaver.saveAs(blob, attachment.title);
}, },
@ -408,7 +556,9 @@ const DetailsView = Backbone.View.extend({
}, },
copyKeyPress: function(editView) { copyKeyPress: function(editView) {
if (this.isHidden()) { return; } if (this.isHidden()) {
return;
}
if (!window.getSelection().toString()) { if (!window.getSelection().toString()) {
const fieldValue = editView.value; const fieldValue = editView.value;
const fieldText = fieldValue && fieldValue.isProtected ? fieldValue.getText() : fieldValue; 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' }); const tip = new Tip(label, { title: Locale.detCopyHint, placement: 'right' });
tip.show(); tip.show();
this.fieldCopyTip = tip; this.fieldCopyTip = tip;
setTimeout(() => { tip.hide(); }, Timeouts.AutoHideHint); setTimeout(() => {
tip.hide();
}, Timeouts.AutoHideHint);
}, },
settingsToggled: function() { settingsToggled: function() {
@ -500,8 +652,11 @@ const DetailsView = Backbone.View.extend({
} }
this.entryUpdated(true); this.entryUpdated(true);
this.fieldViews.forEach(function(fieldView, ix) { this.fieldViews.forEach(function(fieldView, ix) {
if (fieldView instanceof FieldViewCustom && !fieldView.model.newField && if (
!this.model.hasField(fieldView.model.title)) { fieldView instanceof FieldViewCustom &&
!fieldView.model.newField &&
!this.model.hasField(fieldView.model.title)
) {
fieldView.remove(); fieldView.remove();
this.fieldViews.splice(ix, 1); this.fieldViews.splice(ix, 1);
} else { } else {
@ -584,13 +739,17 @@ const DetailsView = Backbone.View.extend({
}, },
addAttachedFiles: function(files) { addAttachedFiles: function(files) {
_.forEach(files, function(file) { _.forEach(
const reader = new FileReader(); files,
reader.onload = () => { function(file) {
this.addAttachment(file.name, reader.result); const reader = new FileReader();
}; reader.onload = () => {
reader.readAsArrayBuffer(file); this.addAttachment(file.name, reader.result);
}, this); };
reader.readAsArrayBuffer(file);
},
this
);
}, },
addAttachment: function(name, data) { addAttachment: function(name, data) {
@ -676,7 +835,8 @@ const DetailsView = Backbone.View.extend({
}, },
focusNextField: function(config) { focusNextField: function(config) {
let found = false, nextFieldView; let found = false,
nextFieldView;
if (config.field === '$Title' && !config.prev) { if (config.field === '$Title' && !config.prev) {
found = true; found = true;
} }
@ -793,10 +953,14 @@ const DetailsView = Backbone.View.extend({
} else { } else {
this.moreView.remove(); this.moreView.remove();
this.moreView = null; this.moreView = null;
const fieldView = new FieldViewCustom({ model: { const fieldView = new FieldViewCustom({
name: '$otp', title: 'otp', newField: 'otp', model: {
value: kdbxweb.ProtectedValue.fromString('') name: '$otp',
}}); title: 'otp',
newField: 'otp',
value: kdbxweb.ProtectedValue.fromString('')
}
});
fieldView.on('change', this.fieldChanged.bind(this)); fieldView.on('change', this.fieldChanged.bind(this));
fieldView.setElement(this.$el.find('.details__body-fields')).render(); fieldView.setElement(this.$el.find('.details__body-fields')).render();
fieldView.edit(); fieldView.edit();

View File

@ -5,7 +5,7 @@ const DragView = Backbone.View.extend({
'mousedown': 'mousedown' 'mousedown': 'mousedown'
}, },
initialize: function (coord) { initialize: function(coord) {
this.setCoord(coord); this.setCoord(coord);
this.mouseDownTime = -1; this.mouseDownTime = -1;
this.mouseDownCount = 0; this.mouseDownCount = 0;
@ -17,7 +17,9 @@ const DragView = Backbone.View.extend({
}, },
render: function() { render: function() {
$('<div/>').addClass('drag-handle__inner').appendTo(this.$el); $('<div/>')
.addClass('drag-handle__inner')
.appendTo(this.$el);
}, },
mousedown: function(e) { mousedown: function(e) {
@ -35,7 +37,9 @@ const DragView = Backbone.View.extend({
} }
this.initialOffset = e[this.offsetProp]; this.initialOffset = e[this.offsetProp];
const cursor = this.$el.css('cursor'); 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('mousemove', this.mousemove.bind(this));
this.dragMask.on('mouseup', this.mouseup.bind(this)); this.dragMask.on('mouseup', this.mouseup.bind(this));
this.trigger('dragstart', { offset: this.initialOffset, coord: this.coord }); this.trigger('dragstart', { offset: this.initialOffset, coord: this.coord });

View File

@ -7,19 +7,19 @@ const DropdownView = Backbone.View.extend({
'click .dropdown__item': 'itemClick' 'click .dropdown__item': 'itemClick'
}, },
initialize: function () { initialize: function() {
this.bodyClick = this.bodyClick.bind(this); this.bodyClick = this.bodyClick.bind(this);
this.listenTo(Backbone, 'show-context-menu', this.bodyClick); this.listenTo(Backbone, 'show-context-menu', this.bodyClick);
$('body').on('click contextmenu keyup', this.bodyClick); $('body').on('click contextmenu keyup', this.bodyClick);
}, },
render: function (config) { render: function(config) {
this.options = config.options; this.options = config.options;
this.renderTemplate(config); this.renderTemplate(config);
this.$el.appendTo(document.body); this.$el.appendTo(document.body);
const ownRect = this.$el[0].getBoundingClientRect(); const ownRect = this.$el[0].getBoundingClientRect();
const bodyRect = document.body.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; let top = config.position.top;
if (left + ownRect.width > bodyRect.right) { if (left + ownRect.width > bodyRect.right) {
left = Math.max(0, bodyRect.right - ownRect.width); left = Math.max(0, bodyRect.right - ownRect.width);

View File

@ -61,7 +61,8 @@ const FieldViewAutocomplete = FieldViewText.extend({
moveAutocomplete: function(next) { moveAutocomplete: function(next) {
const completions = this.model.getCompletions(this.input.val()); const completions = this.model.getCompletions(this.input.val());
if (typeof this.selectedCopmletionIx === 'number') { 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 { } else {
this.selectedCopmletionIx = next ? 0 : completions.length - 1; this.selectedCopmletionIx = next ? 0 : completions.length - 1;
} }
@ -70,10 +71,12 @@ const FieldViewAutocomplete = FieldViewText.extend({
updateAutocomplete: function() { updateAutocomplete: function() {
const completions = this.model.getCompletions(this.input.val()); const completions = this.model.getCompletions(this.input.val());
const completionsHtml = completions.map((item, ix) => { const completionsHtml = completions
const sel = ix === this.selectedCopmletionIx ? 'details__field-autocomplete-item--selected' : ''; .map((item, ix) => {
return '<div class="details__field-autocomplete-item ' + sel + '">' + _.escape(item) + '</div>'; const sel = ix === this.selectedCopmletionIx ? 'details__field-autocomplete-item--selected' : '';
}).join(''); return '<div class="details__field-autocomplete-item ' + sel + '">' + _.escape(item) + '</div>';
})
.join('');
this.autocomplete.html(completionsHtml); this.autocomplete.html(completionsHtml);
this.autocomplete.toggle(!!completionsHtml); this.autocomplete.toggle(!!completionsHtml);
}, },
@ -85,7 +88,9 @@ const FieldViewAutocomplete = FieldViewText.extend({
this.input.val(selectedItem); this.input.val(selectedItem);
this.endEdit(selectedItem); this.endEdit(selectedItem);
} else { } else {
this.afterPaint(function () { this.input.focus(); }); this.afterPaint(function() {
this.input.focus();
});
} }
} }
}); });

View File

@ -22,7 +22,8 @@ const FieldViewCustom = FieldViewText.extend({
this.isProtected = this.value instanceof kdbxweb.ProtectedValue; this.isProtected = this.value instanceof kdbxweb.ProtectedValue;
} }
this.$el.toggleClass('details__field--protected', this.isProtected); 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) .appendTo(this.valueEl)
.mousedown(this.protectBtnClick.bind(this)); .mousedown(this.protectBtnClick.bind(this));
let securityTipTitle = Locale.detLockField; let securityTipTitle = Locale.detLockField;
@ -63,8 +64,11 @@ const FieldViewCustom = FieldViewText.extend({
const text = emptyTitle ? '' : this.model.title || ''; const text = emptyTitle ? '' : this.model.title || '';
this.labelInput = $('<input/>'); this.labelInput = $('<input/>');
this.labelEl.html('').append(this.labelInput); this.labelEl.html('').append(this.labelInput);
this.labelInput.attr({ autocomplete: 'off', spellcheck: 'false' }) this.labelInput
.val(text).focus()[0].setSelectionRange(text.length, text.length); .attr({ autocomplete: 'off', spellcheck: 'false' })
.val(text)
.focus()[0]
.setSelectionRange(text.length, text.length);
this.labelInput.bind({ this.labelInput.bind({
input: this.fieldLabelInput.bind(this), input: this.fieldLabelInput.bind(this),
keydown: this.fieldLabelKeydown.bind(this), keydown: this.fieldLabelKeydown.bind(this),
@ -152,7 +156,9 @@ const FieldViewCustom = FieldViewText.extend({
if (this.labelInput) { if (this.labelInput) {
this.endEditTitle(this.labelInput.val()); this.endEditTitle(this.labelInput.val());
} }
this.setTimeout(function() { this.input.focus(); }); this.setTimeout(function() {
this.input.focus();
});
} }
}); });

View File

@ -44,7 +44,9 @@ const FieldViewDate = FieldViewText.extend({
endEdit: function(newVal, extra) { endEdit: function(newVal, extra) {
if (this.picker) { if (this.picker) {
try { this.picker.destroy(); } catch (e) {} try {
this.picker.destroy();
} catch (e) {}
this.picker = null; this.picker = null;
} }
newVal = new Date(newVal); newVal = new Date(newVal);

View File

@ -4,13 +4,24 @@ const FieldViewSelect = FieldView.extend({
readonly: true, readonly: true,
renderValue: function(value) { renderValue: function(value) {
return '<select>' + return (
value.map(opt => { '<select>' +
return '<option ' + 'value="' + _.escape(opt.id) + '" ' + (opt.selected ? 'selected ' : '') + '>' + value
_.escape(opt.value) + .map(opt => {
'</option>'; return (
}).join('') + '<option ' +
'</select>'; 'value="' +
_.escape(opt.id) +
'" ' +
(opt.selected ? 'selected ' : '') +
'>' +
_.escape(opt.value) +
'</option>'
);
})
.join('') +
'</select>'
);
}, },
render: function() { render: function() {

View File

@ -14,9 +14,14 @@ const FieldViewTags = FieldViewText.extend({
this.model.tags.forEach(tag => { this.model.tags.forEach(tag => {
allTags[tag.toLowerCase()] = tag; allTags[tag.toLowerCase()] = tag;
}); });
return _.unique(val.split(/\s*[;,:]\s*/).filter(_.identity).map(tag => { return _.unique(
return allTags[tag.toLowerCase()] || tag; val
})); .split(/\s*[;,:]\s*/)
.filter(_.identity)
.map(tag => {
return allTags[tag.toLowerCase()] || tag;
})
);
}, },
endEdit: function(newVal, extra) { endEdit: function(newVal, extra) {
@ -60,9 +65,11 @@ const FieldViewTags = FieldViewText.extend({
setTags: function() { setTags: function() {
const availableTags = this.getAvailableTags(); const availableTags = this.getAvailableTags();
const tagsHtml = availableTags.map(tag => { const tagsHtml = availableTags
return '<div class="details__field-autocomplete-item">' + _.escape(tag) + '</div>'; .map(tag => {
}).join(''); return '<div class="details__field-autocomplete-item">' + _.escape(tag) + '</div>';
})
.join('');
this.tagsAutocomplete.html(tagsHtml); this.tagsAutocomplete.html(tagsHtml);
this.tagsAutocomplete.toggle(!!tagsHtml); this.tagsAutocomplete.toggle(!!tagsHtml);
}, },
@ -88,7 +95,9 @@ const FieldViewTags = FieldViewText.extend({
this.input.focus(); this.input.focus();
this.setTags(); this.setTags();
} }
this.afterPaint(function() { this.input.focus(); }); this.afterPaint(function() {
this.input.focus();
});
} }
}); });

View File

@ -10,7 +10,8 @@ const Tip = require('../../util/tip');
const FieldViewText = FieldView.extend({ const FieldViewText = FieldView.extend({
renderValue: function(value) { 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/>'); : _.escape(value || '').replace(/\n/g, '<br/>');
}, },
@ -24,8 +25,11 @@ const FieldViewText = FieldView.extend({
this.$el.toggleClass('details__field--protected', isProtected); this.$el.toggleClass('details__field--protected', isProtected);
this.input = $(document.createElement(this.model.multiline ? 'textarea' : 'input')); this.input = $(document.createElement(this.model.multiline ? 'textarea' : 'input'));
this.valueEl.html('').append(this.input); this.valueEl.html('').append(this.input);
this.input.attr({ autocomplete: 'off', spellcheck: 'false' }) this.input
.val(text).focus()[0].setSelectionRange(text.length, text.length); .attr({ autocomplete: 'off', spellcheck: 'false' })
.val(text)
.focus()[0]
.setSelectionRange(text.length, text.length);
this.input.bind({ this.input.bind({
input: this.fieldValueInput.bind(this), input: this.fieldValueInput.bind(this),
keydown: this.fieldValueKeydown.bind(this), keydown: this.fieldValueKeydown.bind(this),
@ -42,7 +46,9 @@ const FieldViewText = FieldView.extend({
this.createMobileControls(); this.createMobileControls();
} }
if (this.model.canGen) { 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)) .click(this.showGeneratorClick.bind(this))
.mousedown(this.showGenerator.bind(this)); .mousedown(this.showGenerator.bind(this));
} }
@ -78,7 +84,9 @@ const FieldViewText = FieldView.extend({
this.hideGenerator(); this.hideGenerator();
} else { } else {
const fieldRect = this.input[0].getBoundingClientRect(); 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('remove', this.generatorClosed.bind(this));
this.gen.once('result', this.generatorResult.bind(this)); this.gen.once('result', this.generatorResult.bind(this));
} }
@ -231,8 +239,11 @@ const FieldViewText = FieldView.extend({
mobileFieldControlTouchMove(e) { mobileFieldControlTouchMove(e) {
const touch = e.originalEvent.targetTouches[0]; const touch = e.originalEvent.targetTouches[0];
const rect = touch.target.getBoundingClientRect(); const rect = touch.target.getBoundingClientRect();
const inside = touch.clientX >= rect.left && touch.clientX <= rect.right && const inside =
touch.clientY >= rect.top && touch.clientY <= rect.bottom; touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom;
if (inside) { if (inside) {
this.$el.attr('active-mobile-action', $(e.target).data('action')); this.$el.attr('active-mobile-action', $(e.target).data('action'));
} else { } else {

View File

@ -4,7 +4,13 @@ const FieldViewUrl = FieldViewText.extend({
displayUrlRegex: /^http:\/\//i, displayUrlRegex: /^http:\/\//i,
renderValue: function(value) { 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) { fixUrl: function(url) {

View File

@ -13,8 +13,13 @@ const FieldView = Backbone.View.extend({
render: function() { render: function() {
this.value = typeof this.model.value === 'function' ? this.model.value() : this.model.value; 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, this.renderTemplate({
canEditTitle: this.model.newField, protect: this.value && this.value.isProtected }); 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 = this.$el.find('.details__field-value');
this.valueEl.html(this.renderValue(this.value)); this.valueEl.html(this.renderValue(this.value));
this.labelEl = this.$el.find('.details__field-label'); this.labelEl = this.$el.find('.details__field-label');
@ -38,7 +43,10 @@ const FieldView = Backbone.View.extend({
update: function() { update: function() {
if (typeof this.model.value === 'function') { if (typeof this.model.value === 'function') {
const newVal = this.model.value(); 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(); this.render();
} }
} }
@ -121,7 +129,9 @@ const FieldView = Backbone.View.extend({
return; return;
} }
this.editing = false; this.editing = false;
setTimeout(() => { this.preventCopy = false; }, 300); setTimeout(() => {
this.preventCopy = false;
}, 300);
let textEqual; let textEqual;
if (this.value && this.value.isProtected) { if (this.value && this.value.isProtected) {
textEqual = this.value.equals(newVal); textEqual = this.value.equals(newVal);

View File

@ -16,7 +16,7 @@ const FooterView = Backbone.View.extend({
'click .footer__btn-lock': 'lockWorkspace' 'click .footer__btn-lock': 'lockWorkspace'
}, },
initialize: function () { initialize: function() {
this.views = {}; this.views = {};
KeyHandler.onKey(Keys.DOM_VK_L, this.lockWorkspace, this, KeyHandler.SHORTCUT_ACTION, false, true); 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); this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render);
}, },
render: function () { render: function() {
this.renderTemplate({ this.renderTemplate(
files: this.model.files, {
updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0 files: this.model.files,
}, { plain: true }); updateAvailable: ['ready', 'found'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
},
{ plain: true }
);
return this; return this;
}, },
@ -65,12 +68,16 @@ const FooterView = Backbone.View.extend({
const right = bodyRect.right - rect.right; const right = bodyRect.right - rect.right;
const bottom = bodyRect.bottom - rect.top; const bottom = bodyRect.bottom - rect.top;
const generator = new GeneratorView({ model: { copy: true, pos: { right: right, bottom: bottom } } }).render(); 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; this.views.gen = generator;
}, },
showFile: function(e) { 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) { if (fileId) {
Backbone.trigger('show-file', { fileId: fileId }); Backbone.trigger('show-file', { fileId: fileId });
} }

View File

@ -33,11 +33,14 @@ const GeneratorPresetsView = Backbone.View.extend({
if (!this.selected || !this.presets.some(p => p.name === this.selected)) { 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.selected = (this.presets.filter(p => p.default)[0] || this.presets[0]).name;
} }
this.renderTemplate({ this.renderTemplate(
presets: this.presets, {
selected: this.getPreset(this.selected), presets: this.presets,
ranges: this.getSelectedRanges() selected: this.getPreset(this.selected),
}, true); ranges: this.getSelectedRanges()
},
true
);
this.createScroll({ this.createScroll({
root: this.$el.find('.gen-ps')[0], root: this.$el.find('.gen-ps')[0],
scroller: this.$el.find('.scroller')[0], scroller: this.$el.find('.scroller')[0],
@ -97,10 +100,15 @@ const GeneratorPresetsView = Backbone.View.extend({
} }
const selected = this.getPreset(this.selected); const selected = this.getPreset(this.selected);
const preset = { const preset = {
name, title, name,
title,
length: selected.length, length: selected.length,
upper: selected.upper, lower: selected.lower, digits: selected.digits, upper: selected.upper,
special: selected.special, brackets: selected.brackets, ambiguous: selected.ambiguous, lower: selected.lower,
digits: selected.digits,
special: selected.special,
brackets: selected.brackets,
ambiguous: selected.ambiguous,
include: selected.include include: selected.include
}; };
GeneratorPresets.add(preset); GeneratorPresets.add(preset);

View File

@ -29,7 +29,7 @@ const GeneratorView = Backbone.View.extend({
presets: null, presets: null,
preset: null, preset: null,
initialize: function () { initialize: function() {
this.createPresets(); this.createPresets();
const preset = this.preset; const preset = this.preset;
this.gen = _.clone(_.find(this.presets, pr => pr.name === preset)); this.gen = _.clone(_.find(this.presets, pr => pr.name === preset));
@ -40,7 +40,7 @@ const GeneratorView = Backbone.View.extend({
render: function() { render: function() {
const canCopy = document.queryCommandSupported('copy'); 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({ this.renderTemplate({
btnTitle: btnTitle, btnTitle: btnTitle,
showToggleButton: this.model.copy, showToggleButton: this.model.copy,
@ -112,8 +112,10 @@ const GeneratorView = Backbone.View.extend({
}, },
optionChanged: function(option) { optionChanged: function(option) {
if (this.preset === 'Custom' || if (
this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0) { this.preset === 'Custom' ||
(this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0)
) {
return; return;
} }
this.preset = this.gen.name = 'Custom'; this.preset = this.gen.name = 'Custom';
@ -132,7 +134,7 @@ const GeneratorView = Backbone.View.extend({
// AppSettingsModel.instance.unset('generatorHidePassword', { silent: true }); // AppSettingsModel.instance.unset('generatorHidePassword', { silent: true });
AppSettingsModel.instance.set('generatorHidePassword', this.hide); AppSettingsModel.instance.set('generatorHidePassword', this.hide);
const label = this.$el.find('.gen__check-hide-label'); 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(); this.showPassword();
}, },

View File

@ -25,17 +25,20 @@ const GrpView = Backbone.View.extend({
render: function() { render: function() {
this.removeSubView(); this.removeSubView();
if (this.model) { if (this.model) {
this.renderTemplate({ this.renderTemplate(
title: this.model.get('title'), {
icon: this.model.get('icon') || 'folder', title: this.model.get('title'),
customIcon: this.model.get('customIcon'), icon: this.model.get('icon') || 'folder',
enableSearching: this.model.getEffectiveEnableSearching(), customIcon: this.model.get('customIcon'),
readonly: this.model.get('top'), enableSearching: this.model.getEffectiveEnableSearching(),
canAutoType: AutoType.enabled, readonly: this.model.get('top'),
autoTypeSeq: this.model.get('autoTypeSeq'), canAutoType: AutoType.enabled,
autoTypeEnabled: this.model.getEffectiveEnableAutoType(), autoTypeSeq: this.model.get('autoTypeSeq'),
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq() autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
}, true); defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
},
true
);
if (!this.model.get('title')) { if (!this.model.get('title')) {
this.$el.find('#grp__field-title').focus(); this.$el.find('#grp__field-title').focus();
} }
@ -88,8 +91,10 @@ const GrpView = Backbone.View.extend({
focusAutoTypeSeq: function(e) { focusAutoTypeSeq: function(e) {
if (!this.views.hint) { if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render(); this.views.hint = new AutoTypeHintView({ input: e.target }).render();
this.views.hint.on('remove', () => { delete this.views.hint; }); this.views.hint.on('remove', () => {
delete this.views.hint;
});
} }
}, },

View File

@ -22,12 +22,15 @@ const IconSelectView = Backbone.View.extend({
}, },
render: function() { render: function() {
this.renderTemplate({ this.renderTemplate(
sel: this.model.iconId, {
icons: IconMap, sel: this.model.iconId,
canDownloadFavicon: !!this.model.url, icons: IconMap,
customIcons: this.model.file.getCustomIcons() canDownloadFavicon: !!this.model.url,
}, true); customIcons: this.model.file.getCustomIcons()
},
true
);
return this; return this;
}, },
@ -63,13 +66,17 @@ const IconSelectView = Backbone.View.extend({
this.setSpecialImage(img, 'download'); this.setSpecialImage(img, 'download');
this.$el.find('.icon-select__icon-download img').remove(); 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>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; this.downloadingFavicon = false;
}; };
img.onerror = e => { img.onerror = e => {
logger.error('Favicon download error: ' + url, 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>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') .removeClass('icon-select__icon--custom-selected')
.addClass('icon-select__icon--download-error'); .addClass('icon-select__icon--download-error');
this.downloadingFavicon = false; this.downloadingFavicon = false;
@ -103,7 +110,10 @@ const IconSelectView = Backbone.View.extend({
img.onload = () => { img.onload = () => {
this.setSpecialImage(img, 'select'); this.setSpecialImage(img, 'select');
this.$el.find('.icon-select__icon-select img').remove(); 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; img.src = e.target.result;
}; };

View File

@ -65,7 +65,10 @@ const KeyChangeView = Backbone.View.extend({
this.keyFile = null; this.keyFile = null;
this.$el.find('.key-change__keyfile-name').html(''); 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(); this.inputEl.focus();
}, },

View File

@ -31,18 +31,58 @@ const ListSearchView = Backbone.View.extend({
advancedSearchEnabled: false, advancedSearchEnabled: false,
advancedSearch: null, advancedSearch: null,
initialize: function () { initialize: function() {
this.sortOptions = [ 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: 'title',
{ value: 'website', icon: 'sort-alpha-asc', loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchAZ) }, icon: 'sort-alpha-asc',
{ value: '-website', icon: 'sort-alpha-desc', loc: () => Format.capFirst(Locale.website) + ' ' + this.addArrow(Locale.searchZA) }, loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchAZ)
{ 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: '-title',
{ value: '-created', icon: 'sort-numeric-desc', loc: () => Locale.searchCreated + ' ' + this.addArrow(Locale.searchNO) }, icon: 'sort-alpha-desc',
{ value: 'updated', icon: 'sort-numeric-asc', loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchON) }, loc: () => Format.capFirst(Locale.title) + ' ' + this.addArrow(Locale.searchZA)
{ value: '-updated', icon: 'sort-numeric-desc', loc: () => Locale.searchUpdated + ' ' + this.addArrow(Locale.searchNO) }, },
{
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: '-attachments', icon: 'sort-amount-desc', loc: () => Locale.searchAttachments },
{ value: '-rank', icon: 'sort-numeric-desc', loc: () => Locale.searchRank } { value: '-rank', icon: 'sort-numeric-desc', loc: () => Locale.searchRank }
]; ];
@ -52,11 +92,16 @@ const ListSearchView = Backbone.View.extend({
}, this); }, this);
this.views = {}; this.views = {};
this.advancedSearch = { this.advancedSearch = {
user: true, other: true, user: true,
url: true, protect: false, other: true,
notes: true, pass: false, url: true,
cs: false, regex: false, protect: false,
history: false, title: true, notes: true,
pass: false,
cs: false,
regex: false,
history: false,
title: true,
rank: true rank: true
}; };
if (this.model.advancedSearch) { if (this.model.advancedSearch) {
@ -83,9 +128,16 @@ const ListSearchView = Backbone.View.extend({
}, },
setLocale: function() { setLocale: function() {
this.sortOptions.forEach(opt => { opt.text = opt.loc(); }); this.sortOptions.forEach(opt => {
const entryDesc = FeatureDetector.isMobile ? '' : (' <span class="muted-color">(' + Locale.searchShiftClickOr + ' ' + opt.text = opt.loc();
FeatureDetector.altShortcutSymbol(true) + 'N)</span>'); });
const entryDesc = FeatureDetector.isMobile
? ''
: ' <span class="muted-color">(' +
Locale.searchShiftClickOr +
' ' +
FeatureDetector.altShortcutSymbol(true) +
'N)</span>';
this.createOptions = [ this.createOptions = [
{ value: 'entry', icon: 'key', text: Format.capFirst(Locale.entry) + entryDesc }, { value: 'entry', icon: 'key', text: Format.capFirst(Locale.entry) + entryDesc },
{ value: 'group', icon: 'folder', text: Format.capFirst(Locale.group) } { value: 'group', icon: 'folder', text: Format.capFirst(Locale.group) }
@ -105,7 +157,7 @@ const ListSearchView = Backbone.View.extend({
this.stopListening(KeyHandler, 'keypress', this.documentKeyPress); this.stopListening(KeyHandler, 'keypress', this.documentKeyPress);
}, },
render: function () { render: function() {
let searchVal; let searchVal;
if (this.inputEl) { if (this.inputEl) {
searchVal = this.inputEl.val(); searchVal = this.inputEl.val();

Some files were not shown because too many files have changed in this diff Show More