import angular from 'angular' ; import { Injectable } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import { ApiService } from '../../api/api.interface' ; import { BookmarkChangeType , BookmarkContainer } from '../../bookmark/bookmark.enum' ; import { AddBookmarkChangeData , Bookmark , BookmarkChange , BookmarkService , ModifyBookmarkChangeData , RemoveBookmarkChangeData , UpdateBookmarksResult } from '../../bookmark/bookmark.interface' ; import { BookmarkHelperService } from '../../bookmark/bookmark-helper/bookmark-helper.service' ; import { CryptoService } from '../../crypto/crypto.service' ; import { AmbiguousSyncRequestError , ClientDataNotFoundError } from '../../errors/errors' ; import { PlatformService } from '../../global-shared.interface' ; import { LogService } from '../../log/log.service' ; import { NetworkService } from '../../network/network.service' ; import { SettingsService } from '../../settings/settings.service' ; import { StoreKey } from '../../store/store.enum' ; import { StoreService } from '../../store/store.service' ; import { UpgradeService } from '../../upgrade/upgrade.service' ; import { UtilityService } from '../../utility/utility.service' ; import { SyncType } from '../sync.enum' ; import { ProcessSyncResult , Sync , SyncProvider } from '../sync.interface' ; @ autobind @ Injectable ( 'BookmarkSyncProviderService' ) export class BookmarkSyncProviderService implements SyncProvider { $q : ng . IQService ; apiSvc : ApiService ; bookmarkHelperSvc : BookmarkHelperService ; bookmarkSvc : BookmarkService ; cryptoSvc : CryptoService ; logSvc : LogService ; networkSvc : NetworkService ; platformSvc : PlatformService ; settingsSvc : SettingsService ; storeSvc : StoreService ; upgradeSvc : UpgradeService ; utilitySvc : UtilityService ; static $inject = [ '$q' , 'ApiService' , 'BookmarkHelperService' , 'BookmarkService' , 'CryptoService' , 'LogService' , 'NetworkService' , 'PlatformService' , 'SettingsService' , 'StoreService' , 'UpgradeService' , 'UtilityService' ] ; constructor ( $q , ApiSvc , BookmarkHelperSvc , BookmarkSvc , CryptoSvc , LogSvc , NetworkSvc , PlatformSvc , SettingsSvc , StoreSvc , UpgradeSvc , UtilitySvc ) { this . $q = $q ; this . apiSvc = ApiSvc ; this . bookmarkHelperSvc = BookmarkHelperSvc ; this . bookmarkSvc = BookmarkSvc ; this . cryptoSvc = CryptoSvc ; this . logSvc = LogSvc ; this . networkSvc = NetworkSvc ; this . platformSvc = PlatformSvc ; this . settingsSvc = SettingsSvc ; this . storeSvc = StoreSvc ; this . upgradeSvc = UpgradeSvc ; this . utilitySvc = UtilitySvc ; } disable ( ) { return this . $q . all ( [ this . platformSvc . disableNativeEventListeners ( ) , this . storeSvc . remove ( StoreKey . BookmarkIdMappings ) , this . storeSvc . remove ( StoreKey . Bookmarks ) , this . bookmarkHelperSvc . updateCachedBookmarks ( null , null ) ] ) . then ( ( ) => { } ) ; } enable ( ) { return this . platformSvc . enableNativeEventListeners ( ) ; } handleUpdateRemoteFailed ( err , lastResult , sync ) { if ( angular . isUndefined ( lastResult ?? undefined ) ) { return this . $q . resolve ( ) ; } return this . networkSvc . isNetworkConnectionError ( err ) ? ( ( ) => this . cryptoSvc . encryptData ( JSON . stringify ( lastResult ) ) . then ( ( encryptedBookmarks ) => this . bookmarkHelperSvc . updateCachedBookmarks ( lastResult , encryptedBookmarks ) . then ( ( ) => { sync . changeInfo = undefined ; sync . bookmarks = lastResult ; sync . type = SyncType . Remote ; } ) ) ) ( ) : this . $q . resolve ( ) ; } populateNativeBookmarks ( bookmarks ) { return this . bookmarkSvc . clearNativeBookmarks ( ) . then ( ( ) => { const populateStartTime = new Date ( ) ; return this . bookmarkSvc . createNativeBookmarksFromBookmarks ( bookmarks ) . then ( ( numBookmarksCreated ) => { if ( ! angular . isUndefined ( numBookmarksCreated ) ) { this . logSvc . logInfo ( `${ numBookmarksCreated } bookmarks populated in ${ ( ( new Date ( ) as any ) - ( populateStartTime as any ) ) / 1000 }s` ) ; } } ) ; } ) ; } processSync ( sync ) { switch ( sync . type ) { case SyncType . Remote : return this . processRemoteSync ( sync ) ; case SyncType . Local : return this . processLocalSync ( sync ) ; case SyncType . LocalAndRemote : return this . processLocalAndRemoteSync ( sync ) ; case SyncType . Upgrade : return this . processUpgradeSync ( ) ; default : throw new AmbiguousSyncRequestError ( ) ; } } processLocalAndRemoteSync ( sync ) { const processResult = { updateRemote : false } ; let rebuildIdMappings = false ; return ( this . utilitySvc . checkSyncCredentialsExist ( ) . then ( ( ) => this . $q . resolve ( sync . changeInfo ) ) . then ( ( changeInfo ) => this . $q < Bookmark [ ] > ( ( resolve , reject ) => { this . $q . resolve ( ) . then ( ( ) => { if ( sync . bookmarks ) { if ( this . validateBookmarkIds ( sync . bookmarks ) ) { return sync . bookmarks ; } const repairedBookmarks = this . repairBookmarkIds ( sync . bookmarks ) ; return repairedBookmarks ; } if ( ! changeInfo ) { throw new AmbiguousSyncRequestError ( ) ; } return this . bookmarkHelperSvc . getCachedBookmarks ( ) ; } ) . then ( resolve ) . catch ( reject ) ; } ) . then ( ( bookmarks ) => { return ( ( angular . isUndefined ( changeInfo ) ? this . $q . resolve ( undefined ) : this . updateBookmarks ( bookmarks , changeInfo ) ) . then ( ( updateResults ) => this . platformSvc . disableNativeEventListeners ( ) . then ( ( ) => { if ( angular . isUndefined ( updateResults ) ) { if ( angular . isUndefined ( sync . bookmarks ) ) { return bookmarks ; } rebuildIdMappings = true ; return this . populateNativeBookmarks ( bookmarks ) . then ( ( ) => bookmarks ) ; } return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { if ( updateResults . container === BookmarkContainer . Toolbar && ! syncBookmarksToolbar ) { return updateResults . bookmarks ; } return this . bookmarkSvc . processChangeOnNativeBookmarks ( updateResults . bookmark . id , changeInfo . type , updateResults . bookmark ) . then ( ( ) => updateResults . bookmarks ) ; } ) ; } ) . finally ( this . platformSvc . enableNativeEventListeners ) ) ) ; } ) . then ( ( bookmarks ) => this . bookmarkSvc . ensureContainersExist ( bookmarks ) ) . then ( ( bookmarks ) => { processResult . data = bookmarks ; if ( rebuildIdMappings ) { return this . bookmarkSvc . buildIdMappings ( bookmarks ) ; } } ) . then ( ( ) => { processResult . updateRemote = true ; return processResult ; } ) ) ) ; } processLocalSync ( sync ) { const processResult = { updateRemote : false } ; if ( sync . bookmarks ) { return this . populateNativeBookmarks ( sync . bookmarks ) . then ( ( ) => processResult ) ; } return ( this . utilitySvc . checkSyncCredentialsExist ( ) . then ( ( ) => this . apiSvc . getBookmarks ( ) ) . then ( ( response ) => { const { bookmarks : encryptedBookmarks , lastUpdated } = response ; let bookmarks ; return ( this . cryptoSvc . decryptData ( encryptedBookmarks ) . then ( ( bookmarksJson ) => { bookmarks = JSON . parse ( bookmarksJson ) ; if ( ! this . validateBookmarkIds ( bookmarks ) ) { bookmarks = this . repairBookmarkIds ( bookmarks ) ; processResult . updateRemote = true ; } const bookmarksWithMissingContainers = this . bookmarkSvc . ensureContainersExist ( bookmarks ) ; if ( ! angular . equals ( bookmarks , bookmarksWithMissingContainers ) ) { bookmarks = bookmarksWithMissingContainers ; processResult . updateRemote = true ; } processResult . data = bookmarks ; return this . platformSvc . disableNativeEventListeners ( ) . then ( ( ) => this . populateNativeBookmarks ( bookmarks ) ) . then ( ( ) => this . bookmarkSvc . buildIdMappings ( bookmarks ) ) . finally ( this . platformSvc . enableNativeEventListeners ) ; } ) . then ( ( ) => this . storeSvc . set ( StoreKey . LastUpdated , lastUpdated ) ) . then ( ( ) => processResult ) ) ; } ) ) ; } processRemoteSync ( sync ) { const processResult = { updateRemote : false } ; return this . utilitySvc . checkSyncCredentialsExist ( ) . then ( ( ) => this . utilitySvc . isSyncEnabled ( ) ) . then ( ( syncEnabled ) => { if ( ! syncEnabled ) { return ( this . bookmarkSvc as any ) . getNativeBookmarksAsBookmarks ( ) . then ( ( bookmarks ) => { processResult . data = bookmarks ; processResult . updateRemote = true ; return this . bookmarkSvc . buildIdMappings ( bookmarks ) ; } ) ; } return this . bookmarkHelperSvc . getCachedBookmarks ( ) . then ( ( cachedBookmarks ) => { const bookmarksToSync = angular . isUndefined ( sync . bookmarks ) ? cachedBookmarks : sync . bookmarks ; processResult . data = bookmarksToSync ; return ( angular . isUndefined ( sync . changeInfo ) ? this . $q . resolve ( bookmarksToSync ) : this . bookmarkSvc . processNativeChangeOnBookmarks ( sync . changeInfo , bookmarksToSync ) . then ( ( updatedBookmarks ) => this . bookmarkSvc . ensureContainersExist ( updatedBookmarks ) ) ) . then ( ( updatedBookmarks ) => { if ( angular . isUndefined ( updatedBookmarks ) ) { return processResult ; } processResult . data = updatedBookmarks ; processResult . updateRemote = true ; } ) ; } ) ; } ) . then ( ( ) => processResult ) ; } processUpgradeSync ( ) { const processResult = { updateRemote : false } ; return this . storeSvc . get ( [ StoreKey . Password , StoreKey . SyncId ] ) . then ( ( storeContent ) => { if ( ! storeContent . password || ! storeContent . syncId ) { return this . disable ( ) . then ( ( ) => { throw new ClientDataNotFoundError ( ) ; } ) ; } return this . apiSvc . getBookmarks ( ) . then ( ( response ) => { const lastUpdated = response . lastUpdated ; return ( this . cryptoSvc . decryptData ( response . bookmarks ) . then ( ( bookmarksJson ) => { const bookmarks = JSON . parse ( bookmarksJson ) ; return this . $q . all ( [ this . platformSvc . getAppVersion ( ) , this . utilitySvc . getSyncVersion ( ) ] ) . then ( ( result ) => { const [ appVersion , syncVersion ] = result ; return this . upgradeSvc . upgradeBookmarks ( appVersion , syncVersion , bookmarks ) . then ( ( upgradedBookmarks ) => { if ( ! this . validateBookmarkIds ( upgradedBookmarks ) ) { upgradedBookmarks = this . repairBookmarkIds ( upgradedBookmarks ) ; } const bookmarksWithMissingContainers = this . bookmarkSvc . ensureContainersExist ( upgradedBookmarks ) ; if ( ! angular . equals ( upgradedBookmarks , bookmarksWithMissingContainers ) ) { upgradedBookmarks = bookmarksWithMissingContainers ; } processResult . data = upgradedBookmarks ; processResult . updateRemote = true ; return this . populateNativeBookmarks ( upgradedBookmarks ) . then ( ( ) => this . bookmarkSvc . buildIdMappings ( upgradedBookmarks ) ) ; } ) ; } ) ; } ) . then ( ( ) => this . storeSvc . set ( StoreKey . LastUpdated , lastUpdated ) ) . then ( ( ) => processResult ) ) ; } ) ; } ) ; } repairBookmarkIds ( bookmarks ) { let allBookmarks = [ ] ; let idCounter = 1 ; this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { allBookmarks . push ( bookmark as Bookmark ) ; } , bookmarks ) ; allBookmarks . forEach ( ( bookmark ) => { if ( typeof bookmark . id !== 'number' ) { delete bookmark . id ; } } ) ; allBookmarks = allBookmarks . sort ( ( x , y ) => { return x . id - y . id ; } ) ; allBookmarks . forEach ( ( bookmark ) => { bookmark . id = idCounter ; idCounter += 1 ; } ) ; return bookmarks ; } updateBookmarks ( bookmarks , changeInfo ) { switch ( changeInfo . type ) { case BookmarkChangeType . Add : return this . updateBookmarksForChangeTypeAdd ( bookmarks , changeInfo . changeData as AddBookmarkChangeData ) ; case BookmarkChangeType . Modify : return this . updateBookmarksForChangeTypeUpdate ( bookmarks , changeInfo . changeData as ModifyBookmarkChangeData ) ; case BookmarkChangeType . Remove : return this . updateBookmarksForChangeTypeRemove ( bookmarks , changeInfo . changeData as RemoveBookmarkChangeData ) ; default : throw new AmbiguousSyncRequestError ( ) ; } } updateBookmarksForChangeTypeAdd ( bookmarks , changeData ) { const otherContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Other , bookmarks , true ) ; const newBookmark = this . bookmarkHelperSvc . newBookmark ( changeData . metadata . title , changeData . metadata . url , changeData . metadata . description , changeData . metadata . tags , bookmarks ) ; otherContainer . children . push ( newBookmark ) ; return this . $q . resolve ( { bookmark : newBookmark , bookmarks , container : otherContainer . title } as UpdateBookmarksResult ) ; } updateBookmarksForChangeTypeRemove ( bookmarks , changeData ) { const container = this . bookmarkHelperSvc . getContainerByBookmarkId ( changeData . id , bookmarks ) . title ; return this . bookmarkHelperSvc . removeBookmarkById ( changeData . id , bookmarks ) . then ( ( updatedBookmarks ) => { return { bookmark : { id : changeData . id } , bookmarks : updatedBookmarks , container } as UpdateBookmarksResult ; } ) ; } updateBookmarksForChangeTypeUpdate ( bookmarks , changeData ) { const container = this . bookmarkHelperSvc . getContainerByBookmarkId ( changeData . bookmark . id , bookmarks ) . title ; const updateInfo = this . bookmarkHelperSvc . extractBookmarkMetadata ( changeData . bookmark ) ; return this . bookmarkHelperSvc . modifyBookmarkById ( changeData . bookmark . id , updateInfo , bookmarks ) . then ( ( updatedBookmarks ) => { return { bookmark : changeData . bookmark , bookmarks : updatedBookmarks , container } as UpdateBookmarksResult ; } ) ; } validateBookmarkIds ( bookmarks ) { if ( ! bookmarks ?. length ) { return true ; } let bookmarksHaveIds = true ; this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { if ( angular . isUndefined ( bookmark . id ?? undefined ) ) { bookmarksHaveIds = false ; } } , bookmarks ) ; if ( ! bookmarksHaveIds ) { this . logSvc . logWarning ( 'Bookmarks missing ids' ) ; return false ; } const allBookmarks = [ ] ; this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { allBookmarks . push ( bookmark as Bookmark ) ; } , bookmarks ) ; const invalidId = allBookmarks . find ( ( bookmark ) => { return ! angular . isNumber ( bookmark . id ) ; } ) ; if ( ! angular . isUndefined ( invalidId ?? undefined ) ) { this . logSvc . logWarning ( `Invalid bookmark id detected: ${ invalidId . id } (${ invalidId . url })` ) ; return false ; } const uniqueIds = new Set ( allBookmarks . map ( ( x ) => x . id ) ) ; const duplicatesFound = uniqueIds . size < allBookmarks . length ; if ( duplicatesFound ) { this . logSvc . logWarning ( 'Duplicate bookmark ids detected' ) ; return false ; } return true ; } }