Merge branch 'develop'

This commit is contained in:
antelle 2016-07-11 20:26:23 +03:00
commit 2338ca589c
139 changed files with 9037 additions and 496 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ tmp/
build/
coverage/
keys/
*.user
bin/
obj/

View File

@ -16,9 +16,9 @@ module.exports = function(grunt) {
var webpack = require('webpack');
var pkg = require('./package.json');
var dt = new Date().toISOString().replace(/T.*/, '');
var electronVersion = '0.37.4';
var minElectronVersionForUpdate = '0.32.0';
var minElectronVersionForUpdate = '1.0.1';
var zipCommentPlaceholder = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
var electronVersion = pkg.devDependencies['electron-prebuilt'].replace(/^\D/, '');
while (zipCommentPlaceholder.length < 512) {
zipCommentPlaceholder += '.';
@ -98,12 +98,12 @@ module.exports = function(grunt) {
nonull: true
},
'desktop_osx': {
src: 'tmp/desktop/KeeWeb.dmg',
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb-' + pkg.version + '.dmg',
dest: 'dist/desktop/KeeWeb.mac.dmg',
nonull: true
},
'desktop_win': {
src: 'tmp/desktop/KeeWeb Setup.exe',
src: 'tmp/desktop/win/KeeWebSetup-' + pkg.version + '-ia32.exe',
dest: 'dist/desktop/KeeWeb.win32.exe',
nonull: true
},
@ -253,6 +253,9 @@ module.exports = function(grunt) {
Buffer: false,
__filename: false,
__dirname: false
},
externals: {
xmldom: 'null'
}
}
},
@ -299,13 +302,6 @@ module.exports = function(grunt) {
'app-version': pkg.version,
'build-version': '<%= gitinfo.local.branch.current.shortSHA %>'
},
osx: {
options: {
platform: 'darwin',
arch: 'x64',
icon: 'graphics/app.icns'
}
},
linux64: {
options: {
platform: 'linux',
@ -319,58 +315,40 @@ module.exports = function(grunt) {
arch: 'ia32',
icon: 'graphics/app.ico'
}
},
win32: {
options: {
platform: 'win32',
arch: 'ia32',
icon: 'graphics/app.ico',
'version-string': {
CompanyName: 'antelle.github.io',
LegalCopyright: 'Antelle, MIT license',
FileDescription: 'KeeWeb Desktop',
OriginalFilename: 'KeeWeb.exe',
FileVersion: pkg.version,
ProductVersion: pkg.version,
ProductName: 'KeeWeb',
InternalName: 'KeeWeb'
}
}
}
},
'electron-builder': {
options: {
out: path.join(__dirname, 'tmp/desktop'),
basePath: __dirname,
config: {
osx: {
title: 'KeeWeb',
background: path.join(__dirname, 'graphics/dmg-bg.png'),
icon: path.join(__dirname, 'graphics/app.icns'),
'icon-size': 80,
contents: [
{'x': 438, 'y': 344, 'type': 'link', 'path': '/Applications'},
{'x': 192, 'y': 344, 'type': 'file'}
]
},
win: {
title: 'KeeWeb',
icon: path.join(__dirname, 'graphics/app.ico')
}
}
publish: 'never',
dist: false,
projectDir: __dirname,
appDir: 'tmp/desktop/app',
sign: false
},
osx: {
options: {
platform: 'osx',
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app')
platforms: ['osx'],
arch: 'x64'
}
},
win: {
options: {
platform: 'win32',
appPath: path.join(__dirname, 'tmp/desktop/KeeWeb-win32-ia32')
platform: ['win32'],
arch: 'ia32'
}
}
// linux64: {
// options: {
// platform: ['linux'],
// arch: 'x64'
// }
// },
// linux32: {
// options: {
// platform: ['linux'],
// arch: 'ia32'
// }
// }
},
compress: {
linux64: {
@ -488,7 +466,8 @@ module.exports = function(grunt) {
'sign-archive:desktop_update',
'validate-desktop-update',
'electron',
'electron-builder',
'electron-builder:osx',
'electron-builder:win',
'compress:linux64',
'compress:linux32',
'deb:linux64',

View File

@ -8,38 +8,83 @@ var AppModel = require('./models/app-model'),
Alerts = require('./comp/alerts'),
Updater = require('./comp/updater'),
AuthReceiver = require('./comp/auth-receiver'),
ExportApi = require('./comp/export-api'),
ThemeChanger = require('./util/theme-changer'),
Locale = require('./util/locale');
$(function() {
if ((window.parent !== window.top) || window.opener) {
if (isPopup()) {
return AuthReceiver.receive();
}
require('./mixins/view');
require('./helpers');
KeyHandler.init();
IdleTracker.init();
PopupNotifier.init();
loadMixins();
initModules();
var appModel = new AppModel();
if (appModel.settings.get('theme')) {
ThemeChanger.setTheme(appModel.settings.get('theme'));
}
var skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !skipHttpsWarning) {
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
buttons: [
{ result: '', title: Locale.appSecWarnBtn, error: true }
],
complete: showApp
ThemeChanger.setBySettings(appModel.settings);
var configParam = getConfigParam();
if (configParam) {
appModel.loadConfig(configParam, function(err) {
if (err) {
showSettingsLoadError();
} else {
ThemeChanger.setBySettings(appModel.settings);
showApp();
}
});
} else {
showApp();
}
function isPopup() {
return (window.parent !== window.top) || window.opener;
}
function loadMixins() {
require('./mixins/view');
require('./helpers');
}
function initModules() {
KeyHandler.init();
IdleTracker.init();
PopupNotifier.init();
window.kw = ExportApi;
}
function showSettingsLoadError() {
ThemeChanger.setBySettings(appModel.settings);
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appSettingsErrorBody,
buttons: [],
esc: false, enter: false, click: false
});
}
function showApp() {
var skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
if (['https:', 'file:', 'app:'].indexOf(location.protocol) < 0 && !skipHttpsWarning) {
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
buttons: [
{ result: '', title: Locale.appSecWarnBtn, error: true }
],
complete: showView
});
} else {
showView();
}
}
function showView() {
new AppView({ model: appModel }).render();
Updater.init();
}
function getConfigParam() {
var match = location.search.match(/[\?&]config=([^&]+)/i);
if (match && match[1]) {
return match[1];
}
}
});

View File

@ -0,0 +1,15 @@
'use strict';
var Launcher = require('../comp/launcher');
var AutoTypeEmitterFactory = {
create: function(callback) {
if (!Launcher) {
return null;
}
var AutoTypeEmitter = require('./emitter/auto-type-emitter-' + Launcher.platform());
return new AutoTypeEmitter(callback);
}
};
module.exports = AutoTypeEmitterFactory;

View File

@ -0,0 +1,15 @@
'use strict';
var Launcher = require('../comp/launcher');
var AutoTypeHelperFactory = {
create: function() {
if (!Launcher) {
return null;
}
var AutoTypeHelper = require('./helper/auto-type-helper-' + Launcher.platform());
return new AutoTypeHelper();
}
};
module.exports = AutoTypeHelperFactory;

View File

@ -0,0 +1,203 @@
'use strict';
var Logger = require('../util/logger');
var logger = new Logger('auto-type-obfuscator');
logger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
var MaxFakeOps = 30;
var MaxSteps = 1000;
var MaxCopy = 2;
var FakeCharAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789O0oIl';
var AutoTypeObfuscator = function(chars) {
this.chars = chars;
this.inputChars = [];
this.inputCursor = 0;
this.inputSel = 0;
this.ops = [];
this.stepCount = 0;
this.copyCount = 0;
};
AutoTypeObfuscator.prototype.obfuscate = function() {
while (!this.finished()) {
this.step();
if (this.stepCount++ > MaxSteps) {
throw 'Obfuscate failed';
}
}
for (var i = 0; i < this.chars.length; i++) {
this.chars[i] = null;
this.inputChars[i] = null;
}
return this.ops;
};
AutoTypeObfuscator.prototype.finished = function() {
return this.chars.length === this.inputChars.length &&
this.chars.every(function(ch, ix) { return this.inputChars[ix].ch === ch; }, this);
};
AutoTypeObfuscator.prototype.step = function() {
var isFake = this.stepCount < MaxFakeOps && Math.random() > this.stepCount / MaxFakeOps;
if (isFake) {
this.stepFake();
} else {
this.stepReal();
}
if (logger.getLevel() >= Logger.Level.Debug) {
logger.debug('value', this.inputChars.map(function(ic) { return ic.ch; }).join(''));
}
};
AutoTypeObfuscator.prototype.stepFake = function() {
var pos = Math.floor(Math.random() * (this.inputChars.length + 1));
var ch = FakeCharAlphabet[Math.floor(Math.random() * FakeCharAlphabet.length)];
logger.info('step.fake', pos, ch);
this.moveToPos(pos);
var insert = this.inputChars.length === 0 || Math.random() > 0.3;
if (insert) {
this.inputChar(ch);
} else {
var moveLeft = Math.random() > 0.5;
var maxMove = moveLeft ? pos : this.inputChars.length - pos;
if (maxMove === 0) {
moveLeft = !moveLeft;
maxMove = moveLeft ? pos : this.inputChars.length - pos;
}
var moveCount = Math.max(Math.floor(Math.pow(Math.random(), 3) * maxMove), 1);
if (moveCount <= 1 && Math.random() > 0.5) {
this.deleteText(moveLeft);
} else {
this.selectText(moveLeft, moveCount);
if (Math.random() > 0.3) {
this.deleteText(Math.random() > 0.5);
} else {
this.inputChar(ch);
}
}
}
};
AutoTypeObfuscator.prototype.stepReal = function() {
var possibleActions = [];
var inputRealPositions = [];
var i;
for (i = 0; i < this.chars.length; i++) {
inputRealPositions.push(-1);
}
for (i = 0; i < this.inputChars.length; i++) {
var ix = this.inputChars[i].ix;
if (ix === undefined) {
possibleActions.push({ del: true, pos: i });
} else {
inputRealPositions[ix] = i;
}
}
for (i = 0; i < this.chars.length; i++) {
if (inputRealPositions[i] < 0) {
var from = 0, to = this.inputChars.length;
for (var j = 0; j < this.chars.length; j++) {
if (j < i && inputRealPositions[j] >= 0) {
from = inputRealPositions[j] + 1;
}
if (j > i && inputRealPositions[j] >= 0) {
to = inputRealPositions[j];
break;
}
}
possibleActions.push({ ins: true, ch: this.chars[i], ix: i, from: from, to: to });
}
}
var action = possibleActions[Math.floor(Math.random() * possibleActions.length)];
logger.info('step.real', inputRealPositions, action);
if (action.del) {
this.moveToPos(action.pos + 1);
this.deleteText(true);
} else {
var insPos = action.from + Math.floor(Math.random() * (action.to - action.from));
this.moveToPos(insPos);
if (this.copyCount < MaxCopy && action.ch !== '\n' && Math.random() > 0.5) {
this.copyCount++;
this.copyPaste(action.ch);
} else {
this.inputChar(action.ch);
}
this.inputChars[insPos].ix = action.ix;
}
};
AutoTypeObfuscator.prototype.moveToPos = function(pos) {
logger.debug('moveToPos', pos);
while (this.inputCursor > pos) {
this.moveLeft();
}
while (this.inputCursor < pos) {
this.moveRight();
}
};
AutoTypeObfuscator.prototype.moveLeft = function() {
logger.debug('moveLeft');
this.ops.push({ type: 'key', value: 'left' });
this.inputCursor--;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.moveRight = function() {
logger.debug('moveRight');
this.ops.push({ type: 'key', value: 'right' });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.inputChar = function(ch) {
logger.debug('inputChar', ch);
this.ops.push({ type: 'text', value: ch });
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.copyPaste = function(ch) {
logger.debug('copyPaste', ch);
this.ops.push({type: 'cmd', value: 'copyPaste', arg: ch});
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
this.inputCursor++;
this.inputSel = 0;
};
AutoTypeObfuscator.prototype.selectText = function(backward, count) {
logger.debug('selectText', backward ? 'left' : 'right', count);
var ops = [];
for (var i = 0; i < count; i++) {
ops.push({ type: 'key', value: backward ? 'left' : 'right' });
}
if (ops.length === 1) {
ops[0].mod = {'+': true};
this.ops.push(ops[0]);
} else {
this.ops.push({type: 'group', value: ops, mod: {'+': true}});
}
if (backward) {
this.inputCursor -= count;
}
this.inputSel = count;
};
AutoTypeObfuscator.prototype.deleteText = function(backward) {
logger.debug('deleteText', backward ? 'left' : 'right');
this.ops.push({ type: 'key', value: backward ? 'bs' : 'del' });
if (this.inputSel) {
this.inputChars.splice(this.inputCursor, this.inputSel);
this.inputSel = 0;
} else {
this.inputChars.splice(backward ? this.inputCursor - 1 : this.inputCursor, 1);
if (backward) {
this.inputCursor--;
}
}
};
module.exports = AutoTypeObfuscator;

View File

@ -0,0 +1,136 @@
'use strict';
var AutoTypeRunner = require('./auto-type-runner');
var AutoTypeParser = function(sequence) {
this.sequence = sequence;
this.ix = 0;
this.states = [];
};
AutoTypeParser.opSepRegex = /[\s:=]+/;
AutoTypeParser.prototype.parse = function() {
var len = this.sequence.length;
this.pushState();
while (this.ix < len) {
var ch = this.sequence[this.ix];
switch (ch) {
case '{':
this.readOp();
continue;
case '+':
case '%':
case '^':
this.readModifier(ch);
break;
case '(':
this.pushState();
break;
case ')':
this.popState();
break;
case ' ':
break;
case '~':
this.addOp('enter');
break;
default:
this.addChar(ch);
break;
}
this.ix++;
}
if (this.states.length !== 1) {
throw 'Groups count mismatch';
}
return new AutoTypeRunner(this.state().ops);
};
AutoTypeParser.prototype.pushState = function() {
this.states.unshift({
modifiers: null,
ops: []
});
};
AutoTypeParser.prototype.popState = function() {
if (this.states.length <= 1) {
throw 'Unexpected ")" at index ' + this.ix;
}
var state = this.states.shift();
this.addState(state);
};
AutoTypeParser.prototype.state = function() {
return this.states[0];
};
AutoTypeParser.prototype.readOp = function() {
var toIx = this.sequence.indexOf('}', this.ix + 2);
if (toIx < 0) {
throw 'Mismatched "{" at index ' + this.ix;
}
var contents = this.sequence.substring(this.ix + 1, toIx);
this.ix = toIx + 1;
if (contents.length === 1) {
this.addChar(contents);
return;
}
var parts = contents.split(AutoTypeParser.opSepRegex, 2);
if (parts.length > 1 && parts[0].length && parts[1].length) {
var op = parts[0];
var sep = contents.substr(op.length, 1);
var arg = parts[1];
this.addOp(op, sep, arg);
} else {
this.addOp(contents);
}
};
AutoTypeParser.prototype.readModifier = function(modifier) {
var state = this.state();
if (!state.modifiers) {
state.modifiers = {};
}
if (modifier === '^' && state.modifiers['^']) {
delete state.modifiers['^'];
modifier = '^^';
}
state.modifiers[modifier] = true;
};
AutoTypeParser.prototype.resetModifiers = function() {
var state = this.state();
var modifiers = state.modifiers;
state.modifiers = null;
return modifiers;
};
AutoTypeParser.prototype.addState = function(state) {
this.state().ops.push({
type: 'group',
value: state.ops,
mod: this.resetModifiers()
});
};
AutoTypeParser.prototype.addChar = function(ch) {
this.state().ops.push({
type: 'text',
value: ch,
mod: this.resetModifiers()
});
};
AutoTypeParser.prototype.addOp = function(op, sep, arg) {
this.state().ops.push({
type: 'op',
value: op,
mod: this.resetModifiers(),
sep: sep,
arg: arg
});
};
module.exports = AutoTypeParser;

View File

@ -0,0 +1,437 @@
'use strict';
var AutoTypeObfuscator = require('./auto-type-obfuscator'),
AutoTypeEmitterFactory = require('./auto-type-emitter-factory'),
Format = require('../util/format'),
Logger = require('../util/logger');
var emitterLogger = new Logger('auto-type-emitter');
emitterLogger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
var AutoTypeRunner = function(ops) {
this.ops = ops;
this.pendingResolvesCount = 0;
this.entry = null;
this.now = new Date();
};
AutoTypeRunner.PendingResolve = { pending: true };
AutoTypeRunner.Keys = {
tab: 'tab', enter: 'enter', space: 'space',
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn',
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc',
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6',
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16',
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide',
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4',
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9'
};
AutoTypeRunner.Substitutions = {
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); },
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); },
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); },
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); },
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); },
group: function(runner) { return runner.getEntryGroupName(); },
totp: function(runner, op) { return runner.getOtp(op); },
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); },
'dt_simple': function(runner) { return runner.dt('simple'); },
'dt_year': function(runner) { return runner.dt('Y'); },
'dt_month': function(runner) { return runner.dt('M'); },
'dt_day': function(runner) { return runner.dt('D'); },
'dt_hour': function(runner) { return runner.dt('h'); },
'dt_minute': function(runner) { return runner.dt('m'); },
'dt_second': function(runner) { return runner.dt('s'); },
'dt_utc_simple': function(runner) { return runner.udt('simple'); },
'dt_utc_year': function(runner) { return runner.udt('Y'); },
'dt_utc_month': function(runner) { return runner.udt('M'); },
'dt_utc_day': function(runner) { return runner.udt('D'); },
'dt_utc_hour': function(runner) { return runner.udt('h'); },
'dt_utc_minute': function(runner) { return runner.udt('m'); },
'dt_utc_second': function(runner) { return runner.udt('s'); }
};
AutoTypeRunner.prototype.resolve = function(entry, callback) {
this.entry = entry;
try {
this.resolveOps(this.ops);
if (!this.pendingResolvesCount) {
callback();
} else {
this.resolveCallback = callback;
}
} catch (e) {
return callback(e);
}
};
AutoTypeRunner.prototype.resolveOps = function(ops) {
for (var i = 0, len = ops.length; i < len; i++) {
var op = ops[i];
if (op.type === 'group') {
this.resolveOps(op.value);
} else {
this.resolveOp(op);
}
}
};
AutoTypeRunner.prototype.resolveOp = function(op) {
if (op.value.length === 1 && !op.sep) {
// {x}
op.type = 'text';
return;
}
if (op.value.length === 1 && op.sep === ' ') {
// {x 3}
op.type = 'text';
var ch = op.value, text = ch, len = +op.arg;
while (text.length < len) {
text += ch;
}
op.value = text;
return;
}
var lowerValue = op.value.toLowerCase();
var key = AutoTypeRunner.Keys[lowerValue];
if (key) {
if (op.sep === ' ' && +op.arg > 0) {
// {TAB 3}
op.type = 'group';
op.value = [];
var count = +op.arg;
for (var i = 0; i < count; i++) {
op.value.push({type: 'key', value: key});
}
} else {
// {TAB}
op.type = 'key';
op.value = key;
}
return;
}
var substitution = AutoTypeRunner.Substitutions[lowerValue];
if (substitution) {
// {title}
op.type = 'text';
op.value = substitution(this, op);
if (op.value === AutoTypeRunner.PendingResolve) {
this.pendingResolvesCount++;
}
return;
}
if (!this.tryParseCommand(op)) {
throw 'Bad op: ' + op.value;
}
};
AutoTypeRunner.prototype.tryParseCommand = function(op) {
switch (op.value.toLowerCase()) {
case 'clearfield':
// {CLEARFIELD}
op.type = 'group';
op.value = [
{ type: 'key', value: 'end' },
{ type: 'key', value: 'home', mod: { '+': true } },
{ type: 'key', value: 'bs' }
];
return true;
case 'vkey':
// {VKEY 10} {VKEY 0x1F}
op.type = 'key';
op.value = parseInt(op.arg);
if (isNaN(op.value) || op.value <= 0) {
throw 'Bad vkey: ' + op.arg;
}
return true;
case 'delay':
// {DELAY 5} {DELAY=5}
op.type = 'cmd';
op.value = op.sep === '=' ? 'setDelay' : 'wait';
if (!op.arg) {
throw 'Delay requires milliseconds count';
}
if (isNaN(+op.arg)) {
throw 'Bad delay: ' + op.arg;
}
if (op.arg < 0) {
throw 'Delay requires positive interval';
}
op.arg = +op.arg;
return true;
default:
return false;
}
};
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
if (!field || !this.entry) {
return '';
}
field = field.toLowerCase();
var value = null;
_.findKey(this.entry.entry.fields, function(val, f) {
if (f.toLowerCase() === field) {
value = val;
return true;
}
});
if (!value) {
return '';
}
if (value.isProtected) {
op.type = 'group';
var ops = [];
value.forEachChar(function(ch) {
if (ch === 10 || ch === 13) {
ops.push({type: 'key', value: 'enter'});
} else {
ops.push({type: 'text', value: String.fromCharCode(ch)});
}
});
return ops;
} else {
var parts = value.split(/[\r\n]/g);
if (parts.length === 1) {
return value;
}
op.type = 'group';
var partsOps = [];
parts.forEach(function(part) {
if (partsOps.length) {
partsOps.push({type: 'key', value: 'enter'});
}
if (part) {
partsOps.push({type: 'text', value: part});
}
});
return partsOps;
}
};
AutoTypeRunner.prototype.getEntryGroupName = function() {
return this.entry && this.entry.group.get('title');
};
AutoTypeRunner.prototype.dt = function(part) {
switch (part) {
case 'simple':
return this.dt('Y') + this.dt('M') + this.dt('D') + this.dt('h') + this.dt('m') + this.dt('s');
case 'Y':
return this.now.getFullYear().toString();
case 'M':
return Format.pad(this.now.getMonth() + 1, 2);
case 'D':
return Format.pad(this.now.getDate(), 2);
case 'h':
return Format.pad(this.now.getHours(), 2);
case 'm':
return Format.pad(this.now.getMinutes(), 2);
case 's':
return Format.pad(this.now.getSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
AutoTypeRunner.prototype.udt = function(part) {
switch (part) {
case 'simple':
return this.udt('Y') + this.udt('M') + this.udt('D') + this.udt('h') + this.udt('m') + this.udt('s');
case 'Y':
return this.now.getUTCFullYear().toString();
case 'M':
return Format.pad(this.now.getUTCMonth() + 1, 2);
case 'D':
return Format.pad(this.now.getUTCDate(), 2);
case 'h':
return Format.pad(this.now.getUTCHours(), 2);
case 'm':
return Format.pad(this.now.getUTCMinutes(), 2);
case 's':
return Format.pad(this.now.getUTCSeconds(), 2);
default:
throw 'Bad part: ' + part;
}
};
AutoTypeRunner.prototype.getOtp = function(op) {
if (!this.entry) {
return '';
}
this.entry.initOtpGenerator();
if (!this.entry.otpGenerator) {
return '';
}
var that = this;
this.entry.otpGenerator.next(function(otp) {
that.pendingResolved(op, otp, otp ? undefined : 'OTP error');
});
return AutoTypeRunner.PendingResolve;
};
AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
var wasPending = op.value === AutoTypeRunner.PendingResolve;
if (value) {
op.value = value;
}
if (!wasPending) {
return;
}
this.pendingResolvesCount--;
if ((this.pendingResolvesCount === 0 || error) && this.resolveCallback) {
this.resolveCallback(error);
this.resolveCallback = null;
}
};
AutoTypeRunner.prototype.obfuscate = function() {
this.obfuscateOps(this.ops);
};
AutoTypeRunner.prototype.obfuscateOps = function(ops) {
for (var i = 0, len = ops.length; i < len; i++) {
var op = ops[i];
if (op.mod) {
continue;
}
if (op.type === 'text') {
this.obfuscateOp(op);
} else if (op.type === 'group') {
var onlyText = op.value.every(function(grOp) { return grOp.type === 'text' && !grOp.mod; });
if (onlyText) {
this.obfuscateOp(op);
} else {
this.obfuscateOps(op.value);
}
}
}
};
AutoTypeRunner.prototype.obfuscateOp = function(op) {
var letters = [];
if (op.type === 'text') {
if (!op.value || op.value.length <= 1) {
return;
}
letters = op.value.split('');
} else {
op.value.forEach(function(grOp) { letters.push.apply(letters, grOp.value.split('')); });
}
if (letters.length <= 1) {
return;
}
var obfuscator = new AutoTypeObfuscator(letters);
op.value = obfuscator.obfuscate();
op.type = 'group';
};
AutoTypeRunner.prototype.run = function(callback) {
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this));
this.emitterState = {
callback: callback,
stack: [],
ops: this.ops,
opIx: 0,
mod: {},
activeMod: {},
finished: null
};
this.emitNext();
};
AutoTypeRunner.prototype.emitNext = function(err) {
if (err) {
this.emitterState.finished = true;
this.emitterState.callback(err);
return;
}
if (this.emitterState.finished) {
this.emitterState.callback();
return;
}
this.resetEmitterMod(this.emitterState.mod);
if (this.emitterState.opIx >= this.emitterState.ops.length) {
var state = this.emitterState.stack.pop();
if (state) {
_.extend(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
this.emitNext();
} else {
this.resetEmitterMod({});
this.emitterState.finished = true;
emitterLogger.debug('waitComplete');
this.emitter.waitComplete();
}
return;
}
var op = this.emitterState.ops[this.emitterState.opIx];
if (op.type === 'group') {
if (op.mod) {
this.setEmitterMod(op.mod);
}
this.emitterState.stack.push({
ops: this.emitterState.ops,
opIx: this.emitterState.opIx + 1,
mod: _.clone(this.emitterState.mod)
});
_.extend(this.emitterState, {
ops: op.value,
opIx: 0,
mod: _.clone(this.emitterState.activeMod)
});
this.emitNext();
return;
}
this.emitterState.opIx++;
if (op.mod) {
this.setEmitterMod(op.mod);
}
switch (op.type) {
case 'text':
emitterLogger.debug('text', op.value);
if (op.value) {
this.emitter.text(op.value);
}
break;
case 'key':
emitterLogger.debug('key', op.value);
this.emitter.key(op.value);
break;
case 'cmd':
var method = this.emitter[op.value];
if (!method) {
throw 'Bad cmd: ' + op.value;
}
emitterLogger.debug(op.value, op.arg);
method.call(this.emitter, op.arg);
break;
default:
throw 'Bad op: ' + op.type;
}
};
AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
Object.keys(addedMod).forEach(function(mod) {
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
emitterLogger.debug('mod', mod, true);
this.emitter.setMod(mod, true);
this.emitterState.activeMod[mod] = true;
}
}, this);
};
AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
Object.keys(this.emitterState.activeMod).forEach(function(mod) {
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
emitterLogger.debug('mod', mod, false);
this.emitter.setMod(mod, false);
delete this.emitterState.activeMod[mod];
}
}, this);
};
module.exports = AutoTypeRunner;

