extracted select-entry-filter

This commit is contained in:
antelle 2021-04-28 11:29:48 +02:00
parent 338f6d47d1
commit 98ea7a542d
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
9 changed files with 237 additions and 157 deletions

View File

@ -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 };

View File

@ -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();

View File

@ -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 };

View File

@ -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: ''

View File

@ -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"
}

View File

@ -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) => {

View File

@ -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);

View File

@ -10,7 +10,9 @@
<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>
<td>{{user}}</td>
<td>{{url}}</td>
<td class="select-entry__item-options">
<i class="fa fa-ellipsis-h"></i>
</td>
{{#if itemOptions}}
<td class="select-entry__item-options">
<i class="fa fa-ellipsis-h"></i>
</td>
{{/if}}
</tr>

View File

@ -1,17 +1,28 @@
<div class="select-entry">
<div class="select-entry__header">
<h1 class="select-entry__header-text">{{res 'autoTypeHeader'}}</h1>
<h1 class="select-entry__header-text">
{{#if isAutoType}}
{{res 'autoTypeHeader'}}
{{else}}
{{res 'selectEntryHeader'}}
{{/if}}
</h1>
<div class="select-entry__hint" id="select-entry__hint">
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'autoTypeSelectionHint'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{actionSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintAction'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{altSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintOpt'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{shiftSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintShift'}}</div>
{{#if isAutoType}}
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'autoTypeSelectionHint'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{actionSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintAction'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{altSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintOpt'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{shiftSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintShift'}}</div>
{{else}}
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'selectEntryEnterHint'}}</div>
<div class="select-entry__hint-text"><span class="shortcut">{{keyEsc}}</span>: {{res 'selectEntryEscHint'}}</div>
{{/if}}
</div>
{{#if filterText}}
<div class="select-entry__header-filter" id="select-entry__header-filter">
<input type="text" readonly value="{{filterText}}" class="select-entry__header-filter-input" />
<i class="select-entry__header-filter-clear fa fa-times"></i>
</div>
<div class="select-entry__header-filter" id="select-entry__header-filter">
<input type="text" readonly value="{{filterText}}" class="select-entry__header-filter-input" />
<i class="select-entry__header-filter-clear fa fa-times"></i>
</div>
{{/if}}
</div>
<div class="select-entry__message">