keeweb/app/scripts/views/settings/settings-file-view.js

660 lines
23 KiB
JavaScript

const Backbone = require('backbone');
const OpenConfigView = require('../open-config-view');
const FeatureDetector = require('../../util/feature-detector');
const PasswordGenerator = require('../../util/password-generator');
const Alerts = require('../../comp/alerts');
const Launcher = require('../../comp/launcher');
const Storage = require('../../storage');
const Links = require('../../const/links');
const Format = require('../../util/format');
const Locale = require('../../util/locale');
const UrlUtil = require('../../util/url-util');
const FileSaver = require('../../util/file-saver');
const AppSettingsModel = require('../../models/app-settings-model');
const kdbxweb = require('kdbxweb');
const DefaultBackupPath = 'Backups/{name}.{date}.bak';
const DefaultBackupSchedule = '1w';
const SettingsFileView = Backbone.View.extend({
template: require('templates/settings/settings-file.hbs'),
events: {
'click .settings__file-button-save-default': 'saveDefault',
'click .settings__file-button-save-choose': 'toggleChooser',
'click .settings__file-button-close': 'closeFile',
'click .settings__file-save-to-file': 'saveToFile',
'click .settings__file-save-to-xml': 'saveToXml',
'click .settings__file-save-to-storage': 'saveToStorage',
'change #settings__file-key-file': 'keyFileChange',
'click #settings__file-file-select-link': 'triggerSelectFile',
'change #settings__file-file-select': 'fileSelected',
'focus #settings__file-master-pass': 'focusMasterPass',
'input #settings__file-master-pass': 'changeMasterPass',
'blur #settings__file-master-pass': 'blurMasterPass',
'focus #settings__file-confirm-master-pass': 'focusConfirmMasterPass',
'blur #settings__file-confirm-master-pass': 'blurConfirmMasterPass',
'input #settings__file-name': 'changeName',
'input #settings__file-def-user': 'changeDefUser',
'change #settings__file-backup-enabled': 'changeBackupEnabled',
'input #settings__file-backup-path': 'changeBackupPath',
'change #settings__file-backup-storage': 'changeBackupStorage',
'change #settings__file-backup-schedule': 'changeBackupSchedule',
'click .settings__file-button-backup': 'backupFile',
'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-change-force': 'changeKeyChangeForce',
'input .settings__input-kdf': 'changeKdfParameter'
},
appModel: null,
initialize() {
this.listenTo(
this.model,
'change:syncing change:syncError change:syncDate',
this.deferRender
);
},
render() {
const storageProviders = [];
const fileStorage = this.model.get('storage');
let canBackup = false;
Object.keys(Storage).forEach(name => {
const prv = Storage[name];
if (!canBackup && prv.backup && prv.enabled) {
canBackup = true;
}
if (!prv.system && prv.enabled) {
storageProviders.push({
name: prv.name,
icon: prv.icon,
iconSvg: prv.iconSvg,
own: name === fileStorage,
backup: prv.backup
});
}
});
storageProviders.sort((x, y) => (x.uipos || Infinity) - (y.uipos || Infinity));
const backup = this.model.get('backup');
this.renderTemplate({
cmd: FeatureDetector.actionShortcutSymbol(true),
supportFiles: !!Launcher,
desktopLink: Links.Desktop,
name: this.model.get('name'),
path: this.model.get('path'),
storage: this.model.get('storage'),
syncing: this.model.get('syncing'),
syncError: this.model.get('syncError'),
syncDate: Format.dtStr(this.model.get('syncDate')),
password: PasswordGenerator.present(this.model.get('passwordLength')),
defaultUser: this.model.get('defaultUser'),
recycleBinEnabled: this.model.get('recycleBinEnabled'),
backupEnabled: backup && backup.enabled,
backupStorage: backup && backup.storage,
backupPath:
(backup && backup.path) ||
DefaultBackupPath.replace('{name}', this.model.get('name')),
backupSchedule: backup ? backup.schedule : DefaultBackupSchedule,
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,
kdfParameters: this.kdfParametersToUi(this.model.get('kdfParameters')),
storageProviders,
canBackup,
canExportXml: AppSettingsModel.instance.get('canExportXml')
});
if (!this.model.get('created')) {
this.$el
.find('.settings__file-master-pass-warning')
.toggle(this.model.get('passwordChanged'));
this.$el
.find('#settings__file-master-pass-warning-text')
.text(Locale.setFilePassChanged);
}
this.renderKeyFileSelect();
},
kdfParametersToUi(kdfParameters) {
return kdfParameters
? _.extend({}, kdfParameters, { memory: Math.round(kdfParameters.memory / 1024) })
: null;
},
renderKeyFileSelect() {
const keyFileName = this.model.get('keyFileName');
const oldKeyFileName = this.model.get('oldKeyFileName');
const keyFileChanged = this.model.get('keyFileChanged');
const sel = this.$el.find('#settings__file-key-file');
sel.html('');
if (keyFileName && keyFileChanged) {
const text =
keyFileName !== 'Generated'
? Locale.setFileUseKeyFile + ' ' + keyFileName
: Locale.setFileUseGenKeyFile;
$('<option/>')
.val('ex')
.text(text)
.appendTo(sel);
}
if (oldKeyFileName) {
const useText = keyFileChanged
? Locale.setFileUseOldKeyFile
: Locale.setFileUseKeyFile + ' ' + oldKeyFileName;
$('<option/>')
.val('old')
.text(useText)
.appendTo(sel);
}
$('<option/>')
.val('gen')
.text(Locale.setFileGenKeyFile)
.appendTo(sel);
$('<option/>')
.val('none')
.text(Locale.setFileDontUseKeyFile)
.appendTo(sel);
if (keyFileName && keyFileChanged) {
sel.val('ex');
} else if (!keyFileName) {
sel.val('none');
} else if (oldKeyFileName && keyFileName === oldKeyFileName && !keyFileChanged) {
sel.val('old');
}
},
validatePassword(continueCallback) {
if (!this.model.get('passwordLength')) {
Alerts.yesno({
header: Locale.setFileEmptyPass,
body: Locale.setFileEmptyPassBody,
success: () => {
continueCallback();
},
cancel: () => {
this.$el.find('#settings__file-master-pass').focus();
}
});
return false;
}
return true;
},
save(arg) {
if (!arg) {
arg = {};
}
arg.startedByUser = true;
if (!arg.skipValidation) {
const isValid = this.validatePassword(() => {
arg.skipValidation = true;
this.save(arg);
});
if (!isValid) {
return;
}
}
this.appModel.syncFile(this.model, arg);
},
saveDefault() {
this.save();
},
toggleChooser() {
this.$el.find('.settings__file-save-choose').toggleClass('hide');
},
saveToFile(skipValidation) {
if (skipValidation !== true && !this.validatePassword(this.saveToFile.bind(this, true))) {
return;
}
const fileName = this.model.get('name') + '.kdbx';
if (Launcher && !this.model.get('storage')) {
Launcher.getSaveFileName(fileName, path => {
if (path) {
this.save({ storage: 'file', path });
}
});
} else {
this.model.getData(data => {
if (!data) {
return;
}
if (Launcher) {
Launcher.getSaveFileName(fileName, path => {
if (path) {
Storage.file.save(path, null, data, err => {
if (err) {
Alerts.error({
header: Locale.setFileSaveError,
body:
Locale.setFileSaveErrorBody + ' ' + path + ': \n' + err
});
}
});
}
});
} else {
const blob = new Blob([data], { type: 'application/octet-stream' });
FileSaver.saveAs(blob, fileName);
}
});
}
},
saveToXml() {
this.model.getXml(xml => {
const blob = new Blob([xml], { type: 'text/xml' });
FileSaver.saveAs(blob, this.model.get('name') + '.xml');
});
},
saveToStorage(e) {
if (this.model.get('syncing') || this.model.get('demo')) {
return;
}
const storageName = $(e.target)
.closest('.settings__file-save-to-storage')
.data('storage');
const storage = Storage[storageName];
if (!storage) {
return;
}
if (this.model.get('storage') === storageName) {
this.save();
} else {
if (!storage.list) {
if (storage.getOpenConfig) {
const config = _.extend(
{
id: storage.name,
name: Locale[storage.name] || storage.name,
icon: storage.icon,
buttons: false
},
storage.getOpenConfig()
);
const openConfigView = new OpenConfigView({ model: config });
Alerts.alert({
header: '',
body: '',
icon: storage.icon || 'files-o',
buttons: [Alerts.buttons.ok, Alerts.buttons.cancel],
esc: '',
opaque: true,
view: openConfigView,
success: () => {
const storageConfig = openConfigView.getData();
if (!storageConfig) {
return;
}
const opts = _.omit(storageConfig, ['path', 'storage']);
if (opts && Object.keys(opts).length) {
this.model.set('opts', opts);
}
this.save({ storage: storageName, path: storageConfig.path, opts });
}
});
} else {
Alerts.notImplemented();
}
return;
}
this.model.set('syncing', true);
storage.list('', (err, files) => {
this.model.set('syncing', false);
if (err) {
return;
}
const expName = this.model.get('name').toLowerCase();
const existingFile = _.find(files, file => {
return (
!file.dir && UrlUtil.getDataFileName(file.name).toLowerCase() === expName
);
});
if (existingFile) {
Alerts.yesno({
header: Locale.setFileAlreadyExists,
body: Locale.setFileAlreadyExistsBody.replace(
'{}',
this.model.escape('name')
),
success: () => {
this.model.set('syncing', true);
storage.remove(existingFile.path, err => {
this.model.set('syncing', false);
if (!err) {
this.save({ storage: storageName });
}
});
}
});
} else {
this.save({ storage: storageName });
}
});
}
},
closeFile() {
if (this.model.get('modified')) {
Alerts.yesno({
header: Locale.setFileUnsaved,
body: Locale.setFileUnsavedBody,
buttons: [
{ result: 'close', title: Locale.setFileCloseNoSave, error: true },
{ result: '', title: Locale.setFileDontClose }
],
success: result => {
if (result === 'close') {
this.closeFileNoCheck();
}
}
});
} else {
this.closeFileNoCheck();
}
},
closeFileNoCheck() {
this.appModel.closeFile(this.model);
},
keyFileChange(e) {
switch (e.target.value) {
case 'old':
this.selectOldKeyFile();
break;
case 'gen':
this.generateKeyFile();
break;
case 'none':
this.clearKeyFile();
break;
}
},
selectOldKeyFile() {
this.model.resetKeyFile();
this.renderKeyFileSelect();
},
generateKeyFile() {
const keyFile = this.model.generateAndSetKeyFile();
const blob = new Blob([keyFile], { type: 'application/octet-stream' });
FileSaver.saveAs(blob, this.model.get('name') + '.key');
this.renderKeyFileSelect();
},
clearKeyFile() {
this.model.removeKeyFile();
this.renderKeyFileSelect();
},
triggerSelectFile() {
this.$el.find('#settings__file-file-select').click();
},
fileSelected(e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = e => {
const res = e.target.result;
this.model.setKeyFile(res, file.name);
this.renderKeyFileSelect();
};
reader.readAsArrayBuffer(file);
},
focusMasterPass(e) {
e.target.value = '';
e.target.setAttribute('type', 'text');
this.model.set('passwordChanged', false);
},
changeMasterPass(e) {
if (!e.target.value) {
this.model.resetPassword();
this.$el.find('.settings__file-master-pass-warning').hide();
} else {
this.$el.find('#settings__file-confirm-master-pass-group').show();
this.$el
.find('#settings__file-master-pass-warning-text')
.text(Locale.setFilePassChange);
if (!this.model.get('created')) {
this.$el.find('.settings__file-master-pass-warning').show();
}
}
},
blurMasterPass(e) {
if (!e.target.value) {
this.model.resetPassword();
this.resetConfirmMasterPass();
e.target.value = PasswordGenerator.present(this.model.get('passwordLength'));
this.$el.find('.settings__file-master-pass-warning').hide();
}
e.target.setAttribute('type', 'password');
},
resetConfirmMasterPass() {
this.$el.find('#settings__file-confirm-master-pass').val('');
this.$el.find('#settings__file-confirm-master-pass-group').hide();
this.$el.find('#settings__file-master-pass-warning-text').text(Locale.setFilePassChange);
},
focusConfirmMasterPass(e) {
e.target.value = '';
e.target.setAttribute('type', 'text');
},
blurConfirmMasterPass(e) {
e.target.setAttribute('type', 'password');
const masterPassword = this.$el.find('#settings__file-master-pass').val();
const confirmPassword = e.target.value;
if (masterPassword === confirmPassword) {
this.$el
.find('#settings__file-master-pass-warning-text')
.text(Locale.setFilePassChanged);
this.$el.find('.settings__file-confirm-master-pass-warning').hide();
this.model.setPassword(kdbxweb.ProtectedValue.fromString(confirmPassword));
} else {
this.$el
.find('#settings__file-master-pass-warning-text')
.text(Locale.setFilePassChange);
this.$el.find('.settings__file-confirm-master-pass-warning').show();
this.model.resetPassword();
}
},
changeName(e) {
const value = $.trim(e.target.value);
if (!value) {
return;
}
this.model.setName(value);
},
changeDefUser(e) {
const value = $.trim(e.target.value);
this.model.setDefaultUser(value);
},
changeBackupEnabled(e) {
const enabled = e.target.checked;
let backup = this.model.get('backup');
if (!backup) {
backup = { enabled, schedule: DefaultBackupSchedule };
const defaultPath = DefaultBackupPath.replace('{name}', this.model.get('name'));
if (Launcher) {
backup.storage = 'file';
backup.path = Launcher.getDocumentsPath(defaultPath);
} else {
backup.storage = 'dropbox';
backup.path = defaultPath;
}
// } else if (this.model.get('storage') === 'webdav') {
// backup.storage = 'webdav';
// backup.path = this.model.get('path') + '.{date}.bak';
// } else if (this.model.get('storage')) {
// backup.storage = this.model.get('storage');
// backup.path = DefaultBackupPath.replace('{name}', this.model.get('name'));
// } else {
// Object.keys(Storage).forEach(name => {
// var prv = Storage[name];
// if (!backup.storage && !prv.system && prv.enabled) {
// backup.storage = name;
// }
// });
// if (!backup.storage) {
// e.target.checked = false;
// return;
// }
// backup.path = DefaultBackupPath.replace('{name}', this.model.get('name'));
// }
this.$el.find('#settings__file-backup-storage').val(backup.storage);
this.$el.find('#settings__file-backup-path').val(backup.path);
}
this.$el.find('.settings__file-backups').toggleClass('hide', !enabled);
backup.enabled = enabled;
this.setBackup(backup);
},
changeBackupPath(e) {
const backup = this.model.get('backup');
backup.path = e.target.value.trim();
this.setBackup(backup);
},
changeBackupStorage(e) {
const backup = this.model.get('backup');
backup.storage = e.target.value;
this.setBackup(backup);
},
changeBackupSchedule(e) {
const backup = this.model.get('backup');
backup.schedule = e.target.value;
this.setBackup(backup);
},
setBackup(backup) {
this.model.set('backup', backup);
this.appModel.setFileBackup(this.model.id, backup);
},
backupFile() {
if (this.backupInProgress) {
return;
}
const backupButton = this.$el.find('.settings__file-button-backup');
backupButton.text(Locale.setFileBackupNowWorking);
this.model.getData(data => {
if (!data) {
this.backupInProgress = false;
backupButton.text(Locale.setFileBackupNow);
return;
}
this.appModel.backupFile(this.model, data, err => {
this.backupInProgress = false;
backupButton.text(Locale.setFileBackupNow);
if (err) {
let title = '';
let description = '';
if (err.isDir) {
title = Locale.setFileBackupErrorIsDir;
description = Locale.setFileBackupErrorIsDirDescription;
} else {
title = Locale.setFileBackupError;
description = Locale.setFileBackupErrorDescription;
}
Alerts.error({
title,
body:
description +
'<pre class="modal__pre">' +
_.escape(err.toString()) +
'</pre>'
});
}
});
});
},
changeTrash(e) {
this.model.setRecycleBinEnabled(e.target.checked);
},
changeHistoryLength(e) {
if (!e.target.validity.valid) {
return;
}
const value = +e.target.value;
if (isNaN(value)) {
e.target.value = this.model.get('historyMaxItems');
return;
}
this.model.setHistoryMaxItems(value);
},
changeHistorySize(e) {
if (!e.target.validity.valid) {
return;
}
const value = +e.target.value;
if (isNaN(value)) {
e.target.value = this.model.get('historyMaxSize') / 1024 / 1024;
return;
}
this.model.setHistoryMaxSize(value * 1024 * 1024);
},
changeKeyRounds(e) {
if (!e.target.validity.valid) {
return;
}
const value = +e.target.value;
if (isNaN(value)) {
e.target.value = this.model.get('keyEncryptionRounds');
return;
}
this.model.setKeyEncryptionRounds(value);
},
changeKeyChangeForce(e) {
if (!e.target.validity.valid) {
return;
}
let value = Math.round(e.target.value);
if (isNaN(value) || value <= 0) {
value = -1;
}
this.model.setKeyChange(true, value);
},
changeKdfParameter(e) {
if (!e.target.validity.valid) {
return;
}
const field = $(e.target).data('field');
const mul = $(e.target).data('mul') || 1;
const value = e.target.value * mul;
if (isNaN(value)) {
e.target.value = Math.round(this.model.get('kdfParameters')[field] / mul);
return;
}
if (value > 0) {
this.model.setKdfParameter(field, value);
}
}
});
module.exports = SettingsFileView;