diff --git a/app/scripts/collections/external-entry-collection.js b/app/scripts/collections/external-entry-collection.js deleted file mode 100644 index 2eee76bd..00000000 --- a/app/scripts/collections/external-entry-collection.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Collection } from 'framework/collection'; -import { ExternalEntryModel } from 'models/external/external-entry-model'; - -class ExternalEntryCollection extends Collection { - static model = ExternalEntryModel; -} - -export { ExternalEntryCollection }; diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 7203326b..60f62ab9 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -12,7 +12,7 @@ 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'; -import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model'; +import { YubiKeyOtpModel } from 'models/otp-device/yubikey-otp-model'; import { MenuModel } from 'models/menu/menu-model'; import { PluginManager } from 'plugins/plugin-manager'; import { Features } from 'util/features'; @@ -307,12 +307,12 @@ class AppModel { const preparedFilter = this.prepareFilter(filter); const entries = new SearchResultCollection(); - const devicesToMatchOtpEntries = this.files.filter((file) => file.external); + const devicesToMatchOtpEntries = this.files.filter((file) => file.backend === 'otp-device'); const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined; this.files - .filter((file) => !file.external) + .filter((file) => file.backend !== 'otp-device') .forEach((file) => { file.forEachEntry(preparedFilter, (entry) => { if (matchedOtpEntrySet) { @@ -827,7 +827,10 @@ class AppModel { this.scheduleBackupFile(file, data); } if (this.settings.yubiKeyAutoOpen) { - if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) { + if ( + this.attachedYubiKeysCount > 0 && + !this.files.some((f) => f.backend === 'otp-device') + ) { this.tryOpenOtpDeviceInBackground(); } } @@ -1271,7 +1274,9 @@ class AppModel { } const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount; - const hasOpenFiles = this.files.some((file) => file.active && !file.external); + const hasOpenFiles = this.files.some( + (file) => file.active && file.backend !== 'otp-device' + ); if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) { this.tryOpenOtpDeviceInBackground(); @@ -1303,7 +1308,7 @@ class AppModel { return null; } for (const file of this.files) { - if (file.external) { + if (file.backend === 'otp-device') { const matchingEntry = file.getMatchingEntry(entry); if (matchingEntry) { return matchingEntry; diff --git a/app/scripts/models/entry-model.js b/app/scripts/models/entry-model.js index 20537bd4..ddd11ea8 100644 --- a/app/scripts/models/entry-model.js +++ b/app/scripts/models/entry-model.js @@ -505,7 +505,7 @@ class EntryModel extends Model { } try { this.otpGenerator = Otp.parseUrl(otpUrl); - } catch (e) { + } catch { this.otpGenerator = null; } } else { diff --git a/app/scripts/models/external/external-otp-device-model.js b/app/scripts/models/external/external-otp-device-model.js deleted file mode 100644 index 9e6e654e..00000000 --- a/app/scripts/models/external/external-otp-device-model.js +++ /dev/null @@ -1,21 +0,0 @@ -import { ExternalDeviceModel } from 'models/external/external-device-model'; - -class ExternalOtpDeviceModel extends ExternalDeviceModel { - open(callback) { - throw 'Not implemented'; - } - - cancelOpen() { - throw 'Not implemented'; - } - - close(callback) { - throw 'Not implemented'; - } - - getOtp(callback) { - throw 'Not implemented'; - } -} - -export { ExternalOtpDeviceModel }; diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 06eaeccb..608cc0dc 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -766,7 +766,11 @@ FileModel.defineModelProperties({ oldKeyFileHash: null, oldKeyChangeDate: null, encryptedPassword: null, - encryptedPasswordDate: null + encryptedPasswordDate: null, + supportsTags: true, + supportsColors: true, + supportsIcons: true, + supportsExpiration: true }); export { FileModel }; diff --git a/app/scripts/models/otp-device/otp-device-entry-collection.js b/app/scripts/models/otp-device/otp-device-entry-collection.js new file mode 100644 index 00000000..29b9d0b4 --- /dev/null +++ b/app/scripts/models/otp-device/otp-device-entry-collection.js @@ -0,0 +1,8 @@ +import { Collection } from 'framework/collection'; +import { OtpDeviceEntryModel } from './otp-device-entry-model'; + +class OtpDeviceEntryCollection extends Collection { + static model = OtpDeviceEntryModel; +} + +export { OtpDeviceEntryCollection }; diff --git a/app/scripts/models/external/external-entry-model.js b/app/scripts/models/otp-device/otp-device-entry-model.js similarity index 88% rename from app/scripts/models/external/external-entry-model.js rename to app/scripts/models/otp-device/otp-device-entry-model.js index 03232def..63bbeea1 100644 --- a/app/scripts/models/external/external-entry-model.js +++ b/app/scripts/models/otp-device/otp-device-entry-model.js @@ -1,7 +1,7 @@ import { Model } from 'framework/model'; import { EntrySearch } from 'util/entry-search'; -class ExternalEntryModel extends Model { +class OtpDeviceEntryModel extends Model { tags = []; fields = {}; @@ -44,9 +44,10 @@ class ExternalEntryModel extends Model { } } -ExternalEntryModel.defineModelProperties({ +OtpDeviceEntryModel.defineModelProperties({ id: '', - external: true, + file: null, + entry: true, readOnly: true, device: undefined, deviceSubId: null, @@ -59,4 +60,4 @@ ExternalEntryModel.defineModelProperties({ _search: undefined }); -export { ExternalEntryModel }; +export { OtpDeviceEntryModel }; diff --git a/app/scripts/models/external/external-device-model.js b/app/scripts/models/otp-device/otp-device-model.js similarity index 63% rename from app/scripts/models/external/external-device-model.js rename to app/scripts/models/otp-device/otp-device-model.js index 96222c36..6a4ffd55 100644 --- a/app/scripts/models/external/external-device-model.js +++ b/app/scripts/models/otp-device/otp-device-model.js @@ -1,13 +1,11 @@ import { Model } from 'framework/model'; -import { ExternalEntryCollection } from 'collections/external-entry-collection'; +import { OtpDeviceEntryCollection } from './otp-device-entry-collection'; -class ExternalDeviceModel extends Model { - entries = new ExternalEntryCollection(); +class OtpDeviceModel extends Model { + entries = new OtpDeviceEntryCollection(); groups = []; entryMap = {}; - close() {} - forEachEntry(filter, callback) { if (filter.trash || filter.group || filter.tag) { return; @@ -32,11 +30,28 @@ class ExternalDeviceModel extends Model { this.entryMap[entry.id.toLowerCase()] = entry; } } + + open(callback) { + throw 'Not implemented'; + } + + cancelOpen() { + throw 'Not implemented'; + } + + close(callback) { + throw 'Not implemented'; + } + + getOtp(callback) { + throw 'Not implemented'; + } } -ExternalDeviceModel.defineModelProperties({ +OtpDeviceModel.defineModelProperties({ id: '', - external: true, + backend: 'otp-device', + skipOpenList: true, readOnly: true, active: false, entries: undefined, @@ -47,4 +62,4 @@ ExternalDeviceModel.defineModelProperties({ entryMap: undefined }); -export { ExternalDeviceModel }; +export { OtpDeviceModel }; diff --git a/app/scripts/models/external/external-otp-entry-model.js b/app/scripts/models/otp-device/otp-entry-model.js similarity index 89% rename from app/scripts/models/external/external-otp-entry-model.js rename to app/scripts/models/otp-device/otp-entry-model.js index 7d8292ed..05d4e8e1 100644 --- a/app/scripts/models/external/external-otp-entry-model.js +++ b/app/scripts/models/otp-device/otp-entry-model.js @@ -1,6 +1,6 @@ -import { ExternalEntryModel } from 'models/external/external-entry-model'; +import { OtpDeviceEntryModel } from './otp-device-entry-model'; -class ExternalOtpEntryModel extends ExternalEntryModel { +class OtpEntryModel extends OtpDeviceEntryModel { constructor(props) { super({ ...props, @@ -59,10 +59,11 @@ class ExternalOtpEntryModel extends ExternalEntryModel { } } -ExternalOtpEntryModel.defineModelProperties({ +OtpEntryModel.defineModelProperties({ user: undefined, + backend: 'otp-device', otpGenerator: undefined, needsTouch: false }); -export { ExternalOtpEntryModel }; +export { OtpEntryModel }; diff --git a/app/scripts/models/external/yubikey-otp-model.js b/app/scripts/models/otp-device/yubikey-otp-model.js similarity index 92% rename from app/scripts/models/external/yubikey-otp-model.js rename to app/scripts/models/otp-device/yubikey-otp-model.js index def68321..d10066b2 100644 --- a/app/scripts/models/external/yubikey-otp-model.js +++ b/app/scripts/models/otp-device/yubikey-otp-model.js @@ -1,13 +1,13 @@ import { Events } from 'framework/events'; -import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model'; -import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model'; +import { OtpDeviceModel } from './otp-device-model'; +import { OtpEntryModel } from './otp-entry-model'; import { Logger } from 'util/logger'; import { UsbListener } from 'comp/app/usb-listener'; import { YubiKey } from 'comp/app/yubikey'; const logger = new Logger('yubikey'); -class YubiKeyOtpModel extends ExternalOtpDeviceModel { +class YubiKeyOtpModel extends OtpDeviceModel { constructor(props) { super({ id: 'yubikey', @@ -67,8 +67,9 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel { for (const code of codes) { this.entries.push( - new ExternalOtpEntryModel({ + new OtpEntryModel({ id: this.entryId(code.title, code.user), + file: this, device: this, deviceSubId: serial, icon: 'clock', diff --git a/app/scripts/presenters/entry-presenter.js b/app/scripts/presenters/entry-presenter.js index 44444146..6bc8ef9f 100644 --- a/app/scripts/presenters/entry-presenter.js +++ b/app/scripts/presenters/entry-presenter.js @@ -14,9 +14,6 @@ EntryPresenter.prototype = { this.entry = item; } else if (item.group) { this.group = item; - } else if (item.external) { - this.entry = item; - this.external = true; } return this; }, @@ -71,7 +68,7 @@ EntryPresenter.prototype = { if (!this.entry) { return '[' + Locale.listGroup + ']'; } - if (this.external) { + if (this.entry.backend === 'otp-device') { return this.entry.description; } switch (this.descField) { diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 6dcb231e..78689cca 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -616,7 +616,7 @@ class AppView extends View { closeAllFilesAndShowFirst() { let fileToShow = this.model.files.find( - (file) => !file.demo && !file.created && !file.external + (file) => !file.demo && !file.created && !file.skipOpenList ); this.model.closeAllFiles(); if (!fileToShow) { diff --git a/app/scripts/views/details/details-fields.js b/app/scripts/views/details/details-fields.js index d10e909f..520a2ae6 100644 --- a/app/scripts/views/details/details-fields.js +++ b/app/scripts/views/details/details-fields.js @@ -21,7 +21,7 @@ function createDetailsFields(detailsView) { const fieldViews = []; const fieldViewsAside = []; - if (model.external) { + if (model.backend === 'otp-device') { fieldViewsAside.push( new FieldViewReadOnly({ name: 'Device', @@ -73,15 +73,27 @@ function createDetailsFields(detailsView) { }) ); } else { - fieldViewsAside.push( - new FieldViewReadOnly({ - name: 'File', - title: StringFormat.capFirst(Locale.file), - value() { - return model.fileName; - } - }) - ); + if (model.backend) { + fieldViewsAside.push( + new FieldViewReadOnly({ + name: 'Storage', + title: StringFormat.capFirst(Locale.storage), + value() { + return model.fileName; + } + }) + ); + } else { + fieldViewsAside.push( + new FieldViewReadOnly({ + name: 'File', + title: StringFormat.capFirst(Locale.file), + value() { + return model.fileName; + } + }) + ); + } } fieldViews.push( new FieldViewAutocomplete({ @@ -127,26 +139,30 @@ function createDetailsFields(detailsView) { sequence: '{NOTES}' }) ); - fieldViews.push( - new FieldViewTags({ - name: 'Tags', - title: StringFormat.capFirst(Locale.tags), - tags: AppModel.instance.tags, - value() { - return model.tags; - } - }) - ); - fieldViews.push( - new FieldViewDate({ - name: 'Expires', - title: Locale.detExpires, - lessThanNow: '(' + Locale.detExpired + ')', - value() { - return model.expires; - } - }) - ); + if (model.file.supportsTags) { + fieldViews.push( + new FieldViewTags({ + name: 'Tags', + title: StringFormat.capFirst(Locale.tags), + tags: AppModel.instance.tags, + value() { + return model.tags; + } + }) + ); + } + if (model.file.supportsExpiration) { + fieldViews.push( + new FieldViewDate({ + name: 'Expires', + title: Locale.detExpires, + lessThanNow: '(' + Locale.detExpired + ')', + value() { + return model.expires; + } + }) + ); + } fieldViewsAside.push( new FieldViewReadOnly({ name: 'Group', @@ -159,33 +175,39 @@ function createDetailsFields(detailsView) { } }) ); - fieldViewsAside.push( - new FieldViewReadOnly({ - name: 'Created', - title: Locale.detCreated, - value() { - return DateFormat.dtStr(model.created); - } - }) - ); - fieldViewsAside.push( - new FieldViewReadOnly({ - name: 'Updated', - title: Locale.detUpdated, - value() { - return DateFormat.dtStr(model.updated); - } - }) - ); - fieldViewsAside.push( - new FieldViewHistory({ - name: 'History', - title: StringFormat.capFirst(Locale.history), - value() { - return { length: model.historyLength, unsaved: model.unsaved }; - } - }) - ); + if (model.created) { + fieldViewsAside.push( + new FieldViewReadOnly({ + name: 'Created', + title: Locale.detCreated, + value() { + return DateFormat.dtStr(model.created); + } + }) + ); + } + if (model.updated) { + fieldViewsAside.push( + new FieldViewReadOnly({ + name: 'Updated', + title: Locale.detUpdated, + value() { + return DateFormat.dtStr(model.updated); + } + }) + ); + } + if (!model.backend) { + fieldViewsAside.push( + new FieldViewHistory({ + name: 'History', + title: StringFormat.capFirst(Locale.history), + value() { + return { length: model.historyLength, unsaved: model.unsaved }; + } + }) + ); + } if (otpEntry) { fieldViews.push( new FieldViewOtp({ @@ -207,7 +229,7 @@ function createDetailsFields(detailsView) { fieldViews.push( new FieldViewOtp({ name: '$' + field, - title: field, + title: Locale.detOtpField, value() { return model.otpGenerator; }, diff --git a/app/scripts/views/details/details-view.js b/app/scripts/views/details/details-view.js index 79873f08..1017a7ea 100644 --- a/app/scripts/views/details/details-view.js +++ b/app/scripts/views/details/details-view.js @@ -121,6 +121,9 @@ class DetailsView extends View { } const model = { deleted: this.appModel.filter.trash, + canEditColor: this.model.file.supportsColors && !this.model.readOnly, + canEditIcon: this.model.file.supportsIcons && !this.model.readOnly, + showButtons: !this.model.backend && !this.model.readOnly, ...this.model }; this.template = template; @@ -185,7 +188,7 @@ class DetailsView extends View { this.fieldViews = fieldViews.concat(fieldViewsAside); - if (!this.model.external) { + if (!this.model.backend) { this.moreView = new DetailsAddFieldView(); this.moreView.render(); this.moreView.on('add-field', this.addNewField.bind(this)); @@ -363,7 +366,7 @@ class DetailsView extends View { } toggleIcons() { - if (this.model.external) { + if (this.model.backend) { return; } if (this.views.sub && this.views.sub instanceof IconSelectView) { @@ -464,7 +467,7 @@ class DetailsView extends View { } this.model.initOtpGenerator?.(); - if (this.model.external) { + if (this.model.backend === 'otp-device') { return; } @@ -496,7 +499,7 @@ class DetailsView extends View { if (!this.model) { return; } - if (this.model.external) { + if (this.model.backend === 'otp-device') { this.copyOtp(); e.preventDefault(); } @@ -520,7 +523,7 @@ class DetailsView extends View { copyOtp() { const otpField = this.getFieldView('$otp'); - if (this.model.external) { + if (this.model.backend === 'otp-device') { if (!otpField) { return false; } @@ -715,7 +718,7 @@ class DetailsView extends View { } editTitle() { - if (this.model.external) { + if (this.model.backend === 'otp-device') { return; } const input = $('') @@ -875,7 +878,7 @@ class DetailsView extends View { const canCopy = document.queryCommandSupported('copy'); const options = []; if (canCopy) { - if (this.model.external) { + if (this.model.backend === 'otp-device') { options.push({ value: 'det-copy-otp', icon: 'copy', @@ -894,7 +897,7 @@ class DetailsView extends View { text: Locale.detMenuCopyUser }); } - if (!this.model.external) { + if (!this.model.backend) { options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField }); options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone }); if (canCopy) { @@ -985,7 +988,8 @@ class DetailsView extends View { autoType(sequence) { const entry = this.model; - const hasOtp = sequence?.includes('{TOTP}') || (entry.external && !sequence); + const hasOtp = + sequence?.includes('{TOTP}') || (entry.backend === 'otp-device' && !sequence); if (hasOtp) { const otpField = this.getFieldView('$otp'); otpField.refreshOtp((err) => { diff --git a/app/scripts/views/fields/field-view-read-only.js b/app/scripts/views/fields/field-view-read-only.js index e4536a21..9a78aecd 100644 --- a/app/scripts/views/fields/field-view-read-only.js +++ b/app/scripts/views/fields/field-view-read-only.js @@ -5,7 +5,7 @@ class FieldViewReadOnly extends FieldView { readonly = true; renderValue(value) { - value = value.isProtected ? new Array(value.textLength + 1).join('•') : escape(value); + value = value?.isProtected ? new Array(value.textLength + 1).join('•') : escape(value); value = value.replace(/\n/g, '
'); return value; } diff --git a/app/scripts/views/settings/settings-file-external-view.js b/app/scripts/views/settings/settings-file-otp-device-view.js similarity index 78% rename from app/scripts/views/settings/settings-file-external-view.js rename to app/scripts/views/settings/settings-file-otp-device-view.js index 47ab3763..b11a61d4 100644 --- a/app/scripts/views/settings/settings-file-external-view.js +++ b/app/scripts/views/settings/settings-file-otp-device-view.js @@ -1,8 +1,8 @@ import { View } from 'framework/views/view'; import { Events } from 'framework/events'; -import template from 'templates/settings/settings-file-external.hbs'; +import template from 'templates/settings/settings-file-otp-device.hbs'; -class SettingsFileExternalView extends View { +class SettingsFileOtpDeviceView extends View { template = template; events = { @@ -26,4 +26,4 @@ class SettingsFileExternalView extends View { } } -export { SettingsFileExternalView }; +export { SettingsFileOtpDeviceView }; diff --git a/app/scripts/views/settings/settings-view.js b/app/scripts/views/settings/settings-view.js index be77ef45..3db99a56 100644 --- a/app/scripts/views/settings/settings-view.js +++ b/app/scripts/views/settings/settings-view.js @@ -33,8 +33,8 @@ class SettingsView extends View { setPage(e) { let { page, section, file } = e; - if (page === 'file' && file && file.external) { - page = 'file-external'; + if (page === 'file' && file && file.backend === 'otp-device') { + page = 'file-otp-device'; } const module = require('./settings-' + page + '-view'); const viewName = StringFormat.pascalCase(page); diff --git a/app/templates/details/details.hbs b/app/templates/details/details.hbs index cbbbb78e..98529b5b 100644 --- a/app/templates/details/details.hbs +++ b/app/templates/details/details.hbs @@ -4,11 +4,11 @@
- {{#unless readOnly}} + {{#if canEditColor}} @@ -17,18 +17,18 @@ - {{/unless}} + {{/if}}

{{#if title}}{{title}}{{else}}(no title){{/if}}

{{#if customIcon}}
{{else}} {{/if}}
@@ -42,9 +42,9 @@
- {{#unless readOnly}} -
-
+
+
+ {{#if showButtons}}
{{#if deleted~}} @@ -67,7 +67,7 @@ {{/each}}
- {{/unless}} + {{/if}}

{{res 'detDropAttachments'}}

diff --git a/app/templates/settings/settings-file-external.hbs b/app/templates/settings/settings-file-otp-device.hbs similarity index 100% rename from app/templates/settings/settings-file-external.hbs rename to app/templates/settings/settings-file-otp-device.hbs