diff --git a/app/scripts/auto-type/auto-type-filter.js b/app/scripts/auto-type/auto-type-filter.js deleted file mode 100644 index 335fd3e8..00000000 --- a/app/scripts/auto-type/auto-type-filter.js +++ /dev/null @@ -1,90 +0,0 @@ -import { SearchResultCollection } from 'collections/search-result-collection'; -import { Ranking } from 'util/data/ranking'; - -const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/; - -const AutoTypeFilter = function (windowInfo, appModel) { - this.title = windowInfo.title; - this.url = windowInfo.url; - this.text = ''; - this.ignoreWindowInfo = false; - this.appModel = appModel; -}; - -AutoTypeFilter.prototype.getEntries = function () { - const filter = { - text: this.text, - autoType: true - }; - this.prepareFilter(); - let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]); - if (!this.ignoreWindowInfo) { - entries = entries.filter((e) => e[1]); - } - entries = entries.sort((x, y) => - x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1] - ); - entries = entries.map((p) => p[0]); - return new SearchResultCollection(entries, { comparator: 'none' }); -}; - -AutoTypeFilter.prototype.hasWindowInfo = function () { - return this.title || this.url; -}; - -AutoTypeFilter.prototype.prepareFilter = function () { - this.titleLower = this.title ? this.title.toLowerCase() : null; - this.urlLower = this.url ? this.url.toLowerCase() : null; - this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null; -}; - -AutoTypeFilter.prototype.getEntryRank = function (entry) { - let rank = 0; - if (this.titleLower && entry.title) { - rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower); - } - if (this.urlParts) { - if (entry.url) { - const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase()); - if (entryUrlParts) { - const [, scheme, domain, path] = entryUrlParts; - const [, thisScheme, thisDomain, thisPath] = this.urlParts; - if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) { - if (domain === thisDomain) { - rank += 20; - } else { - rank += 10; - } - if (path === thisPath) { - rank += 10; - } else if (path && thisPath) { - if (path.lastIndexOf(thisPath, 0) === 0) { - rank += 5; - } else if (thisPath.lastIndexOf(path, 0) === 0) { - rank += 3; - } - } - if (scheme === thisScheme) { - rank += 1; - } - } else { - if (entry.searchText.indexOf(this.urlLower) >= 0) { - // the url is in some field; include it - rank += 5; - } else { - // another domain; don't show this record at all, ignore title match - return 0; - } - } - } - } else { - if (entry.searchText.indexOf(this.urlLower) >= 0) { - // the url is in some field; include it - rank += 5; - } - } - } - return rank; -}; - -export { AutoTypeFilter }; diff --git a/app/scripts/auto-type/index.js b/app/scripts/auto-type/index.js index 526f194f..2c51faa6 100644 --- a/app/scripts/auto-type/index.js +++ b/app/scripts/auto-type/index.js @@ -1,7 +1,7 @@ import { Events } from 'framework/events'; -import { AutoTypeFilter } from 'auto-type/auto-type-filter'; import { AutoTypeHelper } from 'auto-type/auto-type-helper'; import { AutoTypeParser } from 'auto-type/auto-type-parser'; +import { SelectEntryFilter } from 'comp/app/select-entry-filter'; import { Launcher } from 'comp/launcher'; import { Features } from 'util/features'; import { Alerts } from 'comp/ui/alerts'; @@ -27,7 +27,6 @@ const AutoType = { return; } Events.on('auto-type', (e) => this.handleEvent(e)); - Events.on('main-window-blur', (e) => this.mainWindowBlur(e)); }, handleEvent(e) { @@ -58,12 +57,6 @@ const AutoType = { } }, - mainWindowBlur() { - if (this.selectEntryView) { - this.selectEntryView.emit('result', undefined); - } - }, - runAndHandleResult(result, windowId) { this.run(result, windowId, (err) => { if (err) { @@ -219,7 +212,14 @@ const AutoType = { selectEntryAndRun() { this.getActiveWindowInfo(async (e, windowInfo) => { - const filter = new AutoTypeFilter(windowInfo, AppModel.instance); + const filter = new SelectEntryFilter( + windowInfo, + AppModel.instance, + AppModel.instance.files, + { + autoType: true + } + ); const evt = { filter, windowInfo }; if (!AppModel.instance.files.hasOpenFiles()) { logger.debug('auto-type event delayed'); @@ -254,8 +254,18 @@ const AutoType = { return; } this.focusMainWindow(); - evt.filter.ignoreWindowInfo = true; - this.selectEntryView = new SelectEntryView({ filter: evt.filter }); + + const humanReadableTarget = evt.filter.title || evt.filter.url; + const topMessage = humanReadableTarget + ? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget) + : Locale.autoTypeMsgNoWindow; + + this.selectEntryView = new SelectEntryView({ + isAutoType: true, + itemOptions: true, + filter: evt.filter, + topMessage + }); this.selectEntryView.on('result', (result) => { logger.debug('Entry selected', result); this.selectEntryView.off('result'); @@ -277,6 +287,7 @@ const AutoType = { try { await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage'); } catch { + this.selectEntryView.emit('result', undefined); return; } this.selectEntryView.show(); diff --git a/app/scripts/comp/app/select-entry-filter.js b/app/scripts/comp/app/select-entry-filter.js new file mode 100644 index 00000000..9f20478d --- /dev/null +++ b/app/scripts/comp/app/select-entry-filter.js @@ -0,0 +1,89 @@ +import { SearchResultCollection } from 'collections/search-result-collection'; +import { Ranking } from 'util/data/ranking'; + +const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/; + +class SelectEntryFilter { + constructor(windowInfo, appModel, files, filterOptions) { + this.title = windowInfo.title; + this.url = windowInfo.url; + this.text = ''; + this.appModel = appModel; + this.files = files; + this.filterOptions = filterOptions; + } + + getEntries() { + const filter = { + text: this.text, + ...this.filterOptions + }; + this._prepareFilter(); + let entries = this.appModel + .getEntriesByFilter(filter, this.files) + .map((e) => [e, this._getEntryRank(e)]); + entries = entries.filter((e) => e[1]); + entries = entries.sort((x, y) => + x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1] + ); + entries = entries.map((p) => p[0]); + return new SearchResultCollection(entries, { comparator: 'none' }); + } + + _prepareFilter() { + this.titleLower = this.title ? this.title.toLowerCase() : null; + this.urlLower = this.url ? this.url.toLowerCase() : null; + this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null; + } + + _getEntryRank(entry) { + let rank = 0; + if (this.titleLower && entry.title) { + rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower); + } + if (this.urlParts) { + if (entry.url) { + const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase()); + if (entryUrlParts) { + const [, scheme, domain, path] = entryUrlParts; + const [, thisScheme, thisDomain, thisPath] = this.urlParts; + if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) { + if (domain === thisDomain) { + rank += 20; + } else { + rank += 10; + } + if (path === thisPath) { + rank += 10; + } else if (path && thisPath) { + if (path.lastIndexOf(thisPath, 0) === 0) { + rank += 5; + } else if (thisPath.lastIndexOf(path, 0) === 0) { + rank += 3; + } + } + if (scheme === thisScheme) { + rank += 1; + } + } else { + if (entry.searchText.indexOf(this.urlLower) >= 0) { + // the url is in some field; include it + rank += 5; + } else { + // another domain; don't show this record at all, ignore title match + return 0; + } + } + } + } else { + if (entry.searchText.indexOf(this.urlLower) >= 0) { + // the url is in some field; include it + rank += 5; + } + } + } + return rank; + } +} + +export { SelectEntryFilter }; diff --git a/app/scripts/comp/extension/protocol-impl.js b/app/scripts/comp/extension/protocol-impl.js index ad26a6d8..e5da9648 100644 --- a/app/scripts/comp/extension/protocol-impl.js +++ b/app/scripts/comp/extension/protocol-impl.js @@ -14,6 +14,8 @@ import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-vie import { RuntimeDataModel } from 'models/runtime-data-model'; import { AppSettingsModel } from 'models/app-settings-model'; import { Timeouts } from 'const/timeouts'; +import { SelectEntryView } from 'views/select/select-entry-view'; +import { SelectEntryFilter } from 'comp/app/select-entry-filter'; const KeeWebAssociationId = 'KeeWeb'; const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb') @@ -409,10 +411,30 @@ const ProtocolHandlers = { throw new Error('Empty url'); } - // const files = getAvailableFiles(request); + const files = getAvailableFiles(request); const client = getClient(request); - // throw makeError(Errors.noMatches); + const filter = new SelectEntryFilter({ url: payload.url }, appModel, files); + const entries = filter.getEntries(); + if (!entries.length) { + throw makeError(Errors.noMatches); + } + + const topMessage = Locale.extensionSelectPasswordFor.replace( + '{}', + getHumanReadableExtensionName(client) + ); + const selectEntryView = new SelectEntryView({ + filter, + topMessage + }); + + const result = await selectEntryView.showAndGetResult(); + if (!result?.entry) { + throw makeError(Errors.userRejected); + } + + const entry = result.entry; client.stats.passwordsRead++; @@ -423,13 +445,13 @@ const ProtocolHandlers = { count: 1, entries: [ { - group: 'Group1', - login: 'urls-user', - name: 'URLS', - password: 'urls-passs', + group: entry.group.title, + login: entry.user || '', + name: entry.title || '', + password: entry.password?.getText() || '', skipAutoSubmit: 'false', stringFields: [], - uuid: '7cfc6ceb56674f26bad6ff79d73a06f5' + uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes) } ], id: '' diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 150ad0c6..2509343f 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -154,6 +154,7 @@ "keyChangeMessageExpired": "Master key for this database is expired. Please enter a new key", "keyChangeRepeatPassword": "Password, once again", "keyEnter": "Enter", + "keyEsc": "Esc", "iconFavTitle": "Download and use website favicon", "iconSelCustom": "Select a custom icon", @@ -797,5 +798,10 @@ "extensionSaveEntryHeader": "Save password", "extensionSaveEntryBody": "{} is trying to save a password. Allow this?", "extensionSaveEntryAuto": "Save other passwords automatically in this session", - "extensionSaveEntryNewGroup": "new group" + "extensionSaveEntryNewGroup": "new group", + "extensionSelectPasswordFor": "Select a password for {}", + + "selectEntryHeader": "Select entry", + "selectEntryEnterHint": "use the highlighted entry", + "selectEntryEscHint": "cancel" } diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 643c9b81..ff7b2c01 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -321,7 +321,7 @@ class AppModel { } getEntries() { - const entries = this.getEntriesByFilter(this.filter); + const entries = this.getEntriesByFilter(this.filter, this.files); entries.sortEntries(this.sort, this.filter); if (this.filter.trash) { this.addTrashGroups(entries); @@ -329,15 +329,15 @@ class AppModel { return entries; } - getEntriesByFilter(filter) { + getEntriesByFilter(filter, files) { const preparedFilter = this.prepareFilter(filter); const entries = new SearchResultCollection(); - const devicesToMatchOtpEntries = this.files.filter((file) => file.backend === 'otp-device'); + const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device'); const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined; - this.files + files .filter((file) => file.backend !== 'otp-device') .forEach((file) => { file.forEachEntry(preparedFilter, (entry) => { diff --git a/app/scripts/views/select/select-entry-view.js b/app/scripts/views/select/select-entry-view.js index bed3dde4..fc9fa219 100644 --- a/app/scripts/views/select/select-entry-view.js +++ b/app/scripts/views/select/select-entry-view.js @@ -14,7 +14,7 @@ import itemTemplate from 'templates/select/select-entry-item.hbs'; class SelectEntryView extends View { parent = 'body'; - modal = 'auto-type'; + modal = 'select-entry'; template = template; @@ -32,62 +32,74 @@ class SelectEntryView extends View { constructor(model) { super(model); this.initScroll(); - this.listenTo(Events, 'keypress:auto-type', this.keyPressed); + this.listenTo(Events, 'main-window-blur', this.mainWindowBlur); + this.listenTo(Events, 'keypress:select-entry', this.keyPressed); this.setupKeys(); } setupKeys() { - this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, false, 'auto-type'); - this.onKey(Keys.DOM_VK_RETURN, this.enterPressed, false, 'auto-type'); - this.onKey( - Keys.DOM_VK_RETURN, - this.actionEnterPressed, - KeyHandler.SHORTCUT_ACTION, - 'auto-type' - ); - this.onKey(Keys.DOM_VK_RETURN, this.optEnterPressed, KeyHandler.SHORTCUT_OPT, 'auto-type'); - this.onKey( - Keys.DOM_VK_RETURN, - this.shiftEnterPressed, - KeyHandler.SHORTCUT_SHIFT, - 'auto-type' - ); - this.onKey(Keys.DOM_VK_UP, this.upPressed, false, 'auto-type'); - this.onKey(Keys.DOM_VK_DOWN, this.downPressed, false, 'auto-type'); - this.onKey(Keys.DOM_VK_BACK_SPACE, this.backSpacePressed, false, 'auto-type'); - this.onKey(Keys.DOM_VK_O, this.openKeyPressed, KeyHandler.SHORTCUT_ACTION, 'auto-type'); + this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, false, 'select-entry'); + this.onKey(Keys.DOM_VK_RETURN, this.enterPressed, false, 'select-entry'); + if (this.model.isAutoType) { + this.onKey( + Keys.DOM_VK_RETURN, + this.actionEnterPressed, + KeyHandler.SHORTCUT_ACTION, + 'select-entry' + ); + this.onKey( + Keys.DOM_VK_RETURN, + this.optEnterPressed, + KeyHandler.SHORTCUT_OPT, + 'select-entry' + ); + this.onKey( + Keys.DOM_VK_RETURN, + this.shiftEnterPressed, + KeyHandler.SHORTCUT_SHIFT, + 'select-entry' + ); + this.onKey( + Keys.DOM_VK_O, + this.openKeyPressed, + KeyHandler.SHORTCUT_ACTION, + 'select-entry' + ); + } + this.onKey(Keys.DOM_VK_UP, this.upPressed, false, 'select-entry'); + this.onKey(Keys.DOM_VK_DOWN, this.downPressed, false, 'select-entry'); + this.onKey(Keys.DOM_VK_BACK_SPACE, this.backSpacePressed, false, 'select-entry'); } render() { - let topMessage; - if (this.model.filter.title || this.model.filter.url) { - topMessage = Locale.autoTypeMsgMatchedByWindow.replace( - '{}', - this.model.filter.title || this.model.filter.url - ); - } else { - topMessage = Locale.autoTypeMsgNoWindow; - } const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale'; this.entries = this.model.filter.getEntries(); this.result = this.entries[0]; + const presenter = new EntryPresenter(null, noColor, this.result && this.result.id); + presenter.itemOptions = this.model.itemOptions; + let itemsHtml = ''; const itemTemplate = this.itemTemplate; this.entries.forEach((entry) => { presenter.present(entry); itemsHtml += itemTemplate(presenter, DefaultTemplateOptions); }); + super.render({ + isAutoType: this.model.isAutoType, filterText: this.model.filter.text, - topMessage, + topMessage: this.model.topMessage, itemsHtml, actionSymbol: Shortcuts.actionShortcutSymbol(true), altSymbol: Shortcuts.altShortcutSymbol(true), shiftSymbol: Shortcuts.shiftShortcutSymbol(true), - keyEnter: Locale.keyEnter + keyEnter: Locale.keyEnter, + keyEsc: Locale.keyEsc }); + document.activeElement.blur(); + this.createScroll({ root: this.$el.find('.select-entry__items')[0], scroller: this.$el.find('.scroller')[0], @@ -167,6 +179,10 @@ class SelectEntryView extends View { } } + mainWindowBlur() { + this.emit('result', undefined); + } + keyPressed(e) { if (e.which && e.which !== Keys.DOM_VK_RETURN) { this.model.filter.text += String.fromCharCode(e.which); @@ -219,6 +235,9 @@ class SelectEntryView extends View { if (event) { event.stopImmediatePropagation(); } + if (!this.model.itemOptions) { + return; + } const id = itemEl.data('id'); const entry = this.entries.get(id); @@ -307,6 +326,16 @@ class SelectEntryView extends View { const sequence = e.item; this.closeWithResult(sequence); } + + showAndGetResult() { + this.render(); + return new Promise((resolve) => { + this.once('result', (result) => { + this.remove(); + resolve(result); + }); + }); + } } Object.assign(SelectEntryView.prototype, Scrollable); diff --git a/app/templates/select/select-entry-item.hbs b/app/templates/select/select-entry-item.hbs index 6006bb80..d6fa6d40 100644 --- a/app/templates/select/select-entry-item.hbs +++ b/app/templates/select/select-entry-item.hbs @@ -10,7 +10,9 @@ {{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}} {{user}} {{url}} - - - + {{#if itemOptions}} + + + + {{/if}} diff --git a/app/templates/select/select-entry.hbs b/app/templates/select/select-entry.hbs index 9c3cb833..10ca8744 100644 --- a/app/templates/select/select-entry.hbs +++ b/app/templates/select/select-entry.hbs @@ -1,17 +1,28 @@
-

{{res 'autoTypeHeader'}}

+

+ {{#if isAutoType}} + {{res 'autoTypeHeader'}} + {{else}} + {{res 'selectEntryHeader'}} + {{/if}} +

-
{{keyEnter}}: {{res 'autoTypeSelectionHint'}}
-
{{actionSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintAction'}}
-
{{altSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintOpt'}}
-
{{shiftSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintShift'}}
+ {{#if isAutoType}} +
{{keyEnter}}: {{res 'autoTypeSelectionHint'}}
+
{{actionSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintAction'}}
+
{{altSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintOpt'}}
+
{{shiftSymbol}} {{keyEnter}}: {{res 'autoTypeSelectionHintShift'}}
+ {{else}} +
{{keyEnter}}: {{res 'selectEntryEnterHint'}}
+
{{keyEsc}}: {{res 'selectEntryEscHint'}}
+ {{/if}}
{{#if filterText}} -
- - -
+
+ + +
{{/if}}