mirror of https://github.com/keeweb/keeweb.git
542 lines
14 KiB
JavaScript
542 lines
14 KiB
JavaScript
import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
|
|
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
|
import { StringFormat } from 'util/formatting/string-format';
|
|
import { Logger } from 'util/logger';
|
|
|
|
const emitterLogger = new Logger(
|
|
'auto-type-emitter',
|
|
undefined,
|
|
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
|
|
);
|
|
|
|
const 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(runner, op) {
|
|
return runner.getEntryFieldKeys('Title', op);
|
|
},
|
|
username(runner, op) {
|
|
return runner.getEntryFieldKeys('UserName', op);
|
|
},
|
|
url(runner, op) {
|
|
return runner.getEntryFieldKeys('URL', op);
|
|
},
|
|
password(runner, op) {
|
|
return runner.getEntryFieldKeys('Password', op);
|
|
},
|
|
notes(runner, op) {
|
|
return runner.getEntryFieldKeys('Notes', op);
|
|
},
|
|
group(runner) {
|
|
return runner.getEntryGroupName();
|
|
},
|
|
totp(runner, op) {
|
|
return runner.getOtp(op);
|
|
},
|
|
s(runner, op) {
|
|
return runner.getEntryFieldKeys(op.arg, op);
|
|
},
|
|
'dt_simple'(runner) {
|
|
return runner.dt('simple');
|
|
},
|
|
'dt_year'(runner) {
|
|
return runner.dt('Y');
|
|
},
|
|
'dt_month'(runner) {
|
|
return runner.dt('M');
|
|
},
|
|
'dt_day'(runner) {
|
|
return runner.dt('D');
|
|
},
|
|
'dt_hour'(runner) {
|
|
return runner.dt('h');
|
|
},
|
|
'dt_minute'(runner) {
|
|
return runner.dt('m');
|
|
},
|
|
'dt_second'(runner) {
|
|
return runner.dt('s');
|
|
},
|
|
'dt_utc_simple'(runner) {
|
|
return runner.udt('simple');
|
|
},
|
|
'dt_utc_year'(runner) {
|
|
return runner.udt('Y');
|
|
},
|
|
'dt_utc_month'(runner) {
|
|
return runner.udt('M');
|
|
},
|
|
'dt_utc_day'(runner) {
|
|
return runner.udt('D');
|
|
},
|
|
'dt_utc_hour'(runner) {
|
|
return runner.udt('h');
|
|
},
|
|
'dt_utc_minute'(runner) {
|
|
return runner.udt('m');
|
|
},
|
|
'dt_utc_second'(runner) {
|
|
return runner.udt('s');
|
|
}
|
|
};
|
|
|
|
AutoTypeRunner.prototype.resolve = function (entry, context, callback) {
|
|
this.entry = entry;
|
|
this.context = context;
|
|
try {
|
|
this.resolveOps(this.ops);
|
|
if (!this.pendingResolvesCount) {
|
|
callback();
|
|
} else {
|
|
this.resolveCallback = callback;
|
|
}
|
|
} catch (e) {
|
|
return callback(e);
|
|
}
|
|
};
|
|
|
|
AutoTypeRunner.prototype.resolveOps = function (ops) {
|
|
for (let i = 0, len = ops.length; i < len; i++) {
|
|
const 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';
|
|
const ch = op.value;
|
|
let text = ch;
|
|
const len = +op.arg;
|
|
while (text.length < len) {
|
|
text += ch;
|
|
}
|
|
op.value = text;
|
|
return;
|
|
}
|
|
const lowerValue = op.value.toLowerCase();
|
|
const key = AutoTypeRunner.Keys[lowerValue];
|
|
if (key) {
|
|
if (op.sep === ' ' && +op.arg > 0) {
|
|
// {TAB 3}
|
|
op.type = 'group';
|
|
op.value = [];
|
|
const count = +op.arg;
|
|
for (let i = 0; i < count; i++) {
|
|
op.value.push({ type: 'key', value: key });
|
|
}
|
|
} else {
|
|
// {TAB}
|
|
op.type = 'key';
|
|
op.value = key;
|
|
}
|
|
return;
|
|
}
|
|
if (this.context?.resolved?.[lowerValue]) {
|
|
op.type = 'text';
|
|
op.value = this.context.resolved[lowerValue];
|
|
return;
|
|
}
|
|
const 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 '';
|
|
}
|
|
const value = this.entry.getFieldValue(field);
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
if (value.isProtected) {
|
|
op.type = 'group';
|
|
const ops = [];
|
|
value.forEachChar((ch) => {
|
|
if (ch === 10 || ch === 13) {
|
|
ops.push({ type: 'key', value: 'enter' });
|
|
} else {
|
|
ops.push({ type: 'text', value: String.fromCharCode(ch) });
|
|
}
|
|
});
|
|
return ops;
|
|
} else {
|
|
const parts = value.split(/[\r\n]/g);
|
|
if (parts.length === 1) {
|
|
return value;
|
|
}
|
|
op.type = 'group';
|
|
const partsOps = [];
|
|
parts.forEach((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.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 StringFormat.pad(this.now.getMonth() + 1, 2);
|
|
case 'D':
|
|
return StringFormat.pad(this.now.getDate(), 2);
|
|
case 'h':
|
|
return StringFormat.pad(this.now.getHours(), 2);
|
|
case 'm':
|
|
return StringFormat.pad(this.now.getMinutes(), 2);
|
|
case 's':
|
|
return StringFormat.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 StringFormat.pad(this.now.getUTCMonth() + 1, 2);
|
|
case 'D':
|
|
return StringFormat.pad(this.now.getUTCDate(), 2);
|
|
case 'h':
|
|
return StringFormat.pad(this.now.getUTCHours(), 2);
|
|
case 'm':
|
|
return StringFormat.pad(this.now.getUTCMinutes(), 2);
|
|
case 's':
|
|
return StringFormat.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 '';
|
|
}
|
|
this.entry.otpGenerator.next((err, otp) => {
|
|
this.pendingResolved(op, otp, err);
|
|
});
|
|
return AutoTypeRunner.PendingResolve;
|
|
};
|
|
|
|
AutoTypeRunner.prototype.pendingResolved = function (op, value, error) {
|
|
const 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 (let i = 0, len = ops.length; i < len; i++) {
|
|
const op = ops[i];
|
|
if (op.mod) {
|
|
continue;
|
|
}
|
|
if (op.type === 'text') {
|
|
this.obfuscateOp(op);
|
|
} else if (op.type === 'group') {
|
|
const onlyText = op.value.every((grOp) => grOp.type === 'text' && !grOp.mod);
|
|
if (onlyText) {
|
|
this.obfuscateOp(op);
|
|
} else {
|
|
this.obfuscateOps(op.value);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
|
let letters = [];
|
|
if (op.type === 'text') {
|
|
if (!op.value || op.value.length <= 1) {
|
|
return;
|
|
}
|
|
letters = op.value.split('');
|
|
} else {
|
|
op.value.forEach((grOp) => letters.push(...grOp.value.split('')));
|
|
}
|
|
if (letters.length <= 1) {
|
|
return;
|
|
}
|
|
const obfuscator = new AutoTypeObfuscator(letters);
|
|
op.value = obfuscator.obfuscate();
|
|
op.type = 'group';
|
|
};
|
|
|
|
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
|
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
|
this.emitterState = {
|
|
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) {
|
|
const state = this.emitterState.stack.pop();
|
|
if (state) {
|
|
Object.assign(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;
|
|
}
|
|
const 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: { ...this.emitterState.mod }
|
|
});
|
|
Object.assign(this.emitterState, {
|
|
ops: op.value,
|
|
opIx: 0,
|
|
mod: { ...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);
|
|
} else {
|
|
this.emitNext();
|
|
}
|
|
break;
|
|
case 'key':
|
|
emitterLogger.debug('key', op.value);
|
|
this.emitter.key(op.value);
|
|
break;
|
|
case 'cmd': {
|
|
const 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);
|
|
};
|
|
|
|
export { AutoTypeRunner };
|