View File

@ -0,0 +1,106 @@
'use strict';
var Launcher = require('../../comp/launcher');
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
var KeyMap = {
tab: 48, enter: 36, space: 49,
up: 126, down: 125, left: 123, right: 124, home: 115, end: 119, pgup: 116, pgdn: 121,
ins: 114, del: 117, bs: 51, esc: 53,
win: 55, rwin: 55,
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101,
f10: 109, f11: 103, f12: 111, f13: 105, f14: 107, f15: 113, f16: 106,
add: 69, subtract: 78, multiply: 67, divide: 75,
n0: 82, n1: 83, n2: 84, n3: 85, n4: 86,
n5: 87, n6: 88, n7: 89, n8: 91, n9: 92
};
var ModMap = {
'^': 'command',
'+': 'shift',
'%': 'option',
'^^': 'control'
};
var AutoTypeEmitter = function(callback) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
};
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
if (enabled) {
this.mod[mod] = true;
} else {
delete this.mod[mod];
}
};
AutoTypeEmitter.prototype.text = function(text) {
text = text.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
this.pendingScript.push('keystroke "' + text + '"'+ this.modString());
this.callback();
};
AutoTypeEmitter.prototype.key = function(key) {
if (typeof key !== 'number') {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
key = KeyMap[key];
}
this.pendingScript.push('key code ' + key + this.modString());
this.callback();
};
AutoTypeEmitter.prototype.copyPaste = function(text) {
this.pendingScript.push('delay 0.5');
this.pendingScript.push('set the clipboard to "' + text.replace(/"/g, '\\"') + '"');
this.pendingScript.push('delay 0.5');
this.pendingScript.push('keystroke "v" using command down');
this.pendingScript.push('delay 0.5');
this.callback();
};
AutoTypeEmitter.prototype.wait = function(time) {
this.pendingScript.push('delay ' + (time / 1000));
this.callback();
};
AutoTypeEmitter.prototype.waitComplete = function() {
if (this.pendingScript.length) {
var script = this.pendingScript.join('\n');
this.pendingScript.length = 0;
this.runScript(script);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.setDelay = function(delay) {
this.delay = delay || 0;
this.callback('Not implemented');
};
AutoTypeEmitter.prototype.modString = function() {
var keys = Object.keys(this.mod);
if (!keys.length) {
return '';
}
return ' using {' + keys.map(AutoTypeEmitter.prototype.mapMod).join(', ') + '}';
};
AutoTypeEmitter.prototype.mapMod = function(mod) {
return ModMap[mod] + ' down';
};
AutoTypeEmitter.prototype.runScript = function(script) {
script = 'tell application "System Events" \n' + script + '\nend tell';
Launcher.spawn({
cmd: 'osascript',
data: script,
complete: this.callback
});
};
module.exports = AutoTypeEmitter;

View File

@ -0,0 +1,100 @@
'use strict';
var Launcher = require('../../comp/launcher');
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
var KeyMap = {
tab: 'Tab', enter: 'KP_Enter', space: 'KP_Space',
up: 'Up', down: 'Down', left: 'Left', right: 'Right', home: 'Home', end: 'End', pgup: 'Page_Up', pgdn: 'Page_Down',
ins: 'Insert', del: 'Delete', bs: 'BackSpace', esc: 'Escape',
win: 'Meta_L', rwin: 'Meta_R',
f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
f10: 'F10', f11: 'F11', f12: 'F12', f13: 'F13', f14: 'F14', f15: 'F15', f16: 'F16',
add: 'KP_Add', subtract: 'KP_Subtract', multiply: 'KP_Multiply', divide: 'KP_Divide',
n0: 'KP_0', n1: 'KP_1', n2: 'KP_2', n3: 'KP_3', n4: 'KP_4',
n5: 'KP_5', n6: 'KP_6', n7: 'KP_7', n8: 'KP_8', n9: 'KP_9'
};
var ModMap = {
'^': 'ctrl',
'+': 'shift',
'%': 'alt',
'^^': 'ctrl'
};
var AutoTypeEmitter = function(callback) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
};
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
if (enabled) {
this.mod[ModMap[mod]] = true;
} else {
delete this.mod[ModMap[mod]];
}
};
AutoTypeEmitter.prototype.text = function(text) {
Object.keys(this.mod).forEach(function (mod) {
this.pendingScript.push('keydown ' + ModMap[mod]);
});
this.pendingScript.push('type ' + text);
var that = this;
this.waitComplete(function(err) {
if (err) { return that.callback(err); }
Object.keys(that.mod).forEach(function (mod) {
that.pendingScript.push('keyup ' + ModMap[mod]);
});
that.callback();
});
};
AutoTypeEmitter.prototype.key = function(key) {
if (typeof key !== 'number') {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
key = KeyMap[key].toString(16);
}
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
this.callback();
};
AutoTypeEmitter.prototype.copyPaste = function(text) {
this.pendingScript.push('sleep 0.5');
Launcher.setClipboardText(text);
this.pendingScript.push('key --clearmodifiers shift+Insert');
this.pendingScript.push('sleep 0.5');
this.waitComplete();
};
AutoTypeEmitter.prototype.waitComplete = function(callback) {
if (this.pendingScript.length) {
var script = this.pendingScript.join(' ');
this.pendingScript.length = 0;
this.runScript(script, callback);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.modString = function() {
var mod = '';
Object.keys(this.mod).forEach(function (key) {
mod += key + '+';
});
return mod;
};
AutoTypeEmitter.prototype.runScript = function(script, callback) {
Launcher.spawn({
cmd: 'xdotool',
args: ['-'],
data: script,
complete: callback || this.callback
});
};
module.exports = AutoTypeEmitter;

View File

@ -0,0 +1,106 @@
'use strict';
var Launcher = require('../../comp/launcher'),
AutoTypeHelper = require('../helper/auto-type-helper-win32');
// https://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.send(v=vs.110).aspx
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
var KeyMap = {
tab: '{tab}', enter: '{enter}', space: '{space}',
up: '{up}', down: '{down}', left: '{left}', right: '{right}', home: '{home}', end: '{end}', pgup: '{pgup}', pgdn: '{pgdn}',
ins: '{ins}', del: '{del}', bs: '{bs}', esc: '{esc}',
win: 0x5B, rwin: 0x5C,
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}',
n0: '0', n1: '1', n2: '2', n3: '3', n4: '4',
n5: '5', n6: '6', n7: '7', n8: '8', n9: '9'
};
var ModMap = {
'^': '^',
'+': '+',
'%': '%',
'^^': '^'
};
var TextReplaceRegex = /[\(\)\{}\[\]\+\^%~]/g;
var AutoTypeEmitter = function(callback) {
this.callback = callback;
this.mod = {};
this.pendingScript = [];
};
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
if (enabled) {
this.mod[ModMap[mod]] = true;
} else {
delete this.mod[ModMap[mod]];
}
};
AutoTypeEmitter.prototype.text = function(text) {
text = this.addMod(text.replace(TextReplaceRegex, function(match) { return '{' + match + '}'; }));
this.pendingScript.push('text ' + text);
this.callback();
};
AutoTypeEmitter.prototype.key = function(key) {
if (typeof key !== 'number') {
key = KeyMap[key];
if (!key) {
return this.callback('Bad key: ' + key);
}
}
if (typeof key === 'number') {
this.pendingScript.push('key ' + this.addMod('') + key);
} else {
var text = this.addMod(key);
this.pendingScript.push('text ' + text);
}
this.callback();
};
AutoTypeEmitter.prototype.copyPaste = function(text) {
this.pendingScript.push('copypaste ' + text);
this.callback();
};
AutoTypeEmitter.prototype.wait = function(time) {
this.pendingScript.push('wait ' + time);
this.callback();
};
AutoTypeEmitter.prototype.waitComplete = function() {
if (this.pendingScript.length) {
var script = this.pendingScript.join('\n');
this.pendingScript.length = 0;
this.runScript(script);
} else {
this.callback();
}
};
AutoTypeEmitter.prototype.setDelay = function(delay) {
this.delay = delay || 0;
this.callback('Not implemented');
};
AutoTypeEmitter.prototype.addMod = function(text) {
var keys = Object.keys(this.mod);
if (!keys.length || !text) {
return text;
}
return keys.join('') + (text.length > 1 ? '(' + text + ')' : text);
};
AutoTypeEmitter.prototype.runScript = function(script) {
Launcher.spawn({
cmd: AutoTypeHelper.getHelperPath(),
data: script,
complete: this.callback
});
};
module.exports = AutoTypeEmitter;

View File

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

View File

@ -0,0 +1,18 @@
'use strict';
var Launcher = require('../../comp/launcher');
var AutoTypeHelper = function() {
};
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
Launcher.spawn({
cmd: 'xdotool',
args: ['getactivewindow', 'getwindowname'],
callback: function(err, res) {
return callback(err, res ? res.trim() : undefined);
}
});
};
module.exports = AutoTypeHelper;

View File

@ -0,0 +1,25 @@
'use strict';
var Launcher = require('../../comp/launcher');
var AutoTypeHelper = function() {
};
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
Launcher.spawn({
cmd: AutoTypeHelper.getHelperPath(),
args: ['--window-info'],
complete: function(err, out) {
if (err) { return callback(err); }
var parts = out.split('\n');
return callback(null, (parts[0] || '').trim(),
parts[1] ? parts[1].trim() : undefined);
}
});
};
AutoTypeHelper.getHelperPath = function() {
return Launcher.getAppPath('KeeWebHelper.exe');
};
module.exports = AutoTypeHelper;

View File

@ -0,0 +1,107 @@
'use strict';
var AutoTypeParser = require('./auto-type-parser'),
AutoTypeHelperFactory = require('./auto-type-helper-factory'),
Launcher = require('../comp/launcher'),
Logger = require('../util/logger'),
Timeouts = require('../const/timeouts');
var logger = new Logger('auto-type');
var clearTextAutoTypeLog = localStorage.autoTypeDebug;
var AutoType = {
helper: AutoTypeHelperFactory.create(),
enabled: !!Launcher,
run: function(entry, callback) {
var sequence = entry.getEffectiveAutoTypeSeq();
logger.debug('Start', sequence);
var that = this;
var ts = logger.ts();
try {
var parser = new AutoTypeParser(sequence);
var runner = parser.parse();
logger.debug('Parsed', that.printOps(runner.ops));
runner.resolve(entry, function(err) {
if (err) {
logger.error('Resolve error', err);
return callback && callback(err);
}
logger.debug('Resolved', that.printOps(runner.ops));
if (entry.autoTypeObfuscation) {
try {
runner.obfuscate();
} catch (e) {
logger.error('Obfuscate error', e);
return callback && callback(e);
}
logger.debug('Obfuscated');
}
runner.run(function(err) {
if (err) {
logger.error('Run error', err);
return callback && callback(err);
}
logger.debug('Complete', logger.ts(ts));
return callback && callback();
});
});
} catch (ex) {
logger.error('Parse error', ex);
return callback && callback(ex);
}
},
validate: function(entry, sequence, callback) {
try {
var parser = new AutoTypeParser(sequence);
var runner = parser.parse();
runner.resolve(entry, callback);
} catch (ex) {
return callback(ex);
}
},
printOps: function(ops) {
return '[' + ops.map(this.printOp, this).join(',') + ']';
},
printOp: function(op) {
var mod = op.mod ? Object.keys(op.mod).join('') : '';
if (op.type === 'group') {
return mod + this.printOps(op.value);
}
if (op.type === 'text') {
var value = op.value;
if (!clearTextAutoTypeLog) {
value = value.replace(/./g, '*');
}
return mod + value;
}
return mod + op.type + ':' + op.value;
},
hideWindow: function(callback) {
logger.debug('Hide window');
if (Launcher.hideWindowIfActive()) {
setTimeout(callback, Timeouts.AutoTypeAfterHide);
} else {
callback();
}
},
getActiveWindowTitle: function(callback) {
logger.debug('Get window title');
return this.helper.getActiveWindowTitle(function(err, title, url) {
if (err) {
logger.error('Error get window title', err);
} else {
logger.debug('Window title', title, url);
}
return callback(err, title);
});
}
};
module.exports = AutoType;

