import angular from 'angular' ; import { Injectable } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import { compare } from 'compare-versions' ; import * as detectBrowser from 'detect-browser' ; import XRegExp from 'xregexp' ; import { AppEventType , RoutePath } from '../../app/app.enum' ; import { ClientDataNotFoundError } from '../errors/errors' ; import { ExceptionHandler } from '../errors/errors.interface' ; import Globals from '../global-shared.constants' ; import { BrowserName , PlatformType } from '../global-shared.enum' ; import { Url } from '../global-shared.interface' ; import { LogService } from '../log/log.service' ; import { NetworkService } from '../network/network.service' ; import { StoreKey } from '../store/store.enum' ; import { StoreService } from '../store/store.service' ; @ autobind @ Injectable ( 'UtilityService' ) export class UtilityService { $exceptionHandler : ExceptionHandler ; $http : ng . IHttpService ; $location : ng . ILocationService ; $q : ng . IQService ; $rootScope : ng . IRootScopeService ; logSvc : LogService ; networkSvc : NetworkService ; storeSvc : StoreService ; static $inject = [ '$exceptionHandler' , '$http' , '$location' , '$q' , '$rootScope' , 'LogService' , 'NetworkService' , 'StoreService' ] ; constructor ( $exceptionHandler , $http , $location , $q , $rootScope , LogSvc , NetworkSvc , StoreSvc ) { this . $exceptionHandler = $exceptionHandler ; this . $http = $http ; this . $location = $location ; this . $q = $q ; this . $rootScope = $rootScope ; this . logSvc = LogSvc ; this . networkSvc = NetworkSvc ; this . storeSvc = StoreSvc ; } asyncWhile < T = any > ( data , condition , action ) { const whilst = ( whilstData ) : ng . IPromise < T > => { return condition ( whilstData ) . then ( ( conditionIsTrue ) => conditionIsTrue ? action ( whilstData ) . then ( whilst ) : this . $q . resolve ( whilstData ) ) ; } ; return whilst ( data ) ; } broadcastEvent ( eventType , eventData ? ) { this . $rootScope . $broadcast ( eventType , eventData ) ; } checkCurrentRoute ( route ) { return this . $location . path ( ) . indexOf ( route ) === 0 ; } checkForNewVersion ( currentVersion ) { if ( ! this . networkSvc . isNetworkConnected ( ) ) { return this . $q . resolve ( '' ) ; } return this . $http . get < any > ( Globals . ReleaseLatestUrl ) . then ( ( response ) => { const latestVersion = this . getSemVerAlignedVersion ( response ?. data ?. tag_name ?? '' ) ; if ( ! this . compareVersions ( latestVersion , currentVersion , '>' , true ) ) { return '' ; } this . logSvc . logInfo ( `${ latestVersion } update available` ) ; return latestVersion ; } ) . catch ( ( ) => { this . logSvc . logInfo ( 'Couldn’t check for new version' ) ; return '' ; } ) ; } checkSyncCredentialsExist ( ) { return this . storeSvc . get ( [ StoreKey . Password , StoreKey . SyncId ] ) . then ( ( storeContent ) => { if ( ! storeContent . password || ! storeContent . syncId ) { throw new ClientDataNotFoundError ( ) ; } } ) ; } compareVersions ( firstVersion , secondVersion , operator , includeBeta = false ) { const regex = includeBeta ? /^[vV]?(\d+\.\d+\.\d+(-\w+\.\d+)?)(\.\d+)?$/ : /^[vV]?(\d+\.\d+\.\d+)(\.\d+|-\w+\.\d+)?$/ ; return compare ( firstVersion ?. replace ( regex , '$1' ) , secondVersion ?. replace ( regex , '$1' ) , operator as any ) ; } filterFalsyValues ( values ) { return values . filter ( ( x ) => x ) ; } getBrowserName ( ) { const browserName = detectBrowser . detect ( ) ?. name . replace ( 'edge-chromium' , BrowserName . Edge ) ?? BrowserName . Chrome ; return this . isBraveBrowser ( ) ? BrowserName . Brave : browserName ; } getDateTimeString ( date ) { if ( ! date ) { return '' ; } const second = `0${ date . getSeconds ( ) }` . slice ( - 2 ) ; const minute = `0${ date . getMinutes ( ) }` . slice ( - 2 ) ; const hour = `0${ date . getHours ( ) }` . slice ( - 2 ) ; const day = `0${ date . getDate ( ) }` . slice ( - 2 ) ; const month = `0${ date . getMonth ( ) + 1 }` . slice ( - 2 ) ; const year = date . getFullYear ( ) ; return year + month + day + hour + minute + second ; } getSemVerAlignedVersion ( version ) { return version . replace ( /^[vV]?(\d+\.\d+\.\d+)(\.\d+|-\w+\.\d+)?$/ , '$1' ) ; } getServiceUrl ( ) { return this . storeSvc . get < string > ( StoreKey . ServiceUrl ) . then ( ( cachedServiceUrl ) => { return cachedServiceUrl ?? Globals . URL . DefaultServiceUrl ; } ) ; } getSyncVersion ( ) { return this . storeSvc . get < string > ( StoreKey . SyncVersion ) ; } getTagArrayFromText ( tagText ) { if ( angular . isUndefined ( tagText ?? undefined ) ) { return ; } const tags = tagText . split ( /[,;]/ ) . filter ( ( x ) => x . length > Globals . LookaheadMinChars ) ; const cleanedTags = this . sortWords ( tags . filter ( ( x ) => ! ! x ?. trim ( ) ) . map ( ( x ) => x . trim ( ) ) ) ; return cleanedTags ; } getUniqueishId ( ) { return window . crypto . getRandomValues ( new Uint32Array ( 1 ) ) [ 0 ] . toString ( 36 ) ; } handleEvent ( eventHandler , ... args ) { try { this . $q . resolve ( ) . then ( ( ) => eventHandler ( ... args ) ) . catch ( this . $exceptionHandler ) ; } catch ( err ) { this . $exceptionHandler ( err ) ; } } isBraveBrowser ( ) { return ! angular . isUndefined ( window . navigator . brave ) ; } isMobilePlatform ( platformName ) { return platformName === PlatformType . Android ; } isSyncEnabled ( ) { return this . storeSvc . get < boolean > ( StoreKey . SyncEnabled ) ; } isTextInput ( element ) { return [ 'INPUT' , 'TEXTAREA' ] . indexOf ( element . nodeName ) !== - 1 ; } parseUrl ( url ) { const searchObject = { } ; const parser = document . createElement ( 'a' ) ; parser . href = url ; const queries = parser . search . replace ( /^\?/ , '' ) . split ( '&' ) ; let split ; for ( let i = 0 ; i < queries . length ; i += 1 ) { split = queries [ i ] . split ( '=' ) ; searchObject [ split [ 0 ] ] = split [ 1 ] ; } return { protocol : parser . protocol , host : parser . host , hostname : parser . hostname , port : parser . port , pathname : parser . pathname , search : parser . search , searchObject , hash : parser . hash } ; } sortWords ( words ) { return [ ... new Set ( words ) ] . sort ( ) ; } splitTextIntoWords ( text , locale ) { if ( angular . isUndefined ( text ?? undefined ) ) { return [ ] ; } const words = text ! . toLocaleLowerCase ( locale ) . replace ( /['"]/g , '' ) ; const wordsArr = words . split ( XRegExp ( '[^\\p{L}\\p{Nd}]' , 'g' ) ) ; return this . filterFalsyValues ( wordsArr ) ; } stopEventPropagation ( event ) { event ?. preventDefault ( ) ; ( event as any ) ?. srcEvent ?. stopPropagation ( ) ; } stringsAreEquivalent ( str1 = '' , str2 = '' , locale ? ) { return str1 . localeCompare ( str2 , locale , { sensitivity : 'base' } ) === 0 ; } stripTags ( input ) { return input ? input . replace ( /<(?:.|\n)*?>/gm , '' ) : input ; } syncIdIsValid ( syncId ) { if ( ! syncId ) { return false ; } const hexStringToBytes = ( hexString ) : Uint8Array => { const bytes = new Uint8Array ( hexString . length / 2 ) ; for ( let i = 0 ; i !== bytes . length ; i += 1 ) { bytes [ i ] = parseInt ( hexString . substr ( i * 2 , 2 ) , 16 ) ; } return bytes ; } ; const bytesToGuidString = ( bytes ) : string | undefined => { if ( bytes == null ) { return '' ; } if ( bytes . length !== 16 ) { return '' ; } const _a = ( bytes [ 3 ] << 24 ) | ( bytes [ 2 ] << 16 ) | ( bytes [ 1 ] << 8 ) | bytes [ 0 ] ; const _b = ( bytes [ 5 ] << 8 ) | bytes [ 4 ] ; const _c = ( bytes [ 7 ] << 8 ) | bytes [ 6 ] ; const _d = bytes [ 8 ] ; const _e = bytes [ 9 ] ; const _f = bytes [ 10 ] ; const _g = bytes [ 11 ] ; const _h = bytes [ 12 ] ; const _i = bytes [ 13 ] ; const _j = bytes [ 14 ] ; const _k = bytes [ 15 ] ; const hexToChar = ( a ) : string => { a &= 0xf ; return String . fromCharCode ( a > 9 ? a - 10 + 0x61 : a + 0x30 ) ; } ; const hexsToChars = ( guidChars , offset , a , b , hex ? ) : number => { hex = hex === undefined ? false : hex ; if ( hex ) { guidChars [ ( offset += 1 ) ] = '0' ; guidChars [ ( offset += 1 ) ] = 'x' ; } guidChars [ ( offset += 1 ) ] = hexToChar ( a >> 4 ) ; guidChars [ ( offset += 1 ) ] = hexToChar ( a ) ; if ( hex ) { guidChars [ ( offset += 1 ) ] = ',' ; guidChars [ ( offset += 1 ) ] = '0' ; guidChars [ ( offset += 1 ) ] = 'x' ; } guidChars [ ( offset += 1 ) ] = hexToChar ( b >> 4 ) ; guidChars [ ( offset += 1 ) ] = hexToChar ( b ) ; return offset ; } ; const _toString = ( format ) : string | undefined => { if ( ! format ?. length ) format = 'D' ; let guidChars = [ ] ; let offset = 0 ; let dash = true ; let hex = false ; if ( format . length !== 1 ) { return ; } const formatCh = format [ 0 ] ; if ( formatCh === 'D' || formatCh === 'd' ) { guidChars = new Array ( 36 ) ; } else if ( formatCh === 'N' || formatCh === 'n' ) { guidChars = new Array ( 32 ) ; dash = false ; } else if ( formatCh === 'B' || formatCh === 'b' ) { guidChars = new Array ( 38 ) ; guidChars [ ( offset += 1 ) ] = '{' ; guidChars [ 37 ] = '}' ; } else if ( formatCh === 'P' || formatCh === 'p' ) { guidChars = new Array ( 38 ) ; guidChars [ ( offset += 1 ) ] = '(' ; guidChars [ 37 ] = ')' ; } else if ( formatCh === 'X' || formatCh === 'x' ) { guidChars = new Array ( 68 ) ; guidChars [ ( offset += 1 ) ] = '{' ; guidChars [ 67 ] = '}' ; dash = false ; hex = true ; } else { return ; } if ( hex ) { guidChars [ ( offset += 1 ) ] = '0' ; guidChars [ ( offset += 1 ) ] = 'x' ; offset = hexsToChars ( guidChars , offset , _a >> 24 , _a >> 16 ) ; offset = hexsToChars ( guidChars , offset , _a >> 8 , _a ) ; guidChars [ ( offset += 1 ) ] = ',' ; guidChars [ ( offset += 1 ) ] = '0' ; guidChars [ ( offset += 1 ) ] = 'x' ; offset = hexsToChars ( guidChars , offset , _b >> 8 , _b ) ; guidChars [ ( offset += 1 ) ] = ',' ; guidChars [ ( offset += 1 ) ] = '0' ; guidChars [ ( offset += 1 ) ] = 'x' ; offset = hexsToChars ( guidChars , offset , _c >> 8 , _c ) ; guidChars [ ( offset += 1 ) ] = ',' ; guidChars [ ( offset += 1 ) ] = '{' ; offset = hexsToChars ( guidChars , offset , _d , _e , true ) ; guidChars [ ( offset += 1 ) ] = ',' ; offset = hexsToChars ( guidChars , offset , _f , _g , true ) ; guidChars [ ( offset += 1 ) ] = ',' ; offset = hexsToChars ( guidChars , offset , _h , _i , true ) ; guidChars [ ( offset += 1 ) ] = ',' ; offset = hexsToChars ( guidChars , offset , _j , _k , true ) ; guidChars [ ( offset += 1 ) ] = '}' ; } else { offset = hexsToChars ( guidChars , offset , _a >> 24 , _a >> 16 ) ; offset = hexsToChars ( guidChars , offset , _a >> 8 , _a ) ; if ( dash ) guidChars [ ( offset += 1 ) ] = '-' ; offset = hexsToChars ( guidChars , offset , _b >> 8 , _b ) ; if ( dash ) guidChars [ ( offset += 1 ) ] = '-' ; offset = hexsToChars ( guidChars , offset , _c >> 8 , _c ) ; if ( dash ) guidChars [ ( offset += 1 ) ] = '-' ; offset = hexsToChars ( guidChars , offset , _d , _e ) ; if ( dash ) guidChars [ ( offset += 1 ) ] = '-' ; offset = hexsToChars ( guidChars , offset , _f , _g ) ; offset = hexsToChars ( guidChars , offset , _h , _i ) ; offset = hexsToChars ( guidChars , offset , _j , _k ) ; } return guidChars . join ( '' ) ; } ; return _toString ( 'D' ) ?. split ( ',' ) . join ( '' ) ; } ; return ! ! bytesToGuidString ( hexStringToBytes ( syncId ) ) ; } trimToNearestWord ( text , limit ) { if ( ! text ) { return '' ; } text = text . trim ( ) ; if ( limit >= text . length ) { return text ; } const trimmedText = `${ text . substring ( 0 , text . lastIndexOf ( ' ' , limit ) ) }\u2026` ; return trimmedText ; } updateServiceUrl ( newServiceUrl ) { const url = newServiceUrl . replace ( /\/$/ , '' ) ; return this . storeSvc . set ( StoreKey . ServiceUrl , url ) . then ( ( ) => { this . logSvc . logInfo ( `Service url changed to: ${ url }` ) ; } ) ; } }