keeweb/app/scripts/models/app-model.js

1235 lines
44 KiB
JavaScript
Raw Normal View History

2019-09-16 22:57:56 +02:00
import { Events } from 'framework/events';
2019-09-15 14:16:32 +02:00
import { Storage } from 'storage';
2019-11-03 11:24:33 +01:00
import { SearchResultCollection } from 'collections/search-result-collection';
2019-09-15 14:16:32 +02:00
import { FileCollection } from 'collections/file-collection';
import { FileInfoCollection } from 'collections/file-info-collection';
2019-10-26 22:56:36 +02:00
import { RuntimeInfo } from 'const/runtime-info';
2019-09-15 14:16:32 +02:00
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model';
import { EntryModel } from 'models/entry-model';
import { FileInfoModel } from 'models/file-info-model';
import { FileModel } from 'models/file-model';
import { GroupModel } from 'models/group-model';
2020-04-15 16:50:01 +02:00
import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
2019-09-15 14:16:32 +02:00
import { MenuModel } from 'models/menu/menu-model';
import { PluginManager } from 'plugins/plugin-manager';
import { Features } from 'util/features';
import { DateFormat } from 'util/formatting/date-format';
import { UrlFormat } from 'util/formatting/url-format';
import { IdGenerator } from 'util/generators/id-generator';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
2019-09-18 07:08:23 +02:00
import { noop } from 'util/fn';
2019-09-18 20:42:17 +02:00
import debounce from 'lodash/debounce';
2019-09-15 14:16:32 +02:00
import 'util/kdbxweb/protected-value-ex';
2015-10-17 23:49:24 +02:00
2019-09-17 19:58:39 +02:00
class AppModel {
tags = [];
files = new FileCollection();
2019-09-17 21:39:06 +02:00
fileInfos = FileInfoCollection;
2019-09-17 19:58:39 +02:00
menu = new MenuModel();
filter = {};
sort = 'title';
settings = AppSettingsModel;
activeEntryId = null;
isBeta = RuntimeInfo.beta;
advancedSearch = null;
2015-10-17 23:49:24 +02:00
2019-09-17 19:58:39 +02:00
constructor() {
Events.on('refresh', this.refresh.bind(this));
Events.on('set-filter', this.setFilter.bind(this));
Events.on('add-filter', this.addFilter.bind(this));
Events.on('set-sort', this.setSort.bind(this));
Events.on('empty-trash', this.emptyTrash.bind(this));
Events.on('select-entry', this.selectEntry.bind(this));
Events.on('unset-keyfile', this.unsetKeyFile.bind(this));
2015-12-12 09:53:50 +01:00
this.appLogger = new Logger('app');
2020-05-05 16:34:39 +02:00
AppModel.instance = this;
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
loadConfig(configLocation) {
2017-05-16 21:26:25 +02:00
return new Promise((resolve, reject) => {
2017-08-31 19:08:39 +02:00
this.ensureCanLoadConfig(configLocation);
2017-05-16 21:26:25 +02:00
this.appLogger.debug('Loading config from', configLocation);
const ts = this.appLogger.ts();
const xhr = new XMLHttpRequest();
xhr.open('GET', configLocation);
xhr.responseType = 'json';
xhr.send();
xhr.addEventListener('load', () => {
let response = xhr.response;
if (!response) {
const errorDesc = xhr.statusText === 'OK' ? 'Malformed JSON' : xhr.statusText;
this.appLogger.error('Error loading app config', errorDesc);
return reject('Error loading app config');
2016-09-21 18:44:41 +02:00
}
2017-05-16 21:26:25 +02:00
if (typeof response === 'string') {
try {
response = JSON.parse(response);
} catch (e) {
this.appLogger.error('Error parsing response', e, response);
return reject('Error parsing response');
}
}
if (!response.settings) {
this.appLogger.error('Invalid app config, no settings section', response);
return reject('Invalid app config, no settings section');
}
2019-08-18 08:05:38 +02:00
this.appLogger.info(
'Loaded app config from',
configLocation,
this.appLogger.ts(ts)
);
2017-05-16 21:26:25 +02:00
resolve(response);
});
xhr.addEventListener('error', () => {
this.appLogger.error('Error loading app config', xhr.statusText, xhr.status);
reject('Error loading app config');
});
}).then(config => {
return this.applyUserConfig(config);
2016-06-16 19:00:24 +02:00
});
2019-09-17 19:58:39 +02:00
}
2016-06-16 19:00:24 +02:00
2017-08-31 19:08:39 +02:00
ensureCanLoadConfig(url) {
2019-09-15 08:11:11 +02:00
if (!Features.isSelfHosted) {
2017-08-31 19:08:39 +02:00
throw 'Configs are supported only in self-hosted installations';
}
const link = document.createElement('a');
link.href = url;
const isExternal = link.host && link.host !== location.host;
if (isExternal) {
throw 'Loading config from this location is not allowed';
}
2019-09-17 19:58:39 +02:00
}
2017-08-31 19:08:39 +02:00
applyUserConfig(config) {
this.settings.set(config.settings);
2016-07-31 09:47:56 +02:00
if (config.files) {
2017-02-07 19:17:34 +01:00
if (config.showOnlyFilesFromConfig) {
2019-09-17 21:39:06 +02:00
this.fileInfos.length = 0;
2017-02-07 19:17:34 +01:00
}
2016-07-31 09:47:56 +02:00
config.files
2019-08-16 23:05:39 +02:00
.filter(
file =>
file &&
file.storage &&
file.name &&
file.path &&
!this.fileInfos.getMatch(file.storage, file.name, file.path)
)
.map(
file =>
new FileInfoModel({
id: IdGenerator.uuid(),
name: file.name,
storage: file.storage,
path: file.path,
opts: file.options
})
)
2016-07-31 09:47:56 +02:00
.reverse()
.forEach(fi => this.fileInfos.unshift(fi));
}
2017-05-16 21:26:25 +02:00
if (config.plugins) {
2019-08-16 23:05:39 +02:00
const pluginsPromises = config.plugins.map(plugin =>
2019-09-16 22:01:59 +02:00
PluginManager.installIfNew(plugin.url, plugin.manifest, true)
2019-08-16 23:05:39 +02:00
);
2018-12-20 20:58:10 +01:00
return Promise.all(pluginsPromises).then(() => {
this.settings.set(config.settings);
});
2017-05-16 21:26:25 +02:00
}
if (config.advancedSearch) {
this.advancedSearch = config.advancedSearch;
2019-08-16 23:05:39 +02:00
this.addFilter({ advanced: this.advancedSearch });
}
2019-09-17 19:58:39 +02:00
}
2019-08-18 10:17:09 +02:00
addFile(file) {
2016-06-05 16:49:00 +02:00
if (this.files.get(file.id)) {
2015-12-02 21:50:31 +01:00
return false;
}
2019-09-17 21:39:06 +02:00
this.files.push(file);
2019-09-18 23:37:57 +02:00
for (const group of file.groups) {
2015-12-02 21:50:31 +01:00
this.menu.groupsSection.addItem(group);
2019-09-18 23:37:57 +02:00
}
2015-12-07 20:07:56 +01:00
this._addTags(file);
2015-10-17 23:49:24 +02:00
this._tagsChanged();
this.menu.filesSection.addItem({
icon: 'lock',
2019-09-17 21:39:06 +02:00
title: file.name,
2015-10-17 23:49:24 +02:00
page: 'file',
2019-08-18 10:17:09 +02:00
file
2015-10-17 23:49:24 +02:00
});
this.refresh();
2019-09-17 19:58:39 +02:00
file.on('reload', this.reloadFile.bind(this));
2019-09-28 19:18:24 +02:00
file.on('change', () => Events.emit('file-changed', file));
2020-05-05 21:26:41 +02:00
file.on('ejected', () => this.closeFile(file));
2015-12-02 21:50:31 +01:00
return true;
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
reloadFile(file) {
2019-09-18 23:37:57 +02:00
this.menu.groupsSection.replaceByFile(file, file.groups[0]);
2015-12-05 14:04:09 +01:00
this.updateTags();
2019-09-17 19:58:39 +02:00
}
2015-12-05 14:04:09 +01:00
2019-08-18 10:17:09 +02:00
_addTags(file) {
2017-01-31 07:50:28 +01:00
const tagsHash = {};
2016-07-17 13:30:38 +02:00
this.tags.forEach(tag => {
2015-10-17 23:49:24 +02:00
tagsHash[tag.toLowerCase()] = true;
});
2016-07-17 13:30:38 +02:00
file.forEachEntry({}, entry => {
2019-09-17 22:17:40 +02:00
for (const tag of entry.tags) {
2015-10-17 23:49:24 +02:00
if (!tagsHash[tag.toLowerCase()]) {
tagsHash[tag.toLowerCase()] = true;
2016-07-17 13:30:38 +02:00
this.tags.push(tag);
2015-10-17 23:49:24 +02:00
}
2019-09-17 22:17:40 +02:00
}
2015-12-07 20:07:56 +01:00
});
2015-10-17 23:49:24 +02:00
this.tags.sort();
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
_tagsChanged() {
2015-10-17 23:49:24 +02:00
if (this.tags.length) {
2019-09-18 23:37:57 +02:00
this.menu.tagsSection.scrollable = true;
2019-08-16 23:05:39 +02:00
this.menu.tagsSection.setItems(
this.tags.map(tag => {
2019-08-18 08:05:38 +02:00
return {
title: tag,
icon: 'tag',
filterKey: 'tag',
filterValue: tag,
editable: true
};
2019-08-16 23:05:39 +02:00
})
);
2015-10-17 23:49:24 +02:00
} else {
2019-09-18 23:37:57 +02:00
this.menu.tagsSection.scrollable = false;
2015-10-17 23:49:24 +02:00
this.menu.tagsSection.removeAllItems();
}
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
updateTags() {
2017-01-31 07:50:28 +01:00
const oldTags = this.tags.slice();
2015-10-17 23:49:24 +02:00
this.tags.splice(0, this.tags.length);
2019-09-18 20:42:17 +02:00
for (const file of this.files) {
2015-12-07 20:07:56 +01:00
this._addTags(file);
2019-09-18 20:42:17 +02:00
}
if (oldTags.join(',') !== this.tags.join(',')) {
2015-10-17 23:49:24 +02:00
this._tagsChanged();
}
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
renameTag(from, to) {
2020-04-15 16:50:01 +02:00
this.files.forEach(file => file.renameTag && file.renameTag(from, to));
2016-04-17 22:02:39 +02:00
this.updateTags();
2019-09-17 19:58:39 +02:00
}
2016-04-17 22:02:39 +02:00
2019-08-18 10:17:09 +02:00
closeAllFiles() {
2019-09-17 21:39:06 +02:00
for (const file of this.files) {
2016-02-06 11:59:57 +01:00
file.close();
2016-07-17 13:30:38 +02:00
this.fileClosed(file);
2019-09-17 21:39:06 +02:00
}
this.files.length = 0;
2015-10-17 23:49:24 +02:00
this.menu.groupsSection.removeAllItems();
2019-09-18 23:37:57 +02:00
this.menu.tagsSection.scrollable = false;
2015-10-17 23:49:24 +02:00
this.menu.tagsSection.removeAllItems();
this.menu.filesSection.removeAllItems();
this.tags.splice(0, this.tags.length);
this.filter = {};
2016-07-16 12:01:19 +02:00
this.menu.select({ item: this.menu.allItemsItem });
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
closeFile(file) {
2016-02-06 11:59:57 +01:00
file.close();
this.fileClosed(file);
2015-11-07 21:37:54 +01:00
this.files.remove(file);
2015-12-13 12:19:55 +01:00
this.updateTags();
2015-11-07 21:37:54 +01:00
this.menu.groupsSection.removeByFile(file);
this.menu.filesSection.removeByFile(file);
2019-09-18 23:37:57 +02:00
this.menu.select({ item: this.menu.allItemsSection.items[0] });
2019-09-17 19:58:39 +02:00
}
2015-11-07 21:37:54 +01:00
2019-08-18 10:17:09 +02:00
emptyTrash() {
2020-04-15 16:50:01 +02:00
this.files.forEach(file => file.emptyTrash && file.emptyTrash());
2015-11-08 22:25:00 +01:00
this.refresh();
2019-09-17 19:58:39 +02:00
}
2015-11-08 22:25:00 +01:00
2019-08-18 10:17:09 +02:00
setFilter(filter) {
2019-03-09 12:13:41 +01:00
this.filter = this.prepareFilter(filter);
2019-09-17 19:50:42 +02:00
this.filter.subGroups = this.settings.expandGroups;
if (!this.filter.advanced && this.advancedSearch) {
this.filter.advanced = this.advancedSearch;
}
2017-01-31 07:50:28 +01:00
const entries = this.getEntries();
if (!this.activeEntryId || !entries.get(this.activeEntryId)) {
2019-09-18 23:37:57 +02:00
const firstEntry = entries[0];
this.activeEntryId = firstEntry ? firstEntry.id : null;
}
2019-09-16 22:57:56 +02:00
Events.emit('filter', { filter: this.filter, sort: this.sort, entries });
Events.emit('entry-selected', entries.get(this.activeEntryId));
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
refresh() {
2015-10-17 23:49:24 +02:00
this.setFilter(this.filter);
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
selectEntry(entry) {
2016-08-21 19:39:02 +02:00
this.activeEntryId = entry.id;
this.refresh();
2019-09-17 19:58:39 +02:00
}
2016-08-21 19:39:02 +02:00
2019-08-18 10:17:09 +02:00
addFilter(filter) {
2019-09-17 22:17:40 +02:00
this.setFilter(Object.assign(this.filter, filter));
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
setSort(sort) {
2015-10-17 23:49:24 +02:00
this.sort = sort;
this.setFilter(this.filter);
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
getEntries() {
2017-01-31 07:50:28 +01:00
const entries = this.getEntriesByFilter(this.filter);
2019-02-05 19:08:06 +01:00
entries.sortEntries(this.sort, this.filter);
2016-07-31 20:38:08 +02:00
if (this.filter.trash) {
2015-11-08 22:25:00 +01:00
this.addTrashGroups(entries);
}
2015-10-17 23:49:24 +02:00
return entries;
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-08-18 10:17:09 +02:00
getEntriesByFilter(filter) {
const preparedFilter = this.prepareFilter(filter);
2019-11-03 11:24:33 +01:00
const entries = new SearchResultCollection();
const devicesToMatchOtpEntries = this.files.filter(file => file.external);
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
this.files
.filter(file => !file.external)
.forEach(file => {
file.forEachEntry(preparedFilter, entry => {
if (matchedOtpEntrySet) {
for (const device of devicesToMatchOtpEntries) {
const matchingEntry = device.getMatchingEntry(entry);
if (matchingEntry) {
matchedOtpEntrySet.add(matchingEntry);
}
}
}
entries.push(entry);
});
});
if (devicesToMatchOtpEntries.length) {
for (const device of devicesToMatchOtpEntries) {
device.forEachEntry(preparedFilter, entry => {
if (!matchedOtpEntrySet || !matchedOtpEntrySet.has(entry)) {
entries.push(entry);
}
});
}
}
2016-07-24 22:57:12 +02:00
return entries;
2019-09-17 19:58:39 +02:00
}
2016-07-24 22:57:12 +02:00
2019-08-18 10:17:09 +02:00
addTrashGroups(collection) {
2016-07-17 13:30:38 +02:00
this.files.forEach(file => {
2020-04-15 16:50:01 +02:00
const trashGroup = file.getTrashGroup && file.getTrashGroup();
2015-11-08 22:25:00 +01:00
if (trashGroup) {
2016-07-17 13:30:38 +02:00
trashGroup.getOwnSubGroups().forEach(group => {
2015-11-08 22:25:00 +01:00
collection.unshift(GroupModel.fromGroup(group, file, trashGroup));
});
}
});
2019-09-17 19:58:39 +02:00
}
2015-11-08 22:25:00 +01:00
2019-08-18 10:17:09 +02:00
prepareFilter(filter) {
2019-09-17 23:44:17 +02:00
filter = { ...filter };
2019-09-30 20:31:12 +02:00
2019-03-30 23:05:48 +01:00
filter.textLower = filter.text ? filter.text.toLowerCase() : '';
2019-09-30 20:31:12 +02:00
filter.textParts = null;
filter.textLowerParts = null;
const exact = filter.advanced && filter.advanced.exact;
if (!exact && filter.text) {
const textParts = filter.text.split(/\s+/).filter(s => s);
if (textParts.length) {
filter.textParts = textParts;
filter.textLowerParts = filter.textLower.split(/\s+/).filter(s => s);
}
}
2019-03-30 23:05:48 +01:00
filter.tagLower = filter.tag ? filter.tag.toLowerCase() : '';
2019-09-30 20:31:12 +02:00
2015-10-17 23:49:24 +02:00
return filter;
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2020-05-09 23:55:57 +02:00
getFirstSelectedGroupForCreation() {
2017-01-31 07:50:28 +01:00
const selGroupId = this.filter.group;
2019-08-16 23:05:39 +02:00
let file, group;
2015-10-17 23:49:24 +02:00
if (selGroupId) {
this.files.some(f => {
file = f;
2015-10-17 23:49:24 +02:00
group = f.getGroup(selGroupId);
return group;
2016-07-17 13:30:38 +02:00
});
2015-10-17 23:49:24 +02:00
}
if (!group) {
2020-05-09 23:55:57 +02:00
file = this.files.find(f => f.active && !f.readOnly);
2019-09-18 23:37:57 +02:00
group = file.groups[0];
2015-10-17 23:49:24 +02:00
}
2019-08-18 10:17:09 +02:00
return { group, file };
2019-09-17 19:58:39 +02:00
}
2015-10-31 20:09:32 +01:00
2019-08-18 10:17:09 +02:00
completeUserNames(part) {
2017-01-31 07:50:28 +01:00
const userNames = {};
2016-07-17 13:30:38 +02:00
this.files.forEach(file => {
2019-08-18 08:05:38 +02:00
file.forEachEntry(
{ text: part, textLower: part.toLowerCase(), advanced: { user: true } },
entry => {
const userName = entry.user;
if (userName) {
userNames[userName] = (userNames[userName] || 0) + 1;
}
2016-02-28 12:16:05 +01:00
}
2019-08-18 08:05:38 +02:00
);
2016-02-28 12:16:05 +01:00
});
2019-09-18 20:42:17 +02:00
const matches = Object.entries(userNames);
2016-07-17 13:30:38 +02:00
matches.sort((x, y) => y[1] - x[1]);
2017-01-31 07:50:28 +01:00
const maxResults = 5;
2016-02-28 12:16:05 +01:00
if (matches.length > maxResults) {
matches.length = maxResults;
}
2016-07-17 13:30:38 +02:00
return matches.map(m => m[0]);
2019-09-17 19:58:39 +02:00
}
2016-02-28 12:16:05 +01:00
2019-08-18 10:17:09 +02:00
getEntryTemplates() {
2017-05-02 21:22:08 +02:00
const entryTemplates = [];
this.files.forEach(file => {
file.forEachEntryTemplate?.(entry => {
entryTemplates.push({ file, entry });
});
2017-05-02 21:22:08 +02:00
});
return entryTemplates;
2019-09-17 19:58:39 +02:00
}
2017-05-02 21:22:08 +02:00
canCreateEntries() {
return this.files.some(f => f.active && !f.readOnly);
}
2019-08-18 10:17:09 +02:00
createNewEntry(args) {
2020-05-09 23:55:57 +02:00
const sel = this.getFirstSelectedGroupForCreation();
2017-05-02 21:22:08 +02:00
if (args && args.template) {
if (sel.file !== args.template.file) {
sel.file = args.template.file;
2019-09-18 23:37:57 +02:00
sel.group = args.template.file.groups[0];
2017-05-02 21:22:08 +02:00
}
const templateEntry = args.template.entry;
const newEntry = EntryModel.newEntry(sel.group, sel.file);
newEntry.copyFromTemplate(templateEntry);
return newEntry;
} else {
return EntryModel.newEntry(sel.group, sel.file, {
tag: this.filter.tag
});
2017-05-02 21:22:08 +02:00
}
2019-09-17 19:58:39 +02:00
}
2015-10-31 20:09:32 +01:00
2019-08-18 10:17:09 +02:00
createNewGroup() {
2020-05-09 23:55:57 +02:00
const sel = this.getFirstSelectedGroupForCreation();
2015-10-31 20:09:32 +01:00
return GroupModel.newGroup(sel.group, sel.file);
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-08-18 10:17:09 +02:00
createNewTemplateEntry() {
2020-05-09 23:55:57 +02:00
const file = this.getFirstSelectedGroupForCreation().file;
2017-05-03 20:44:16 +02:00
const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup();
return EntryModel.newEntry(group, file);
2019-09-17 19:58:39 +02:00
}
2017-05-03 20:44:16 +02:00
2019-08-18 10:17:09 +02:00
createDemoFile() {
2015-12-06 21:32:41 +01:00
if (!this.files.getByName('Demo')) {
2017-01-31 07:50:28 +01:00
const demoFile = new FileModel({ id: IdGenerator.uuid() });
2016-07-17 13:30:38 +02:00
demoFile.openDemo(() => {
this.addFile(demoFile);
2015-12-06 21:32:41 +01:00
});
return true;
} else {
return false;
}
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-09-25 23:24:38 +02:00
createNewFile(name) {
if (!name) {
for (let i = 0; ; i++) {
name = Locale.openNewFile + (i || '');
2019-09-25 23:24:38 +02:00
if (!this.files.getByName(name) && !this.fileInfos.getByName(name)) {
break;
}
2015-12-06 21:32:41 +01:00
}
}
2017-01-31 07:50:28 +01:00
const newFile = new FileModel({ id: IdGenerator.uuid() });
2015-12-06 21:32:41 +01:00
newFile.create(name);
this.addFile(newFile);
2019-09-25 23:24:38 +02:00
return newFile;
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-08-18 10:17:09 +02:00
openFile(params, callback) {
2017-01-31 07:50:28 +01:00
const logger = new Logger('open', params.name);
2015-12-12 09:53:50 +01:00
logger.info('File open request');
2019-08-16 23:05:39 +02:00
const fileInfo = params.id
? this.fileInfos.get(params.id)
: this.fileInfos.getMatch(params.storage, params.name, params.path);
2019-09-17 21:39:06 +02:00
if (!params.opts && fileInfo && fileInfo.opts) {
params.opts = fileInfo.opts;
2016-03-12 21:08:49 +01:00
}
2019-09-17 21:39:06 +02:00
if (fileInfo && fileInfo.modified) {
2015-12-12 09:53:50 +01:00
logger.info('Open file from cache because it is modified');
2019-08-16 23:05:39 +02:00
this.openFileFromCache(
params,
(err, file) => {
if (!err && file) {
logger.info('Sync just opened modified file');
2019-09-17 23:44:17 +02:00
setTimeout(() => this.syncFile(file), 0);
2019-08-16 23:05:39 +02:00
}
callback(err);
},
fileInfo
);
2015-12-07 20:07:56 +01:00
} else if (params.fileData) {
2015-12-12 09:53:50 +01:00
logger.info('Open file from supplied content');
2017-01-31 07:50:28 +01:00
const needSaveToCache = params.storage !== 'file';
this.openFileWithData(params, callback, fileInfo, params.fileData, needSaveToCache);
2015-12-12 16:43:43 +01:00
} else if (!params.storage) {
logger.info('Open file from cache as main storage');
this.openFileFromCache(params, callback, fileInfo);
2019-08-16 23:05:39 +02:00
} else if (
fileInfo &&
2019-09-17 21:39:06 +02:00
fileInfo.openDate &&
fileInfo.rev === params.rev &&
fileInfo.storage !== 'file'
2019-08-16 23:05:39 +02:00
) {
2015-12-12 09:53:50 +01:00
logger.info('Open file from cache because it is latest');
this.openFileFromCache(
params,
(err, file) => {
2015-12-08 20:18:35 +01:00
if (err) {
if (err.name === 'KdbxError') {
return callback(err);
}
2019-08-18 08:05:38 +02:00
logger.info(
'Error loading file from cache, trying to open from storage',
2019-08-18 08:05:38 +02:00
err
);
this.openFileFromStorage(params, callback, fileInfo, logger, true);
2015-12-07 20:07:56 +01:00
} else {
callback(err, file);
2015-12-07 20:07:56 +01:00
}
},
fileInfo
);
2019-09-17 21:39:06 +02:00
} else if (!fileInfo || !fileInfo.openDate || params.storage === 'file') {
this.openFileFromStorage(params, callback, fileInfo, logger);
} else {
logger.info('Open file from cache, will sync after load', params.storage);
2019-08-16 23:05:39 +02:00
this.openFileFromCache(
params,
(err, file) => {
if (!err && file) {
logger.info('Sync just opened file');
2019-09-17 23:44:17 +02:00
setTimeout(() => this.syncFile(file), 0);
callback(err);
} else {
if (err.name === 'KdbxError') {
return callback(err);
}
logger.info(
'Error loading file from cache, trying to open from storage',
err
);
this.openFileFromStorage(params, callback, fileInfo, logger, true);
2019-08-16 23:05:39 +02:00
}
},
fileInfo
);
2015-12-07 20:07:56 +01:00
}
2019-09-17 19:58:39 +02:00
}
2015-12-07 20:07:56 +01:00
2019-08-18 10:17:09 +02:00
openFileFromCache(params, callback, fileInfo) {
2016-07-17 13:30:38 +02:00
Storage.cache.load(fileInfo.id, null, (err, data) => {
2019-08-17 19:30:42 +02:00
if (!data) {
err = Locale.openFileNoCacheError;
}
2015-12-12 16:43:43 +01:00
new Logger('open', params.name).info('Loaded file from cache', err);
2015-12-07 20:07:56 +01:00
if (err) {
callback(err);
} else {
2016-07-17 13:30:38 +02:00
this.openFileWithData(params, callback, fileInfo, data);
2015-12-07 20:07:56 +01:00
}
});
2019-09-17 19:58:39 +02:00
}
2015-12-07 20:07:56 +01:00
openFileFromStorage(params, callback, fileInfo, logger, noCache) {
logger.info('Open file from storage', params.storage);
const storage = Storage[params.storage];
const storageLoad = () => {
logger.info('Load from storage');
storage.load(params.path, params.opts, (err, data, stat) => {
if (err) {
2019-09-17 21:39:06 +02:00
if (fileInfo && fileInfo.openDate) {
logger.info('Open file from cache because of storage load error', err);
this.openFileFromCache(params, callback, fileInfo);
} else {
logger.info('Storage load error', err);
callback(err);
}
} else {
logger.info('Open file from content loaded from storage');
params.fileData = data;
params.rev = (stat && stat.rev) || null;
const needSaveToCache = storage.name !== 'file';
this.openFileWithData(params, callback, fileInfo, data, needSaveToCache);
}
});
};
2019-09-17 21:39:06 +02:00
const cacheRev = (fileInfo && fileInfo.rev) || null;
if (cacheRev && storage.stat) {
logger.info('Stat file');
storage.stat(params.path, params.opts, (err, stat) => {
if (
!noCache &&
fileInfo &&
storage.name !== 'file' &&
(err || (stat && stat.rev === cacheRev))
) {
logger.info(
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
err
);
this.openFileFromCache(params, callback, fileInfo);
} else if (stat) {
logger.info(
'Open file from storage (' + stat.rev + ', local ' + cacheRev + ')'
);
storageLoad();
} else {
logger.info('Stat error', err);
callback(err);
}
});
} else {
storageLoad();
}
2019-09-17 19:58:39 +02:00
}
2019-08-18 10:17:09 +02:00
openFileWithData(params, callback, fileInfo, data, updateCacheOnSuccess) {
2017-01-31 07:50:28 +01:00
const logger = new Logger('open', params.name);
2017-06-01 21:35:56 +02:00
let needLoadKeyFile = false;
2019-09-17 21:39:06 +02:00
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
params.keyFileName = fileInfo.keyFileName;
2019-09-17 19:50:42 +02:00
if (this.settings.rememberKeyFiles === 'data') {
2019-09-17 21:39:06 +02:00
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.keyFileHash);
} else if (this.settings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
params.keyFilePath = fileInfo.keyFilePath;
if (Storage.file.enabled) {
2017-06-01 21:35:56 +02:00
needLoadKeyFile = true;
}
}
2017-12-07 18:46:46 +01:00
} else if (params.keyFilePath && !params.keyFileData && !fileInfo) {
needLoadKeyFile = true;
2016-02-14 12:20:21 +01:00
}
2017-01-31 07:50:28 +01:00
const file = new FileModel({
id: fileInfo ? fileInfo.id : IdGenerator.uuid(),
2015-12-06 21:32:41 +01:00
name: params.name,
storage: params.storage,
path: params.path,
2016-08-16 22:11:54 +02:00
keyFileName: params.keyFileName,
keyFilePath: params.keyFilePath,
2019-09-17 21:39:06 +02:00
backup: (fileInfo && fileInfo.backup) || null,
fingerprint: (fileInfo && fileInfo.fingerprint) || null
2015-12-06 21:32:41 +01:00
});
2017-06-01 21:35:56 +02:00
const openComplete = err => {
2015-12-07 20:07:56 +01:00
if (err) {
2015-12-06 21:32:41 +01:00
return callback(err);
}
2016-07-17 13:30:38 +02:00
if (this.files.get(file.id)) {
2015-12-07 20:07:56 +01:00
return callback('Duplicate file id');
}
2019-09-17 21:39:06 +02:00
if (fileInfo && fileInfo.modified) {
if (fileInfo.editState) {
2015-12-12 09:53:50 +01:00
logger.info('Loaded local edit state');
2019-09-17 21:39:06 +02:00
file.setLocalEditState(fileInfo.editState);
2015-12-10 22:31:47 +01:00
}
logger.info('Mark file as modified');
2019-09-17 21:39:06 +02:00
file.modified = true;
2015-12-11 21:51:16 +01:00
}
if (fileInfo) {
2019-09-17 21:39:06 +02:00
file.syncDate = fileInfo.syncDate;
2015-12-10 22:31:47 +01:00
}
2016-02-06 11:59:57 +01:00
if (updateCacheOnSuccess) {
2015-12-12 09:53:50 +01:00
logger.info('Save loaded file to cache');
Storage.cache.save(file.id, null, params.fileData);
2015-12-07 20:07:56 +01:00
}
2019-09-17 21:39:06 +02:00
const rev = params.rev || (fileInfo && fileInfo.rev);
2016-07-17 13:30:38 +02:00
this.setFileOpts(file, params.opts);
this.addToLastOpenFiles(file, rev);
this.addFile(file);
callback(null, file);
this.fileOpened(file, data, params);
2017-06-01 21:35:56 +02:00
};
const open = () => {
file.open(params.password, data, params.keyFileData, openComplete);
};
if (needLoadKeyFile) {
Storage.file.load(params.keyFilePath, {}, (err, data) => {
if (err) {
logger.info('Storage load error', err);
callback(err);
} else {
params.keyFileData = data;
open();
}
});
} else {
open();
}
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-08-18 10:17:09 +02:00
importFileWithXml(params, callback) {
2017-01-31 07:50:28 +01:00
const logger = new Logger('import', params.name);
logger.info('File import request with supplied xml');
2017-01-31 07:50:28 +01:00
const file = new FileModel({
id: IdGenerator.uuid(),
2016-03-01 04:29:20 +01:00
name: params.name,
storage: params.storage,
path: params.path
});
2016-07-17 13:30:38 +02:00
file.importWithXml(params.fileXml, err => {
logger.info('Import xml complete ' + (err ? 'with error' : ''), err);
2016-03-01 04:29:20 +01:00
if (err) {
return callback(err);
}
2016-07-17 13:30:38 +02:00
this.addFile(file);
this.fileOpened(file);
2015-12-06 21:32:41 +01:00
});
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-08-18 10:17:09 +02:00
addToLastOpenFiles(file, rev) {
2019-08-16 23:05:39 +02:00
this.appLogger.debug(
'Add last open file',
file.id,
2019-09-17 21:39:06 +02:00
file.name,
file.storage,
file.path,
2019-08-16 23:05:39 +02:00
rev
);
2017-01-31 07:50:28 +01:00
const dt = new Date();
const fileInfo = new FileInfoModel({
id: file.id,
2019-09-17 21:39:06 +02:00
name: file.name,
storage: file.storage,
path: file.path,
2016-03-19 13:43:50 +01:00
opts: this.getStoreOpts(file),
2019-09-17 21:39:06 +02:00
modified: file.modified,
2015-12-10 22:31:47 +01:00
editState: file.getLocalEditState(),
2019-08-18 10:17:09 +02:00
rev,
2019-09-17 21:39:06 +02:00
syncDate: file.syncDate || dt,
2016-08-20 10:51:26 +02:00
openDate: dt,
2019-09-17 21:39:06 +02:00
backup: file.backup,
fingerprint: file.fingerprint
2015-12-06 21:32:41 +01:00
});
2019-09-17 19:50:42 +02:00
switch (this.settings.rememberKeyFiles) {
case 'data':
fileInfo.set({
2019-09-17 21:39:06 +02:00
keyFileName: file.keyFileName || null,
keyFileHash: file.getKeyFileHash()
});
break;
case 'path':
fileInfo.set({
2019-09-17 21:39:06 +02:00
keyFileName: file.keyFileName || null,
keyFilePath: file.keyFilePath || null
});
2016-02-14 12:20:21 +01:00
}
this.fileInfos.remove(file.id);
2015-12-06 23:13:29 +01:00
this.fileInfos.unshift(fileInfo);
2015-12-06 21:32:41 +01:00
this.fileInfos.save();
2019-09-17 19:58:39 +02:00
}
2015-12-06 21:32:41 +01:00
2019-08-18 10:17:09 +02:00
getStoreOpts(file) {
2019-09-17 21:39:06 +02:00
const opts = file.opts;
const storage = file.storage;
2016-07-17 13:30:38 +02:00
if (Storage[storage] && Storage[storage].fileOptsToStoreOpts && opts) {
2016-03-19 13:43:50 +01:00
return Storage[storage].fileOptsToStoreOpts(opts, file);
}
return null;
2019-09-17 19:58:39 +02:00
}
2016-03-19 13:43:50 +01:00
2019-08-18 10:17:09 +02:00
setFileOpts(file, opts) {
2019-09-17 21:39:06 +02:00
const storage = file.storage;
2016-07-17 13:30:38 +02:00
if (Storage[storage] && Storage[storage].storeOptsToFileOpts && opts) {
2019-09-17 21:39:06 +02:00
file.opts = Storage[storage].storeOptsToFileOpts(opts, file);
2016-03-19 13:43:50 +01:00
}
2019-09-17 19:58:39 +02:00
}
2016-03-19 13:43:50 +01:00
2019-08-18 10:17:09 +02:00
fileOpened(file, data, params) {
2019-09-17 21:39:06 +02:00
if (file.storage === 'file') {
2019-08-16 23:05:39 +02:00
Storage.file.watch(
2019-09-17 21:39:06 +02:00
file.path,
2019-09-18 20:42:17 +02:00
debounce(() => {
2019-08-16 23:05:39 +02:00
this.syncFile(file);
}, Timeouts.FileChangeSync)
);
2016-02-06 11:59:57 +01:00
}
2016-07-03 18:46:43 +02:00
if (file.isKeyChangePending(true)) {
2019-09-16 22:57:56 +02:00
Events.emit('key-change-pending', { file });
2016-07-03 18:46:43 +02:00
}
2019-09-17 21:39:06 +02:00
const backup = file.backup;
2016-08-21 18:46:44 +02:00
if (data && backup && backup.enabled && backup.pending) {
this.scheduleBackupFile(file, data);
}
2017-09-29 20:37:38 +02:00
if (params) {
this.saveFileFingerprint(file, params.password);
}
2019-09-17 19:58:39 +02:00
}
2016-02-06 11:59:57 +01:00
2019-08-18 10:17:09 +02:00
fileClosed(file) {
2019-09-17 21:39:06 +02:00
if (file.storage === 'file') {
Storage.file.unwatch(file.path);
2016-02-06 11:59:57 +01:00
}
2019-09-17 19:58:39 +02:00
}
2016-02-06 11:59:57 +01:00
2019-08-18 10:17:09 +02:00
removeFileInfo(id) {
2015-12-06 21:32:41 +01:00
Storage.cache.remove(id);
this.fileInfos.remove(id);
this.fileInfos.save();
2019-09-17 19:58:39 +02:00
}
2015-12-08 22:00:31 +01:00
2019-08-18 10:17:09 +02:00
getFileInfo(file) {
2019-08-16 23:05:39 +02:00
return (
this.fileInfos.get(file.id) ||
2019-09-17 21:39:06 +02:00
this.fileInfos.getMatch(file.storage, file.name, file.path)
2019-08-16 23:05:39 +02:00
);
2019-09-17 19:58:39 +02:00
}
2016-02-14 12:20:21 +01:00
2019-08-18 10:17:09 +02:00
syncFile(file, options, callback) {
2019-09-17 21:39:06 +02:00
if (file.demo) {
2015-12-11 21:51:16 +01:00
return callback && callback();
}
2019-09-17 21:39:06 +02:00
if (file.syncing) {
return callback && callback('Sync in progress');
}
if (!options) {
options = {};
2015-12-08 22:00:31 +01:00
}
2019-09-17 21:39:06 +02:00
const logger = new Logger('sync', file.name);
const storage = options.storage || file.storage;
let path = options.path || file.path;
const opts = options.opts || file.opts;
if (storage && Storage[storage].getPathForName && (!path || storage !== file.storage)) {
path = Storage[storage].getPathForName(file.name);
2015-12-12 16:43:43 +01:00
}
2019-09-17 23:44:17 +02:00
const optionsForLogging = { ...options };
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) {
2019-09-17 23:44:17 +02:00
optionsForLogging.opts = { ...optionsForLogging.opts };
optionsForLogging.opts.password = '***';
}
logger.info('Sync started', storage, path, optionsForLogging);
2017-01-31 07:50:28 +01:00
let fileInfo = this.getFileInfo(file);
2015-12-10 20:44:02 +01:00
if (!fileInfo) {
2015-12-12 09:53:50 +01:00
logger.info('Create new file info');
2017-01-31 07:50:28 +01:00
const dt = new Date();
2015-12-10 20:44:02 +01:00
fileInfo = new FileInfoModel({
id: IdGenerator.uuid(),
2019-09-17 21:39:06 +02:00
name: file.name,
storage: file.storage,
path: file.path,
2016-03-19 13:43:50 +01:00
opts: this.getStoreOpts(file),
2019-09-17 21:39:06 +02:00
modified: file.modified,
2015-12-10 20:44:02 +01:00
editState: null,
rev: null,
2015-12-11 21:51:16 +01:00
syncDate: dt,
2016-08-16 22:11:54 +02:00
openDate: dt,
2019-09-17 21:39:06 +02:00
backup: file.backup
2015-12-10 20:44:02 +01:00
});
}
2015-12-11 21:51:16 +01:00
file.setSyncProgress();
2017-01-31 07:50:28 +01:00
const complete = (err, savedToCache) => {
2019-08-16 23:05:39 +02:00
if (!err) {
savedToCache = true;
}
2016-02-06 11:59:57 +01:00
logger.info('Sync finished', err || 'no error');
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
fileInfo.set({
2019-09-17 21:39:06 +02:00
name: file.name,
2019-08-18 10:17:09 +02:00
storage,
path,
2016-09-02 18:39:58 +02:00
opts: this.getStoreOpts(file),
2019-09-17 21:39:06 +02:00
modified: file.modified,
2015-12-11 21:51:16 +01:00
editState: file.getLocalEditState(),
2019-09-17 21:39:06 +02:00
syncDate: file.syncDate
});
2019-09-17 19:50:42 +02:00
if (this.settings.rememberKeyFiles === 'data') {
2016-02-14 12:20:21 +01:00
fileInfo.set({
2019-09-17 21:39:06 +02:00
keyFileName: file.keyFileName || null,
2016-02-14 12:20:21 +01:00
keyFileHash: file.getKeyFileHash()
});
}
2016-09-02 18:39:58 +02:00
if (!this.fileInfos.get(fileInfo.id)) {
this.fileInfos.unshift(fileInfo);
}
2016-09-02 18:39:58 +02:00
this.fileInfos.save();
2019-08-16 23:05:39 +02:00
if (callback) {
callback(err);
}
};
2015-12-10 20:44:02 +01:00
if (!storage) {
2019-09-17 21:39:06 +02:00
if (!file.modified && fileInfo.id === file.id) {
2015-12-12 09:53:50 +01:00
logger.info('Local, not modified');
2015-12-10 22:31:47 +01:00
return complete();
2015-12-10 20:44:02 +01:00
}
2015-12-12 09:53:50 +01:00
logger.info('Local, save to cache');
2016-07-17 13:30:38 +02:00
file.getData((data, err) => {
2019-08-16 23:05:39 +02:00
if (err) {
return complete(err);
}
Storage.cache.save(fileInfo.id, null, data, err => {
2016-02-06 11:59:57 +01:00
logger.info('Saved to cache', err || 'no error');
2015-12-10 22:31:47 +01:00
complete(err);
2016-08-21 18:46:44 +02:00
if (!err) {
this.scheduleBackupFile(file, data);
}
2015-12-10 20:44:02 +01:00
});
});
} else {
2017-01-31 07:50:28 +01:00
const maxLoadLoops = 3;
let loadLoops = 0;
const loadFromStorageAndMerge = () => {
2015-12-10 20:44:02 +01:00
if (++loadLoops === maxLoadLoops) {
2015-12-12 16:43:43 +01:00
return complete('Too many load attempts');
2015-12-10 20:44:02 +01:00
}
2015-12-12 16:43:43 +01:00
logger.info('Load from storage, attempt ' + loadLoops);
2016-07-17 13:30:38 +02:00
Storage[storage].load(path, opts, (err, data, stat) => {
2016-02-06 11:59:57 +01:00
logger.info('Load from storage', stat, err || 'no error');
2019-08-16 23:05:39 +02:00
if (err) {
return complete(err);
}
file.mergeOrUpdate(data, options.remoteKey, err => {
2016-02-06 11:59:57 +01:00
logger.info('Merge complete', err || 'no error');
2016-09-02 18:39:58 +02:00
this.refresh();
if (err) {
if (err.code === 'InvalidKey') {
logger.info('Remote key changed, request to enter new key');
2019-09-16 22:57:56 +02:00
Events.emit('remote-key-changed', { file });
}
return complete(err);
}
2015-12-10 20:44:02 +01:00
if (stat && stat.rev) {
2015-12-12 16:43:43 +01:00
logger.info('Update rev in file info');
2019-09-17 21:39:06 +02:00
fileInfo.rev = stat.rev;
2015-12-10 20:44:02 +01:00
}
2019-09-17 21:39:06 +02:00
file.syncDate = new Date();
if (file.modified) {
logger.info('Updated sync date, saving modified file');
2015-12-10 22:31:47 +01:00
saveToCacheAndStorage();
2019-09-17 21:39:06 +02:00
} else if (file.dirty) {
2015-12-12 09:53:50 +01:00
logger.info('Saving not modified dirty file to cache');
2019-08-16 23:05:39 +02:00
Storage.cache.save(fileInfo.id, null, data, err => {
if (err) {
return complete(err);
}
2019-09-17 21:39:06 +02:00
file.dirty = false;
2015-12-12 09:53:50 +01:00
logger.info('Complete, remove dirty flag');
2015-12-11 21:51:16 +01:00
complete();
});
2015-12-10 20:44:02 +01:00
} else {
2015-12-12 09:53:50 +01:00
logger.info('Complete, no changes');
2015-12-10 22:31:47 +01:00
complete();
2015-12-10 20:44:02 +01:00
}
});
});
};
2019-08-16 23:05:39 +02:00
const saveToStorage = data => {
2015-12-12 09:53:50 +01:00
logger.info('Save data to storage');
2019-09-17 21:39:06 +02:00
const storageRev = fileInfo.storage === storage ? fileInfo.rev : undefined;
2019-08-16 23:05:39 +02:00
Storage[storage].save(
path,
opts,
data,
(err, stat) => {
if (err && err.revConflict) {
logger.info('Save rev conflict, reloading from storage');
loadFromStorageAndMerge();
} else if (err) {
logger.info('Error saving data to storage');
complete(err);
} else {
if (stat && stat.rev) {
logger.info('Update rev in file info');
2019-09-17 21:39:06 +02:00
fileInfo.rev = stat.rev;
2019-08-16 23:05:39 +02:00
}
if (stat && stat.path) {
logger.info('Update path in file info', stat.path);
2019-09-17 21:39:06 +02:00
file.path = stat.path;
fileInfo.path = stat.path;
2019-08-16 23:05:39 +02:00
path = stat.path;
}
2019-09-17 21:39:06 +02:00
file.syncDate = new Date();
2019-08-16 23:05:39 +02:00
logger.info('Save to storage complete, update sync date');
this.scheduleBackupFile(file, data);
complete();
2016-03-27 20:14:31 +02:00
}
2019-08-16 23:05:39 +02:00
},
storageRev
);
2015-12-10 22:31:47 +01:00
};
2017-05-02 21:22:08 +02:00
const saveToCacheAndStorage = () => {
logger.info('Getting file data for saving');
file.getData((data, err) => {
2019-08-16 23:05:39 +02:00
if (err) {
return complete(err);
}
2017-05-02 21:22:08 +02:00
if (storage === 'file') {
logger.info('Saving to file storage');
saveToStorage(data);
2019-09-17 21:39:06 +02:00
} else if (!file.dirty) {
2017-05-02 21:22:08 +02:00
logger.info('Saving to storage, skip cache because not dirty');
saveToStorage(data);
} else {
logger.info('Saving to cache');
2019-08-16 23:05:39 +02:00
Storage.cache.save(fileInfo.id, null, data, err => {
if (err) {
return complete(err);
}
2019-09-17 21:39:06 +02:00
file.dirty = false;
2017-05-02 21:22:08 +02:00
logger.info('Saved to cache, saving to storage');
saveToStorage(data);
});
}
});
};
2016-02-06 11:59:57 +01:00
logger.info('Stat file');
2016-07-17 13:30:38 +02:00
Storage[storage].stat(path, opts, (err, stat) => {
2016-02-06 11:59:57 +01:00
if (err) {
if (err.notFound) {
logger.info('File does not exist in storage, creating');
saveToCacheAndStorage();
2019-09-17 21:39:06 +02:00
} else if (file.dirty) {
2016-02-06 11:59:57 +01:00
logger.info('Stat error, dirty, save to cache', err || 'no error');
2019-08-16 23:05:39 +02:00
file.getData(data => {
2016-02-06 11:59:57 +01:00
if (data) {
2019-08-16 23:05:39 +02:00
Storage.cache.save(fileInfo.id, null, data, e => {
2016-02-06 11:59:57 +01:00
if (!e) {
2019-09-17 21:39:06 +02:00
file.dirty = false;
2016-02-06 11:59:57 +01:00
}
2019-08-18 08:05:38 +02:00
logger.info(
'Saved to cache, exit with error',
err || 'no error'
);
2016-02-06 11:59:57 +01:00
complete(err);
});
}
});
2015-12-10 20:44:02 +01:00
} else {
2016-02-06 11:59:57 +01:00
logger.info('Stat error, not dirty', err || 'no error');
complete(err);
2015-12-10 20:44:02 +01:00
}
2019-09-17 21:39:06 +02:00
} else if (stat.rev === fileInfo.rev) {
if (file.modified) {
logger.info('Stat found same version, modified, saving');
2016-02-06 11:59:57 +01:00
saveToCacheAndStorage();
} else {
logger.info('Stat found same version, not modified');
complete();
}
} else {
logger.info('Found new version, loading from storage');
loadFromStorageAndMerge();
}
});
2015-12-10 20:44:02 +01:00
}
2019-09-17 19:58:39 +02:00
}
2016-02-14 12:20:21 +01:00
2019-08-18 10:17:09 +02:00
clearStoredKeyFiles() {
2019-09-17 21:39:06 +02:00
for (const fileInfo of this.fileInfos) {
2016-02-14 12:20:21 +01:00
fileInfo.set({
keyFileName: null,
keyFilePath: null,
2016-02-14 12:20:21 +01:00
keyFileHash: null
});
2019-09-17 21:39:06 +02:00
}
2016-02-14 12:20:21 +01:00
this.fileInfos.save();
2019-09-17 19:58:39 +02:00
}
2016-08-16 23:24:08 +02:00
2019-08-18 10:17:09 +02:00
unsetKeyFile(fileId) {
2018-12-28 18:51:03 +01:00
const fileInfo = this.fileInfos.get(fileId);
fileInfo.set({
keyFileName: null,
keyFilePath: null,
keyFileHash: null
});
2018-12-28 18:51:03 +01:00
this.fileInfos.save();
2019-09-17 19:58:39 +02:00
}
2019-08-18 10:17:09 +02:00
setFileBackup(fileId, backup) {
2017-01-31 07:50:28 +01:00
const fileInfo = this.fileInfos.get(fileId);
2016-08-16 23:24:08 +02:00
if (fileInfo) {
2019-09-17 21:39:06 +02:00
fileInfo.backup = backup;
2016-08-16 23:24:08 +02:00
}
this.fileInfos.save();
2019-09-17 19:58:39 +02:00
}
2016-08-20 10:01:33 +02:00
2019-08-18 10:17:09 +02:00
backupFile(file, data, callback) {
2019-09-17 21:39:06 +02:00
const opts = file.opts;
let backup = file.backup;
const logger = new Logger('backup', file.name);
2016-08-20 10:01:33 +02:00
if (!backup || !backup.storage || !backup.path) {
return callback('Invalid backup settings');
}
2019-09-15 08:11:11 +02:00
let path = backup.path.replace('{date}', DateFormat.dtStrFs(new Date()));
2016-08-20 10:01:33 +02:00
logger.info('Backup file to', backup.storage, path);
2017-01-31 07:50:28 +01:00
const saveToFolder = () => {
2016-08-21 17:43:59 +02:00
if (Storage[backup.storage].getPathForName) {
path = Storage[backup.storage].getPathForName(path);
}
2019-08-16 23:05:39 +02:00
Storage[backup.storage].save(path, opts, data, err => {
2016-08-21 17:43:59 +02:00
if (err) {
logger.error('Backup error', err);
} else {
logger.info('Backup complete');
2019-09-17 21:39:06 +02:00
backup = file.backup;
2016-08-21 18:46:44 +02:00
backup.lastTime = Date.now();
delete backup.pending;
2019-09-17 21:39:06 +02:00
file.backup = backup;
2016-08-21 18:46:44 +02:00
this.setFileBackup(file.id, backup);
2016-08-21 17:43:59 +02:00
}
callback(err);
});
};
2019-09-15 08:11:11 +02:00
let folderPath = UrlFormat.fileToDir(path);
2016-08-21 17:43:59 +02:00
if (Storage[backup.storage].getPathForName) {
folderPath = Storage[backup.storage].getPathForName(folderPath).replace('.kdbx', '');
}
2017-02-05 23:34:37 +01:00
Storage[backup.storage].stat(folderPath, opts, err => {
2016-08-20 10:01:33 +02:00
if (err) {
2016-08-21 17:43:59 +02:00
if (err.notFound) {
logger.info('Backup folder does not exist');
if (!Storage[backup.storage].mkdir) {
return callback('Mkdir not supported by ' + backup.storage);
}
2017-02-05 23:34:37 +01:00
Storage[backup.storage].mkdir(folderPath, err => {
2016-08-21 17:43:59 +02:00
if (err) {
logger.error('Error creating backup folder', err);
callback('Error creating backup folder');
} else {
logger.info('Backup folder created');
saveToFolder();
}
});
} else {
logger.error('Stat folder error', err);
callback('Cannot stat backup folder');
}
2016-08-20 10:01:33 +02:00
} else {
2016-08-21 17:43:59 +02:00
logger.info('Backup folder exists, saving');
saveToFolder();
2016-08-20 10:01:33 +02:00
}
});
2019-09-17 19:58:39 +02:00
}
2016-08-21 18:46:44 +02:00
2019-08-18 10:17:09 +02:00
scheduleBackupFile(file, data) {
2019-09-17 21:39:06 +02:00
const backup = file.backup;
2016-08-21 18:46:44 +02:00
if (!backup || !backup.enabled) {
return;
}
2019-09-17 21:39:06 +02:00
const logger = new Logger('backup', file.name);
2016-08-21 18:46:44 +02:00
let needBackup = false;
if (!backup.lastTime) {
needBackup = true;
logger.debug('No last backup time, backup now');
} else {
2017-01-31 07:50:28 +01:00
const dt = new Date(backup.lastTime);
2016-08-21 18:46:44 +02:00
switch (backup.schedule) {
case '0':
break;
case '1d':
dt.setDate(dt.getDate() + 1);
break;
case '1w':
dt.setDate(dt.getDate() + 7);
break;
case '1m':
dt.setMonth(dt.getMonth() + 1);
break;
default:
return;
}
if (dt.getTime() <= Date.now()) {
needBackup = true;
}
2019-08-16 23:05:39 +02:00
logger.debug(
'Last backup time: ' +
new Date(backup.lastTime) +
', schedule: ' +
backup.schedule +
', next time: ' +
dt +
', ' +
(needBackup ? 'backup now' : 'skip backup')
);
2016-08-21 18:46:44 +02:00
}
if (!backup.pending) {
backup.pending = true;
this.setFileBackup(file.id, backup);
}
if (needBackup) {
2019-09-18 07:08:23 +02:00
this.backupFile(file, data, noop);
2016-08-21 18:46:44 +02:00
}
2019-09-17 19:58:39 +02:00
}
2019-08-18 10:17:09 +02:00
saveFileFingerprint(file, password) {
2019-09-17 21:39:06 +02:00
if (Launcher && Launcher.fingerprints && !file.fingerprint) {
const fileInfo = this.fileInfos.get(file.id);
2017-04-25 22:52:07 +02:00
Launcher.fingerprints.register(file.id, password, token => {
if (token) {
2019-09-17 21:39:06 +02:00
fileInfo.fingerprint = token;
2017-04-25 22:52:07 +02:00
this.fileInfos.save();
}
});
}
2015-10-17 23:49:24 +02:00
}
2020-04-15 16:50:01 +02:00
openOtpDevice(callback) {
2020-05-05 20:03:38 +02:00
const device = new YubiKeyOtpModel();
2020-04-15 16:50:01 +02:00
device.open(err => {
if (!err) {
this.addFile(device);
}
callback(err);
});
return device;
}
2020-05-09 23:55:57 +02:00
getMatchingOtpEntry(entry) {
if (!this.settings.yubiKeyMatchEntries) {
return null;
}
for (const file of this.files) {
if (file.external) {
const matchingEntry = file.getMatchingEntry(entry);
if (matchingEntry) {
return matchingEntry;
}
}
}
2020-05-09 23:55:57 +02:00
}
2019-09-17 19:58:39 +02:00
}
2015-10-17 23:49:24 +02:00
2019-09-15 14:16:32 +02:00
export { AppModel };