View File

@ -20,10 +20,6 @@ var FileCollection = Backbone.Collection.extend({
getByName: function(name) {
return this.find(function(file) { return file.get('name').toLowerCase() === name.toLowerCase(); });
},
getById: function(id) {
return this.find(function(file) { return file.get('id') === id; });
}
});

View File

@ -263,6 +263,17 @@ var DropboxLink = {
this._getClient(function(err) { complete(err); }, overrideAppKey);
},
logout: function() {
if (this._dropboxClient) {
try {
this._dropboxClient.signOut();
} catch (e) {
} finally {
this._dropboxClient.reset();
}
}
},
resetClient: function() {
this._dropboxClient = null;
},

View File

@ -0,0 +1,12 @@
'use strict';
var AppSettingsModel = require('../models/app-settings-model');
var ExportApi = {
settings: {
get: function(key) { return key ? AppSettingsModel.instance.get(key) : AppSettingsModel.instance.toJSON(); },
set: function(key, value) { AppSettingsModel.instance.set(key, value); }
}
};
module.exports = ExportApi;

View File

@ -1,28 +1,38 @@
'use strict';
var Backbone = require('backbone'),
Locale = require('../util/locale');
Locale = require('../util/locale'),
Logger = require('../util/logger');
var Launcher;
var logger = new Logger('launcher');
if (window.process && window.process.versions && window.process.versions.electron) {
/* jshint node:true */
Launcher = {
name: 'electron',
version: window.process.versions.electron,
req: window.require,
electron: function() {
return this.req('electron');
},
remoteApp: function() {
return this.electron().remote.app;
},
remReq: function(mod) {
return this.req('remote').require(mod);
return this.electron().remote.require(mod);
},
openLink: function(href) {
this.req('shell').openExternal(href);
this.electron().shell.openExternal(href);
},
devTools: true,
openDevTools: function() {
this.req('remote').getCurrentWindow().openDevTools();
this.electron().remote.getCurrentWindow().openDevTools();
},
getSaveFileName: function(defaultPath, cb) {
if (defaultPath) {
var homePath = this.remReq('app').getPath('userDesktop');
var homePath = this.remReq('electron').app.getPath('userDesktop');
defaultPath = this.req('path').join(homePath, defaultPath);
}
this.remReq('dialog').showSaveDialog({
@ -32,10 +42,13 @@ if (window.process && window.process.versions && window.process.versions.electro
}, cb);
},
getUserDataPath: function(fileName) {
return this.req('path').join(this.remReq('app').getPath('userData'), fileName || '');
return this.req('path').join(this.remoteApp().getPath('userData'), fileName || '');
},
getTempPath: function(fileName) {
return this.req('path').join(this.remReq('app').getPath('temp'), fileName || '');
return this.req('path').join(this.remoteApp().getPath('temp'), fileName || '');
},
getAppPath: function(fileName) {
return this.req('path').join(__dirname, fileName || '');
},
writeFile: function(path, data) {
this.req('fs').writeFileSync(path, new window.Buffer(data));
@ -69,7 +82,7 @@ if (window.process && window.process.versions && window.process.versions.electro
this.requestExit();
},
requestExit: function() {
var app = this.remReq('app');
var app = this.remoteApp();
if (this.restartPending) {
app.restartApp();
} else {
@ -84,25 +97,25 @@ if (window.process && window.process.versions && window.process.versions.electro
this.restartPending = false;
},
setClipboardText: function(text) {
return this.req('clipboard').writeText(text);
return this.electron().clipboard.writeText(text);
},
getClipboardText: function() {
return this.req('clipboard').readText();
return this.electron().clipboard.readText();
},
clearClipboardText: function() {
return this.req('clipboard').clear();
return this.electron().clipboard.clear();
},
minimizeApp: function() {
this.remReq('app').minimizeApp();
this.remoteApp().minimizeApp();
},
canMinimize: function() {
return process.platform !== 'darwin';
},
updaterEnabled: function() {
return this.req('remote').process.argv.indexOf('--disable-updater') === -1;
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
},
resolveProxy: function(url, callback) {
var window = this.remReq('app').getMainWindow();
var window = this.remoteApp().getMainWindow();
var session = window.webContents.session;
session.resolveProxy(url, function(proxy) {
var match = /^proxy\s+([\w\.]+):(\d+)+\s*/i.exec(proxy);
@ -111,7 +124,60 @@ if (window.process && window.process.versions && window.process.versions.electro
});
},
openWindow: function(opts) {
return this.remReq('app').openWindow(opts);
return this.remoteApp().openWindow(opts);
},
hideWindowIfActive: function() {
var app = this.remoteApp();
var win = app.getMainWindow();
var visible = win.isVisible(), focused = win.isFocused();
if (!visible || !focused) {
return false;
}
if (process.platform === 'darwin') {
app.hide();
} else {
win.minimize();
}
return true;
},
spawn: function(config) {
var ts = logger.ts();
var complete = config.complete;
var ps = this.req('child_process').spawn(config.cmd, config.args);
[ps.stdin, ps.stdout, ps.stderr].forEach(function(s) { s.setEncoding('utf-8'); });
var stderr = '';
var stdout = '';
ps.stderr.on('data', function(d) { stderr += d.toString('utf-8'); });
ps.stdout.on('data', function(d) { stdout += d.toString('utf-8'); });
ps.on('close', function(code) {
stdout = stdout.trim();
stderr = stderr.trim();
var msg = 'spawn ' + config.cmd + ': ' + code + ', ' + logger.ts(ts);
if (code) {
logger.error(msg + '\n' + stdout + '\n' + stderr);
} else {
logger.info(msg + (stdout ? '\n' + stdout : ''));
}
if (complete) {
complete(code ? 'Exit code ' + code : null, stdout, code);
complete = null;
}
});
ps.on('error', function(err) {
logger.error('spawn error: ' + config.cmd + ', ' + logger.ts(ts), err);
if (complete) {
complete(err);
complete = null;
}
});
if (config.data) {
ps.stdin.write(config.data);
ps.stdin.end();
}
return ps;
},
platform: function() {
return process.platform;
}
};
Backbone.on('launcher-exit-request', function() {

View File

@ -5,7 +5,7 @@ var kdbxweb = require('kdbxweb');
var SecureInput = function() {
this.el = null;
this.minChar = 0x1400 + Math.round(Math.random() * 100);
this.maxLen = 128;
this.maxLen = 1024;
this.length = 0;
this.pseudoValue = '';
this.salt = new Uint32Array(0);

View File

@ -38,10 +38,7 @@ var Updater = {
},
init: function() {
var willCheckNow = this.scheduleNextCheck();
if (!willCheckNow && this.getAutoUpdateType()) {
this.check();
}
this.scheduleNextCheck();
if (!Launcher && window.applicationCache) {
window.applicationCache.addEventListener('updateready', this.checkAppCacheUpdateReady.bind(this));
this.checkAppCacheUpdateReady();
@ -63,7 +60,6 @@ var Updater = {
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
return timeDiff === this.MinUpdateTimeout;
},
check: function(startedByUser) {

View File

@ -1,16 +1,17 @@
'use strict';
var Links = {
Repo: 'https://github.com/antelle/keeweb',
Desktop: 'https://github.com/antelle/keeweb/releases/latest',
Repo: 'https://github.com/keeweb/keeweb',
Desktop: 'https://github.com/keeweb/keeweb/releases/latest',
WebApp: 'https://app.keeweb.info',
BetaWebApp: 'https://beta.keeweb.info',
License: 'https://github.com/antelle/keeweb/blob/master/LICENSE.txt',
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE.txt',
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/antelle/keeweb#self-hosting',
Manifest: 'https://antelle.github.io/keeweb/manifest.appcache'
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
Manifest: 'https://app.keeweb.info/manifest.appcache',
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type'
};
module.exports = Links;

View File

@ -7,7 +7,9 @@ var Timeouts = {
FileChangeSync: 3000,
BeforeAutoLock: 300,
CheckWindowClosed: 300,
OtpFadeDuration: 10000
OtpFadeDuration: 10000,
AutoTypeAfterHide: 100,
DrobDownClickWait: 500
};
module.exports = Timeouts;

View File

@ -36,10 +36,12 @@ var Scrollable = {
if (this.scroll) {
this.scroll.update();
this.requestAnimationFrame(function() {
this.scroll.update();
var barHeight = this.scrollerBar.height(),
wrapperHeight = this.scrollerBarWrapper.height();
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
if (this.scroll) {
this.scroll.update();
var barHeight = this.scrollerBar.height(),
wrapperHeight = this.scrollerBarWrapper.height();
this.scrollerBarWrapper.toggleClass('invisible', barHeight >= wrapperHeight);
}
});
}
},

View File

@ -69,6 +69,16 @@ _.extend(Backbone.View.prototype, {
remove: function() {
this.trigger('remove');
this.removeInnerViews();
if (this.scroll) {
try { this.scroll.dispose(); }
catch (e) { }
}
Tip.hideTips(this.$el);
this._parentRemove(arguments);
},
removeInnerViews: function() {
if (this.views) {
_.each(this.views, function(view) {
if (view) {
@ -81,13 +91,8 @@ _.extend(Backbone.View.prototype, {
}
}
});
this.views = {};
}
if (this.scroll) {
try { this.scroll.dispose(); }
catch (e) { }
}
Tip.hideTips(this.$el);
this._parentRemove(arguments);
},
deferRender: function() {

View File

@ -41,8 +41,31 @@ var AppModel = Backbone.Model.extend({
this.appLogger = new Logger('app');
},
loadConfig: function(configLocation, callback) {
this.appLogger.debug('Loading config from', configLocation);
var ts = this.appLogger.ts();
var xhr = new XMLHttpRequest();
xhr.open('GET', configLocation);
xhr.responseType = 'json';
xhr.send();
var that = this;
xhr.addEventListener('load', function() {
if (!xhr.response) {
that.appLogger.error('Error loading app config', xhr.statusText);
return callback(true);
}
that.appLogger.info('Loaded app config from', configLocation, that.appLogger.ts(ts));
that.settings.set(xhr.response);
callback();
});
xhr.addEventListener('error', function() {
that.appLogger.error('Error loading app config', xhr.statusText, xhr.status);
callback(true);
});
},
addFile: function(file) {
if (this.files.getById(file.id)) {
if (this.files.get(file.id)) {
return false;
}
this.files.add(file);
@ -63,10 +86,7 @@ var AppModel = Backbone.Model.extend({
},
reloadFile: function(file) {
this.menu.groupsSection.removeByFile(file, true);
file.get('groups').forEach(function (group) {
this.menu.groupsSection.addItem(group);
}, this);
this.menu.groupsSection.replaceByFile(file, file.get('groups').first());
this.updateTags();
},
@ -91,7 +111,7 @@ var AppModel = Backbone.Model.extend({
if (this.tags.length) {
this.menu.tagsSection.set('scrollable', true);
this.menu.tagsSection.setItems(this.tags.map(function (tag) {
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag};
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true};
}));
} else {
this.menu.tagsSection.set('scrollable', false);
@ -110,6 +130,13 @@ var AppModel = Backbone.Model.extend({
}
},
renameTag: function(from, to) {
this.files.forEach(function(file) {
file.renameTag(from, to);
});
this.updateTags();
},
closeAllFiles: function() {
var that = this;
this.files.each(function(file) {
@ -133,7 +160,7 @@ var AppModel = Backbone.Model.extend({
this.updateTags();
this.menu.groupsSection.removeByFile(file);
this.menu.filesSection.removeByFile(file);
this.refresh();
this.menu.select({ item: this.menu.allItemsSection.get('items').first() });
},
emptyTrash: function() {
@ -256,7 +283,7 @@ var AppModel = Backbone.Model.extend({
createDemoFile: function() {
var that = this;
if (!this.files.getByName('Demo')) {
var demoFile = new FileModel();
var demoFile = new FileModel({ id: IdGenerator.uuid() });
demoFile.openDemo(function() {
that.addFile(demoFile);
});
@ -274,7 +301,7 @@ var AppModel = Backbone.Model.extend({
break;
}
}
var newFile = new FileModel();
var newFile = new FileModel({ id: IdGenerator.uuid() });
newFile.create(name);
this.addFile(newFile);
},
@ -298,7 +325,8 @@ var AppModel = Backbone.Model.extend({
}, fileInfo);
} else if (params.fileData) {
logger.info('Open file from supplied content');
this.openFileWithData(params, callback, fileInfo, params.fileData, true);
var needSaveToCache = params.storage !== 'file';
this.openFileWithData(params, callback, fileInfo, params.fileData, needSaveToCache);
} else if (!params.storage) {
logger.info('Open file from cache as main storage');
this.openFileFromCache(params, callback, fileInfo);
@ -323,7 +351,8 @@ var AppModel = Backbone.Model.extend({
logger.info('Open file from content loaded from storage');
params.fileData = data;
params.rev = stat && stat.rev || null;
that.openFileWithData(params, callback, fileInfo, data, true);
var needSaveToCache = storage.name !== 'file';
that.openFileWithData(params, callback, fileInfo, data, needSaveToCache);
}
});
};
@ -331,7 +360,7 @@ var AppModel = Backbone.Model.extend({
if (cacheRev && storage.stat) {
logger.info('Stat file');
storage.stat(params.path, params.opts, function(err, stat) {
if (fileInfo && (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);
that.openFileFromCache(params, callback, fileInfo);
} else if (stat) {
@ -346,7 +375,7 @@ var AppModel = Backbone.Model.extend({
storageLoad();
}
} else {
logger.info('Open file from cache, after load will sync', params.storage);
logger.info('Open file from cache, will sync after load', params.storage);
this.openFileFromCache(params, function(err, file) {
if (!err && file) {
logger.info('Sync just opened file');
@ -376,6 +405,7 @@ var AppModel = Backbone.Model.extend({
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.get('keyFileHash'));
}
var file = new FileModel({
id: fileInfo ? fileInfo.id : IdGenerator.uuid(),
name: params.name,
storage: params.storage,
path: params.path,
@ -400,18 +430,16 @@ var AppModel = Backbone.Model.extend({
if (fileInfo) {
file.set('syncDate', fileInfo.get('syncDate'));
}
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
file.set('cacheId', cacheId);
if (updateCacheOnSuccess) {
logger.info('Save loaded file to cache');
Storage.cache.save(cacheId, null, params.fileData);
Storage.cache.save(file.id, null, params.fileData);
}
var rev = params.rev || fileInfo && fileInfo.get('rev');
that.setFileOpts(file, params.opts);
that.addToLastOpenFiles(file, rev);
that.addFile(file);
that.fileOpened(file);
callback(null, file);
that.fileOpened(file);
});
},
@ -419,6 +447,7 @@ var AppModel = Backbone.Model.extend({
var logger = new Logger('import', params.name);
logger.info('File import request with supplied xml');
var file = new FileModel({
id: IdGenerator.uuid(),
name: params.name,
storage: params.storage,
path: params.path
@ -435,10 +464,10 @@ var AppModel = Backbone.Model.extend({
},
addToLastOpenFiles: function(file, rev) {
this.appLogger.debug('Add last open file', file.get('cacheId'), 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);
var dt = new Date();
var fileInfo = new FileInfoModel({
id: file.get('cacheId'),
id: file.id,
name: file.get('name'),
storage: file.get('storage'),
path: file.get('path'),
@ -455,7 +484,7 @@ var AppModel = Backbone.Model.extend({
keyFileHash: file.getKeyFileHash()
});
}
this.fileInfos.remove(file.get('cacheId'));
this.fileInfos.remove(file.id);
this.fileInfos.unshift(fileInfo);
this.fileInfos.save();
},
@ -482,6 +511,9 @@ var AppModel = Backbone.Model.extend({
that.syncFile(file);
}, Timeouts.FileChangeSync));
}
if (file.isKeyChangePending(true)) {
Backbone.trigger('key-change-pending', { file: file });
}
},
fileClosed: function(file) {
@ -497,7 +529,7 @@ var AppModel = Backbone.Model.extend({
},
getFileInfo: function(file) {
return file.get('cacheId') ? this.fileInfos.get(file.get('cacheId')) :
return this.fileInfos.get(file.id) ||
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'));
},
@ -542,7 +574,6 @@ var AppModel = Backbone.Model.extend({
if (!err) { savedToCache = true; }
logger.info('Sync finished', err || 'no error');
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
file.set('cacheId', fileInfo.id);
fileInfo.set({
name: file.get('name'),
storage: storage,
@ -550,8 +581,7 @@ var AppModel = Backbone.Model.extend({
opts: that.getStoreOpts(file),
modified: file.get('modified'),
editState: file.getLocalEditState(),
syncDate: file.get('syncDate'),
cacheId: fileInfo.id
syncDate: file.get('syncDate')
});
if (that.settings.get('rememberKeyFiles')) {
fileInfo.set({
@ -566,7 +596,7 @@ var AppModel = Backbone.Model.extend({
if (callback) { callback(err); }
};
if (!storage) {
if (!file.get('modified') && fileInfo.id === file.get('cacheId')) {
if (!file.get('modified') && fileInfo.id === file.id) {
logger.info('Local, not modified');
return complete();
}
@ -604,7 +634,7 @@ var AppModel = Backbone.Model.extend({
}
file.set('syncDate', new Date());
if (file.get('modified')) {
logger.info('Updated sync date, saving modified file to cache and storage');
logger.info('Updated sync date, saving modified file');
saveToCacheAndStorage();
} else if (file.get('dirty')) {
logger.info('Saving not modified dirty file to cache');
@ -622,11 +652,14 @@ var AppModel = Backbone.Model.extend({
});
};
var saveToCacheAndStorage = function() {
logger.info('Save to cache and storage');
logger.info('Getting file data for saving');
file.getData(function(data, err) {
if (err) { return complete(err); }
if (!file.get('dirty')) {
logger.info('Save to storage, skip cache because not dirty');
if (storage === 'file') {
logger.info('Saving to file storage');
saveToStorage(data);
} else if (!file.get('dirty')) {
logger.info('Saving to storage, skip cache because not dirty');
saveToStorage(data);
} else {
logger.info('Saving to cache');
@ -690,7 +723,7 @@ var AppModel = Backbone.Model.extend({
}
} else if (stat.rev === fileInfo.get('rev')) {
if (file.get('modified')) {
logger.info('Stat found same version, modified, saving to cache and storage');
logger.info('Stat found same version, modified, saving');
saveToCacheAndStorage();
} else {
logger.info('Stat found same version, not modified');

View File

@ -25,6 +25,7 @@ var AppSettingsModel = Backbone.Model.extend({
hideEmptyFields: false,
skipHttpsWarning: false,
demoOpened: false,
fontSize: 0,
dropbox: true,
webdav: true,
gdrive: true,
@ -50,9 +51,4 @@ var AppSettingsModel = Backbone.Model.extend({
AppSettingsModel.instance = new AppSettingsModel();
AppSettingsModel.instance.load();
window.kwSettings = {
get: function(key) { return AppSettingsModel.instance.get(key); },
set: function(key, value) { AppSettingsModel.instance.set(key, value); }
};
module.exports = AppSettingsModel;

View File

@ -21,7 +21,7 @@ var EntryModel = Backbone.Model.extend({
this.entry = entry;
this.group = group;
this.file = file;
if (this.id === entry.uuid.id) {
if (this.get('uuid') === entry.uuid.id) {
this._checkUpdatedEntry();
}
this._fillByEntry();
@ -29,8 +29,9 @@ var EntryModel = Backbone.Model.extend({
_fillByEntry: function() {
var entry = this.entry;
this.set({id: entry.uuid.id}, {silent: true});
this.set({id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id}, {silent: true});
this.fileName = this.file.get('name');
this.groupName = this.group.get('title');
this.title = entry.fields.Title || '';
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
this.notes = entry.fields.Notes || '';
@ -52,12 +53,16 @@ var EntryModel = Backbone.Model.extend({
this._buildSearchText();
this._buildSearchTags();
this._buildSearchColor();
this._buildAutoType();
},
_checkUpdatedEntry: function() {
if (this.isJustCreated) {
this.isJustCreated = false;
}
if (this.canBeDeleted) {
this.canBeDeleted = false;
}
if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) {
this.unsaved = false;
}
@ -96,6 +101,17 @@ var EntryModel = Backbone.Model.extend({
this.searchColor = this.color;
},
_buildAutoType: function() {
this.autoTypeEnabled = this.entry.autoType.enabled;
this.autoTypeObfuscation = this.entry.autoType.obfuscation === kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
this.autoTypeSequence = this.entry.autoType.defaultSequence;
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
},
_convertAutoTypeItem: function(item) {
return { window: item.window, sequence: item.keystrokeSequence };
},
_iconFromId: function(id) {
return IconMap[id];
},
@ -140,6 +156,15 @@ var EntryModel = Backbone.Model.extend({
this.entry.times.update();
},
setSaved: function() {
if (this.unsaved) {
this.unsaved = false;
}
if (this.canBeDeleted) {
this.canBeDeleted = false;
}
},
matches: function(filter) {
return !filter ||
(!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
@ -267,6 +292,19 @@ var EntryModel = Backbone.Model.extend({
this._fillByEntry();
},
renameTag: function(from, to) {
var ix = _.findIndex(this.entry.tags, function(tag) { return tag.toLowerCase() === from.toLowerCase(); });
if (ix < 0) {
return;
}
this._entryModified();
this.entry.tags.splice(ix, 1);
if (to) {
this.entry.tags.push(to);
}
this._fillByEntry();
},
setField: function(field, val) {
var hasValue = val && (typeof val === 'string' || val.isProtected && val.byteLength);
if (hasValue || this.builtInFields.indexOf(field) >= 0) {
@ -363,11 +401,28 @@ var EntryModel = Backbone.Model.extend({
},
removeWithoutHistory: function() {
var ix = this.group.group.entries.indexOf(this.entry);
if (ix >= 0) {
this.group.group.entries.splice(ix, 1);
if (this.canBeDeleted) {
var ix = this.group.group.entries.indexOf(this.entry);
if (ix >= 0) {
this.group.group.entries.splice(ix, 1);
}
this.file.reload();
}
},
moveToFile: function(file) {
if (this.canBeDeleted) {
this.removeWithoutHistory();
this.group = file.get('groups').first();
this.file = file;
this._fillByEntry();
this.entry.times.update();
this.group.group.entries.push(this.entry);
this.group.addEntry(this);
this.isJustCreated = true;
this.unsaved = true;
this.file.setModified();
}
this.file.reload();
},
initOtpGenerator: function() {
@ -438,6 +493,49 @@ var EntryModel = Backbone.Model.extend({
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
delete this.entry.fields['TOTP Seed'];
delete this.entry.fields['TOTP Settings'];
},
getEffectiveEnableAutoType: function() {
if (typeof this.entry.autoType.enabled === 'boolean') {
return this.entry.autoType.enabled;
}
return this.group.getEffectiveEnableAutoType();
},
getEffectiveAutoTypeSeq: function() {
return this.entry.autoType.defaultSequence || this.group.getEffectiveAutoTypeSeq();
},
setEnableAutoType: function(enabled) {
this._entryModified();
if (enabled === this.group.getEffectiveEnableAutoType()) {
enabled = null;
}
this.entry.autoType.enabled = enabled;
this._buildAutoType();
},
setAutoTypeObfuscation: function(enabled) {
this._entryModified();
this.entry.autoType.obfuscation =
enabled ? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard : kdbxweb.Consts.AutoTypeObfuscationOptions.None;
this._buildAutoType();
},
setAutoTypeSeq: function(seq) {
this._entryModified();
this.entry.autoType.defaultSequence = seq || undefined;
this._buildAutoType();
},
getGroupPath: function() {
var group = this.group;
var groupPath = [];
while (group) {
groupPath.unshift(group.get('title'));
group = group.parentGroup;
}
return groupPath;
}
});
@ -454,6 +552,7 @@ EntryModel.newEntry = function(group, file) {
model.entry.times.update();
model.unsaved = true;
model.isJustCreated = true;
model.canBeDeleted = true;
group.addEntry(model);
file.setModified();
return model;

View File

@ -13,6 +13,7 @@ var logger = new Logger('file');
var FileModel = Backbone.Model.extend({
defaults: {
id: '',
uuid: '',
name: '',
keyFileName: '',
passwordLength: 0,
@ -29,10 +30,10 @@ var FileModel = Backbone.Model.extend({
oldKeyFileName: '',
passwordChanged: false,
keyFileChanged: false,
keyChangeForce: -1,
syncing: false,
syncError: null,
syncDate: null,
cacheId: null
syncDate: null
},
db: null,
@ -136,27 +137,31 @@ var FileModel = Backbone.Model.extend({
readModel: function() {
var groups = new GroupCollection();
this.set({
id: this.db.getDefaultGroup().uuid.toString(),
uuid: this.db.getDefaultGroup().uuid.toString(),
groups: groups,
defaultUser: this.db.meta.defaultUser,
recycleBinEnabled: this.db.meta.recycleBinEnabled,
historyMaxItems: this.db.meta.historyMaxItems,
historyMaxSize: this.db.meta.historyMaxSize,
keyEncryptionRounds: this.db.header.keyEncryptionRounds
keyEncryptionRounds: this.db.header.keyEncryptionRounds,
keyChangeForce: this.db.meta.keyChangeForce
}, { silent: true });
this.db.groups.forEach(function(group) {
var groupModel = this.getGroup(group.uuid.id);
var groupModel = this.getGroup(this.subId(group.uuid.id));
if (groupModel) {
groupModel.setGroup(group, this);
} else {
groupModel = GroupModel.fromGroup(group, this);
}
groupModel.set({title: this.get('name')});
groups.add(groupModel);
}, this);
this.buildObjectMap();
},
subId: function(id) {
return this.id + ':' + id;
},
buildObjectMap: function() {
var entryMap = {};
var groupMap = {};
@ -257,7 +262,7 @@ var FileModel = Backbone.Model.extend({
forEachEntry: function(filter, callback) {
var top = this;
if (filter.trash) {
top = this.getGroup(this.db.meta.recycleBinUuid ? this.db.meta.recycleBinUuid.id : null);
top = this.getGroup(this.db.meta.recycleBinUuid ? this.subId(this.db.meta.recycleBinUuid.id) : null);
} else if (filter.group) {
top = this.getGroup(filter.group);
}
@ -282,7 +287,7 @@ var FileModel = Backbone.Model.extend({
},
getTrashGroup: function() {
return this.db.meta.recycleBinEnabled ? this.getGroup(this.db.meta.recycleBinUuid.id) : null;
return this.db.meta.recycleBinEnabled ? this.getGroup(this.subId(this.db.meta.recycleBinUuid.id)) : null;
},
setModified: function() {
@ -340,7 +345,7 @@ var FileModel = Backbone.Model.extend({
}
this.setOpenFile({ passwordLength: this.get('passwordLength') });
this.forEachEntry({}, function(entry) {
entry.unsaved = false;
entry.setSaved();
});
},
@ -391,6 +396,28 @@ var FileModel = Backbone.Model.extend({
this.setModified();
},
isKeyChangePending: function(force) {
if (!this.db.meta.keyChanged) {
return false;
}
var expiryDays = force ? this.db.meta.keyChangeForce : this.db.meta.keyChangeRec;
if (!expiryDays || expiryDays < 0 || isNaN(expiryDays)) {
return false;
}
var daysDiff = (Date.now() - this.db.meta.keyChanged) / 1000 / 3600 / 24;
return daysDiff > expiryDays;
},
setKeyChange: function(force, days) {
if (isNaN(days) || !days || days < 0) {
days = -1;
}
var prop = force ? 'keyChangeForce' : 'keyChangeRec';
this.db.meta[prop] = days;
this.set(prop, days);
this.setModified();
},
setName: function(name) {
this.db.meta.name = name;
this.db.meta.nameChanged = new Date();
@ -455,9 +482,15 @@ var FileModel = Backbone.Model.extend({
},
addCustomIcon: function(iconData) {
var id = kdbxweb.KdbxUuid.random();
this.db.meta.customIcons[id] = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData));
return id.toString();
var uuid = kdbxweb.KdbxUuid.random();
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData));
return uuid.toString();
},
renameTag: function(from, to) {
this.forEachEntry({}, function(entry) {
entry.renameTag(from, to);
});
}
});

View File

@ -8,6 +8,8 @@ var MenuItemModel = require('./menu/menu-item-model'),
KdbxIcons = kdbxweb.Consts.Icons,
GroupCollection, EntryCollection;
var DefaultAutoTypeSequence = '{USERNAME}{TAB}{PASSWORD}{ENTER}';
var GroupModel = MenuItemModel.extend({
defaults: _.extend({}, MenuItemModel.prototype.defaults, {
iconId: 0,
@ -17,7 +19,9 @@ var GroupModel = MenuItemModel.extend({
top: false,
drag: true,
drop: true,
enableSearching: true
enableSearching: true,
enableAutoType: null,
autoTypeSeq: null
}),
initialize: function() {
@ -27,16 +31,21 @@ var GroupModel = MenuItemModel.extend({
setGroup: function(group, file, parentGroup) {
var isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id;
var id = file.subId(group.uuid.id);
this.set({
id: group.uuid.id,
id: id,
uuid: group.uuid.id,
expanded: group.expanded,
visible: !isRecycleBin,
items: new GroupCollection(),
entries: new EntryCollection(),
filterValue: group.uuid.id,
filterValue: id,
enableSearching: group.enableSearching,
enableAutoType: group.enableAutoType,
autoTypeSeq: group.defaultAutoTypeSeq,
top: !parentGroup,
drag: !!parentGroup
drag: !!parentGroup,
collapsible: !!parentGroup
}, { silent: true });
this.group = group;
this.file = file;
@ -45,7 +54,7 @@ var GroupModel = MenuItemModel.extend({
var items = this.get('items'),
entries = this.get('entries');
group.groups.forEach(function(subGroup) {
var existing = file.getGroup(subGroup.uuid);
var existing = file.getGroup(file.subId(subGroup.uuid.id));
if (existing) {
existing.setGroup(subGroup, file, this);
items.add(existing);
@ -54,7 +63,7 @@ var GroupModel = MenuItemModel.extend({
}
}, this);
group.entries.forEach(function(entry) {
var existing = file.getEntry(entry.uuid);
var existing = file.getEntry(file.subId(entry.uuid.id));
if (existing) {
existing.setEntry(entry, this, file);
entries.add(existing);
@ -66,11 +75,12 @@ var GroupModel = MenuItemModel.extend({
_fillByGroup: function(silent) {
this.set({
title: this.group.name,
title: this.parentGroup ? this.group.name : this.file.get('name'),
iconId: this.group.icon,
icon: this._iconFromId(this.group.icon),
customIcon: this._buildCustomIcon(),
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null
customIconId: this.group.customIcon ? this.group.customIcon.toString() : null,
expanded: this.group.expanded !== false
}, { silent: silent });
},
@ -93,7 +103,8 @@ var GroupModel = MenuItemModel.extend({
if (this.isJustCreated) {
this.isJustCreated = false;
}
// this.group.times.update(); // for now, we don't remember this setting
this.file.setModified();
this.group.times.update();
},
forEachGroup: function(callback, includeDisabled) {
@ -146,15 +157,88 @@ var GroupModel = MenuItemModel.extend({
},
setExpanded: function(expanded) {
this._groupModified();
// this._groupModified(); // it's not good to mark the file as modified when a group is collapsed
this.group.expanded = expanded;
this.set('expanded', expanded);
},
setEnableSearching: function(enabled) {
this._groupModified();
var parentEnableSearching = true;
var parentGroup = this.parentGroup;
while (parentGroup) {
if (typeof parentGroup.get('enableSearching') === 'boolean') {
parentEnableSearching = parentGroup.get('enableSearching');
break;
}
parentGroup = parentGroup.parentGroup;
}
if (enabled === parentEnableSearching) {
enabled = null;
}
this.group.enableSearching = enabled;
this.set('enableSearching', enabled);
this.set('enableSearching', this.group.enableSearching);
},
getEffectiveEnableSearching: function() {
var grp = this;
while (grp) {
if (typeof grp.get('enableSearching') === 'boolean') {
return grp.get('enableSearching');
}
grp = grp.parentGroup;
}
return true;
},
setEnableAutoType: function(enabled) {
this._groupModified();
var parentEnableAutoType = true;
var parentGroup = this.parentGroup;
while (parentGroup) {
if (typeof parentGroup.get('enableAutoType') === 'boolean') {
parentEnableAutoType = parentGroup.get('enableAutoType');
break;
}
parentGroup = parentGroup.parentGroup;
}
if (enabled === parentEnableAutoType) {
enabled = null;
}
this.group.enableAutoType = enabled;
this.set('enableAutoType', this.group.enableAutoType);
},
getEffectiveEnableAutoType: function() {
var grp = this;
while (grp) {
if (typeof grp.get('enableAutoType') === 'boolean') {
return grp.get('enableAutoType');
}
grp = grp.parentGroup;
}
return true;
},
setAutoTypeSeq: function(seq) {
this._groupModified();
this.group.defaultAutoTypeSeq = seq || undefined;
this.set('autoTypeSeq', this.group.defaultAutoTypeSeq);
},
getEffectiveAutoTypeSeq: function() {
var grp = this;
while (grp) {
if (grp.get('autoTypeSeq')) {
return grp.get('autoTypeSeq');
}
grp = grp.parentGroup;
}
return DefaultAutoTypeSequence;
},
getParentEffectiveAutoTypeSeq: function() {
return this.parentGroup ? this.parentGroup.getEffectiveAutoTypeSeq() : DefaultAutoTypeSequence;
},
moveToTrash: function() {

View File

@ -20,7 +20,8 @@ var MenuItemModel = Backbone.Model.extend({
drag: false,
drop: false,
filterKey: null,
filterValue: null
filterValue: null,
collapsible: false
},
initialize: function(model) {

View File

@ -27,20 +27,27 @@ var MenuItemModel = Backbone.Model.extend({
this.trigger('change-items');
},
removeByFile: function(file, skipEvent) {
removeByFile: function(file) {
var items = this.get('items');
var toRemove;
items.each(function(item) {
items.find(function(item) {
if (item.file === file || item.get('file') === file) {
toRemove = item;
items.remove(item);
return true;
}
});
if (toRemove) {
items.remove(toRemove);
}
if (!skipEvent) {
this.trigger('change-items');
}
this.trigger('change-items');
},
replaceByFile: function(file, newItem) {
var items = this.get('items');
items.find(function(item, ix) {
if (item.file === file || item.get('file') === file) {
items.remove(item);
items.add(newItem, { at: ix });
return true;
}
});
this.trigger('change-items');
},
setItems: function(items) {

View File

@ -19,7 +19,7 @@ EntryPresenter.prototype = {
}
return this;
},
get id() { return this.entry ? this.entry.id : this.group.get('id'); },
get id() { return this.entry ? this.entry.id : this.group.id; },
get icon() { return this.entry ? this.entry.icon : (this.group.get('icon') || 'folder'); },
get customIcon() { return this.entry ? this.entry.customIcon : undefined; },
get color() { return this.entry ? (this.entry.color || (this.entry.customIcon ? this.noColor : undefined)) : undefined; },
@ -31,7 +31,9 @@ EntryPresenter.prototype = {
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 : false; },
get tags() { return this.entry ? this.entry.tags : undefined; },
get groupName() { return this.entry ? this.entry.groupName : undefined; },
get fileName() { return this.entry ? this.entry.fileName : undefined; },
get description() {
if (!this.entry) {
return '[' + Locale.listGroup + ']';

View File

@ -37,6 +37,10 @@ _.extend(StorageBase.prototype, {
return this;
},
setEnabled: function(enabled) {
this.enabled = enabled;
},
_xhr: function(config) {
var xhr = new XMLHttpRequest();
if (config.responseType) {
@ -186,6 +190,18 @@ _.extend(StorageBase.prototype, {
this._oauthToken.expired = true;
this.runtimeData.set(this.name + 'OAuthToken', this._oauthToken);
this._oauthAuthorize(callback);
},
_oauthRevokeToken: function(url) {
var token = this.runtimeData.get(this.name + 'OAuthToken');
if (token) {
this._xhr({
url: url.replace('{token}', token.accessToken),
statuses: [200, 401]
});
this.runtimeData.unset(this.name + 'OAuthToken');
this._oauthToken = null;
}
}
});

View File

@ -223,6 +223,13 @@ var StorageDropbox = StorageBase.extend({
that.logger.debug('Removed', path, that.logger.ts(ts));
return callback && callback(err);
}, _.noop);
},
setEnabled: function(enabled) {
if (!enabled) {
DropboxLink.logout();
}
StorageBase.prototype.setEnabled.call(this, enabled);
}
});

View File

@ -180,6 +180,13 @@ var StorageGDrive = StorageBase.extend({
});
},
setEnabled: function(enabled) {
if (!enabled) {
this._oauthRevokeToken('https://accounts.google.com/o/oauth2/revoke?token={token}');
}
StorageBase.prototype.setEnabled.call(this, enabled);
},
_getOAuthConfig: function() {
var clientId = this.appSettings.get('gdriveClientId') || GDriveClientId;
return {

View File

@ -197,6 +197,16 @@ var StorageOneDrive = StorageBase.extend({
});
},
setEnabled: function(enabled) {
if (!enabled) {
var url = 'https://login.live.com/oauth20_logout.srf?client_id={client_id}&redirect_uri={url}'
.replace('{client_id}', this._getClientId())
.replace('{url}', this._getOauthRedirectUrl());
this._oauthRevokeToken(url);
}
StorageBase.prototype.setEnabled.call(this, enabled);
},
_getClientId: function() {
var clientId = this.appSettings.get('onedriveClientId');
if (!clientId) {

View File

@ -84,9 +84,13 @@ var StorageWebDav = StorageBase.extend({
that._request(_.defaults({ op: 'Save:delete', method: 'DELETE', path: tmpPath }, saveOpts));
return cb({ revConflict: true }, xhr, stat);
}
var movePath = path;
if (movePath.indexOf('://') < 0) {
movePath = location.href.replace(/[^/]*$/, movePath);
}
that._request(_.defaults({
op: 'Save:move', method: 'MOVE', path: tmpPath, nostat: true,
headers: { Destination: path, 'Overwrite': 'T' }
headers: { Destination: movePath, 'Overwrite': 'T' }
}, saveOpts), function(err) {
if (err) { return cb(err); }
that._request(_.defaults({
@ -103,7 +107,7 @@ var StorageWebDav = StorageBase.extend({
fileOptsToStoreOpts: function(opts, file) {
var result = {user: opts.user, encpass: opts.encpass};
if (opts.password) {
var fileId = file.get('id');
var fileId = file.get('uuid');
var password = opts.password;
var encpass = '';
for (var i = 0; i < password.length; i++) {
@ -117,7 +121,7 @@ var StorageWebDav = StorageBase.extend({
storeOptsToFileOpts: function(opts, file) {
var result = {user: opts.user, password: opts.password};
if (opts.encpass) {
var fileId = file.get('id');
var fileId = file.get('uuid');
var encpass = atob(opts.encpass);
var password = '';
for (var i = 0; i < encpass.length; i++) {
@ -173,8 +177,8 @@ var StorageWebDav = StorageBase.extend({
that.logger.debug(config.op + ' error', config.path, 'aborted', that.logger.ts(ts));
if (callback) { callback('aborted', xhr); callback = null; }
});
xhr.responseType = 'arraybuffer';
xhr.open(config.method, config.path);
xhr.responseType = 'arraybuffer';
if (config.user) {
xhr.setRequestHeader('Authorization', 'Basic ' + btoa(config.user + ':' + config.password));
}

View File

@ -10,12 +10,21 @@ var Format = {
}
return str;
},
padStr: function(str, len) {
while (str.length < len) {
str += ' ';
}
return str;
},
dtStr: function(dt) {
return dt ? this.dStr(dt) + ' ' + this.pad(dt.getHours(), 2) + ':' + this.pad(dt.getMinutes(), 2) +
':' + this.pad(dt.getSeconds(), 2) : '';
},
dStr: function(dt) {
return dt ? dt.getDate() + ' ' + Locale.monthsShort[dt.getMonth()] + ' ' + dt.getFullYear() : '';
},
capFirst: function(str) {
return str[0].toUpperCase() + str.substr(1);
}
};

View File

@ -15,6 +15,7 @@ var Locale = {
website: 'website',
tags: 'tags',
notes: 'notes',
group: 'group',
noTitle: 'no title',
or: 'or',
notImplemented: 'Not Implemented',
@ -39,6 +40,7 @@ var Locale = {
menuEmptyTrash: 'Empty Trash',
menuEmptyTrashAlert: 'Empty Trash?',
menuEmptyTrashAlertBody: 'You will not be able to put items back',
menuItemCollapsed: 'Double-click to expand',
alertYes: 'Yes',
alertNo: 'No',
@ -71,9 +73,25 @@ var Locale = {
grpTitle: 'Group',
grpSearch: 'Enable searching entries in this group',
grpAutoType: 'Enable auto-type',
grpAutoTypeSeq: 'Auto-type sequence',
grpAutoTypeSeqDefault: 'Use default auto-type sequence',
grpTrash: 'Delete group with all entries',
keyChangeTitle: 'Master Key Changed',
keyChangeMessage: 'Master key was changed for this database. Please enter a new key',
tagTitle: 'Tag',
tagTrash: 'Remove tag from all entries',
tagRename: 'Rename',
tagTrashQuestion: 'Remove tag from all entries?',
tagTrashQuestionBody: 'This tag will be removed from all entries. There will be no easy way to put it back.',
tagExists: 'Tag already exists',
tagExistsBody: 'Tag with this name already exists. Please choose another name.',
tagBadName: 'Bad name',
tagBadNameBody: 'Tag name can not contain characters `,`, `;`, `:`. Please remove them.',
keyChangeTitleRemote: 'Master Key Changed',
keyChangeMessageRemote: 'Master key was changed for this database. Please enter a new key',
keyChangeTitleExpired: 'Master Key Expired',
keyChangeMessageExpired: 'Master key for this database is expired. Please enter a new key',
iconFavTitle: 'Download and use website favicon',
iconSelCustom: 'Select custom icon',
@ -113,6 +131,7 @@ var Locale = {
openDemo: 'Demo',
openSettings: 'Settings',
openCaps: 'Caps Lock is on',
openClickToOpen: 'Click to open a file',
openKeyFile: 'key file',
openKeyFileDropbox: '(from dropbox)',
openDropHere: 'drop files here',
@ -171,7 +190,7 @@ var Locale = {
detHistoryCurState: 'current state',
detHistoryCurUnsavedState: 'current unsaved state',
detBackToList: 'back to list',
detSetIconColor: 'Change icon color',
detSetIconColor: 'Change color',
detSetIcon: 'Change icon',
detDropAttachments: 'drop attachments here',
detDelEntry: 'Delete',
@ -184,6 +203,7 @@ var Locale = {
detExpires: 'Expires',
detExpired: 'expired',
detFile: 'File',
detGroup: 'Group',
detCreated: 'Created',
detUpdated: 'Updated',
detHistory: 'History',
@ -202,13 +222,22 @@ var Locale = {
detMenuHideEmpty: 'Hide empty fields',
detMenuAddField: 'Add {}',
detSetupOtp: 'One-time passwords',
detAutoType: 'Auto-type',
detAutoTypeEnabled: 'Enable auto-type for this entry',
detAutoTypeSequence: 'Keystrokes',
detAutoTypeInput: 'Input',
detAutoTypeShortcuts: 'Shortcuts',
detAutoTypeShortcutsDesc: '{} or {} while the app is inactive',
detAutoTypeObfuscation: 'Mix real keystrokes with random',
detAutoTypeWindow: 'Window',
detAutoTypeInputWindow: 'Window title',
detSetupOtpAlert: 'Scan the QR code',
detSetupOtpAlertBody: 'Please copy the QR code which is displayed on the authorization page.',
detSetupOtpAlertBody1: '1. go to the authorization page',
detSetupOtpAlertBody2: '2. make a screenshot of the QR code {}',
detSetupOtpAlertBody3: '3. paste it here {}',
detSetupOtpAlertBody3Mobile: '3. select it or scan with your camera using Select/Scan button below',
detSetupOtpAlertBody4: 'If you can\'t scan code, click Enter code manually',
detSetupOtpAlertBody4: 'If you can\'t scan the code, click Enter code manually',
detSetupOtpManualButton: 'Enter code manually',
detSetupOtpScanButton: 'Select/Scan',
detSetupOtpAlertBodyWith: 'with {}',
@ -220,6 +249,11 @@ var Locale = {
detOtpQrWrong: 'Wrong QR code',
detOtpQrWrongBody: 'Your QR code was successfully scanned but it doesn\'t contain one-time password data.',
autoTypeEntryFields: 'Entry fields',
autoTypeModifiers: 'Modifier keys',
autoTypeKeys: 'Keys',
autoTypeLink: 'more...',
appSecWarn: 'Not Secure!',
appSecWarnBody1: 'You have loaded this app with insecure connection. ' +
'Someone may be watching you and stealing your passwords. ' +
@ -239,6 +273,8 @@ var Locale = {
appSaveError: 'Save Error',
appSaveErrorBody: 'Failed to auto-save file',
appSaveErrorBodyMul: 'Failed to auto-save files:',
appSettingsError: 'Error loading app',
appSettingsErrorBody: 'There was an error loading app settings. Please double check app url or contact your administrator.',
setGenTitle: 'General Settings',
setGenUpdate: 'Update',
@ -267,6 +303,14 @@ var Locale = {
setGenDownloadAndRestart: 'Download update and restart',
setGenAppearance: 'Appearance',
setGenTheme: 'Theme',
setGenThemeFb: 'Flat blue',
setGenThemeDb: 'Dark brown',
setGenThemeWh: 'White',
setGenThemeHc: 'High contrast',
setGenFontSize: 'Font size',
setGenFontSizeNormal: 'Normal',
setGenFontSizeLarge: 'Large',
setGenFontSizeLargest: 'Largest',
setGenShowSubgroups: 'Show entries from all subgroups',
setGenTableView: 'Entries list table view',
setGenColorfulIcons: 'Colorful custom icons in list',
@ -286,14 +330,16 @@ var Locale = {
setGenLockCopy: 'Auto-lock on password copy',
setGenStorage: 'Storage',
setGenAdvanced: 'Advanced',
setGenShowAdvanced: 'Show advanced settings (these settings may be dangerous)',
setGenDevTools: 'Show dev tools',
setGenTryBeta: 'Try beta version for one time',
setGenTryBetaWarning: 'Unsaved files',
setGenTryBetaWarningBody: 'Please save all files and click this button again',
setGenShowAppLogs: 'Show app logs',
setFilePath: 'File path',
setFileStorage: 'This file is opened from {}.',
setFileIntl: 'This file is stored in internal app storage',
setFileIntl: 'This file is stored in the internal app storage',
setFileLocalHint: 'Want to work seamlessly with local files?',
setFileDownloadApp: 'Download a desktop app',
setFileSave: 'Save',
@ -320,6 +366,7 @@ var Locale = {
setFileHistSize: 'History size, total MB per file',
setFileAdvanced: 'Advanced',
setFileRounds: 'Key encryption rounds',
setFileKeyChangeForce: 'Ask to change key after (days)',
setFileUseKeyFile: 'Use key file',
setFileUseGenKeyFile: 'Use generated key file',
setFileUseOldKeyFile: 'Use old key file',
@ -348,6 +395,7 @@ var Locale = {
setShCopyPass: 'copy password or selected field',
setShCopyUser: 'copy username',
setShCopyUrl: 'copy website',
setShAutoType: 'auto-type selected entry',
setShPrev: 'go to previous item',
setShNext: 'go to next item',
setShCreateEntry: 'create entry',
@ -358,6 +406,7 @@ var Locale = {
setShCopyPassGlobal: 'copy password (when app is in background)',
setShCopyUserGlobal: 'copy username (when app is in background)',
setShCopyUrlGlobal: 'copy website (when app is in background)',
setShAutoTypeGlobal: 'auto-type (when app is in background)',
setShLock: 'lock database',
setAboutTitle: 'About',

View File

@ -3,8 +3,22 @@
/* globals console */
/* globals performance */
var Level = {
Off: 0,
Error: 1,
Warn: 2,
Info: 3,
Debug: 4,
All: 5
};
var MaxLogsToSave = 100;
var lastLogs = [];
var Logger = function(name, id) {
this.prefix = (name ? name + (id ? ':' + id : '') : 'default');
this.level = Level.All;
};
Logger.prototype.ts = function(ts) {
@ -21,22 +35,55 @@ Logger.prototype.getPrefix = function() {
Logger.prototype.debug = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.debug.apply(console, arguments);
if (this.level > Level.Debug) {
Logger.saveLast('debug', arguments);
console.debug.apply(console, arguments);
}
};
Logger.prototype.info = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.log.apply(console, arguments);
if (this.level > Level.Info) {
Logger.saveLast('info', arguments);
console.log.apply(console, arguments);
}
};
Logger.prototype.warn = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.warn.apply(console, arguments);
if (this.level > Level.Warn) {
Logger.saveLast('warn', arguments);
console.warn.apply(console, arguments);
}
};
Logger.prototype.error = function() {
arguments[0] = this.getPrefix() + arguments[0];
console.error.apply(console, arguments);
if (this.level > Level.Error) {
Logger.saveLast('error', arguments);
console.error.apply(console, arguments);
}
};
Logger.prototype.setLevel = function(level) {
this.level = level;
};
Logger.prototype.getLevel = function() {
return this.level;
};
Logger.saveLast = function(level, args) {
lastLogs.push({ level: level, args: Array.prototype.slice.call(args) });
if (lastLogs.length > MaxLogsToSave) {
lastLogs.shift();
}
};
Logger.getLast = function() {
return lastLogs;
};
Logger.Level = Level;
module.exports = Logger;

View File

@ -1,6 +1,15 @@
'use strict';
var ThemeChanger = {
setBySettings: function(settings) {
if (settings.get('theme')) {
this.setTheme(settings.get('theme'));
}
if (settings.get('fontSize')) {
this.setFontSize(settings.get('fontSize'));
}
},
setTheme: function(theme) {
_.forEach(document.body.classList, function(cls) {
if (/^th\-/.test(cls)) {
@ -12,6 +21,10 @@ var ThemeChanger = {
if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
}
},
setFontSize: function(fontSize) {
document.documentElement.style.fontSize = fontSize ? (12 + fontSize * 2) + 'px' : '';
}
};

View File

@ -139,21 +139,35 @@ Tip.createTips = function(container) {
return;
}
container.find('[title]').each(function(ix, el) {
var tip = new Tip($(el));
tip.init();
el._tip = tip;
Tip.createTip(el);
});
};
Tip.createTip = function(el) {
if (!Tip.enabled) {
return;
}
var tip = new Tip($(el));
tip.init();
el._tip = tip;
};
Tip.hideTips = function(container) {
if (!Tip.enabled) {
return;
}
container.find('[data-title]').each(function(ix, el) {
if (el._tip) {
el._tip.hide();
}
Tip.hideTip(el);
});
};
Tip.hideTip = function(el) {
if (!Tip.enabled) {
return;
}
if (el._tip) {
el._tip.hide();
}
};
module.exports = Tip;

View File

@ -8,6 +8,7 @@ var Backbone = require('backbone'),
ListWrapView = require('../views/list-wrap-view'),
DetailsView = require('../views/details/details-view'),
GrpView = require('../views/grp-view'),
TagView = require('../views/tag-view'),
OpenView = require('../views/open-view'),
SettingsView = require('../views/settings/settings-view'),
KeyChangeView = require('../views/key-change-view'),
@ -48,11 +49,13 @@ var AppView = Backbone.View.extend({
this.views.details = new DetailsView();
this.views.details.appModel = this.model;
this.views.grp = new GrpView();
this.views.tag = new TagView({ model: this.model });
this.views.menu.listenDrag(this.views.menuDrag);
this.views.list.listenDrag(this.views.listDrag);
this.listenTo(this.model.settings, 'change:theme', this.setTheme);
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
this.listenTo(this.model.files, 'update reset', this.fileListUpdated);
this.listenTo(Backbone, 'select-all', this.selectAll);
@ -62,10 +65,12 @@ var AppView = Backbone.View.extend({
this.listenTo(Backbone, 'open-file', this.toggleOpenFile);
this.listenTo(Backbone, 'save-all', this.saveAll);
this.listenTo(Backbone, 'remote-key-changed', this.remoteKeyChanged);
this.listenTo(Backbone, 'key-change-pending', this.keyChangePending);
this.listenTo(Backbone, 'toggle-settings', this.toggleSettings);
this.listenTo(Backbone, 'toggle-menu', this.toggleMenu);
this.listenTo(Backbone, 'toggle-details', this.toggleDetails);
this.listenTo(Backbone, 'edit-group', this.editGroup);
this.listenTo(Backbone, 'edit-tag', this.editTag);
this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile);
this.listenTo(Backbone, 'user-idle', this.userIdle);
this.listenTo(Backbone, 'app-minimized', this.appMinimized);
@ -94,6 +99,7 @@ var AppView = Backbone.View.extend({
this.views.listDrag.setElement(this.$el.find('.app__list-drag')).render();
this.views.details.setElement(this.$el.find('.app__details')).render();
this.views.grp.setElement(this.$el.find('.app__grp')).render().hide();
this.views.tag.setElement(this.$el.find('.app__tag')).render().hide();
this.showLastOpenFile();
return this;
},
@ -106,6 +112,7 @@ var AppView = Backbone.View.extend({
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.tag.hide();
this.views.footer.toggle(this.model.files.hasOpenFiles());
this.hideSettings();
this.hideOpenFile();
@ -145,6 +152,7 @@ var AppView = Backbone.View.extend({
this.views.listDrag.show();
this.views.details.show();
this.views.grp.hide();
this.views.tag.hide();
this.views.footer.show();
this.hideOpenFile();
this.hideSettings();
@ -182,6 +190,7 @@ var AppView = Backbone.View.extend({
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.tag.hide();
this.hideOpenFile();
this.hideKeyChange();
this.views.settings = new SettingsView({ model: this.model });
@ -198,11 +207,24 @@ var AppView = Backbone.View.extend({
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.tag.hide();
this.views.grp.show();
},
showKeyChange: function(file) {
if (this.views.keyChange || Alerts.alertDisplayed) {
showEditTag: function() {
this.views.listWrap.hide();
this.views.list.hide();
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.tag.show();
},
showKeyChange: function(file, viewConfig) {
if (Alerts.alertDisplayed) {
return;
}
if (this.views.keyChange && this.views.keyChange.model.remote) {
return;
}
this.hideSettings();
@ -212,7 +234,10 @@ var AppView = Backbone.View.extend({
this.views.listDrag.hide();
this.views.details.hide();
this.views.grp.hide();
this.views.keyChange = new KeyChangeView({ model: file });
this.views.tag.hide();
this.views.keyChange = new KeyChangeView({
model: { file: file, expired: viewConfig.expired, remote: viewConfig.remote }
});
this.views.keyChange.setElement(this.$el.find('.app__body')).render();
this.views.keyChange.on('accept', this.keyChangeAccept.bind(this));
this.views.keyChange.on('cancel', this.showEntries.bind(this));
@ -322,7 +347,7 @@ var AppView = Backbone.View.extend({
menuSelect: function(opt) {
this.model.menu.select(opt);
if (!this.views.grp.isHidden()) {
if (!this.views.grp.isHidden() || !this.views.tag.isHidden()) {
this.showEntries();
}
},
@ -445,18 +470,31 @@ var AppView = Backbone.View.extend({
},
remoteKeyChanged: function(e) {
this.showKeyChange(e.file);
this.showKeyChange(e.file, { remote: true });
},
keyChangePending: function(e) {
this.showKeyChange(e.file, { expired: true });
},
keyChangeAccept: function(e) {
this.showEntries();
this.model.syncFile(e.file, {
remoteKey: {
password: e.password,
keyFileName: e.keyFileName,
keyFileData: e.keyFileData
if (e.expired) {
e.file.setPassword(e.password);
if (e.keyFileData && e.keyFileName) {
e.file.setKeyFile(e.keyFileData, e.keyFileName);
} else {
e.file.removeKeyFile();
}
});
} else {
this.model.syncFile(e.file, {
remoteKey: {
password: e.password,
keyFileName: e.keyFileName,
keyFileData: e.keyFileData
}
});
}
},
toggleSettings: function(page) {
@ -503,6 +541,15 @@ var AppView = Backbone.View.extend({
}
},
editTag: function(tag) {
if (tag && this.views.tag.isHidden()) {
this.showEditTag();
this.views.tag.showTag(tag);
} else {
this.showEntries();
}
},
contextmenu: function(e) {
if (['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) < 0) {
e.preventDefault();
@ -521,6 +568,10 @@ var AppView = Backbone.View.extend({
ThemeChanger.setTheme(this.model.settings.get('theme'));
},
setFontSize: function() {
ThemeChanger.setFontSize(this.model.settings.get('fontSize'));
},
extLinkClick: function(e) {
if (Launcher) {
e.preventDefault();

View File

@ -0,0 +1,82 @@
'use strict';
var Backbone = require('backbone'),
FeatureDetector = require('../util/feature-detector'),
Links = require('../const/links'),
Timeouts = require('../const/timeouts');
var AutoTypeHintView = Backbone.View.extend({
template: require('templates/auto-type-hint.hbs'),
events: {},
initialize: function(opts) {
this.input = opts.input;
this.bodyClick = this.bodyClick.bind(this);
this.inputBlur = this.inputBlur.bind(this);
$('body').on('click', this.bodyClick);
this.input.addEventListener('blur', this.inputBlur);
},
render: function () {
this.renderTemplate({
cmd: FeatureDetector.isMac() ? 'command' : 'ctrl',
hasCtrl: FeatureDetector.isMac(),
link: Links.AutoType
});
var rect = this.input.getBoundingClientRect();
this.$el.appendTo(document.body).css({
left: rect.left, top: rect.bottom + 1, width: rect.width
});
var selfRect = this.$el[0].getBoundingClientRect();
var bodyRect = document.body.getBoundingClientRect();
if (selfRect.bottom > bodyRect.bottom) {
this.$el.css('height', selfRect.height + bodyRect.bottom - selfRect.bottom - 1);
}
return this;
},
remove: function() {
$('body').off('click', this.bodyClick);
this.input.removeEventListener('blur', this.inputBlur);
Backbone.View.prototype.remove.apply(this, arguments);
},
bodyClick: function(e) {
if (this.removeTimer) {
clearTimeout(this.removeTimer);
this.removeTimer = null;
}
if (e.target === this.input) {
e.stopPropagation();
return;
}
if ($.contains(this.$el[0], e.target) || e.target === this.$el[0]) {
e.stopPropagation();
if (e.target.tagName.toLowerCase() === 'a' && !e.target.href) {
var text = $(e.target).text();
if (text[0] !== '{') {
text = text.split(' ')[0];
}
this.insertText(text);
}
this.input.focus();
} else {
this.remove();
}
},
inputBlur: function() {
if (!this.removeTimer) {
this.removeTimer = setTimeout(this.remove.bind(this), Timeouts.DrobDownClickWait);
}
},
insertText: function(text) {
var pos = this.input.selectionEnd || this.input.value.length;
this.input.value = this.input.value.substr(0, pos) + text + this.input.value.substr(pos);
this.input.selectionStart = this.input.selectionEnd = pos + text.length;
}
});
module.exports = AutoTypeHintView;

View File

@ -0,0 +1,77 @@
'use strict';
var Backbone = require('backbone'),
AutoTypeHintView = require('../auto-type-hint-view'),
Locale = require('../../util/locale'),
FeatureDetector = require('../../util/feature-detector'),
AutoType = require('../../auto-type');
var DetailsAutoTypeView = Backbone.View.extend({
template: require('templates/details/details-auto-type.hbs'),
events: {
'focus #details__auto-type-sequence': 'seqFocus',
'input #details__auto-type-sequence': 'seqInput',
'keypress #details__auto-type-sequence': 'seqKeyPress',
'keydown #details__auto-type-sequence': 'seqKeyDown',
'change #details__auto-type-enabled': 'enabledChange',
'change #details__auto-type-obfuscation': 'obfuscationChange'
},
initialize: function() {
this.views = {};
},
render: function() {
var detAutoTypeShortcutsDesc = Locale.detAutoTypeShortcutsDesc
.replace('{}', FeatureDetector.actionShortcutSymbol() + 'T')
.replace('{}', FeatureDetector.globalShortcutSymbol() + 'T');
this.renderTemplate({
enabled: this.model.getEffectiveEnableAutoType(),
obfuscation: this.model.autoTypeObfuscation,
sequence: this.model.autoTypeSequence,
windows: this.model.autoTypeWindows,
defaultSequence: this.model.group.getEffectiveAutoTypeSeq(),
detAutoTypeShortcutsDesc: detAutoTypeShortcutsDesc
});
return this;
},
seqInput: function(e) {
var that = this;
var el = e.target;
var seq = $.trim(el.value);
AutoType.validate(this.model, seq, function(err) {
$(el).toggleClass('input--error', !!err);
if (!err) {
that.model.setAutoTypeSeq(seq);
}
});
},
seqKeyPress: function(e) {
e.stopPropagation();
},
seqKeyDown: function(e) {
e.stopPropagation();
},
seqFocus: function(e) {
if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render();
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
}
},
enabledChange: function(e) {
this.model.setEnableAutoType(e.target.checked);
},
obfuscationChange: function(e) {
this.model.setAutoTypeObfuscation(e.target.checked);
}
});
module.exports = DetailsAutoTypeView;

View File

@ -6,6 +6,7 @@ var Backbone = require('backbone'),
AppSettingsModel = require('../../models/app-settings-model'),
Scrollable = require('../../mixins/scrollable'),
FieldViewText = require('../fields/field-view-text'),
FieldViewSelect = require('../fields/field-view-select'),
FieldViewAutocomplete = require('../fields/field-view-autocomplete'),
FieldViewDate = require('../fields/field-view-date'),
FieldViewTags = require('../fields/field-view-tags'),
@ -18,12 +19,14 @@ var Backbone = require('backbone'),
DetailsHistoryView = require('./details-history-view'),
DetailsAttachmentView = require('./details-attachment-view'),
DetailsAddFieldView = require('./details-add-field-view'),
DetailsAutoTypeView = require('./details-auto-type-view'),
DropdownView = require('../../views/dropdown-view'),
Keys = require('../../const/keys'),
KeyHandler = require('../../comp/key-handler'),
Alerts = require('../../comp/alerts'),
CopyPaste = require('../../comp/copy-paste'),
OtpQrReqder = require('../../comp/otp-qr-reader'),
AutoType = require('../../auto-type'),
Format = require('../../util/format'),
Locale = require('../../util/locale'),
Tip = require('../../util/tip'),
@ -51,6 +54,8 @@ var DetailsView = Backbone.View.extend({
'click .details__buttons-trash': 'moveToTrash',
'click .details__buttons-trash-del': 'deleteFromTrash',
'click .details__back-button': 'backClick',
'click .details__attachment-add': 'attachmentBtnClick',
'change .details__attachment-input-file': 'attachmentFileChange',
'dragover .details': 'dragover',
'dragleave .details': 'dragleave',
'drop .details': 'drop'
@ -62,6 +67,7 @@ var DetailsView = Backbone.View.extend({
this.initScroll();
this.listenTo(Backbone, 'select-entry', this.showEntry);
this.listenTo(Backbone, 'copy-password', this.copyPassword);
this.listenTo(Backbone, 'auto-type', this.autoTypeGlobal);
this.listenTo(Backbone, 'copy-user', this.copyUserName);
this.listenTo(Backbone, 'copy-url', this.copyUrl);
this.listenTo(OtpQrReqder, 'qr-read', this.otpCodeRead);
@ -69,6 +75,7 @@ var DetailsView = Backbone.View.extend({
KeyHandler.onKey(Keys.DOM_VK_C, this.copyPassword, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_B, this.copyUserName, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_U, this.copyUrl, this, KeyHandler.SHORTCUT_ACTION, false, true);
KeyHandler.onKey(Keys.DOM_VK_T, this.autoType, this, KeyHandler.SHORTCUT_ACTION);
KeyHandler.onKey(Keys.DOM_VK_DELETE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
KeyHandler.onKey(Keys.DOM_VK_BACK_SPACE, this.deleteKeyPress, this, KeyHandler.SHORTCUT_ACTION);
},
@ -95,14 +102,7 @@ var DetailsView = Backbone.View.extend({
render: function () {
this.removeScroll();
this.removeFieldViews();
if (this.views.sub) {
this.views.sub.remove();
delete this.views.sub;
}
if (this.views.dropdownView) {
this.views.dropdownView.remove();
delete this.views.dropdownView;
}
this.removeInnerViews();
if (!this.model) {
this.$el.html(this.emptyTemplate());
return;
@ -135,6 +135,17 @@ var DetailsView = Backbone.View.extend({
addFieldViews: function() {
var model = this.model;
if (model.isJustCreated && this.appModel.files.length > 1) {
var fileNames = this.appModel.files.map(function(file) {
return { id: file.id, value: file.get('name'), selected: file === this.model.file };
}, this);
this.fileEditView = new FieldViewSelect({ model: { name: '$File', title: Locale.detFile,
value: function() { return fileNames; } } });
this.fieldViews.push(this.fileEditView);
} else {
this.fieldViews.push(new FieldViewReadOnly({ model: { name: 'File', title: Locale.detFile,
value: function() { return model.fileName; } } }));
}
this.userEditView = new FieldViewAutocomplete({ model: { name: '$UserName', title: Locale.detUser,
value: function() { return model.user; }, getCompletions: this.getUserNameCompletions.bind(this) } });
this.fieldViews.push(this.userEditView);
@ -150,8 +161,8 @@ var DetailsView = Backbone.View.extend({
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: 'File', title: Locale.detFile,
value: function() { return model.fileName; } } }));
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,
@ -239,6 +250,9 @@ var DetailsView = Backbone.View.extend({
moreOptions.push({value: 'toggle-empty', icon: 'eye-slash', text: Locale.detMenuHideEmpty});
}
moreOptions.push({value: 'otp', icon: 'clock-o', text: Locale.detSetupOtp});
if (AutoType.enabled) {
moreOptions.push({value: 'auto-type', icon: 'keyboard-o', text: Locale.detAutoType});
}
var rect = this.moreView.labelEl[0].getBoundingClientRect();
dropdownView.render({
position: {top: rect.bottom, left: rect.left},
@ -264,6 +278,9 @@ var DetailsView = Backbone.View.extend({
case 'otp':
this.setupOtp();
break;
case 'auto-type':
this.toggleAutoType();
break;
default:
if (e.item.lastIndexOf('add:', 0) === 0) {
var fieldName = e.item.substr(4);
@ -454,6 +471,13 @@ var DetailsView = Backbone.View.extend({
this.model.setField(fieldName, e.val);
this.entryUpdated();
return;
} else if (fieldName === 'File') {
var newFile = this.appModel.files.get(e.val);
this.model.moveToFile(newFile);
this.appModel.activeEntryId = this.model.id;
this.entryUpdated();
Backbone.trigger('select-entry', this.model);
return;
} else if (fieldName) {
this.model.setField(fieldName, e.val);
}
@ -562,6 +586,18 @@ var DetailsView = Backbone.View.extend({
this.$el.find('.details').removeClass('details--drag');
this.dragging = false;
var files = e.target.files || e.originalEvent.dataTransfer.files;
this.addAttachedFiles(files);
},
attachmentBtnClick: function() {
this.$el.find('.details__attachment-input-file')[0].click();
},
attachmentFileChange: function(e) {
this.addAttachedFiles(e.target.files);
},
addAttachedFiles: function(files) {
_.forEach(files, function(file) {
var reader = new FileReader();
reader.onload = (function() {
@ -738,6 +774,32 @@ var DetailsView = Backbone.View.extend({
fieldView.edit();
this.fieldViews.push(fieldView);
}
},
toggleAutoType: function() {
if (this.views.autoType) {
this.views.autoType.remove();
delete this.views.autoType;
return;
}
this.views.autoType = new DetailsAutoTypeView({
el: this.$el.find('.details__body-after'),
model: this.model
}).render();
},
autoType: function() {
var entry = this.model;
// AutoType.getActiveWindowTitle(function() {
// console.log(arguments);
// });
AutoType.hideWindow(function() {
AutoType.run(entry);
});
},
autoTypeGlobal: function() {
// TODO
}
});

View File

@ -35,8 +35,9 @@ var DropdownView = Backbone.View.extend({
itemClick: function(e) {
e.stopPropagation();
var selected = $(e.target).closest('.dropdown__item').data('value');
this.trigger('select', { item: selected });
var el = $(e.target).closest('.dropdown__item');
var selected = el.data('value');
this.trigger('select', { item: selected, el: el });
}
});

View File

@ -21,7 +21,9 @@ var FieldViewAutocomplete = FieldViewText.extend({
width: fieldRect.width - 2
});
this.autocomplete.mousedown(this.autocompleteClick.bind(this));
if (!this.input.val()) {
if (this.input.val()) {
this.autocomplete.hide();
} else {
this.updateAutocomplete();
}
},

View File

@ -0,0 +1,44 @@
'use strict';
var FieldView = require('./field-view');
var FieldViewSelect = FieldView.extend({
readonly: true,
renderValue: function(value) {
return '<select>' +
value.map(function(opt) {
return '<option ' + 'value="' + _.escape(opt.id) + '" ' + (opt.selected ? 'selected ' : '') + '>' +
_.escape(opt.value) +
'</option>';
}).join('') +
'</select>';
},
render: function() {
var that = this;
FieldView.prototype.render.call(this);
this.valueEl.addClass('details__field-value--select');
this.valueEl.find('select:first').change(function(e) {
that.triggerChange({ val: e.target.value, field: that.model.name });
});
},
fieldLabelClick: function() {},
fieldValueClick: function() {},
edit: function() {},
startEdit: function() {},
endEdit: function(newVal, extra) {
if (!this.editing) {
return;
}
delete this.input;
FieldView.prototype.endEdit.call(this, newVal, extra);
}
});
module.exports = FieldViewSelect;

View File

@ -2,7 +2,8 @@
var Backbone = require('backbone'),
FeatureDetector = require('../../util/feature-detector'),
CopyPaste = require('../../comp/copy-paste');
CopyPaste = require('../../comp/copy-paste'),
Tip = require('../../util/tip');
var FieldView = Backbone.View.extend({
template: require('templates/details/field.hbs'),
@ -12,16 +13,30 @@ var FieldView = Backbone.View.extend({
'click .details__field-value': 'fieldValueClick'
},
render: function () {
render: function() {
this.value = typeof this.model.value === 'function' ? this.model.value() : this.model.value;
this.renderTemplate({ editable: !this.readonly, multiline: this.model.multiline, title: this.model.title,
canEditTitle: this.model.newField, protect: this.value && this.value.isProtected });
this.valueEl = this.$el.find('.details__field-value');
this.valueEl.html(this.renderValue(this.value));
this.labelEl = this.$el.find('.details__field-label');
if (this.model.tip) {
this.tip = typeof this.model.tip === 'function' ? this.model.tip() : this.model.tip;
if (this.tip) {
this.valueEl.attr('title', this.tip);
Tip.createTip(this.valueEl);
}
}
return this;
},
remove: function() {
if (this.tip) {
Tip.hideTip(this.valueEl);
}
Backbone.View.prototype.remove.apply(this, arguments);
},
update: function() {
if (typeof this.model.value === 'function') {
var newVal = this.model.value();
@ -70,7 +85,7 @@ var FieldView = Backbone.View.extend({
range.selectNodeContents(this.valueEl[0]);
selection.removeAllRanges();
selection.addRange(range);
copyRes = CopyPaste.copy(this.valueEl.text());
copyRes = CopyPaste.copy(this.valueEl[0].innerText || this.valueEl.text());
if (copyRes) {
selection.removeAllRanges();
this.trigger('copy', { source: this, copyRes: copyRes });
@ -121,11 +136,15 @@ var FieldView = Backbone.View.extend({
arg = extra;
}
if (arg) {
arg.sender = this;
this.trigger('change', arg);
this.triggerChange(arg);
}
this.valueEl.html(this.renderValue(this.value));
this.$el.removeClass('details__field--edit');
},
triggerChange: function(arg) {
arg.sender = this;
this.trigger('change', arg);
}
});

View File

@ -2,7 +2,9 @@
var Backbone = require('backbone'),
Scrollable = require('../mixins/scrollable'),
IconSelectView = require('./icon-select-view');
IconSelectView = require('./icon-select-view'),
AutoTypeHintView = require('./auto-type-hint-view'),
AutoType = require('../auto-type');
var GrpView = Backbone.View.extend({
template: require('templates/grp.hbs'),
@ -12,7 +14,10 @@ var GrpView = Backbone.View.extend({
'click .grp__buttons-trash': 'moveToTrash',
'click .grp__back-button': 'returnToApp',
'input #grp__field-title': 'changeTitle',
'change #grp__check-search': 'setEnableSearching'
'focus #grp__field-auto-type-seq': 'focusAutoTypeSeq',
'input #grp__field-auto-type-seq': 'changeAutoTypeSeq',
'change #grp__check-search': 'setEnableSearching',
'change #grp__check-auto-type': 'setEnableAutoType'
},
initialize: function() {
@ -26,8 +31,12 @@ var GrpView = Backbone.View.extend({
title: this.model.get('title'),
icon: this.model.get('icon') || 'folder',
customIcon: this.model.get('customIcon'),
enableSearching: this.model.get('enableSearching') !== false,
readonly: this.model.get('top')
enableSearching: this.model.getEffectiveEnableSearching(),
readonly: this.model.get('top'),
canAutoType: AutoType.enabled,
autoTypeSeq: this.model.get('autoTypeSeq'),
autoTypeEnabled: this.model.getEffectiveEnableAutoType(),
defaultAutoTypeSeq: this.model.getParentEffectiveAutoTypeSeq()
}, { plain: true });
if (!this.model.get('title')) {
this.$el.find('#grp__field-title').focus();
@ -68,6 +77,25 @@ var GrpView = Backbone.View.extend({
}
},
changeAutoTypeSeq: function(e) {
var that = this;
var el = e.target;
var seq = $.trim(el.value);
AutoType.validate(null, seq, function(err) {
$(e.target).toggleClass('input--error', !!err);
if (!err) {
that.model.setAutoTypeSeq(seq);
}
});
},
focusAutoTypeSeq: function(e) {
if (!this.views.hint) {
this.views.hint = new AutoTypeHintView({input: e.target}).render();
this.views.hint.on('remove', (function() {delete this.views.hint; }).bind(this));
}
},
showIconsSelect: function() {
if (this.views.sub) {
this.removeSubView();
@ -107,6 +135,11 @@ var GrpView = Backbone.View.extend({
this.model.setEnableSearching(enabled);
},
setEnableAutoType: function(e) {
var enabled = e.target.checked;
this.model.setEnableAutoType(enabled);
},
returnToApp: function() {
Backbone.trigger('edit-group');
}

View File

@ -25,11 +25,13 @@ var KeyChangeView = Backbone.View.extend({
},
render: function() {
this.keyFileName = this.model.get('keyFileName') || null;
this.keyFileName = this.model.file.get('keyFileName') || null;
this.keyFileData = null;
this.renderTemplate({
fileName: this.model.get('name'),
keyFileName: this.model.get('keyFileName')
fileName: this.model.file.get('name'),
keyFileName: this.model.file.get('keyFileName'),
title: this.model.expired ? Locale.keyChangeTitleExpired : Locale.keyChangeTitleRemote,
message: this.model.expired ? Locale.keyChangeMessageExpired : Locale.keyChangeMessageRemote
});
this.$el.find('.key-change__keyfile-name').text(this.keyFileName ? ': ' + this.keyFileName : '');
this.inputEl = this.$el.find('.key-change__pass');
@ -81,7 +83,8 @@ var KeyChangeView = Backbone.View.extend({
accept: function() {
this.trigger('accept', {
file: this.model,
file: this.model.file,
expired: this.model.expired,
password: this.passwordInput.value,
keyFileName: this.keyFileName,
keyFileData: this.keyFileData

View File

@ -4,9 +4,12 @@ var Backbone = require('backbone'),
Resizable = require('../mixins/resizable'),
Scrollable = require('../mixins/scrollable'),
ListSearchView = require('./list-search-view'),
DropdownView = require('./dropdown-view'),
EntryPresenter = require('../presenters/entry-presenter'),
DragDropInfo = require('../comp/drag-drop-info'),
AppSettingsModel = require('../models/app-settings-model');
AppSettingsModel = require('../models/app-settings-model'),
Locale = require('../util/locale'),
Format = require('../util/format');
var ListView = Backbone.View.extend({
template: require('templates/list.hbs'),
@ -14,6 +17,7 @@ var ListView = Backbone.View.extend({
events: {
'click .list__item': 'itemClick',
'click .list__table-options': 'tableOptionsClick',
'dragstart .list__item': 'itemDragStart'
},
@ -26,6 +30,17 @@ var ListView = Backbone.View.extend({
itemsEl: null,
tableColumns: [
{ val: 'title', name: 'title', enabled: true },
{ val: 'user', name: 'user', enabled: true },
{ val: 'url', name: 'website', enabled: true },
{ val: 'tags', name: 'tags', enabled: true },
{ val: 'notes', name: 'notes', enabled: true },
{ val: 'groupName', name: 'group', enabled: false },
{ val: 'fileName', name: 'file', enabled: false }
],
initialize: function () {
this.initScroll();
this.views = {};
@ -64,12 +79,19 @@ var ListView = Backbone.View.extend({
var itemsTemplate = this.getItemsTemplate();
var noColor = AppSettingsModel.instance.get('colorfulIcons') ? '' : 'grayscale';
var presenter = new EntryPresenter(this.getDescField(), noColor, this.model.activeEntryId);
var columns = {};
this.tableColumns.forEach(function(col) {
if (col.enabled) {
columns[col.val] = true;
}
});
presenter.columns = columns;
var itemsHtml = '';
this.items.forEach(function (item) {
presenter.present(item);
itemsHtml += itemTemplate(presenter);
}, this);
var html = itemsTemplate({ items: itemsHtml });
var html = itemsTemplate({ items: itemsHtml, columns: this.tableColumns });
this.itemsEl.html(html);
} else {
this.itemsEl.html(this.emptyTemplate());
@ -206,6 +228,47 @@ var ListView = Backbone.View.extend({
e.originalEvent.dataTransfer.setData('text/entry', id);
e.originalEvent.dataTransfer.effectAllowed = 'move';
DragDropInfo.dragObject = this.items.get(id);
},
tableOptionsClick: function(e) {
e.stopImmediatePropagation();
if (this.views.optionsDropdown) {
this.hideOptionsDropdown();
return;
}
var view = new DropdownView();
this.listenTo(view, 'cancel', this.hideOptionsDropdown);
this.listenTo(view, 'select', this.optionsDropdownSelect);
var targetElRect = this.$el.find('.list__table-options')[0].getBoundingClientRect();
var options = this.tableColumns.map(function(col) {
return {
value: col.val,
icon: col.enabled ? 'check-square-o' : 'square-o',
text: Format.capFirst(Locale[col.name])
};
});
view.render({
position: {
top: targetElRect.bottom,
left: targetElRect.left
},
options: options
});
this.views.optionsDropdown = view;
},
hideOptionsDropdown: function() {
if (this.views.optionsDropdown) {
this.views.optionsDropdown.remove();
delete this.views.optionsDropdown;
}
},
optionsDropdownSelect: function(e) {
var col = _.find(this.tableColumns, function(c) { return c.val === e.item; });
col.enabled = !col.enabled;
e.el.find('i:first').toggleClass('fa-check-square-o fa-square-o');
this.render();
}
});

View File

@ -49,7 +49,7 @@ var MenuItemView = Backbone.View.extend({
render: function() {
this.removeInnerViews();
this.renderTemplate(this.model.attributes);
this.iconEl = this.$el.find('i');
this.iconEl = this.$el.find('i.menu__item-icon');
var items = this.model.get('items');
if (items) {
items.forEach(function (item) {
@ -156,7 +156,14 @@ var MenuItemView = Backbone.View.extend({
editItem: function(e) {
if (this.model.get('active') && this.model.get('editable')) {
e.stopPropagation();
Backbone.trigger('edit-group', this.model);
switch (this.model.get('filterKey')) {
case 'tag':
Backbone.trigger('edit-tag', this.model);
break;
case 'group':
Backbone.trigger('edit-group', this.model);
break;
}
}
},
@ -173,7 +180,8 @@ var MenuItemView = Backbone.View.extend({
},
dropAllowed: function(e) {
return ['text/group', 'text/entry'].indexOf(e.originalEvent.dataTransfer.types[0]) >= 0;
var types = e.originalEvent.dataTransfer.types;
return types.indexOf('text/group') >= 0 || types.indexOf('text/entry') >= 0;
},
dragstart: function(e) {

View File

@ -7,6 +7,7 @@ var Backbone = require('backbone'),
Alerts = require('../comp/alerts'),
SecureInput = require('../comp/secure-input'),
DropboxLink = require('../comp/dropbox-link'),
FeatureDetector = require('../util/feature-detector'),
Logger = require('../util/logger'),
Locale = require('../util/locale'),
UrlUtil = require('../util/url-util'),
@ -82,6 +83,12 @@ var OpenView = Backbone.View.extend({
return this;
},
focusInput: function() {
if (!FeatureDetector.isMobile()) {
this.inputEl.focus();
}
},
getLastOpenFiles: function() {
return this.model.fileInfos.map(function(f) {
var icon = 'file-text';
@ -123,7 +130,7 @@ var OpenView = Backbone.View.extend({
esc: '',
enter: '',
success: function(res) {
that.inputEl.focus();
that.focusInput();
if (res === 'skip') {
that.model.settings.set('skipOpenLocalWarn', true);
}
@ -220,13 +227,13 @@ var OpenView = Backbone.View.extend({
this.$el.find('.open__settings-key-file').removeClass('hide');
this.inputEl[0].removeAttribute('readonly');
this.inputEl[0].setAttribute('placeholder', Locale.openPassFor + ' ' + this.params.name);
this.inputEl.focus();
this.focusInput();
},
displayOpenKeyFile: function() {
this.$el.toggleClass('open--key-file', !!this.params.keyFileName);
this.$el.find('.open__settings-key-file-name').text(this.params.keyFileName || Locale.openKeyFile);
this.inputEl.focus();
this.focusInput();
},
setFile: function(file, keyFile, fileReadyCallback) {
@ -442,6 +449,10 @@ var OpenView = Backbone.View.extend({
},
openDb: function() {
if (this.params.id && this.model.files.get(this.params.id)) {
this.trigger('close');
return;
}
if (this.busy || !this.params.name) {
return;
}
@ -458,7 +469,7 @@ var OpenView = Backbone.View.extend({
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err);
if (err) {
logger.error('Error opening file', err);
this.inputEl.focus();
this.focusInput();
this.inputEl[0].selectionStart = 0;
this.inputEl[0].selectionEnd = this.inputEl.val().length;
if (err.code !== 'InvalidKey') {
@ -603,7 +614,7 @@ var OpenView = Backbone.View.extend({
}
this.$el.find('.open__pass-area').removeClass('hide');
this.$el.find('.open__config').addClass('hide');
this.inputEl.focus();
this.focusInput();
},
applyConfig: function(config) {

View File

@ -34,7 +34,8 @@ var SettingsFileView = Backbone.View.extend({
'change #settings__file-trash': 'changeTrash',
'input #settings__file-hist-len': 'changeHistoryLength',
'input #settings__file-hist-size': 'changeHistorySize',
'input #settings__file-key-rounds': 'changeKeyRounds'
'input #settings__file-key-rounds': 'changeKeyRounds',
'input #settings__file-key-change-force': 'changeKeyChangeForce'
},
appModel: null,
@ -69,6 +70,7 @@ var SettingsFileView = Backbone.View.extend({
historyMaxItems: this.model.get('historyMaxItems'),
historyMaxSize: Math.round(this.model.get('historyMaxSize') / 1024 / 1024),
keyEncryptionRounds: this.model.get('keyEncryptionRounds'),
keyChangeForce: this.model.get('keyChangeForce') > 0 ? this.model.get('keyChangeForce') : null,
storageProviders: storageProviders
});
if (!this.model.get('created')) {
@ -383,6 +385,14 @@ var SettingsFileView = Backbone.View.extend({
return;
}
this.model.setKeyEncryptionRounds(value);
},
changeKeyChangeForce: function(e) {
var value = Math.round(e.target.value);
if (isNaN(value) || value <= 0) {
value = -1;
}
this.model.setKeyChange(true, value);
}
});

View File

@ -2,6 +2,7 @@
var Backbone = require('backbone'),
SettingsPrvView = require('./settings-prv-view'),
SettingsLogsView = require('./settings-logs-view'),
Launcher = require('../../comp/launcher'),
Updater = require('../../comp/updater'),
Format = require('../../util/format'),
@ -19,6 +20,7 @@ var SettingsGeneralView = Backbone.View.extend({
events: {
'change .settings__general-theme': 'changeTheme',
'change .settings__general-font-size': 'changeFontSize',
'change .settings__general-expand': 'changeExpandGroups',
'change .settings__general-auto-update': 'changeAutoUpdate',
'change .settings__general-idle-minutes': 'changeIdleMinutes',
@ -35,19 +37,23 @@ var SettingsGeneralView = Backbone.View.extend({
'click .settings__general-download-update-btn': 'downloadUpdate',
'click .settings__general-update-found-btn': 'installFoundUpdate',
'change .settings__general-prv-check': 'changeStorageEnabled',
'click .settings__general-show-advanced': 'showAdvancedSettings',
'click .settings__general-dev-tools-link': 'openDevTools',
'click .settings__general-try-beta-link': 'tryBeta'
'click .settings__general-try-beta-link': 'tryBeta',
'click .settings__general-show-logs-link': 'showLogs'
},
views: {},
views: null,
allThemes: {
fb: 'Flat blue',
db: 'Dark brown',
wh: 'White'
fb: Locale.setGenThemeFb,
db: Locale.setGenThemeDb,
wh: Locale.setGenThemeWh,
hc: Locale.setGenThemeHc
},
initialize: function() {
this.views = {};
this.listenTo(UpdateModel.instance, 'change:status', this.render, this);
this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render, this);
},
@ -60,6 +66,7 @@ var SettingsGeneralView = Backbone.View.extend({
this.renderTemplate({
themes: this.allThemes,
activeTheme: AppSettingsModel.instance.get('theme'),
fontSize: AppSettingsModel.instance.get('fontSize'),
expandGroups: AppSettingsModel.instance.get('expandGroups'),
canClearClipboard: !!Launcher,
clipboardSeconds: AppSettingsModel.instance.get('clipboardSeconds'),
@ -163,6 +170,11 @@ var SettingsGeneralView = Backbone.View.extend({
AppSettingsModel.instance.set('theme', theme);
},
changeFontSize: function(e) {
var fontSize = +e.target.value;
AppSettingsModel.instance.set('fontSize', fontSize);
},
changeClipboard: function(e) {
var clipboardSeconds = +e.target.value;
AppSettingsModel.instance.set('clipboardSeconds', clipboardSeconds);
@ -252,12 +264,17 @@ var SettingsGeneralView = Backbone.View.extend({
changeStorageEnabled: function(e) {
var storage = Storage[$(e.target).data('storage')];
if (storage) {
storage.enabled = e.target.checked;
storage.setEnabled(e.target.checked);
AppSettingsModel.instance.set(storage.name, storage.enabled);
this.$el.find('.settings__general-' + storage.name).toggleClass('hide', !e.target.checked);
}
},
showAdvancedSettings: function() {
this.$el.find('.settings__general-show-advanced, .settings__general-advanced').toggleClass('hide');
this.scrollToBottom();
},
openDevTools: function() {
if (Launcher) {
Launcher.openDevTools();
@ -273,6 +290,18 @@ var SettingsGeneralView = Backbone.View.extend({
} else {
location.href = Links.BetaWebApp;
}
},
showLogs: function() {
if (this.views.logView) {
this.views.logView.remove();
}
this.views.logView = new SettingsLogsView({ el: this.$el.find('.settings__general-advanced') }).render();
this.scrollToBottom();
},
scrollToBottom: function() {
this.$el.closest('.scroller').scrollTop(this.$el.height());
}
});

View File

@ -0,0 +1,22 @@
'use strict';
var Backbone = require('backbone'),
Logger = require('../../util/logger'),
Format = require('../../util/format');
var SettingsLogView = Backbone.View.extend({
template: require('templates/settings/settings-logs-view.hbs'),
render: function() {
var logs = Logger.getLast().map(function(item) {
return {
level: item.level,
msg: '[' + Format.padStr(item.level.toUpperCase(), 5) + '] ' + item.args.join(' ')
};
});
this.renderTemplate({ logs: logs });
return this;
}
});
module.exports = SettingsLogView;

View File

@ -13,7 +13,8 @@ var SettingsShortcutsView = Backbone.View.extend({
alt: FeatureDetector.altShortcutSymbol(true),
global: FeatureDetector.globalShortcutSymbol(true),
globalIsLarge: FeatureDetector.globalShortcutIsLarge(),
globalShortcutsSupported: !!Launcher
globalShortcutsSupported: !!Launcher,
autoTypeSupported: !!Launcher
});
}
});

View File

@ -0,0 +1,69 @@
'use strict';
var Backbone = require('backbone'),
Locale = require('../util/locale'),
Alerts = require('../comp/alerts');
var TagView = Backbone.View.extend({
template: require('templates/tag.hbs'),
events: {
'click .tag__buttons-trash': 'moveToTrash',
'click .tag__back-button': 'returnToApp',
'click .tag__btn-rename': 'renameTag'
},
initialize: function() {
this.appModel = this.model;
},
render: function() {
if (this.model) {
this.renderTemplate({
title: this.model.get('title')
}, { plain: true });
}
return this;
},
showTag: function(tag) {
this.model = tag;
this.render();
},
renameTag: function() {
var title = $.trim(this.$el.find('#tag__field-title').val());
if (!title || title === this.model.get('title')) {
return;
}
if (/[;,:]/.test(title)) {
Alerts.error({ header: Locale.tagBadName, body: Locale.tagBadNameBody });
return;
}
if (this.appModel.tags.some(function(t) { return t.toLowerCase() === title.toLowerCase(); })) {
Alerts.error({ header: Locale.tagExists, body: Locale.tagExistsBody });
return;
}
this.appModel.renameTag(this.model.get('title'), title);
Backbone.trigger('select-all');
},
moveToTrash: function() {
this.title = null;
var that = this;
Alerts.yesno({
header: Locale.tagTrashQuestion,
body: Locale.tagTrashQuestionBody,
success: function() {
that.appModel.renameTag(that.model.get('title'), undefined);
Backbone.trigger('select-all');
}
});
},
returnToApp: function() {
Backbone.trigger('edit-tag');
}
});
module.exports = TagView;

View File

@ -87,7 +87,7 @@
}
}
&__grp {
&__grp, &__tag {
@include flex(1);
@include display(flex);
overflow: hidden;

View File

@ -127,6 +127,7 @@
@include flex(1);
@include display(flex);
@include align-items(stretch);
@include align-content(flex-start);
@include flex-direction(row);
@include justify-content(flex-start);
@include flex-wrap(wrap);
@ -147,6 +148,10 @@
@include flex-direction(column);
@include justify-content(flex-start);
}
&-after {
@include flex(100% 1 0);
}
}
&__field {
@ -203,8 +208,9 @@
border-radius: $base-border-radius;
&:hover {
transition: border-color $base-duration $base-timing;
border: 1px solid;
@include th {
border: 1px solid light-border-color();
border-color: light-border-color();
box-shadow: 0 0 3px form-box-shadow-color();
}
.details__field-value-add-label {
@ -218,7 +224,6 @@
}
.details__field--multiline & {
width: 0;
word-break: break-all;
white-space: pre-wrap;
}
.details__field--edit &,
@ -241,6 +246,10 @@
line-height: 1.5em;
overflow: hidden;
}
>label {
font-weight: normal;
@include user-select(none);
}
.details__body-aside & {
@include th { color: muted-color(); }
a { @include th { color: muted-color(); } }
@ -268,6 +277,16 @@
&:before { content: $fa-var-unlock; }
.details__field--protected & { &:before { content: $fa-var-lock; } }
}
&--select {
border-width: 0;
padding: 0;
.details__field--editable:hover & { border-width: 0; }
}
>select {
margin: 0;
width: 100%;
padding: 0 $base-padding-h;
}
}
&--no-select {
@ -318,15 +337,13 @@
text-align: center;
overflow: hidden;
transition: color $base-duration $base-timing;
display: none;
@include nomobile { display: block; }
&:hover {
@include th { color: medium-color(); }
}
&-title {
display: none;
transition: color $slow-transition-out;
margin-right: .4em;
margin-right: $base-padding-h;
color: transparent;
.details__attachment-add:hover & {
display: inline;

View File

@ -20,7 +20,6 @@
text-align: center;
}
&__body {
@include flex(0);
@include display(flex);
@include align-items(flex-start);
@include flex-direction(column);

View File

@ -48,7 +48,7 @@
&-icon-search {
@include th { color: muted-color(); }
position: absolute;
top: .6em;
top: .5em;
right: .5em;
cursor: pointer;
&:hover {
@ -91,15 +91,27 @@
}
&__table {
border-collapse: collapse;
width: calc(100% - 2px);
td, th {
padding: $base-padding;
text-align: left;
&:first-child {
text-align: center;
}
}
th:first-child {
padding: 0;
width: 3em;
}
&-options {
@include icon-btn();
cursor: pointer;
}
}
&__item {
padding: $base-padding-px;
padding: 6px 10px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@ -111,7 +123,7 @@
}
&:not(.list__item--table) {
height: 32px;
height: 3rem;
}
&--expired {

View File

@ -57,6 +57,17 @@
display: none;
}
&-collapse {
display: none;
.menu__item--collapsed>& {
display: block;
position: absolute;
cursor: pointer;
@include position(absolute, 50% null null 1em);
@include transform(translateY(-50%));
}
}
&-body {
@include area-selectable();
padding: $base-padding;

View File

@ -34,6 +34,13 @@
@include size(1em);
}
}
@include mobile() {
.open__icons--lower & {
margin: 8px;
&-i, &-svg { font-size: 2em; }
&-text { font-size: .5em; }
}
}
}
&__pass {

View File

@ -114,4 +114,16 @@
&__general-prv {
margin-bottom: $base-padding-v;
}
&__logs {
@include user-select(text);
margin-top: $base-padding-v;
&-log {
margin: 0;
white-space: pre-wrap;
&--debug { color: $white; opacity: .8; }
&--info { color: $white; }
&--warn { color: $yellow; }
&--error { color: $red; }
}
}
}

View File

@ -0,0 +1,32 @@
.tag {
@include flex(1);
@include display(flex);
@include align-items(stretch);
@include flex-direction(column);
@include justify-content(flex-start);
width: 100%;
user-select: none;
&__back-button {
cursor: pointer;
position: absolute;
top: 0;
right: $base-padding-h;
padding: $base-padding-v * 2 0 1px 0;
z-index: 1;
}
&__space {
@include flex(1);
}
&__buttons {
@include display(flex);
@include flex-direction(row);
margin-top: $base-padding-v;
&-trash {
@include icon-btn($error:true);
}
}
}

View File

@ -12,5 +12,4 @@
@import "buttons";
@import "forms";
@import "lists";
@import "tables";
@import "typography";

View File

@ -6,8 +6,10 @@ html {
body {
overflow: auto;
-webkit-overflow-scrolling: touch;
cursor: default;
position: fixed;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: transparent;
}
noscript {

View File

@ -4,7 +4,7 @@
cursor: pointer;
display: inline-block;
font-family: $base-font-family;
font-size: $base-font-size;
font-size: 1rem;
-webkit-font-smoothing: antialiased;
font-weight: 600;
line-height: 1;

View File

@ -16,24 +16,22 @@ $violet: #d946db;
@function error-color() { @return th(error-color); }
// Text
@function muted-color() { @return mix(medium-color(), background-color(), 30%); }
@function muted-color-border() { @return mix(medium-color(), background-color(), 15%); }
@function muted-color() { @return mix(medium-color(), background-color(), th(mute-percent)); }
@function muted-color-border() { @return mix(medium-color(), background-color(), th(mute-percent) / 2); }
@function text-selection-bg-color() { @return rgba(action-color(), .3); }
@function text-selection-bg-color-error() { @return rgba(error-color(), .8); }
@function text-contrast-color($bg) { @if (lightness($bg) >= lightness(background-color())) { @return text-color(); } @else { @return background-color(); } }
// Borders, shadows
@function base-border-color() { @return mix(medium-color(), background-color(), 50%); }
@function accent-border-color() { @return mix(medium-color(), background-color(), 65%); }
@function light-border-color() { @return mix(medium-color(), background-color(), 10%); }
@function light-border-color() { @return mix(medium-color(), background-color(), th(light-border-percent)); }
@function form-box-shadow-color() { @return rgba($black, 0.06); }
@function form-box-shadow-color-focus() { @return adjust-color(action-color(), $lightness: -5%, $alpha: -0.3); }
@function form-box-shadow-color-focus-error() { @return adjust-color(error-color(), $lightness: -5%, $alpha: -0.3); }
@function dropdown-box-shadow-color() { @return rgba(medium-color(), .05); }
// Backgrounds
@function secondary-background-color() { @return mix(medium-color(), background-color(), 10%); }
@function intermediate-background-color() { @return mix(medium-color(), background-color(), 3%); }
@function intermediate-pressed-background-color() { @return mix(medium-color(), background-color(), 2.6%); }

View File

@ -16,13 +16,13 @@ input,
select {
display: block;
font-family: $base-font-family;
font-size: $base-font-size;
font-size: 1rem;
}
label {
display: inline-block;
font-family: $base-font-family;
font-size: $base-font-size;
font-size: 1rem;
font-weight: 600;
margin-bottom: $small-spacing / 2;
@ -39,7 +39,7 @@ input[type=text], input[type=password], textarea, input:not([type]) {
border-radius: $base-border-radius;
box-sizing: border-box;
font-family: $base-font-family;
font-size: $base-font-size;
font-size: 1rem;
margin-bottom: $small-spacing;
padding: $base-spacing / 3;
transition: border-color $base-duration $base-timing;

View File

@ -1,25 +0,0 @@
//table {
// border-collapse: collapse;
// font-feature-settings: "kern", "liga", "tnum";
// margin: $small-spacing 0;
// table-layout: fixed;
// width: 100%;
//}
//
//th {
// @include th { border-bottom: 1px solid light-border-color(); }
// font-weight: 600;
// padding: $small-spacing 0;
// text-align: left;
//}
//
//td {
// @include th { border-bottom: base-border(); }
// padding: $small-spacing 0;
//}
//
//tr,
//td,
//th {
// vertical-align: middle;
//}

View File

@ -1,3 +1,7 @@
html {
font-size: $base-font-size;
}
body {
@include size(100%);
@include user-select(none);
@ -5,12 +9,10 @@ body {
color: text-color();
background-color: background-color();
}
overflow: auto;
font-family: $base-font-family;
font-feature-settings: "kern", "liga";
font-size: $base-font-size;
font-feature-settings: "kern", "liga 0";
font-size: 1rem;
line-height: $base-line-height;
cursor: default;
}
@-moz-document url-prefix() {
@ -28,13 +30,13 @@ h6 {
margin: 0 0 $small-spacing;
}
$small-header-font-size: modular-scale(2, $base-font-size);
$large-header-font-size: modular-scale(3, $base-font-size);
$small-header-font-size: modular-scale(2, 1rem);
$large-header-font-size: modular-scale(3, 1rem);
h6 { font-size: $base-font-size; }
h5 { font-size: modular-scale(1, $base-font-size, 1.1); }
h4 { font-size: modular-scale(2, $base-font-size, 1.1); }
h3 { font-size: modular-scale(3, $base-font-size, 1.1); }
h6 { font-size: 1rem; }
h5 { font-size: modular-scale(1, 1rem, 1.1); }
h4 { font-size: modular-scale(2, 1rem, 1.1); }
h3 { font-size: modular-scale(3, 1rem, 1.1); }
h2 { font-size: $small-header-font-size; }
h1 { font-size: $large-header-font-size; }

View File

@ -1,7 +1,7 @@
.modal {
@include position(absolute, 0 null null 0);
@include size(100%);
@include th { background-color: rgba(background-color(), .9); }
@include th { background-color: rgba(background-color(), th(modal-opacity)); }
z-index: $z-index-modal;
transition: background-color $base-duration $base-timing;

View File

@ -7,8 +7,9 @@
@import "base/base";
@import "utils/drag";
@import "utils/common-dropdown";
@import "utils/auto-type-hint";
@import "utils/drag";
@import "utils/selection";
@import "common/dates";
@ -24,6 +25,7 @@
@import "areas/details";
@import "areas/footer";
@import "areas/grp";
@import "areas/tag";
@import "areas/generator";
@import "areas/key-change";
@import "areas/list";

View File

@ -1,5 +1,7 @@
$themes: ();
@import "theme-defaults";
@import "dark-brown";
@import "flat-blue";
@import "white";
@import "high-contrast";

View File

@ -1,9 +1,9 @@
$themes: map-merge($themes, (
db: (
db: map-merge($theme-defaults, (
background-color: #342F2E,
medium-color: #FED9D8,
text-color: #FFEAE9,
action-color: #2C9957,
error-color: #FD6D67,
)
))
));

View File

@ -1,9 +1,9 @@
$themes: map-merge($themes, (
fb: (
fb: map-merge($theme-defaults, (
background-color: #282C34,
medium-color: #ABB2BF,
text-color: #D7DAE0,
action-color: #528BFF,
error-color: #C34034,
)
))
));

View File

@ -0,0 +1,12 @@
$themes: map-merge($themes, (
hc: map-merge($theme-defaults, (
background-color: #FAFAFA,
medium-color: #050505,
text-color: #050505,
action-color: #2d72d7,
error-color: #e74859,
mute-percent: 60%,
light-border-percent: 50%,
modal-opacity: 1,
))
));

View File

@ -0,0 +1,5 @@
$theme-defaults: (
mute-percent: 30%,
light-border-percent: 10%,
modal-opacity: .9,
)

View File

@ -1,9 +1,9 @@
$themes: map-merge($themes, (
wh: (
wh: map-merge($theme-defaults, (
background-color: #FAFAFA,
medium-color: #050505,
text-color: #424243,
action-color: #475FD7,
error-color: #E75675,
)
))
));

View File

@ -0,0 +1,24 @@
.auto-type-hint {
@include common-dropdown;
position: absolute;
z-index: $z-index-no-modal;
border-radius: $base-border-radius;
padding: $base-padding;
box-sizing: border-box;
overflow: hidden;
&__block {
margin-bottom: $base-padding-v;
>a, >b {
font-weight: normal;
display: inline-block;
margin-right: $base-padding-h;
margin-bottom: $base-padding-v;
}
}
&__link-details {
position: absolute;
right: 0;
top: 0;
margin: $base-padding;
}
}

View File

@ -9,6 +9,7 @@
<div class="app__details"></div>
</div>
<div class="app__grp"></div>
<div class="app__tag"></div>
</div>
<div class="app__footer"></div>
</div>

View File

@ -0,0 +1,17 @@
<div class="auto-type-hint">
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeEntryFields'}}:</div>
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeModifiers'}}:</div>
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeKeys'}}:</div>
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
</div>
</div>

View File

@ -0,0 +1,38 @@
<div class="details__auto-type">
<div class="details__field">
<div class="details__field-label">{{res 'detAutoType'}}</div>
<div class="details__field-value">
<input type="checkbox" class="input-base" id="details__auto-type-enabled" {{#if enabled}}checked{{/if}} />
<label for="details__auto-type-enabled">{{res 'detAutoTypeEnabled'}}</label>
</div>
</div>
<div class="details__field">
<div class="details__field-label">{{res 'detAutoTypeSequence'}}</div>
<div class="details__field-value">
<input type="text" id="details__auto-type-sequence" maxlength="1024"
value="{{sequence}}" placeholder="{{defaultSequence}}" />
</div>
</div>
<div class="details__field">
<div class="details__field-label">{{res 'detAutoTypeInput'}}</div>
<div class="details__field-value">
<input type="checkbox" class="input-base" id="details__auto-type-obfuscation" {{#if obfuscation}}checked{{/if}} />
<label for="details__auto-type-obfuscation">{{res 'detAutoTypeObfuscation'}}</label>
</div>
</div>
<div class="details__field">
<div class="details__field-label">{{res 'detAutoTypeShortcuts'}}</div>
<div class="details__field-value">{{{detAutoTypeShortcutsDesc}}}</div>
</div>
{{!--{{#each windows as |win|}}
<div class="details__field">
<div class="details__field-label">{{res 'detAutoTypeWindow'}}</div>
<div class="details__field-value details__auto-type-windows">
<input type="text" maxlength="1024" class="details__auto-type-window-title"
value="{{win.window}}" placeholder="{{res 'detAutoTypeInputWindow'}}" />
<input type="text" maxlength="1024" class="details__auto-type-window-sequence"
value="{{win.sequence}}" placeholder="{{../defaultSequence}}" />
</div>
</div>
{{/each}}--}}
</div>

View File

@ -26,6 +26,7 @@
</div>
<div class="details__body-aside">
</div>
<div class="details__body-after"></div>
</div>
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
</div>
@ -36,6 +37,12 @@
<i class="details__buttons-trash fa fa-trash-o" title="{{res 'detDelEntry'}}" tip-placement="top"></i>
{{~/if~}}
<div class="details__attachments">
<input type="file" class="details__attachment-input-file hide-by-pos" multiple />
{{#ifneq attachments.length 0}}
<div class="details__attachment-add">
<i class="fa fa-paperclip"></i>
</div>
{{/ifneq}}
{{#each attachments as |attachment ix|}}
<div class="details__attachment" data-id="{{ix}}"><i class="fa fa-{{attachment.icon}}"></i> {{attachment.title}}</div>
{{else}}

View File

@ -22,11 +22,24 @@
<i class="fa fa-{{icon}} grp__icon"></i>
{{/if}}
<div class="grp__icons"></div>
{{#if canAutoType}}
{{#unless readonly}}
<div>
<input type="checkbox" class="input-base" id="grp__check-auto-type" {{#if autoTypeEnabled}}checked{{/if}} />
<label for="grp__check-auto-type">{{res 'grpAutoType'}}</label>
</div>
{{/unless}}
<div class="grp__field">
<label for="grp__field-auto-type-seq">{{res 'grpAutoTypeSeq'}}:</label>
<input type="text" class="input-base" id="grp__field-auto-type-seq" value="{{autoTypeSeq}}"
size="50" maxlength="1024" placeholder="{{res 'grpAutoTypeSeqDefault'}}: {{defaultAutoTypeSeq}}" />
</div>
{{/if}}
</div>
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
{{#unless readonly}}
<div class="grp__buttons">
<i class="grp__buttons-trash fa fa-trash-o"></i>
<i class="grp__buttons-trash fa fa-trash-o" title="{{res 'grpTrash'}}" tip-placement="right"></i>
</div>
{{/unless}}
</div>

View File

@ -1,11 +1,11 @@
<div class="key-change">
<i class="key-change__icon fa fa-lock"></i>
<div class="key-change__header">{{fileName}}: {{res 'keyChangeTitle'}}</div>
<div class="key-change__header">{{fileName}}: {{title}}</div>
<div class="key-change__body">
<div class="key-change__message">{{res 'keyChangeMessage'}}:</div>
<div class="key-change__message">{{message}}:</div>
<div class="key-change__input">
<input class="key-change__file hide-by-pos" type="file" />
<input class="key-change__pass" type="password" size="30" autocomplete="off" maxlength="128" autofocus />
<input class="key-change__pass" type="password" size="30" autocomplete="off" maxlength="1024" autofocus />
<div class="key-change__keyfile">
<i class="fa fa-key"></i> {{res 'openKeyFile'}}<span class="key-change__keyfile-name"></span>
</div>

View File

@ -6,9 +6,11 @@
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
{{~/if~}}
</td>
<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>
<td>{{user}}</td>
<td>{{url}}</td>
<td>{{tags}}</td>
<td>{{notes}}</td>
{{#if columns.title}}<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>{{/if}}
{{#if columns.user}}<td>{{user}}</td>{{/if}}
{{#if columns.url}}<td>{{url}}</td>{{/if}}
{{#if columns.tags}}<td>{{tags}}</td>{{/if}}
{{#if columns.notes}}<td>{{notes}}</td>{{/if}}
{{#if columns.groupName}}<td>{{groupName}}</td>{{/if}}
{{#if columns.fileName}}<td>{{fileName}}</td>{{/if}}
</tr>

View File

@ -5,7 +5,10 @@
</div>
<div class="list__search-field-wrap">
<input type="text" class="list__search-field input-padding-right" autocomplete="off">
<i class="list__search-icon-search fa fa-search" title="{{res 'searchAdvTitle'}}"></i>
<div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}">
<i class="fa fa-search"></i>
<i class="fa fa-caret-down"></i>
</div>
</div>
<div class="list__search-btn-new" title="{{res 'searchAddNew'}}">
<i class="fa fa-plus"></i>

View File

@ -1,12 +1,10 @@
<table class="list__table">
<thead>
<tr>
<th></th>
<th>{{Res 'title'}}</th>
<th>{{Res 'user'}}</th>
<th>{{Res 'website'}}</th>
<th>{{Res 'tags'}}</th>
<th>{{Res 'notes'}}</th>
<th><i class="fa fa-bars muted-color list__table-options"></i></th>
{{#each columns as |col|}}
{{#if col.enabled}}<th>{{Res col.name}}</th>{{/if}}
{{/each}}
</tr>
</thead>
<tbody>

View File

@ -4,6 +4,7 @@
{{~#if options.length}} menu__item--with-options {{/if~}}
{{~#if cls}} {{cls}}{{/if~}}
">
{{#if collapsible}}<i class="menu__item-collapse fa fa-ellipsis-v muted-color" title="{{res 'menuItemCollapsed'}}"></i>{{/if}}
<div class="menu__item-body" {{#if drag}}draggable="true"{{/if}}>
{{#if customIcon~}}
<img src="{{{customIcon}}}" class="menu__item-icon menu__item-icon--image" />

View File

@ -48,8 +48,8 @@
<div class="open__pass-warning muted-color invisible"><i class="fa fa-exclamation-triangle"></i> {{res 'openCaps'}}</div>
</div>
<div class="open__pass-field-wrap">
<input class="open__pass-input" type="password" size="30" autocomplete="off" maxlength="128"
placeholder="Click to open a file" readonly />
<input class="open__pass-input" type="password" size="30" autocomplete="off" maxlength="1024"
placeholder="{{res 'openClickToOpen'}}" readonly />
<div class="open__pass-enter-btn"><i class="fa fa-level-down fa-rotate-90"></i></div>
<div class="open__pass-opening-icon"><i class="fa fa-spinner fa-spin"></i></div>
</div>

View File

@ -19,12 +19,10 @@
<h3>Core components</h3>
<ul>
<li><a href="https://github.com/antelle/kdbxweb" target="_blank">kdbxweb</a><span class="muted-color">, web kdbx library</span></li>
<li><a href="https://github.com/keeweb/kdbxweb" target="_blank">kdbxweb</a><span class="muted-color">, web kdbx library</span></li>
<li><a href="https://github.com/vibornoff/asmcrypto.js/" target="_blank">asmcrypto</a><span class="muted-color">, JavaScript cryptographic library
with performance in mind</span></li>
<li><a href="http://nodeca.github.io/pako/" target="_blank">pako</a><span class="muted-color">, zlib port to JavaScript, very fast</span></li>
<li><a href="https://github.com/jindw/xmldom" target="_blank">xmldom</a><span class="muted-color">, a pure js W3C standard based DOMParser
and XMLSerializer</span></li>
</ul>
<h3>UI components</h3>

View File

@ -73,4 +73,7 @@
<h2>{{res 'setFileAdvanced'}}</h2>
<label for="settings__file-key-rounds">{{res 'setFileRounds'}}:</label>
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-key-rounds" value="{{keyEncryptionRounds}}" />
<label for="settings__file-key-rounds">{{res 'setFileKeyChangeForce'}}:</label>
<input type="text" pattern="\d*" class="settings__input input-base" id="settings__file-key-change-force" value="{{keyChangeForce}}" />
</div>

View File

@ -45,6 +45,14 @@
{{/each}}
</select>
</div>
<div>
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
<select class="settings__general-font-size settings__select input-base" id="settings__general-font-size">
<option value="0" {{#ifeq fontSize 0}}selected{{/ifeq}}>{{res 'setGenFontSizeNormal'}}</option>
<option value="1" {{#ifeq fontSize 1}}selected{{/ifeq}}>{{res 'setGenFontSizeLarge'}}</option>
<option value="2" {{#ifeq fontSize 2}}selected{{/ifeq}}>{{res 'setGenFontSizeLargest'}}</option>
</select>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-expand" id="settings__general-expand" {{#if expandGroups}}checked{{/if}} />
<label for="settings__general-expand">{{res 'setGenShowSubgroups'}}</label>
@ -120,9 +128,13 @@
<div class="settings__general-prv-wrap settings__general-{{prv.name}} {{#ifeq prv.enabled false}}hide{{/ifeq}}"></div>
{{/each}}
{{#if devTools}}
<h2>{{res 'setGenAdvanced'}}</h2>
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
{{/if}}
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
<div class="settings__general-advanced hide">
{{#if devTools}}
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
{{/if}}
<button class="btn-silent settings__general-show-logs-link">{{res 'setGenShowAppLogs'}}</button>
</div>
</div>

View File

@ -0,0 +1,5 @@
<div class="settings__logs">
{{#each logs as |log|}}
<pre class="settings__logs-log settings__logs-log--{{level}}">{{msg}}</pre>
{{/each}}
</div>

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