2019-09-15 14:16:32 +02:00
|
|
|
import Backbone from 'backbone';
|
|
|
|
import kdbxweb from 'kdbxweb';
|
|
|
|
import { KdbxToHtml } from 'comp/format/kdbx-to-html';
|
|
|
|
import { IconMap } from 'const/icon-map';
|
|
|
|
import { AttachmentModel } from 'models/attachment-model';
|
|
|
|
import { Color } from 'util/data/color';
|
|
|
|
import { Otp } from 'util/data/otp';
|
|
|
|
import { Ranking } from 'util/data/ranking';
|
|
|
|
import { IconUrlFormat } from 'util/formatting/icon-url-format';
|
2019-09-18 07:12:06 +02:00
|
|
|
import { omit } from 'util/fn';
|
2017-01-31 07:50:28 +01:00
|
|
|
|
|
|
|
const EntryModel = Backbone.Model.extend({
|
2015-10-17 23:49:24 +02:00
|
|
|
defaults: {},
|
2016-08-14 20:53:55 +02:00
|
|
|
|
2016-01-13 19:00:22 +01:00
|
|
|
urlRegex: /^https?:\/\//i,
|
2016-08-14 20:53:55 +02:00
|
|
|
fieldRefRegex: /^\{REF:([TNPAU])@I:(\w{32})}$/,
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
builtInFields: [
|
|
|
|
'Title',
|
|
|
|
'Password',
|
|
|
|
'UserName',
|
|
|
|
'URL',
|
|
|
|
'Notes',
|
|
|
|
'TOTP Seed',
|
|
|
|
'TOTP Settings',
|
|
|
|
'_etm_template_uuid'
|
|
|
|
],
|
2016-08-14 20:53:55 +02:00
|
|
|
fieldRefFields: ['title', 'password', 'user', 'url', 'notes'],
|
|
|
|
fieldRefIds: { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' },
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setEntry(entry, group, file) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.entry = entry;
|
|
|
|
this.group = group;
|
|
|
|
this.file = file;
|
2016-06-05 12:25:50 +02:00
|
|
|
if (this.get('uuid') === entry.uuid.id) {
|
2015-12-05 14:04:09 +01:00
|
|
|
this._checkUpdatedEntry();
|
|
|
|
}
|
2016-08-14 20:53:55 +02:00
|
|
|
// we cannot calculate field references now because database index has not yet been built
|
|
|
|
this.hasFieldRefs = false;
|
2015-10-17 23:49:24 +02:00
|
|
|
this._fillByEntry();
|
2016-08-14 20:53:55 +02:00
|
|
|
this.hasFieldRefs = true;
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_fillByEntry() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const entry = this.entry;
|
2019-08-16 23:05:39 +02:00
|
|
|
this.set({ id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id }, { silent: true });
|
2019-09-17 21:39:06 +02:00
|
|
|
this.fileName = this.file.name;
|
2016-06-04 14:47:56 +02:00
|
|
|
this.groupName = this.group.get('title');
|
2016-09-19 19:31:52 +02:00
|
|
|
this.title = this._getFieldString('Title');
|
2015-10-31 20:27:31 +01:00
|
|
|
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
|
2016-09-19 19:31:52 +02:00
|
|
|
this.notes = this._getFieldString('Notes');
|
|
|
|
this.url = this._getFieldString('URL');
|
2017-01-29 13:04:49 +01:00
|
|
|
this.displayUrl = this._getDisplayUrl(this._getFieldString('URL'));
|
2016-09-19 19:31:52 +02:00
|
|
|
this.user = this._getFieldString('UserName');
|
2015-10-17 23:49:24 +02:00
|
|
|
this.iconId = entry.icon;
|
|
|
|
this.icon = this._iconFromId(entry.icon);
|
|
|
|
this.tags = entry.tags;
|
|
|
|
this.color = this._colorToModel(entry.bgColor) || this._colorToModel(entry.fgColor);
|
|
|
|
this.fields = this._fieldsToModel(entry.fields);
|
|
|
|
this.attachments = this._attachmentsToModel(entry.binaries);
|
|
|
|
this.created = entry.times.creationTime;
|
|
|
|
this.updated = entry.times.lastModTime;
|
|
|
|
this.expires = entry.times.expires ? entry.times.expiryTime : undefined;
|
|
|
|
this.expired = entry.times.expires && entry.times.expiryTime <= new Date();
|
|
|
|
this.historyLength = entry.history.length;
|
2015-11-12 22:52:28 +01:00
|
|
|
this._buildCustomIcon();
|
2015-10-17 23:49:24 +02:00
|
|
|
this._buildSearchText();
|
|
|
|
this._buildSearchTags();
|
|
|
|
this._buildSearchColor();
|
2016-04-23 16:50:40 +02:00
|
|
|
this._buildAutoType();
|
2016-08-14 20:53:55 +02:00
|
|
|
if (this.hasFieldRefs) {
|
|
|
|
this.resolveFieldReferences();
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_getFieldString(field) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const val = this.entry.fields[field];
|
2016-09-19 19:31:52 +02:00
|
|
|
if (!val) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
if (val.isProtected) {
|
|
|
|
return val.getText();
|
|
|
|
}
|
|
|
|
return val.toString();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_checkUpdatedEntry() {
|
2015-12-05 14:04:09 +01:00
|
|
|
if (this.isJustCreated) {
|
|
|
|
this.isJustCreated = false;
|
|
|
|
}
|
2016-06-05 16:49:00 +02:00
|
|
|
if (this.canBeDeleted) {
|
|
|
|
this.canBeDeleted = false;
|
|
|
|
}
|
2015-12-05 14:04:09 +01:00
|
|
|
if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) {
|
|
|
|
this.unsaved = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_buildSearchText() {
|
2017-01-31 07:50:28 +01:00
|
|
|
let text = '';
|
2019-09-17 22:17:40 +02:00
|
|
|
for (const value of Object.values(this.entry.fields)) {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (typeof value === 'string') {
|
|
|
|
text += value.toLowerCase() + '\n';
|
|
|
|
}
|
2019-09-17 22:17:40 +02:00
|
|
|
}
|
2016-07-17 13:30:38 +02:00
|
|
|
this.entry.tags.forEach(tag => {
|
2015-10-17 23:49:24 +02:00
|
|
|
text += tag.toLowerCase() + '\n';
|
|
|
|
});
|
2016-07-17 13:30:38 +02:00
|
|
|
this.attachments.forEach(att => {
|
2015-10-17 23:49:24 +02:00
|
|
|
text += att.title.toLowerCase() + '\n';
|
|
|
|
});
|
|
|
|
this.searchText = text;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_buildCustomIcon() {
|
2015-11-12 22:52:28 +01:00
|
|
|
this.customIcon = null;
|
2015-11-21 23:15:51 +01:00
|
|
|
this.customIconId = null;
|
2015-11-12 22:52:28 +01:00
|
|
|
if (this.entry.customIcon) {
|
2019-09-15 08:11:11 +02:00
|
|
|
this.customIcon = IconUrlFormat.toDataUrl(
|
2019-08-18 08:05:38 +02:00
|
|
|
this.file.db.meta.customIcons[this.entry.customIcon]
|
|
|
|
);
|
2015-11-21 23:15:51 +01:00
|
|
|
this.customIconId = this.entry.customIcon.toString();
|
2015-11-12 22:52:28 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_buildSearchTags() {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.searchTags = this.entry.tags.map(tag => tag.toLowerCase());
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_buildSearchColor() {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.searchColor = this.color;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_buildAutoType() {
|
2016-04-23 16:50:40 +02:00
|
|
|
this.autoTypeEnabled = this.entry.autoType.enabled;
|
2019-08-16 23:05:39 +02:00
|
|
|
this.autoTypeObfuscation =
|
2019-08-18 08:05:38 +02:00
|
|
|
this.entry.autoType.obfuscation ===
|
|
|
|
kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
|
2016-04-23 16:50:40 +02:00
|
|
|
this.autoTypeSequence = this.entry.autoType.defaultSequence;
|
2016-04-26 21:40:18 +02:00
|
|
|
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_convertAutoTypeItem(item) {
|
2016-04-26 21:40:18 +02:00
|
|
|
return { window: item.window, sequence: item.keystrokeSequence };
|
2016-04-23 16:50:40 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_iconFromId(id) {
|
2015-10-17 23:49:24 +02:00
|
|
|
return IconMap[id];
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_getDisplayUrl(url) {
|
2016-01-13 19:00:22 +01:00
|
|
|
if (!url) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return url.replace(this.urlRegex, '');
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_colorToModel(color) {
|
2015-10-17 23:49:24 +02:00
|
|
|
return color ? Color.getNearest(color) : null;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_fieldsToModel(fields) {
|
2019-09-18 07:12:06 +02:00
|
|
|
return omit(fields, this.builtInFields);
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_attachmentsToModel(binaries) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const att = [];
|
2019-09-17 22:17:40 +02:00
|
|
|
for (let [title, data] of Object.entries(binaries)) {
|
|
|
|
if (data && data.ref) {
|
|
|
|
data = data.value;
|
|
|
|
}
|
|
|
|
if (data) {
|
|
|
|
att.push(AttachmentModel.fromAttachment({ data, title }));
|
|
|
|
}
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
return att;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_entryModified() {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (!this.unsaved) {
|
|
|
|
this.unsaved = true;
|
|
|
|
this.entry.pushHistory();
|
|
|
|
this.file.setModified();
|
|
|
|
}
|
2015-10-27 22:07:48 +01:00
|
|
|
if (this.isJustCreated) {
|
|
|
|
this.isJustCreated = false;
|
2015-10-27 20:40:34 +01:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
this.entry.times.update();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setSaved() {
|
2016-06-05 16:49:00 +02:00
|
|
|
if (this.unsaved) {
|
|
|
|
this.unsaved = false;
|
|
|
|
}
|
|
|
|
if (this.canBeDeleted) {
|
|
|
|
this.canBeDeleted = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matches(filter) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return (
|
|
|
|
!filter ||
|
|
|
|
((!filter.tagLower || this.searchTags.indexOf(filter.tagLower) >= 0) &&
|
|
|
|
(!filter.textLower ||
|
2019-08-18 08:05:38 +02:00
|
|
|
(filter.advanced
|
|
|
|
? this.matchesAdv(filter)
|
|
|
|
: this.searchText.indexOf(filter.textLower) >= 0)) &&
|
|
|
|
(!filter.color ||
|
|
|
|
(filter.color === true && this.searchColor) ||
|
|
|
|
this.searchColor === filter.color) &&
|
2019-08-16 23:05:39 +02:00
|
|
|
(!filter.autoType || this.autoTypeEnabled))
|
|
|
|
);
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchesAdv(filter) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const adv = filter.advanced;
|
2019-08-16 23:05:39 +02:00
|
|
|
let search, match;
|
2016-01-13 21:33:14 +01:00
|
|
|
if (adv.regex) {
|
2016-07-17 13:30:38 +02:00
|
|
|
try {
|
|
|
|
search = new RegExp(filter.text, adv.cs ? '' : 'i');
|
2019-08-16 23:05:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-01-13 21:33:14 +01:00
|
|
|
match = this.matchRegex;
|
|
|
|
} else if (adv.cs) {
|
|
|
|
search = filter.text;
|
|
|
|
match = this.matchString;
|
|
|
|
} else {
|
|
|
|
search = filter.textLower;
|
|
|
|
match = this.matchStringLower;
|
|
|
|
}
|
|
|
|
if (this.matchEntry(this.entry, adv, match, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (adv.history) {
|
2017-01-31 07:50:28 +01:00
|
|
|
for (let i = 0, len = this.entry.history.length; i < len; i++) {
|
2016-01-13 21:33:14 +01:00
|
|
|
if (this.matchEntry(this.entry.history[0], adv, match, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchString(str, find) {
|
2016-01-17 12:36:52 +01:00
|
|
|
if (str.isProtected) {
|
|
|
|
return str.includes(find);
|
|
|
|
}
|
2016-01-13 21:33:14 +01:00
|
|
|
return str.indexOf(find) >= 0;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchStringLower(str, findLower) {
|
2016-01-17 12:36:52 +01:00
|
|
|
if (str.isProtected) {
|
|
|
|
return str.includesLower(findLower);
|
|
|
|
}
|
2016-01-13 21:33:14 +01:00
|
|
|
return str.toLowerCase().indexOf(findLower) >= 0;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchRegex(str, regex) {
|
2016-01-17 12:36:52 +01:00
|
|
|
if (str.isProtected) {
|
|
|
|
str = str.getText();
|
|
|
|
}
|
2016-01-13 21:33:14 +01:00
|
|
|
return regex.test(str);
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchEntry(entry, adv, compare, search) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const matchField = this.matchField;
|
2016-01-13 21:33:14 +01:00
|
|
|
if (adv.user && matchField(entry, 'UserName', compare, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (adv.url && matchField(entry, 'URL', compare, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (adv.notes && matchField(entry, 'Notes', compare, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (adv.pass && matchField(entry, 'Password', compare, search)) {
|
|
|
|
return true;
|
|
|
|
}
|
2016-02-25 20:50:31 +01:00
|
|
|
if (adv.title && matchField(entry, 'Title', compare, search)) {
|
2016-01-13 21:33:14 +01:00
|
|
|
return true;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
let matches = false;
|
2016-01-13 21:33:14 +01:00
|
|
|
if (adv.other || adv.protect) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const builtInFields = this.builtInFields;
|
|
|
|
const fieldNames = Object.keys(entry.fields);
|
2016-07-17 13:30:38 +02:00
|
|
|
matches = fieldNames.some(field => {
|
2016-01-13 21:33:14 +01:00
|
|
|
if (builtInFields.indexOf(field) >= 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (typeof entry.fields[field] === 'string') {
|
|
|
|
return adv.other && matchField(entry, field, compare, search);
|
|
|
|
} else {
|
|
|
|
return adv.protect && matchField(entry, field, compare, search);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return matches;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
matchField(entry, field, compare, search) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const val = entry.fields[field];
|
2016-01-17 12:36:52 +01:00
|
|
|
return val ? compare(val, search) : false;
|
2016-01-13 21:33:14 +01:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
resolveFieldReferences() {
|
2016-08-14 20:53:55 +02:00
|
|
|
this.hasFieldRefs = false;
|
|
|
|
this.fieldRefFields.forEach(field => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const fieldValue = this[field];
|
|
|
|
const refValue = this._resolveFieldReference(fieldValue);
|
2016-08-14 21:32:13 +02:00
|
|
|
if (refValue !== undefined) {
|
|
|
|
this[field] = refValue;
|
|
|
|
this.hasFieldRefs = true;
|
2016-08-14 20:53:55 +02:00
|
|
|
}
|
2016-08-14 21:32:13 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getFieldValue(field) {
|
2016-08-14 21:32:13 +02:00
|
|
|
field = field.toLowerCase();
|
|
|
|
let resolvedField;
|
|
|
|
Object.keys(this.entry.fields).some(entryField => {
|
|
|
|
if (entryField.toLowerCase() === field) {
|
|
|
|
resolvedField = entryField;
|
|
|
|
return true;
|
2016-08-14 20:53:55 +02:00
|
|
|
}
|
2019-08-18 10:17:09 +02:00
|
|
|
return false;
|
2016-08-14 20:53:55 +02:00
|
|
|
});
|
2016-08-14 21:32:13 +02:00
|
|
|
if (resolvedField) {
|
|
|
|
let fieldValue = this.entry.fields[resolvedField];
|
2017-01-31 07:50:28 +01:00
|
|
|
const refValue = this._resolveFieldReference(fieldValue);
|
2016-08-14 21:32:13 +02:00
|
|
|
if (refValue !== undefined) {
|
|
|
|
fieldValue = refValue;
|
|
|
|
}
|
|
|
|
return fieldValue;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_resolveFieldReference(fieldValue) {
|
2016-08-14 21:32:13 +02:00
|
|
|
if (!fieldValue) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (fieldValue.isProtected && fieldValue.isFieldReference()) {
|
|
|
|
fieldValue = fieldValue.getText();
|
|
|
|
}
|
|
|
|
if (typeof fieldValue !== 'string') {
|
|
|
|
return;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const match = fieldValue.match(this.fieldRefRegex);
|
2016-08-14 21:32:13 +02:00
|
|
|
if (!match) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this._getReferenceValue(match[1], match[2]);
|
2016-08-14 20:53:55 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
_getReferenceValue(fieldRefId, idStr) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const id = new Uint8Array(16);
|
2016-08-14 20:53:55 +02:00
|
|
|
for (let i = 0; i < 16; i++) {
|
|
|
|
id[i] = parseInt(idStr.substr(i * 2, 2), 16);
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const uuid = new kdbxweb.KdbxUuid(id);
|
|
|
|
const entry = this.file.getEntry(this.file.subId(uuid.id));
|
2016-08-14 20:53:55 +02:00
|
|
|
if (!entry) {
|
2016-08-14 21:32:13 +02:00
|
|
|
return;
|
2016-08-14 20:53:55 +02:00
|
|
|
}
|
|
|
|
return entry.entry.fields[this.fieldRefIds[fieldRefId]];
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setColor(color) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.bgColor = Color.getKnownBgColor(color);
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setIcon(iconId) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.icon = iconId;
|
2015-11-21 23:15:51 +01:00
|
|
|
this.entry.customIcon = undefined;
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setCustomIcon(customIconId) {
|
2015-11-21 23:15:51 +01:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.customIcon = new kdbxweb.KdbxUuid(customIconId);
|
2015-10-17 23:49:24 +02:00
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setExpires(dt) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.times.expiryTime = dt instanceof Date ? dt : undefined;
|
|
|
|
this.entry.times.expires = !!dt;
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setTags(tags) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.tags = tags;
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
renameTag(from, to) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const ix = _.findIndex(this.entry.tags, tag => tag.toLowerCase() === from.toLowerCase());
|
2016-04-17 22:02:39 +02:00
|
|
|
if (ix < 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._entryModified();
|
|
|
|
this.entry.tags.splice(ix, 1);
|
|
|
|
if (to) {
|
|
|
|
this.entry.tags.push(to);
|
|
|
|
}
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setField(field, val, allowEmpty) {
|
2019-08-16 23:05:39 +02:00
|
|
|
const hasValue = val && (typeof val === 'string' || (val.isProtected && val.byteLength));
|
2017-05-03 21:21:29 +02:00
|
|
|
if (hasValue || allowEmpty || this.builtInFields.indexOf(field) >= 0) {
|
2016-03-05 14:59:36 +01:00
|
|
|
this._entryModified();
|
2019-03-31 15:05:48 +02:00
|
|
|
val = this.sanitizeFieldValue(val);
|
2015-10-17 23:49:24 +02:00
|
|
|
this.entry.fields[field] = val;
|
2019-08-16 21:36:22 +02:00
|
|
|
} else if (Object.prototype.hasOwnProperty.call(this.entry.fields, field)) {
|
2016-03-05 14:59:36 +01:00
|
|
|
this._entryModified();
|
2015-10-17 23:49:24 +02:00
|
|
|
delete this.entry.fields[field];
|
|
|
|
}
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
sanitizeFieldValue(val) {
|
2019-03-31 15:05:48 +02:00
|
|
|
if (val && !val.isProtected && val.indexOf('\x1A') >= 0) {
|
|
|
|
// https://github.com/keeweb/keeweb/issues/910
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
|
|
val = val.replace(/\x1A/g, '');
|
|
|
|
}
|
|
|
|
return val;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
hasField(field) {
|
2019-08-16 21:36:22 +02:00
|
|
|
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
addAttachment(name, data) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
2017-01-29 23:28:04 +01:00
|
|
|
return this.file.db.createBinary(data).then(binaryRef => {
|
|
|
|
this.entry.binaries[name] = binaryRef;
|
|
|
|
this._fillByEntry();
|
|
|
|
});
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
removeAttachment(name) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this._entryModified();
|
|
|
|
delete this.entry.binaries[name];
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getHistory() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const history = this.entry.history.map(function(rec) {
|
2015-10-17 23:49:24 +02:00
|
|
|
return EntryModel.fromEntry(rec, this.group, this.file);
|
|
|
|
}, this);
|
|
|
|
history.push(this);
|
2016-07-17 13:30:38 +02:00
|
|
|
history.sort((x, y) => x.updated - y.updated);
|
2015-10-17 23:49:24 +02:00
|
|
|
return history;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
deleteHistory(historyEntry) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const ix = this.entry.history.indexOf(historyEntry);
|
2015-10-17 23:49:24 +02:00
|
|
|
if (ix >= 0) {
|
2015-12-05 14:04:09 +01:00
|
|
|
this.entry.removeHistory(ix);
|
2018-12-21 08:35:10 +01:00
|
|
|
this.file.setModified();
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
revertToHistoryState(historyEntry) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const ix = this.entry.history.indexOf(historyEntry);
|
2015-10-17 23:49:24 +02:00
|
|
|
if (ix < 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.entry.pushHistory();
|
|
|
|
this.unsaved = true;
|
|
|
|
this.file.setModified();
|
|
|
|
this.entry.fields = {};
|
|
|
|
this.entry.binaries = {};
|
|
|
|
this.entry.copyFrom(historyEntry);
|
|
|
|
this._entryModified();
|
|
|
|
this._fillByEntry();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
discardUnsaved() {
|
2015-12-05 14:04:09 +01:00
|
|
|
if (this.unsaved && this.entry.history.length) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.unsaved = false;
|
2017-01-31 07:50:28 +01:00
|
|
|
const historyEntry = this.entry.history[this.entry.history.length - 1];
|
2015-12-05 14:04:09 +01:00
|
|
|
this.entry.removeHistory(this.entry.history.length - 1);
|
2015-10-17 23:49:24 +02:00
|
|
|
this.entry.fields = {};
|
|
|
|
this.entry.binaries = {};
|
|
|
|
this.entry.copyFrom(historyEntry);
|
|
|
|
this._fillByEntry();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
moveToTrash() {
|
2015-11-19 06:55:45 +01:00
|
|
|
this.file.setModified();
|
|
|
|
if (this.isJustCreated) {
|
|
|
|
this.isJustCreated = false;
|
|
|
|
}
|
2015-11-04 09:06:49 +01:00
|
|
|
this.file.db.remove(this.entry);
|
2015-12-05 14:04:09 +01:00
|
|
|
this.file.reload();
|
2015-10-27 20:40:34 +01:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
deleteFromTrash() {
|
2015-11-19 06:55:45 +01:00
|
|
|
this.file.setModified();
|
2015-11-09 19:15:39 +01:00
|
|
|
this.file.db.move(this.entry, null);
|
2015-12-05 14:04:09 +01:00
|
|
|
this.file.reload();
|
2015-11-09 19:15:39 +01:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
removeWithoutHistory() {
|
2016-06-05 16:49:00 +02:00
|
|
|
if (this.canBeDeleted) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const ix = this.group.group.entries.indexOf(this.entry);
|
2016-06-05 16:49:00 +02:00
|
|
|
if (ix >= 0) {
|
|
|
|
this.group.group.entries.splice(ix, 1);
|
|
|
|
}
|
|
|
|
this.file.reload();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
moveToFile(file) {
|
2016-06-05 16:49:00 +02:00
|
|
|
if (this.canBeDeleted) {
|
|
|
|
this.removeWithoutHistory();
|
2019-09-17 21:39:06 +02:00
|
|
|
this.group = file.groups.first();
|
2016-06-05 16:49:00 +02:00
|
|
|
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();
|
2015-10-27 20:40:34 +01:00
|
|
|
}
|
2016-04-01 23:09:16 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
initOtpGenerator() {
|
2017-01-31 07:50:28 +01:00
|
|
|
let otpUrl;
|
2016-04-01 23:09:16 +02:00
|
|
|
if (this.fields.otp) {
|
|
|
|
otpUrl = this.fields.otp;
|
|
|
|
if (otpUrl.isProtected) {
|
|
|
|
otpUrl = otpUrl.getText();
|
|
|
|
}
|
2016-08-29 19:43:21 +02:00
|
|
|
if (Otp.isSecret(otpUrl.replace(/\s/g, ''))) {
|
2016-08-28 12:09:00 +02:00
|
|
|
otpUrl = Otp.makeUrl(otpUrl.replace(/\s/g, '').toUpperCase());
|
2016-04-04 20:45:17 +02:00
|
|
|
} else if (otpUrl.toLowerCase().lastIndexOf('otpauth:', 0) !== 0) {
|
2016-04-01 23:09:16 +02:00
|
|
|
// KeeOTP plugin format
|
2017-01-31 07:50:28 +01:00
|
|
|
const args = {};
|
2016-07-17 13:30:38 +02:00
|
|
|
otpUrl.split('&').forEach(part => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const parts = part.split('=', 2);
|
2016-04-01 23:09:16 +02:00
|
|
|
args[parts[0]] = decodeURIComponent(parts[1]).replace(/=/g, '');
|
|
|
|
});
|
|
|
|
if (args.key) {
|
2016-04-04 20:45:17 +02:00
|
|
|
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
|
|
|
}
|
2016-04-03 12:44:43 +02:00
|
|
|
} else if (this.entry.fields['TOTP Seed']) {
|
2016-04-01 23:09:16 +02:00
|
|
|
// TrayTOTP plugin format
|
2017-01-31 07:50:28 +01:00
|
|
|
let secret = this.entry.fields['TOTP Seed'];
|
2016-04-04 20:45:17 +02:00
|
|
|
if (secret.isProtected) {
|
|
|
|
secret = secret.getText();
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
2016-04-04 20:45:17 +02:00
|
|
|
if (secret) {
|
2017-01-31 07:50:28 +01:00
|
|
|
let settings = this.entry.fields['TOTP Settings'];
|
2016-04-04 20:45:17 +02:00
|
|
|
if (settings && settings.isProtected) {
|
|
|
|
settings = settings.getText();
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
let period, digits;
|
2016-04-04 20:45:17 +02:00
|
|
|
if (settings) {
|
|
|
|
settings = settings.split(';');
|
|
|
|
if (settings.length > 0 && settings[0] > 0) {
|
|
|
|
period = settings[0];
|
|
|
|
}
|
|
|
|
if (settings.length > 1 && settings[1] > 0) {
|
|
|
|
digits = settings[1];
|
|
|
|
}
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
2016-04-04 20:45:17 +02:00
|
|
|
otpUrl = Otp.makeUrl(secret, period, digits);
|
|
|
|
this.fields.otp = kdbxweb.ProtectedValue.fromString(otpUrl);
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (otpUrl) {
|
2016-04-02 15:20:48 +02:00
|
|
|
if (this.otpGenerator && this.otpGenerator.url === otpUrl) {
|
|
|
|
return;
|
|
|
|
}
|
2016-04-01 23:09:16 +02:00
|
|
|
try {
|
|
|
|
this.otpGenerator = Otp.parseUrl(otpUrl);
|
|
|
|
} catch (e) {
|
|
|
|
this.otpGenerator = null;
|
|
|
|
}
|
2016-04-02 15:20:48 +02:00
|
|
|
} else {
|
|
|
|
this.otpGenerator = null;
|
2016-04-01 23:09:16 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setOtp(otp) {
|
2016-04-01 23:09:16 +02:00
|
|
|
this.otpGenerator = otp;
|
2016-04-02 15:20:48 +02:00
|
|
|
this.setOtpUrl(otp.url);
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setOtpUrl(url) {
|
2016-04-04 20:45:17 +02:00
|
|
|
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
|
2016-04-03 12:54:51 +02:00
|
|
|
delete this.entry.fields['TOTP Seed'];
|
|
|
|
delete this.entry.fields['TOTP Settings'];
|
2016-04-23 16:50:40 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getEffectiveEnableAutoType() {
|
2016-04-23 16:50:40 +02:00
|
|
|
if (typeof this.entry.autoType.enabled === 'boolean') {
|
|
|
|
return this.entry.autoType.enabled;
|
|
|
|
}
|
|
|
|
return this.group.getEffectiveEnableAutoType();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getEffectiveAutoTypeSeq() {
|
2016-04-23 17:05:33 +02:00
|
|
|
return this.entry.autoType.defaultSequence || this.group.getEffectiveAutoTypeSeq();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setEnableAutoType(enabled) {
|
2016-04-23 16:50:40 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.autoType.enabled = enabled;
|
|
|
|
this._buildAutoType();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setAutoTypeObfuscation(enabled) {
|
2016-04-23 16:50:40 +02:00
|
|
|
this._entryModified();
|
2019-08-16 23:05:39 +02:00
|
|
|
this.entry.autoType.obfuscation = enabled
|
|
|
|
? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard
|
|
|
|
: kdbxweb.Consts.AutoTypeObfuscationOptions.None;
|
2016-04-23 16:50:40 +02:00
|
|
|
this._buildAutoType();
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
setAutoTypeSeq(seq) {
|
2016-04-23 16:50:40 +02:00
|
|
|
this._entryModified();
|
|
|
|
this.entry.autoType.defaultSequence = seq || undefined;
|
|
|
|
this._buildAutoType();
|
2016-06-04 14:47:56 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getGroupPath() {
|
2017-01-31 07:50:28 +01:00
|
|
|
let group = this.group;
|
|
|
|
const groupPath = [];
|
2016-06-04 14:47:56 +02:00
|
|
|
while (group) {
|
|
|
|
groupPath.unshift(group.get('title'));
|
|
|
|
group = group.parentGroup;
|
|
|
|
}
|
|
|
|
return groupPath;
|
2016-08-21 19:39:02 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
cloneEntry(nameSuffix) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const newEntry = EntryModel.newEntry(this.group, this.file);
|
2017-06-03 16:14:03 +02:00
|
|
|
const uuid = newEntry.entry.uuid;
|
2016-08-21 19:39:02 +02:00
|
|
|
newEntry.entry.copyFrom(this.entry);
|
2017-05-24 23:02:19 +02:00
|
|
|
newEntry.entry.uuid = uuid;
|
2016-08-21 19:39:02 +02:00
|
|
|
newEntry.entry.times.update();
|
2017-11-06 18:59:59 +01:00
|
|
|
newEntry.entry.times.creationTime = newEntry.entry.times.lastModTime;
|
2016-08-21 19:39:02 +02:00
|
|
|
newEntry.entry.fields.Title = this.title + nameSuffix;
|
|
|
|
newEntry._fillByEntry();
|
|
|
|
this.file.reload();
|
|
|
|
return newEntry;
|
2017-05-02 21:22:08 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
copyFromTemplate(templateEntry) {
|
2017-05-24 23:02:19 +02:00
|
|
|
const uuid = this.entry.uuid;
|
2017-05-02 21:22:08 +02:00
|
|
|
this.entry.copyFrom(templateEntry.entry);
|
2017-05-24 23:02:19 +02:00
|
|
|
this.entry.uuid = uuid;
|
2017-11-06 18:59:59 +01:00
|
|
|
this.entry.times.update();
|
|
|
|
this.entry.times.creationTime = this.entry.times.lastModTime;
|
2017-05-02 21:22:08 +02:00
|
|
|
this.entry.fields.Title = '';
|
|
|
|
this._fillByEntry();
|
2019-01-04 09:05:44 +01:00
|
|
|
},
|
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
getRank(filter) {
|
|
|
|
const searchString = filter.textLower;
|
|
|
|
|
2019-03-15 10:30:00 +01:00
|
|
|
if (!searchString) {
|
2019-03-15 19:19:53 +01:00
|
|
|
// no search string given, so rank all items the same
|
2019-03-15 10:30:00 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
const checkProtectedFields = filter.advanced && filter.advanced.protect;
|
2019-01-04 09:05:44 +01:00
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
const fieldWeights = {
|
|
|
|
'Title': 10,
|
|
|
|
'URL': 8,
|
|
|
|
'UserName': 5,
|
|
|
|
'Notes': 2
|
|
|
|
};
|
2019-01-04 09:05:44 +01:00
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
const defaultFieldWeight = 2;
|
2019-01-04 09:05:44 +01:00
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
const allFields = Object.keys(fieldWeights).concat(Object.keys(this.fields));
|
2019-01-04 09:05:44 +01:00
|
|
|
|
2019-08-18 16:14:47 +02:00
|
|
|
return allFields.reduce((rank, fieldName) => {
|
|
|
|
const val = this.entry.fields[fieldName];
|
|
|
|
if (!val) {
|
|
|
|
return rank;
|
|
|
|
}
|
|
|
|
if (val.isProtected && (!checkProtectedFields || !val.length)) {
|
|
|
|
return rank;
|
|
|
|
}
|
|
|
|
const stringRank = Ranking.getStringRank(searchString, val);
|
|
|
|
const fieldWeight = fieldWeights[fieldName] || defaultFieldWeight;
|
|
|
|
return rank + stringRank * fieldWeight;
|
|
|
|
}, 0);
|
2019-09-14 17:06:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
getHtml() {
|
|
|
|
return KdbxToHtml.entryToHtml(this.file.db, this.entry);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
EntryModel.fromEntry = function(entry, group, file) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const model = new EntryModel();
|
2015-10-17 23:49:24 +02:00
|
|
|
model.setEntry(entry, group, file);
|
|
|
|
return model;
|
|
|
|
};
|
|
|
|
|
|
|
|
EntryModel.newEntry = function(group, file) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const model = new EntryModel();
|
|
|
|
const entry = file.db.createEntry(group.group);
|
2015-10-17 23:49:24 +02:00
|
|
|
model.setEntry(entry, group, file);
|
|
|
|
model.entry.times.update();
|
|
|
|
model.unsaved = true;
|
2015-10-27 22:07:48 +01:00
|
|
|
model.isJustCreated = true;
|
2016-06-05 16:49:00 +02:00
|
|
|
model.canBeDeleted = true;
|
2015-10-27 20:40:34 +01:00
|
|
|
group.addEntry(model);
|
2015-10-17 23:49:24 +02:00
|
|
|
file.setModified();
|
|
|
|
return model;
|
|
|
|
};
|
|
|
|
|
2019-09-15 14:16:32 +02:00
|
|
|
export { EntryModel };
|