2018-02-28 21:15:10 +01:00
import * as lunr from "lunr" ;
import { CipherService } from "../abstractions/cipher.service" ;
2021-04-07 16:51:34 +02:00
import { I18nService } from "../abstractions/i18n.service" ;
2020-12-11 17:44:57 +01:00
import { LogService } from "../abstractions/log.service" ;
2018-02-28 21:15:10 +01:00
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service" ;
2018-09-12 16:22:46 +02:00
import { CipherType } from "../enums/cipherType" ;
2018-08-13 15:42:52 +02:00
import { FieldType } from "../enums/fieldType" ;
2018-09-04 03:51:19 +02:00
import { UriMatchType } from "../enums/uriMatchType" ;
2022-02-22 15:39:11 +01:00
import { CipherView } from "../models/view/cipherView" ;
2021-01-23 02:54:22 +01:00
import { SendView } from "../models/view/sendView" ;
2018-08-20 14:41:12 +02:00
2018-02-28 21:15:10 +01:00
export class SearchService implements SearchServiceAbstraction {
2021-04-22 21:53:45 +02:00
indexedEntityId? : string = null ;
2018-08-13 15:42:52 +02:00
private indexing = false ;
private index : lunr.Index = null ;
2021-04-07 16:51:34 +02:00
private searchableMinLength = 2 ;
2018-08-13 15:42:52 +02:00
2021-04-07 16:51:34 +02:00
constructor (
private cipherService : CipherService ,
private logService : LogService ,
private i18nService : I18nService
) {
if ( [ "zh-CN" , "zh-TW" ] . indexOf ( i18nService . locale ) !== - 1 ) {
this . searchableMinLength = 1 ;
2019-04-12 15:56:15 +02:00
}
2022-05-19 13:15:08 +02:00
//register lunr pipeline function
lunr . Pipeline . registerFunction ( this . normalizeAccentsPipelineFunction , "normalizeAccents" ) ;
2021-12-16 13:36:21 +01:00
}
2018-02-28 21:15:10 +01:00
2018-08-13 15:42:52 +02:00
clearIndex ( ) : void {
2021-04-23 20:55:57 +02:00
this . indexedEntityId = null ;
2018-08-13 15:42:52 +02:00
this . index = null ;
2021-12-16 13:36:21 +01:00
}
2018-08-13 17:52:55 +02:00
isSearchable ( query : string ) : boolean {
2022-05-27 15:50:08 +02:00
query = SearchService . normalizeSearchQuery ( query ) ;
2021-04-07 16:51:34 +02:00
const notSearchable =
query == null ||
( this . index == null && query . length < this . searchableMinLength ) ||
( this . index != null && query . length < this . searchableMinLength && query . indexOf ( ">" ) !== 0 ) ;
2018-08-13 17:52:55 +02:00
return ! notSearchable ;
2018-02-28 21:15:10 +01:00
}
2021-04-07 16:51:34 +02:00
async indexCiphers ( indexedEntityId? : string , ciphers? : CipherView [ ] ) : Promise < void > {
if ( this . indexing ) {
2018-08-13 17:52:55 +02:00
return ;
}
2021-04-22 21:53:45 +02:00
this . logService . time ( "search indexing" ) ;
2018-08-13 15:42:52 +02:00
this . indexing = true ;
2021-04-22 21:53:45 +02:00
this . indexedEntityId = indexedEntityId ;
2018-08-13 15:42:52 +02:00
this . index = null ;
2018-02-28 21:15:10 +01:00
const builder = new lunr . Builder ( ) ;
2022-05-19 13:15:08 +02:00
builder . pipeline . add ( this . normalizeAccentsPipelineFunction ) ;
2018-02-28 21:15:10 +01:00
builder . ref ( "id" ) ;
2021-04-22 21:53:45 +02:00
builder . field ( "shortid" , { boost : 100 , extractor : ( c : CipherView ) = > c . id . substr ( 0 , 8 ) } ) ;
2022-05-19 13:15:08 +02:00
builder . field ( "name" , {
boost : 10 ,
} ) ;
2020-04-14 21:16:18 +02:00
builder . field ( "subtitle" , {
2018-09-12 16:22:46 +02:00
boost : 5 ,
2021-04-22 21:53:45 +02:00
extractor : ( c : CipherView ) = > {
2018-08-13 15:42:52 +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 ;
2021-12-16 13:36:21 +01:00
} ,
} ) ;
2018-02-28 21:15:10 +01:00
builder . field ( "notes" ) ;
2020-04-14 21:16:18 +02:00
builder . field ( "login.username" , {
extractor : ( c : CipherView ) = >
2019-01-25 15:30:21 +01:00
c . type === CipherType . Login && c . login != null ? c.login.username : null ,
2021-12-16 13:36:21 +01:00
} ) ;
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 ) ,
2021-12-16 13:36:21 +01:00
} ) ;
2020-04-14 21:16:18 +02:00
builder . field ( "attachments_joined" , {
2018-08-13 22:00:21 +02:00
extractor : ( c : CipherView ) = > this . attachmentExtractor ( c , true ) ,
2021-12-16 13:36:21 +01:00
} ) ;
2020-04-14 21:16:18 +02:00
builder . field ( "organizationid" , { extractor : ( c : CipherView ) = > c . organizationId } ) ;
2021-04-02 23:32:30 +02:00
ciphers = ciphers || ( await this . cipherService . getAllDecrypted ( ) ) ;
2021-02-04 16:49:23 +01:00
ciphers . forEach ( ( c ) = > builder . add ( c ) ) ;
2018-02-28 21:15:10 +01:00
this . index = builder . build ( ) ;
2020-12-11 17:44:57 +01:00
2020-04-14 21:16:18 +02:00
this . indexing = false ;
2021-04-23 20:55:57 +02:00
2018-08-13 15:42:52 +02:00
this . logService . timeEnd ( "search indexing" ) ;
2021-12-16 13:36:21 +01:00
}
2020-12-11 17:44:57 +01:00
async searchCiphers (
query : string ,
2020-08-12 21:42:42 +02:00
filter : ( ( cipher : CipherView ) = > boolean ) | ( ( cipher : CipherView ) = > boolean ) [ ] = null ,
2020-04-08 22:44:13 +02:00
ciphers : CipherView [ ] = null
2020-12-11 17:44:57 +01:00
) : Promise < CipherView [ ] > {
2018-02-28 21:15:10 +01:00
const results : CipherView [ ] = [ ] ;
2020-12-11 17:44:57 +01:00
if ( query != null ) {
2022-05-27 15:50:08 +02:00
query = SearchService . normalizeSearchQuery ( query . trim ( ) . toLowerCase ( ) ) ;
2021-12-16 13:36:21 +01:00
}
2018-08-13 15:42:52 +02:00
if ( query === "" ) {
query = null ;
2018-02-28 21:15:10 +01:00
}
2018-08-17 05:32:37 +02:00
if ( ciphers == null ) {
ciphers = await this . cipherService . getAllDecrypted ( ) ;
}
2020-04-03 22:32:15 +02:00
2020-04-08 22:44:13 +02:00
if ( filter != null && Array . isArray ( filter ) && filter . length > 0 ) {
2021-02-04 16:49:23 +01:00
ciphers = ciphers . filter ( ( c ) = > filter . every ( ( f ) = > f == null || f ( c ) ) ) ;
2020-04-08 22:44:13 +02:00
} else if ( filter != null ) {
ciphers = ciphers . filter ( filter as ( cipher : CipherView ) = > boolean ) ;
}
2018-08-13 15:42:52 +02:00
2018-08-13 17:52:55 +02:00
if ( ! this . isSearchable ( query ) ) {
2018-08-13 15:42:52 +02:00
return ciphers ;
}
2019-07-23 14:28:53 +02:00
if ( this . indexing ) {
2021-02-04 16:49:23 +01:00
await new Promise ( ( r ) = > setTimeout ( r , 250 ) ) ;
2019-07-23 14:28:53 +02:00
if ( this . indexing ) {
2021-02-04 16:49:23 +01:00
await new Promise ( ( r ) = > setTimeout ( r , 500 ) ) ;
2019-07-23 14:28:53 +02:00
}
}
2018-08-17 05:32:37 +02:00
const index = this . getIndexForSearch ( ) ;
if ( index == null ) {
2018-08-13 15:42:52 +02:00
// Fall back to basic search if index is not available
2018-08-13 20:28:10 +02:00
return this . searchCiphersBasic ( ciphers , query ) ;
2018-02-28 21:15:10 +01:00
}
const ciphersMap = new Map < string , CipherView > ( ) ;
2021-02-04 16:49:23 +01:00
ciphers . forEach ( ( c ) = > ciphersMap . set ( c . id , c ) ) ;
2018-02-28 21:15:10 +01:00
2018-08-13 15:42:52 +02:00
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 ( ) ) ;
2021-10-19 10:32:14 +02:00
} catch ( e ) {
this . logService . error ( e ) ;
}
2018-08-13 15:42:52 +02:00
} else {
2018-09-04 03:51:19 +02:00
const soWild = lunr . Query . wildcard . LEADING | lunr . Query . wildcard . TRAILING ;
2021-02-04 16:49:23 +01:00
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 , { } ) ;
2018-08-13 15:42:52 +02:00
} ) ;
} ) ;
}
2018-02-28 21:15:10 +01:00
2018-08-13 15:42:52 +02:00
if ( searchResults != null ) {
2021-02-04 16:49:23 +01:00
searchResults . forEach ( ( r ) = > {
2018-08-13 15:42:52 +02:00
if ( ciphersMap . has ( r . ref ) ) {
results . push ( ciphersMap . get ( r . ref ) ) ;
}
} ) ;
2018-02-28 21:15:10 +01:00
}
2018-08-13 15:42:52 +02:00
return results ;
2021-12-16 13:36:21 +01:00
}
2018-08-13 20:28:10 +02:00
2022-02-22 15:39:11 +01:00
searchCiphersBasic ( ciphers : CipherView [ ] , query : string , deleted = false ) {
2022-05-27 15:50:08 +02:00
query = SearchService . normalizeSearchQuery ( query . trim ( ) . toLowerCase ( ) ) ;
2021-02-04 16:49:23 +01:00
return ciphers . filter ( ( c ) = > {
2020-04-03 22:32:15 +02:00
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
2021-01-23 02:54:22 +01:00
searchSends ( sends : SendView [ ] , query : string ) {
2022-05-27 15:50:08 +02:00
query = SearchService . normalizeSearchQuery ( query . trim ( ) . toLocaleLowerCase ( ) ) ;
2022-05-23 11:20:30 +02:00
if ( query === null ) {
return sends ;
}
const sendsMatched : SendView [ ] = [ ] ;
const lowPriorityMatched : SendView [ ] = [ ] ;
sends . forEach ( ( s ) = > {
2021-01-23 02:54:22 +01:00
if ( s . name != null && s . name . toLowerCase ( ) . indexOf ( query ) > - 1 ) {
2022-05-23 11:20:30 +02:00
sendsMatched . push ( s ) ;
} else if (
2021-01-29 22:08:52 +01:00
query . length >= 8 &&
( s . id . startsWith ( query ) ||
s . accessId . toLocaleLowerCase ( ) . startsWith ( query ) ||
( s . file ? . id != null && s . file . id . startsWith ( query ) ) )
) {
2022-05-23 11:20:30 +02:00
lowPriorityMatched . push ( s ) ;
} else if ( s . notes != null && s . notes . toLowerCase ( ) . indexOf ( query ) > - 1 ) {
lowPriorityMatched . push ( s ) ;
} else if ( s . text ? . text != null && s . text . text . toLowerCase ( ) . indexOf ( query ) > - 1 ) {
lowPriorityMatched . push ( s ) ;
} else if ( s . file ? . fileName != null && s . file . fileName . toLowerCase ( ) . indexOf ( query ) > - 1 ) {
lowPriorityMatched . push ( s ) ;
2021-01-23 02:54:22 +01:00
}
} ) ;
2022-05-23 11:20:30 +02:00
return sendsMatched . concat ( lowPriorityMatched ) ;
2021-01-23 02:54:22 +01:00
}
2018-08-17 05:32:37 +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 [ ] = [ ] ;
2021-02-04 16:49:23 +01:00
c . fields . forEach ( ( f ) = > {
2018-08-13 22:00:21 +02:00
if ( f . name != null ) {
fields . push ( f . name ) ;
}
if ( f . type === FieldType . Text && f . value != null ) {
fields . push ( f . value ) ;
}
} ) ;
2021-02-04 16:49:23 +01:00
fields = fields . filter ( ( f ) = > f . trim ( ) !== "" ) ;
2018-08-13 22:00:21 +02:00
if ( fields . length === 0 ) {
return null ;
}
return joined ? fields . join ( " " ) : fields ;
2021-12-16 13:36:21 +01:00
}
2018-08-13 22:00:21 +02:00
private attachmentExtractor ( c : CipherView , joined : boolean ) {
if ( ! c . hasAttachments ) {
return null ;
}
let attachments : string [ ] = [ ] ;
2021-02-04 16:49:23 +01:00
c . attachments . forEach ( ( a ) = > {
2018-08-13 22:00:21 +02:00
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 ) ;
}
2021-12-16 13:36:21 +01:00
}
} ) ;
2018-08-13 22:00:21 +02:00
attachments = attachments . filter ( ( f ) = > f . trim ( ) !== "" ) ;
if ( attachments . length === 0 ) {
return null ;
}
return joined ? attachments . join ( " " ) : attachments ;
2021-12-16 13:36:21 +01:00
}
2018-09-04 03:51:19 +02:00
private uriExtractor ( c : CipherView ) {
if ( c . type !== CipherType . Login || c . login == null || ! c . login . hasUris ) {
return null ;
}
const uris : string [ ] = [ ] ;
2021-02-04 16:49:23 +01:00
c . login . uris . forEach ( ( u ) = > {
2019-01-25 15:30:21 +01:00
if ( u . uri == null || u . uri === "" ) {
2021-12-16 13:36:21 +01:00
return ;
}
2018-09-04 03:51:19 +02:00
if ( u . hostname != null ) {
uris . push ( u . hostname ) ;
2021-12-16 13:36:21 +01:00
return ;
}
2018-09-04 03:51:19 +02:00
let uri = u . uri ;
if ( u . match !== UriMatchType . RegularExpression ) {
const protocolIndex = uri . indexOf ( "://" ) ;
if ( protocolIndex > - 1 ) {
uri = uri . substr ( protocolIndex + 3 ) ;
2021-12-16 13:36:21 +01:00
}
2018-09-04 03:51:19 +02:00
const queryIndex = uri . search ( /\?|&|#/ ) ;
if ( queryIndex > - 1 ) {
uri = uri . substring ( 0 , queryIndex ) ;
2021-12-16 13:36:21 +01:00
}
}
2018-09-04 03:51:19 +02:00
uris . push ( uri ) ;
2021-12-16 13:36:21 +01:00
} ) ;
2018-09-04 03:51:19 +02:00
return uris . length > 0 ? uris : null ;
2021-12-16 13:36:21 +01:00
}
2022-05-19 13:15:08 +02:00
private normalizeAccentsPipelineFunction ( token : lunr.Token ) : any {
const searchableFields = [ "name" , "login.username" , "subtitle" , "notes" ] ;
const fields = ( token as any ) . metadata [ "fields" ] ;
const checkFields = fields . every ( ( i : any ) = > searchableFields . includes ( i ) ) ;
if ( checkFields ) {
2022-05-27 15:50:08 +02:00
return SearchService . normalizeSearchQuery ( token . toString ( ) ) ;
2022-05-19 13:15:08 +02:00
}
return token ;
}
2022-05-25 00:40:17 +02:00
// Remove accents/diacritics characters from text. This regex is equivalent to the Diacritic unicode property escape, i.e. it will match all diacritic characters.
2022-05-27 15:50:08 +02:00
static normalizeSearchQuery ( query : string ) : string {
2022-05-25 00:40:17 +02:00
return query ? . normalize ( "NFD" ) . replace ( /[\u0300-\u036f]/g , "" ) ;
}
2018-02-28 21:15:10 +01:00
}