import angular from 'angular' ; import { Injectable } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import browser , { Bookmarks as NativeBookmarks } from 'webextension-polyfill' ; import { BookmarkChangeType , BookmarkContainer , BookmarkType } from '../../../../shared/bookmark/bookmark.enum' ; import { AddNativeBookmarkChangeData , Bookmark , BookmarkChange , ModifyNativeBookmarkChangeData , MoveNativeBookmarkChangeData } from '../../../../shared/bookmark/bookmark.interface' ; import { BaseError , BookmarkMappingNotFoundError , ContainerNotFoundError , FailedCreateNativeBookmarksError , FailedRemoveNativeBookmarksError } from '../../../../shared/errors/errors' ; import Globals from '../../../../shared/global-shared.constants' ; import { WebpageMetadata } from '../../../../shared/global-shared.interface' ; import { WebExtBookmarkService } from '../../../shared/webext-bookmark/webext-bookmark.service' ; @ autobind @ Injectable ( 'BookmarkService' ) export class ChromiumBookmarkService extends WebExtBookmarkService { otherBookmarksNodeId = '2' ; toolbarBookmarksNodeId = '1' ; unsupportedContainers = [ BookmarkContainer . Menu ] ; clearNativeBookmarks ( ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; const toolbarBookmarksId = nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; const clearOthers = browser . bookmarks . getChildren ( otherBookmarksId ) . then ( ( results ) => { return this . $q . all ( results . map ( ( child ) => { return this . removeNativeBookmarks ( child . id ) ; } ) ) ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Error clearing other bookmarks' ) ; throw err ; } ) ; const clearToolbar = this . $q ( ( resolve , reject ) => { return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { if ( ! syncBookmarksToolbar ) { this . logSvc . logInfo ( 'Not clearing toolbar' ) ; resolve ( ) ; return ; } return browser . bookmarks . getChildren ( toolbarBookmarksId ) . then ( ( results ) => { return this . $q . all ( results . map ( ( child ) => { return this . removeNativeBookmarks ( child . id ) ; } ) ) ; } ) ; } ) . then ( resolve ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Error clearing bookmarks toolbar' ) ; reject ( err ) ; } ) ; } ) ; return this . $q . all ( [ clearOthers , clearToolbar ] ) . then ( ( ) => { } ) ; } ) . catch ( ( err ) => { throw new FailedRemoveNativeBookmarksError ( undefined , err ) ; } ) ; } convertNativeBookmarkToSeparator ( bookmark ) { return this . isNativeBookmarkInToolbarContainer ( bookmark ) . then ( ( inToolbar ) => { if ( ( bookmark . url === this . platformSvc . getNewTabUrl ( ) && ! inToolbar && bookmark . title === Globals . Bookmarks . HorizontalSeparatorTitle ) || ( inToolbar && bookmark . title === Globals . Bookmarks . VerticalSeparatorTitle ) ) { return bookmark ; } return this . disableEventListeners ( ) . then ( ( ) => { const title = inToolbar ? Globals . Bookmarks . VerticalSeparatorTitle : Globals . Bookmarks . HorizontalSeparatorTitle ; if ( ( ! inToolbar && bookmark . title === Globals . Bookmarks . VerticalSeparatorTitle ) || ( inToolbar && bookmark . title === Globals . Bookmarks . HorizontalSeparatorTitle ) ) { return browser . bookmarks . update ( bookmark . id , { title } ) ; } const separator = { index : bookmark . index , parentId : bookmark . parentId , title , url : this . platformSvc . getNewTabUrl ( ) } ; return browser . bookmarks . remove ( bookmark . id ) . then ( ( ) => { return browser . bookmarks . create ( separator ) ; } ) ; } ) . finally ( this . enableEventListeners ) ; } ) . then ( ( nativeSeparator ) => { nativeSeparator . type = BookmarkType . Separator ; return nativeSeparator ; } ) ; } createNativeBookmarksFromBookmarks ( bookmarks ) { const menuContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Menu , bookmarks ) ; const otherContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Other , bookmarks ) ; const toolbarContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Toolbar , bookmarks ) ; return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; const toolbarBookmarksId = nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; let populateMenu = this . $q . resolve ( 0 ) ; if ( menuContainer ) { populateMenu = browser . bookmarks . getSubTree ( otherBookmarksId ) . then ( ( ) => { return this . createNativeBookmarkTree ( otherBookmarksId , [ menuContainer ] , toolbarBookmarksId ) ; } ) . catch ( ( err ) => { this . logSvc . logInfo ( 'Error populating bookmarks menu.' ) ; throw err ; } ) ; } let populateOther = this . $q . resolve ( 0 ) ; if ( otherContainer ) { populateOther = browser . bookmarks . getSubTree ( otherBookmarksId ) . then ( ( ) => { return this . createNativeBookmarkTree ( otherBookmarksId , otherContainer . children , toolbarBookmarksId ) ; } ) . catch ( ( err ) => { this . logSvc . logInfo ( 'Error populating other bookmarks.' ) ; throw err ; } ) ; } const populateToolbar = this . $q < number > ( ( resolve , reject ) => { if ( ! toolbarContainer ) { return resolve ( 0 ) ; } return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { if ( ! syncBookmarksToolbar ) { this . logSvc . logInfo ( 'Not populating toolbar' ) ; resolve ( ) ; return ; } return browser . bookmarks . getSubTree ( toolbarBookmarksId ) . then ( ( ) => { return this . createNativeBookmarkTree ( toolbarBookmarksId , toolbarContainer . children ) ; } ) ; } ) . then ( resolve ) . catch ( ( err ) => { this . logSvc . logInfo ( 'Error populating bookmarks toolbar.' ) ; reject ( err ) ; } ) ; } ) ; return this . $q . all ( [ populateMenu , populateOther , populateToolbar ] ) ; } ) . then ( ( totals ) => { return this . reorderUnsupportedContainers ( ) . then ( ( ) => { return totals . filter ( Boolean ) . reduce ( ( a , b ) => a + b , 0 ) ; } ) ; } ) ; } createNativeSeparator ( parentId , nativeToolbarContainerId ) { const newSeparator = { parentId , title : parentId === nativeToolbarContainerId ? Globals . Bookmarks . VerticalSeparatorTitle : Globals . Bookmarks . HorizontalSeparatorTitle , url : this . platformSvc . getNewTabUrl ( ) } ; return browser . bookmarks . create ( newSeparator ) . catch ( ( err ) => { this . logSvc . logInfo ( 'Failed to create native separator' ) ; throw new FailedCreateNativeBookmarksError ( undefined , err ) ; } ) ; } disableEventListeners ( ) { return this . $q . all ( [ ( browser . bookmarks as any ) . onChildrenReordered . removeListener ( this . onNativeBookmarkChildrenReordered ) , browser . bookmarks . onCreated . removeListener ( this . onNativeBookmarkCreated ) , browser . bookmarks . onRemoved . removeListener ( this . onNativeBookmarkRemoved ) , browser . bookmarks . onChanged . removeListener ( this . onNativeBookmarkChanged ) , browser . bookmarks . onMoved . removeListener ( this . onNativeBookmarkMoved ) ] ) . then ( ( ) => { } ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Failed to disable event listeners' ) ; throw new BaseError ( undefined , err ) ; } ) ; } enableEventListeners ( ) { return this . disableEventListeners ( ) . then ( ( ) => { return this . utilitySvc . isSyncEnabled ( ) ; } ) . then ( ( syncEnabled ) => { if ( ! syncEnabled ) { return ; } browser . bookmarks . onCreated . addListener ( this . onNativeBookmarkCreated ) ; browser . bookmarks . onRemoved . addListener ( this . onNativeBookmarkRemoved ) ; browser . bookmarks . onChanged . addListener ( this . onNativeBookmarkChanged ) ; ( browser . bookmarks as any ) . onChildrenReordered . addListener ( this . onNativeBookmarkChildrenReordered ) ; browser . bookmarks . onMoved . addListener ( this . onNativeBookmarkMoved ) ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( 'Failed to enable event listeners' ) ; throw new BaseError ( undefined , err ) ; } ) ; } ensureContainersExist ( bookmarks ) { if ( angular . isUndefined ( bookmarks ) ) { return ; } const bookmarksToReturn = angular . copy ( bookmarks ) ; this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Other , bookmarksToReturn , true ) ; this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Toolbar , bookmarksToReturn , true ) ; return bookmarksToReturn . sort ( ( x , y ) => { if ( x . title < y . title ) { return - 1 ; } if ( x . title > y . title ) { return 1 ; } return 0 ; } ) ; } getNativeBookmarksAsBookmarks ( ) { let allNativeBookmarks = [ ] ; return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const menuBookmarksId = nativeContainerIds . get ( BookmarkContainer . Menu ) ; const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; const toolbarBookmarksId = nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; const getMenuBookmarks = menuBookmarksId === undefined ? this . $q . resolve < Bookmark [ ] > ( undefined ) : browser . bookmarks . getSubTree ( menuBookmarksId ) . then ( ( subTree ) => { const [ menuContainer ] = subTree ; return this . bookmarkHelperSvc . getNativeBookmarksAsBookmarks ( this . getNativeBookmarksWithSeparators ( menuContainer . children ) ) ; } ) ; const getOtherBookmarks = otherBookmarksId === undefined ? this . $q . resolve < Bookmark [ ] > ( undefined ) : browser . bookmarks . getSubTree ( otherBookmarksId ) . then ( ( subTree ) => { const [ otherContainer ] = subTree ; if ( otherContainer . children . length === 0 ) { return ; } this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { allNativeBookmarks . push ( bookmark ) ; } , otherContainer . children ) ; const bookmarksWithoutContainers = this . bookmarkHelperSvc . getNativeBookmarksAsBookmarks ( this . getNativeBookmarksWithSeparators ( otherContainer . children ) ) . filter ( ( x ) => { return ! this . unsupportedContainers . find ( ( y ) => { return y === x . title ; } ) ; } ) ; return bookmarksWithoutContainers ; } ) ; const getToolbarBookmarks = toolbarBookmarksId === undefined ? this . $q . resolve < Bookmark [ ] > ( undefined ) : browser . bookmarks . getSubTree ( toolbarBookmarksId ) . then ( ( results ) => { const [ toolbarContainer ] = results ; return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { if ( syncBookmarksToolbar && toolbarContainer . children . length > 0 ) { this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { allNativeBookmarks . push ( bookmark ) ; } , toolbarContainer . children ) ; return this . bookmarkHelperSvc . getNativeBookmarksAsBookmarks ( this . getNativeBookmarksWithSeparators ( toolbarContainer . children ) ) ; } } ) ; } ) ; return this . $q . all ( [ getMenuBookmarks , getOtherBookmarks , getToolbarBookmarks ] ) . then ( ( results ) => { const [ menuBookmarks , otherBookmarks , toolbarBookmarks ] = results ; const bookmarks = [ ] ; const otherContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Other , bookmarks , true ) ; if ( otherBookmarks ?. length > 0 ) { otherContainer . children = otherBookmarks ; } const toolbarContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Toolbar , bookmarks , true ) ; if ( toolbarBookmarks ?. length > 0 ) { toolbarContainer . children = toolbarBookmarks ; } let menuContainer ; if ( menuBookmarksId !== undefined ) { menuContainer = this . bookmarkHelperSvc . getContainer ( BookmarkContainer . Menu , bookmarks , true ) ; if ( menuBookmarks ?. length > 0 ) { menuContainer . children = menuBookmarks ; } } [ otherContainer , toolbarContainer , menuContainer ] . forEach ( ( container ) => { if ( ! container ) { return ; } allNativeBookmarks = allNativeBookmarks . filter ( ( bookmark ) => { return bookmark . title !== container . title ; } ) ; } ) ; allNativeBookmarks = allNativeBookmarks . sort ( ( x , y ) => { return x . dateAdded - y . dateAdded ; } ) ; allNativeBookmarks . forEach ( ( nativeBookmark ) => { this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { if ( ! bookmark . id && ( ( ! nativeBookmark . url && bookmark . title === nativeBookmark . title ) || ( nativeBookmark . url && bookmark . url === nativeBookmark . url ) ) ) { bookmark . id = this . bookmarkHelperSvc . getNewBookmarkId ( bookmarks ) ; } } , bookmarks ) ; } ) ; this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { if ( ! bookmark . id ) { bookmark . id = this . bookmarkHelperSvc . getNewBookmarkId ( bookmarks ) ; } } , bookmarks ) ; return bookmarks ; } ) ; } ) ; } getNativeBookmarksWithSeparators ( nativeBookmarks ) { this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { if ( this . isSeparator ( bookmark ) ) { bookmark . type = BookmarkType . Separator ; } return bookmark ; } , nativeBookmarks ) ; return nativeBookmarks ; } getNativeContainerIds ( ) { return this . utilitySvc . isSyncEnabled ( ) . then ( ( syncEnabled ) => ( syncEnabled ? this . bookmarkHelperSvc . getCachedBookmarks ( ) : undefined ) ) . then ( ( bookmarks ) => { const containerIds = new Map < BookmarkContainer , string > ( ) ; if ( ! angular . isUndefined ( bookmarks ) ) { bookmarks . forEach ( ( x ) => { containerIds . set ( x . title as BookmarkContainer , undefined ) ; } ) ; } return browser . bookmarks . getTree ( ) . then ( ( tree ) => { const [ root ] = tree ; const otherBookmarksNode = root . children . find ( ( x ) => { return x . id === this . otherBookmarksNodeId ; } ) ; const toolbarBookmarksNode = root . children . find ( ( x ) => { return x . id === this . toolbarBookmarksNodeId ; } ) ; if ( ! otherBookmarksNode || ! toolbarBookmarksNode ) { if ( ! otherBookmarksNode ) { this . logSvc . logWarning ( 'Missing container: other bookmarks' ) ; } if ( ! toolbarBookmarksNode ) { this . logSvc . logWarning ( 'Missing container: toolbar bookmarks' ) ; } throw new ContainerNotFoundError ( ) ; } const menuBookmarksNode = otherBookmarksNode . children . find ( ( x ) => { return x . title === BookmarkContainer . Menu ; } ) ; containerIds . set ( BookmarkContainer . Other , otherBookmarksNode . id ) ; containerIds . set ( BookmarkContainer . Toolbar , toolbarBookmarksNode . id ) ; if ( ! angular . isUndefined ( menuBookmarksNode ) ) { containerIds . set ( BookmarkContainer . Menu , menuBookmarksNode . id ) ; } return containerIds ; } ) ; } ) ; } isSeparator ( nativeBookmark ) { const separatorRegex = /^[-─]{1,}$/ ; return ( ! angular . isUndefined ( nativeBookmark . title ) && ( ( separatorRegex . test ( nativeBookmark . title ?? '' ) && ! nativeBookmark . children ?. length ) || ( ( nativeBookmark . title ! . indexOf ( Globals . Bookmarks . HorizontalSeparatorTitle ) >= 0 || nativeBookmark . title === Globals . Bookmarks . VerticalSeparatorTitle ) && nativeBookmark . url === this . platformSvc . getNewTabUrl ! ( ) ) ) ) ; } onNativeBookmarkChildrenReordered ( ... args ) { this . logSvc . logInfo ( 'onChildrenReordered event detected' ) ; this . queueNativeBookmarkEvent ( BookmarkChangeType . ChildrenReordered , ... args ) ; } syncNativeBookmarkChanged ( id ? ) { return browser . bookmarks . getSubTree ( id ) . then ( ( results ) => { const [ changedBookmark ] = results ; ( this . isSeparator ( changedBookmark ) ? this . convertNativeBookmarkToSeparator ( changedBookmark ) : this . $q . resolve ( changedBookmark ) ) . then ( ( bookmarkNode ) => { let updateMappingPromise ; if ( bookmarkNode . id !== id ) { updateMappingPromise = this . bookmarkIdMapperSvc . get ( id ) . then ( ( idMapping ) => { if ( ! idMapping ) { throw new BookmarkMappingNotFoundError ( ) ; } return this . bookmarkIdMapperSvc . remove ( idMapping . syncedId ) . then ( ( ) => { const newMapping = this . bookmarkIdMapperSvc . createMapping ( idMapping . syncedId , bookmarkNode . id ) ; return this . bookmarkIdMapperSvc . add ( newMapping ) ; } ) ; } ) ; } else { updateMappingPromise = this . $q . resolve ( ) ; } return updateMappingPromise . then ( ( ) => { const data = { nativeBookmark : bookmarkNode } ; const changeInfo = { changeData : data , type : BookmarkChangeType . Modify } ; this . syncChange ( changeInfo ) ; } ) ; } ) ; } ) ; } syncNativeBookmarkCreated ( id ? , nativeBookmark ? ) { return ( this . isSeparator ( nativeBookmark ) ? this . convertNativeBookmarkToSeparator ( nativeBookmark ) : this . $q . resolve ( nativeBookmark ) ) . then ( ( bookmarkNode ) => { const data = { nativeBookmark : bookmarkNode } ; const changeInfo = { changeData : data , type : BookmarkChangeType . Add } ; return ( bookmarkNode . url && ! this . isSeparator ( bookmarkNode ) ? this . checkPermsAndGetPageMetadata ( ) : this . $q . resolve < WebpageMetadata > ( null ) ) . then ( ( metadata ) => { if ( metadata && bookmarkNode . url === metadata . url ) { ( changeInfo . changeData as AddNativeBookmarkChangeData ) . nativeBookmark . title = this . utilitySvc . stripTags ( metadata . title ) ; ( changeInfo . changeData as AddNativeBookmarkChangeData ) . nativeBookmark . description = this . utilitySvc . stripTags ( metadata . description ) ; ( changeInfo . changeData as AddNativeBookmarkChangeData ) . nativeBookmark . tags = this . utilitySvc . getTagArrayFromText ( metadata . tags ) ; } this . syncChange ( changeInfo ) ; } ) ; } ) ; } syncNativeBookmarkMoved ( id ? , moveInfo ? ) { return browser . bookmarks . get ( id ) . then ( ( results ) => { const [ movedBookmark ] = results ; return ( this . isSeparator ( movedBookmark ) ? this . convertNativeBookmarkToSeparator ( movedBookmark ) : this . $q . resolve ( movedBookmark ) ) . then ( ( bookmarkNode ) => { let updateMappingPromise ; if ( bookmarkNode . id !== id ) { updateMappingPromise = this . bookmarkIdMapperSvc . get ( id ) . then ( ( idMapping ) => { if ( ! idMapping ) { throw new BookmarkMappingNotFoundError ( ) ; } return this . bookmarkIdMapperSvc . remove ( idMapping . syncedId ) . then ( ( ) => { const newMapping = this . bookmarkIdMapperSvc . createMapping ( idMapping . syncedId , bookmarkNode . id ) ; return this . bookmarkIdMapperSvc . add ( newMapping ) ; } ) ; } ) ; } else { updateMappingPromise = this . $q . resolve ( ) ; } return updateMappingPromise . then ( ( ) => { const data = { ... moveInfo , id } ; const changeInfo = { changeData : data , type : BookmarkChangeType . Move } ; this . syncChange ( changeInfo ) ; } ) ; } ) ; } ) ; } }