bitwarden-estensione-browser/src/services/search.service.ts

238 lines
8.4 KiB
TypeScript
Raw Normal View History

import * as lunr from 'lunr';
import { CipherView } from '../models/view/cipherView';
import { CipherService } from '../abstractions/cipher.service';
import { LogService } from '../abstractions/log.service';
import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service';
2018-09-12 16:22:46 +02:00
import { CipherType } from '../enums/cipherType';
import { FieldType } from '../enums/fieldType';
import { UriMatchType } from '../enums/uriMatchType';
2018-08-20 14:41:12 +02:00
export class SearchService implements SearchServiceAbstraction {
private indexing = false;
private index: lunr.Index = null;
constructor(private cipherService: CipherService, private logService: LogService) {
}
clearIndex(): void {
this.index = null;
}
2018-08-13 17:52:55 +02:00
isSearchable(query: string): boolean {
const notSearchable = query == null || (this.index == null && query.length < 2) ||
(this.index != null && query.length < 2 && query.indexOf('>') !== 0);
return !notSearchable;
}
async indexCiphers(): Promise<void> {
if (this.indexing) {
return;
}
this.logService.time('search indexing');
this.indexing = true;
this.index = null;
const builder = new lunr.Builder();
builder.ref('id');
2020-04-14 21:16:18 +02:00
builder.field('shortid', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
builder.field('name', { boost: 10 });
builder.field('subtitle', {
2018-09-12 16:22:46 +02:00
boost: 5,
extractor: (c: CipherView) => {
2018-09-12 16:27:21 +02:00
if (c.subTitle != null && c.type === CipherType.Card) {
return c.subTitle.replace(/\*/g, '');
2018-09-12 16:22:46 +02:00
}
return c.subTitle;
},
});
builder.field('notes');
2020-04-14 21:16:18 +02:00
builder.field('login.username', {
2019-01-25 15:30:21 +01:00
extractor: (c: CipherView) => c.type === CipherType.Login && c.login != null ? c.login.username : null,
});
2020-04-14 21:16:18 +02:00
builder.field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
builder.field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
builder.field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
builder.field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) });
builder.field('attachments_joined',
2018-08-13 22:00:21 +02:00
{ extractor: (c: CipherView) => this.attachmentExtractor(c, true) });
2020-04-14 21:16:18 +02:00
builder.field('organizationid', { extractor: (c: CipherView) => c.organizationId });
const ciphers = await this.cipherService.getAllDecrypted();
ciphers.forEach((c) => builder.add(c));
this.index = builder.build();
this.indexing = false;
this.logService.timeEnd('search indexing');
}
async searchCiphers(query: string,
filter: (((cipher: CipherView) => boolean) | (((cipher: CipherView) => boolean)[])) = null,
ciphers: CipherView[] = null):
Promise<CipherView[]> {
const results: CipherView[] = [];
if (query != null) {
query = query.trim().toLowerCase();
}
if (query === '') {
query = null;
}
if (ciphers == null) {
ciphers = await this.cipherService.getAllDecrypted();
}
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
2018-08-13 17:52:55 +02:00
if (!this.isSearchable(query)) {
return ciphers;
}
2019-07-23 14:28:53 +02:00
if (this.indexing) {
await new Promise((r) => setTimeout(r, 250));
if (this.indexing) {
await new Promise((r) => setTimeout(r, 500));
}
}
const index = this.getIndexForSearch();
if (index == null) {
// Fall back to basic search if index is not available
2018-08-13 20:28:10 +02:00
return this.searchCiphersBasic(ciphers, query);
}
const ciphersMap = new Map<string, CipherView>();
ciphers.forEach((c) => ciphersMap.set(c.id, c));
let searchResults: lunr.Index.Result[] = null;
const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0;
if (isQueryString) {
try {
2018-08-17 17:07:50 +02:00
searchResults = index.search(query.substr(1).trim());
} catch { }
} else {
// tslint:disable-next-line
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
searchResults = index.query((q) => {
lunr.tokenizer(query).forEach((token) => {
2019-01-28 17:06:28 +01:00
const t = token.toString();
q.term(t, { fields: ['name'], wildcard: soWild });
q.term(t, { fields: ['subtitle'], wildcard: soWild });
q.term(t, { fields: ['login.uris'], wildcard: soWild });
q.term(t, {});
});
});
}
if (searchResults != null) {
searchResults.forEach((r) => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
}
return results;
}
2018-08-13 20:28:10 +02:00
searchCiphersBasic(ciphers: CipherView[], query: string, deleted: boolean = false) {
2018-08-13 20:28:10 +02:00
query = query.trim().toLowerCase();
return ciphers.filter((c) => {
if (deleted !== c.isDeleted) {
return false;
}
2018-08-13 20:28:10 +02:00
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && c.id.startsWith(query)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
return true;
}
return false;
});
}
2018-08-13 22:00:21 +02:00
getIndexForSearch(): lunr.Index {
return this.index;
}
2018-08-13 22:00:21 +02:00
private fieldExtractor(c: CipherView, joined: boolean) {
if (!c.hasFields) {
return null;
}
let fields: string[] = [];
c.fields.forEach((f) => {
if (f.name != null) {
fields.push(f.name);
}
if (f.type === FieldType.Text && f.value != null) {
fields.push(f.value);
}
});
fields = fields.filter((f) => f.trim() !== '');
if (fields.length === 0) {
return null;
}
return joined ? fields.join(' ') : fields;
}
private attachmentExtractor(c: CipherView, joined: boolean) {
if (!c.hasAttachments) {
return null;
}
let attachments: string[] = [];
c.attachments.forEach((a) => {
if (a != null && a.fileName != null) {
if (joined && a.fileName.indexOf('.') > -1) {
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf('.')));
} else {
attachments.push(a.fileName);
}
}
});
attachments = attachments.filter((f) => f.trim() !== '');
if (attachments.length === 0) {
return null;
}
return joined ? attachments.join(' ') : attachments;
}
private uriExtractor(c: CipherView) {
2019-01-25 15:30:21 +01:00
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
return null;
}
const uris: string[] = [];
c.login.uris.forEach((u) => {
if (u.uri == null || u.uri === '') {
return;
}
if (u.hostname != null) {
uris.push(u.hostname);
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
const protocolIndex = uri.indexOf('://');
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);
}
const queryIndex = uri.search(/\?|&|#/);
if (queryIndex > -1) {
uri = uri.substring(0, queryIndex);
}
}
uris.push(uri);
});
return uris.length > 0 ? uris : null;
}
}