diff --git a/src/modules/android/android-shared/android-bookmark/android-bookmark.service.ts b/src/modules/android/android-shared/android-bookmark/android-bookmark.service.ts index e009b6c42..3abd0c8a9 100644 --- a/src/modules/android/android-shared/android-bookmark/android-bookmark.service.ts +++ b/src/modules/android/android-shared/android-bookmark/android-bookmark.service.ts @@ -30,6 +30,10 @@ export default class AndroidBookmarkService implements BookmarkService { return bookmarks; } + identifySupportedContainers(): ng.IPromise { + return this.methodNotApplicable(); + } + methodNotApplicable(): ng.IPromise { // Unused for this platform return this.$q.resolve(); diff --git a/src/modules/app/app-settings/backup-restore-settings/backup-restore-settings.component.ts b/src/modules/app/app-settings/backup-restore-settings/backup-restore-settings.component.ts index 644987596..759dd265d 100644 --- a/src/modules/app/app-settings/backup-restore-settings/backup-restore-settings.component.ts +++ b/src/modules/app/app-settings/backup-restore-settings/backup-restore-settings.component.ts @@ -287,7 +287,7 @@ export default class BackupRestoreSettingsComponent implements OnInit { }) .then((bookmarks) => { // Clean bookmarks for export - return cleanRecursive(this.bookmarkHelperSvc.removeEmptyContainers(bookmarks)); + return cleanRecursive(bookmarks.filter((container) => container.children?.length)); }); } diff --git a/src/modules/shared/bookmark/bookmark-helper/bookmark-helper.service.ts b/src/modules/shared/bookmark/bookmark-helper/bookmark-helper.service.ts index 984132706..ab7c62eb7 100644 --- a/src/modules/shared/bookmark/bookmark-helper/bookmark-helper.service.ts +++ b/src/modules/shared/bookmark/bookmark-helper/bookmark-helper.service.ts @@ -213,15 +213,8 @@ export default class BookmarkHelperService { } getBookmarkType(bookmark: Bookmark): BookmarkType { - const bookmarkType = BookmarkType.Bookmark; - // Check if container - if ( - bookmark.title === BookmarkContainer.Menu || - bookmark.title === BookmarkContainer.Mobile || - bookmark.title === BookmarkContainer.Other || - bookmark.title === BookmarkContainer.Toolbar - ) { + if (Object.values(BookmarkContainer).includes(bookmark.title as BookmarkContainer)) { return BookmarkType.Container; } @@ -235,7 +228,7 @@ export default class BookmarkHelperService { return BookmarkType.Separator; } - return bookmarkType; + return BookmarkType.Bookmark; } getCachedBookmarks(): ng.IPromise { @@ -522,32 +515,6 @@ export default class BookmarkHelperService { return this.$q.resolve(updatedBookmarks); } - removeEmptyContainers(bookmarks: Bookmark[]): Bookmark[] { - const menuContainer = this.getContainer(BookmarkContainer.Menu, bookmarks); - const mobileContainer = this.getContainer(BookmarkContainer.Mobile, bookmarks); - const otherContainer = this.getContainer(BookmarkContainer.Other, bookmarks); - const toolbarContainer = this.getContainer(BookmarkContainer.Toolbar, bookmarks); - const removeArr: Bookmark[] = []; - - if (!menuContainer?.children?.length) { - removeArr.push(menuContainer); - } - - if (!mobileContainer?.children?.length) { - removeArr.push(mobileContainer); - } - - if (!otherContainer?.children?.length) { - removeArr.push(otherContainer); - } - - if (!toolbarContainer?.children?.length) { - removeArr.push(toolbarContainer); - } - - return bookmarks.filter((x) => !removeArr.includes(x)); - } - searchBookmarks(query: any): ng.IPromise { if (!query) { query = { keywords: [] }; diff --git a/src/modules/shared/bookmark/bookmark.enum.ts b/src/modules/shared/bookmark/bookmark.enum.ts index c7d8a2399..f0a628dfc 100644 --- a/src/modules/shared/bookmark/bookmark.enum.ts +++ b/src/modules/shared/bookmark/bookmark.enum.ts @@ -6,6 +6,9 @@ enum BookmarkChangeType { Remove = 'remove' } +// when adding a new container, add a translation to bookmark-helper.service.ts getBookmarkTitleForDisplay(...) +// do NOT reference any of these constants unless you absolutely have to +// (e.g. .Toolbar in conjunction with SettingsSvc.syncBookmarksToolbar() ) enum BookmarkContainer { Menu = '[xbs] Menu', Mobile = '[xbs] Mobile', diff --git a/src/modules/shared/bookmark/bookmark.interface.ts b/src/modules/shared/bookmark/bookmark.interface.ts index 2e4297ac1..b6e899c2e 100644 --- a/src/modules/shared/bookmark/bookmark.interface.ts +++ b/src/modules/shared/bookmark/bookmark.interface.ts @@ -41,6 +41,7 @@ export interface BookmarkService { clearNativeBookmarks: () => ng.IPromise; createNativeBookmarksFromBookmarks: (bookmarks: Bookmark[]) => ng.IPromise; ensureContainersExist: (bookmarks: Bookmark[]) => Bookmark[]; + identifySupportedContainers(): ng.IPromise; processNativeChangeOnBookmarks: (changeInfo: BookmarkChange, bookmarks: Bookmark[]) => ng.IPromise; processChangeOnNativeBookmarks: ( id: number, diff --git a/src/modules/shared/sync/bookmark-sync-provider/bookmark-sync-provider.service.ts b/src/modules/shared/sync/bookmark-sync-provider/bookmark-sync-provider.service.ts index e3d6d6c37..0bffb8b1f 100644 --- a/src/modules/shared/sync/bookmark-sync-provider/bookmark-sync-provider.service.ts +++ b/src/modules/shared/sync/bookmark-sync-provider/bookmark-sync-provider.service.ts @@ -137,24 +137,26 @@ export default class BookmarkSyncProviderService implements SyncProvider { } processSync(sync: Sync): ng.IPromise { - // Process sync - switch (sync.type) { - // Sync native bookmarks to service - case SyncType.Remote: - return this.processRemoteSync(sync); - // Overwrite native bookmarks with synced bookmarks - case SyncType.Local: - return this.processLocalSync(sync); - // Sync bookmarks to service and overwrite native bookmarks - case SyncType.LocalAndRemote: - return this.processLocalAndRemoteSync(sync); - // Upgrade sync to current version - case SyncType.Upgrade: - return this.processUpgradeSync(); - // Ambiguous sync - default: - throw new Exceptions.AmbiguousSyncRequestException(); - } + return this.bookmarkSvc.identifySupportedContainers().then(() => { + // Process sync + switch (sync.type) { + // Sync native bookmarks to service + case SyncType.Remote: + return this.processRemoteSync(sync); + // Overwrite native bookmarks with synced bookmarks + case SyncType.Local: + return this.processLocalSync(sync); + // Sync bookmarks to service and overwrite native bookmarks + case SyncType.LocalAndRemote: + return this.processLocalAndRemoteSync(sync); + // Upgrade sync to current version + case SyncType.Upgrade: + return this.processUpgradeSync(); + // Ambiguous sync + default: + throw new Exceptions.AmbiguousSyncRequestException(); + } + }); } processLocalAndRemoteSync(sync: Sync): ng.IPromise { diff --git a/src/modules/shared/utility/utility.service.ts b/src/modules/shared/utility/utility.service.ts index 9cc20aa85..657b73508 100644 --- a/src/modules/shared/utility/utility.service.ts +++ b/src/modules/shared/utility/utility.service.ts @@ -16,6 +16,10 @@ import NetworkService from '../network/network.service'; import { StoreKey } from '../store/store.enum'; import StoreService from '../store/store.service'; +declare global { + const opr: any; +} + @autobind @Injectable('UtilityService') export default class UtilityService { @@ -157,6 +161,23 @@ export default class UtilityService { return !angular.isUndefined(window.navigator.brave); } + // as per https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser/9851769#9851769 + // Opera 20 - 74 + isOperaBrowser(): boolean { + const windowsAny: any = window; + // eslint-disable-next-line no-undef + return (!!windowsAny.opr && !!opr.addons) || navigator.userAgent.indexOf(' OPR/') >= 0; + } + // Chrome 1 - 88 + isChromeLikeBrowser(): boolean { + const windowsAny: any = window; + return !!windowsAny.chrome && (!!windowsAny.chrome.webstore || !!windowsAny.chrome.runtime); + } + // Edge (based on chromium) detection + isEdgeChromiumBrowser(): boolean { + return this.isChromeLikeBrowser() && navigator.userAgent.indexOf('Edg') !== -1; + } + isMobilePlatform(platformName: string): boolean { return platformName === PlatformType.Android; } diff --git a/src/modules/webext/chromium/shared/chromium-bookmark/chromium-bookmark.service.ts b/src/modules/webext/chromium/shared/chromium-bookmark/chromium-bookmark.service.ts index 96db20d2c..855d248a6 100644 --- a/src/modules/webext/chromium/shared/chromium-bookmark/chromium-bookmark.service.ts +++ b/src/modules/webext/chromium/shared/chromium-bookmark/chromium-bookmark.service.ts @@ -5,7 +5,6 @@ import { Bookmarks as NativeBookmarks, browser } from 'webextension-polyfill-ts' import { BookmarkChangeType, BookmarkContainer, BookmarkType } from '../../../../shared/bookmark/bookmark.enum'; import { AddNativeBookmarkChangeData, - Bookmark, BookmarkChange, ModifyNativeBookmarkChangeData, MoveNativeBookmarkChangeData @@ -18,73 +17,15 @@ import WebExtBookmarkService from '../../../shared/webext-bookmark/webext-bookma @autobind @Injectable('BookmarkService') export default class ChromiumBookmarkService extends WebExtBookmarkService { - otherBookmarksNodeId = '2'; - toolbarBookmarksNodeId = '1'; - unsupportedContainers = [BookmarkContainer.Menu, BookmarkContainer.Mobile]; - - clearNativeBookmarks(): ng.IPromise { - // Get native container ids - return this.getNativeContainerIds() - .then((nativeContainerIds) => { - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Clear other bookmarks - 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; - }); - - // Clear bookmarks toolbar if enabled - 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 Exceptions.FailedRemoveNativeBookmarksException(undefined, err); - }); - } - convertNativeBookmarkToSeparator( bookmark: NativeBookmarks.BookmarkTreeNode ): ng.IPromise { // Check if bookmark is in toolbar - return this.isNativeBookmarkInToolbarContainer(bookmark) + return this.isNativeBookmarkIdOfToolbarContainer(bookmark.parentId) .then((inToolbar) => { // Skip process if bookmark is not in toolbar and already native separator if ( - (bookmark.url === this.platformSvc.getNewTabUrl() && + (bookmark.url === this.platformSvc.getNewTabUrl!() && !inToolbar && bookmark.title === Globals.Bookmarks.HorizontalSeparatorTitle) || (inToolbar && bookmark.title === Globals.Bookmarks.VerticalSeparatorTitle) @@ -112,7 +53,7 @@ export default class ChromiumBookmarkService extends WebExtBookmarkService { index: bookmark.index, parentId: bookmark.parentId, title, - url: this.platformSvc.getNewTabUrl() + url: this.platformSvc.getNewTabUrl!() }; return browser.bookmarks.remove(bookmark.id).then(() => { return browser.bookmarks.create(separator); @@ -127,113 +68,22 @@ export default class ChromiumBookmarkService extends WebExtBookmarkService { }); } - createNativeBookmarksFromBookmarks(bookmarks: Bookmark[]): ng.IPromise { - // Get containers - const menuContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Menu, bookmarks); - const mobileContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Mobile, bookmarks); - const otherContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks); - const toolbarContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarks); - - // Get native container ids - return this.getNativeContainerIds() - .then((nativeContainerIds) => { - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Populate menu bookmarks in other bookmarks - 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; - }); - } - - // Populate mobile bookmarks in other bookmarks - let populateMobile = this.$q.resolve(0); - if (mobileContainer) { - populateMobile = browser.bookmarks - .getSubTree(otherBookmarksId) - .then(() => { - return this.createNativeBookmarkTree(otherBookmarksId, [mobileContainer], toolbarBookmarksId); - }) - .catch((err) => { - this.logSvc.logInfo('Error populating mobile bookmarks.'); - throw err; - }); - } - - // Populate other bookmarks - 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; - }); - } - - // Populate bookmarks toolbar if enabled - const populateToolbar = this.$q((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, populateMobile, populateOther, populateToolbar]); + createNativeSeparator(parentId: string): ng.IPromise { + return this.isNativeBookmarkIdOfToolbarContainer(parentId) + .then((inToolbar) => { + const newSeparator: NativeBookmarks.CreateDetails = { + parentId, + title: inToolbar ? Globals.Bookmarks.VerticalSeparatorTitle : Globals.Bookmarks.HorizontalSeparatorTitle, + url: this.platformSvc.getNewTabUrl!() + }; + return browser.bookmarks.create(newSeparator); }) - .then((totals) => { - // Move native unsupported containers into the correct order - return this.reorderUnsupportedContainers().then(() => { - return totals.filter(Boolean).reduce((a, b) => a + b, 0); - }); + .catch((err) => { + this.logSvc.logInfo('Failed to create native separator'); + throw new Exceptions.FailedCreateNativeBookmarksException(undefined, err); }); } - createNativeSeparator( - parentId: string, - nativeToolbarContainerId: string - ): ng.IPromise { - const newSeparator: NativeBookmarks.CreateDetails = { - 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 Exceptions.FailedCreateNativeBookmarksException(undefined, err); - }); - } - disableEventListeners(): ng.IPromise { return this.$q .all([ @@ -271,183 +121,6 @@ export default class ChromiumBookmarkService extends WebExtBookmarkService { }); } - ensureContainersExist(bookmarks: Bookmark[]): Bookmark[] { - if (angular.isUndefined(bookmarks)) { - return; - } - - // Add supported containers - const bookmarksToReturn = angular.copy(bookmarks); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarksToReturn, true); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarksToReturn, true); - - // Return sorted containers - return bookmarksToReturn.sort((x, y) => { - if (x.title < y.title) { - return -1; - } - if (x.title > y.title) { - return 1; - } - return 0; - }); - } - - getNativeBookmarksAsBookmarks(): ng.IPromise { - let allNativeBookmarks = []; - - // Get native container ids - return this.getNativeContainerIds().then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Get menu bookmarks - const getMenuBookmarks = - menuBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(menuBookmarksId).then((subTree) => { - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks( - this.getNativeBookmarksWithSeparators(subTree[0].children) - ); - }); - - // Get mobile bookmarks - const getMobileBookmarks = - mobileBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(mobileBookmarksId).then((subTree) => { - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks( - this.getNativeBookmarksWithSeparators(subTree[0].children) - ); - }); - - // Get other bookmarks - const getOtherBookmarks = - otherBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(otherBookmarksId).then((subTree) => { - const otherBookmarks = subTree[0]; - if (otherBookmarks.children.length === 0) { - return; - } - - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(otherBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - - // Remove any unsupported container folders present - const bookmarksWithoutContainers = this.bookmarkHelperSvc - .getNativeBookmarksAsBookmarks(this.getNativeBookmarksWithSeparators(otherBookmarks.children)) - .filter((x) => { - return !this.unsupportedContainers.find((y) => { - return y === x.title; - }); - }); - return bookmarksWithoutContainers; - }); - - // Get toolbar bookmarks if enabled - const getToolbarBookmarks = - toolbarBookmarksId === undefined - ? this.$q.resolve(undefined) - : browser.bookmarks.getSubTree(toolbarBookmarksId).then((results) => { - const toolbarBookmarks = results[0]; - return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { - if (syncBookmarksToolbar && toolbarBookmarks.children.length > 0) { - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(toolbarBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks( - this.getNativeBookmarksWithSeparators(toolbarBookmarks.children) - ); - } - }); - }); - - return this.$q - .all([getMenuBookmarks, getMobileBookmarks, getOtherBookmarks, getToolbarBookmarks]) - .then((results) => { - const menuBookmarks = results[0]; - const mobileBookmarks = results[1]; - const otherBookmarks = results[2]; - const toolbarBookmarks = results[3]; - const bookmarks: Bookmark[] = []; - - // Add other container if bookmarks present - const otherContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks, true); - if (otherBookmarks?.length > 0) { - otherContainer.children = otherBookmarks; - } - - // Add toolbar container if bookmarks present - const toolbarContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarks, true); - if (toolbarBookmarks?.length > 0) { - toolbarContainer.children = toolbarBookmarks; - } - - // Add menu container if bookmarks present - let menuContainer: Bookmark; - if (menuBookmarksId !== undefined) { - menuContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Menu, bookmarks, true); - if (menuBookmarks?.length > 0) { - menuContainer.children = menuBookmarks; - } - } - - // Add mobile container if bookmarks present - let mobileContainer: Bookmark; - if (mobileBookmarksId !== undefined) { - mobileContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Mobile, bookmarks, true); - if (mobileBookmarks?.length > 0) { - mobileContainer.children = mobileBookmarks; - } - } - - // Filter containers from flat array of bookmarks - [otherContainer, toolbarContainer, menuContainer, mobileContainer].forEach((container) => { - if (!container) { - return; - } - - allNativeBookmarks = allNativeBookmarks.filter((bookmark) => { - return bookmark.title !== container.title; - }); - }); - - // Sort by date added asc - allNativeBookmarks = allNativeBookmarks.sort((x, y) => { - return x.dateAdded - y.dateAdded; - }); - - // Iterate native bookmarks to add unique bookmark ids in correct order - allNativeBookmarks.forEach((nativeBookmark) => { - this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { - if ( - !bookmark.id && - ((!nativeBookmark.url && bookmark.title === nativeBookmark.title) || - (nativeBookmark.url && bookmark.url === nativeBookmark.url)) - ) { - bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); - } - }); - }); - - // Find and fix any bookmarks missing ids - this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { - if (!bookmark.id) { - bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); - } - }); - - return bookmarks; - }); - }); - } - getNativeBookmarksWithSeparators( nativeBookmarks: NativeBookmarks.BookmarkTreeNode[] ): NativeBookmarks.BookmarkTreeNode[] { @@ -456,65 +129,89 @@ export default class ChromiumBookmarkService extends WebExtBookmarkService { if (this.isSeparator(bookmark)) { bookmark.type = BookmarkType.Separator; } - return bookmark; }); return nativeBookmarks; } - getNativeContainerIds(): ng.IPromise> { - return this.utilitySvc - .isSyncEnabled() - .then((syncEnabled) => (syncEnabled ? this.bookmarkHelperSvc.getCachedBookmarks() : undefined)) - .then((bookmarks) => { - // Initialise container ids object using containers defined in bookmarks - const containerIds = new Map(); - if (!angular.isUndefined(bookmarks)) { - bookmarks.forEach((x) => { - containerIds.set(x.title as BookmarkContainer, undefined); - }); - } + browserDetection: { isOpera: boolean }; + getBrowserDetection() { + if (this.browserDetection) return this.browserDetection; - // Populate container ids - return browser.bookmarks.getTree().then((tree) => { - // Get the root child nodes - const otherBookmarksNode = tree[0].children.find((x) => { - return x.id === this.otherBookmarksNodeId; - }); - const toolbarBookmarksNode = tree[0].children.find((x) => { - return x.id === this.toolbarBookmarksNodeId; - }); + const browserDetection: any = {}; + browserDetection.isChromeLike = this.utilitySvc.isChromeLikeBrowser(); + browserDetection.isOpera = this.utilitySvc.isOperaBrowser(); + browserDetection.isEdgeChromium = this.utilitySvc.isEdgeChromiumBrowser(); - // Throw an error if a native container node is not found - if (!otherBookmarksNode || !toolbarBookmarksNode) { - if (!otherBookmarksNode) { - this.logSvc.logWarning('Missing container: other bookmarks'); - } - if (!toolbarBookmarksNode) { - this.logSvc.logWarning('Missing container: toolbar bookmarks'); - } - throw new Exceptions.ContainerNotFoundException(); - } + this.browserDetection = browserDetection; + return this.browserDetection; + } - // Check for unsupported containers - const menuBookmarksNode = otherBookmarksNode.children.find((x) => { - return x.title === BookmarkContainer.Menu; - }); - const mobileBookmarksNode = otherBookmarksNode.children.find((x) => { - return x.title === BookmarkContainer.Mobile; + chromiumSupportedContainersInfo = { + map: new Map([ + [BookmarkContainer.Toolbar, { id: '1', throwIfNotFound: false }], + [BookmarkContainer.Other, { id: '2', throwIfNotFound: false }], + [BookmarkContainer.Mobile, { id: '3', throwIfNotFound: false }] + ]), + default: [BookmarkContainer.Other, BookmarkContainer.Mobile] + }; + + operaSupportedContainersInfo = { + map: new Map([ + [BookmarkContainer.Toolbar, { id: 'bookmarks_bar', throwIfNotFound: false }], + [BookmarkContainer.Other, { id: 'other', throwIfNotFound: true }] + // [, { id: 'unsorted', throwIfNotFound: false }], + // [, { id: 'user_root', throwIfNotFound: false }], + // [, { id: 'shared', throwIfNotFound: false }], + // [, { id: 'trash', throwIfNotFound: false }], + // [, { id: 'speed_dial', throwIfNotFound: false }], + ]), + default: [BookmarkContainer.Other] + }; + + getNativeContainerInfo(containerName: BookmarkContainer): ng.IPromise<{ id?: string; throwIfNotFound: boolean }> { + const browserDetection = this.getBrowserDetection(); + if (browserDetection.isOpera) { + this.logSvc.logInfo('detected browser: Opera'); + const getByName: ( + id: string, + callback: (node: NativeBookmarks.BookmarkTreeNode) => void + ) => void = (browser as any).bookmarks.getRootByName; + + const baseInfo = this.operaSupportedContainersInfo.map.get(containerName); + if (baseInfo) { + return this.$q((resolve) => { + getByName(baseInfo.id, (node) => { + resolve({ id: node.id, throwIfNotFound: baseInfo.throwIfNotFound }); }); - - // Add container ids to result - containerIds.set(BookmarkContainer.Other, otherBookmarksNode.id); - containerIds.set(BookmarkContainer.Toolbar, toolbarBookmarksNode.id); - if (!angular.isUndefined(menuBookmarksNode)) { - containerIds.set(BookmarkContainer.Menu, menuBookmarksNode.id); - } - if (!angular.isUndefined(mobileBookmarksNode)) { - containerIds.set(BookmarkContainer.Mobile, mobileBookmarksNode.id); - } - return containerIds; }); + // eslint-disable-next-line no-else-return + } else { + return this.$q.resolve({ id: undefined, throwIfNotFound: false }); + } + // eslint-disable-next-line no-else-return + } else { + this.logSvc.logInfo('detected browser: generic Chromium'); + return browser.bookmarks.getTree().then((tree) => { + const baseInfo = this.chromiumSupportedContainersInfo.map.get(containerName); + let info: { id?: string; throwIfNotFound: boolean }; + if (baseInfo && tree[0].children!.find((x) => x.id === baseInfo.id)) { + info = { ...baseInfo }; // make a copy + } else { + info = { id: undefined, throwIfNotFound: false }; + } + return info; }); + } + } + + getDefaultNativeContainerCandidates(): BookmarkContainer[] { + const browserDetection = this.getBrowserDetection(); + if (browserDetection.isOpera) { + return this.operaSupportedContainersInfo.default; + // eslint-disable-next-line no-else-return + } else { + return this.chromiumSupportedContainersInfo.default; + } } isSeparator(nativeBookmark: NativeBookmarks.BookmarkTreeNode): boolean { @@ -578,7 +275,7 @@ export default class ChromiumBookmarkService extends WebExtBookmarkService { }); } - syncNativeBookmarkCreated(id?: string, nativeBookmark?: NativeBookmarks.BookmarkTreeNode): ng.IPromise { + syncNativeBookmarkCreated(id: string, nativeBookmark: NativeBookmarks.BookmarkTreeNode): ng.IPromise { // If bookmark is separator update native bookmark properties return (this.isSeparator(nativeBookmark) ? this.convertNativeBookmarkToSeparator(nativeBookmark) diff --git a/src/modules/webext/firefox/shared/firefox-bookmark/firefox-bookmark.service.ts b/src/modules/webext/firefox/shared/firefox-bookmark/firefox-bookmark.service.ts index 382e218d7..eb61b2e5d 100644 --- a/src/modules/webext/firefox/shared/firefox-bookmark/firefox-bookmark.service.ts +++ b/src/modules/webext/firefox/shared/firefox-bookmark/firefox-bookmark.service.ts @@ -1,11 +1,9 @@ -import angular from 'angular'; import { Injectable } from 'angular-ts-decorators'; import autobind from 'autobind-decorator'; import { Bookmarks as NativeBookmarks, browser } from 'webextension-polyfill-ts'; import { BookmarkChangeType, BookmarkContainer } from '../../../../shared/bookmark/bookmark.enum'; import { AddNativeBookmarkChangeData, - Bookmark, BookmarkChange, ModifyNativeBookmarkChangeData, MoveNativeBookmarkChangeData @@ -17,187 +15,6 @@ import WebExtBookmarkService from '../../../shared/webext-bookmark/webext-bookma @autobind @Injectable('BookmarkService') export default class FirefoxBookmarkService extends WebExtBookmarkService { - unsupportedContainers = []; - - clearNativeBookmarks(): ng.IPromise { - // Get native container ids - return this.getNativeContainerIds() - .then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Clear menu bookmarks - const clearMenu = browser.bookmarks - .getChildren(menuBookmarksId) - .then((results) => { - return this.$q.all( - results.map((child) => { - return this.removeNativeBookmarks(child.id); - }) - ); - }) - .catch((err) => { - this.logSvc.logWarning('Error clearing bookmarks menu'); - throw err; - }); - - // Clear mobile bookmarks - const clearMobile = browser.bookmarks - .getChildren(mobileBookmarksId) - .then((results) => { - return this.$q.all( - results.map((child) => { - return this.removeNativeBookmarks(child.id); - }) - ); - }) - .catch((err) => { - this.logSvc.logWarning('Error clearing mobile bookmarks'); - throw err; - }); - - // Clear other bookmarks - 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; - }); - - // Clear bookmarks toolbar if enabled - 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([clearMenu, clearMobile, clearOthers, clearToolbar]).then(() => {}); - }) - .catch((err) => { - throw new Exceptions.FailedRemoveNativeBookmarksException(undefined, err); - }); - } - - createNativeBookmarksFromBookmarks(bookmarks: Bookmark[]): ng.IPromise { - // Get containers - const menuContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Menu, bookmarks); - const mobileContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Mobile, bookmarks); - const otherContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks); - const toolbarContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarks); - - // Get native container ids - return this.getNativeContainerIds() - .then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Populate menu bookmarks - let populateMenu = this.$q.resolve(0); - if (menuContainer) { - populateMenu = browser.bookmarks - .getSubTree(menuBookmarksId) - .then(() => { - return this.createNativeBookmarkTree(menuBookmarksId, menuContainer.children); - }) - .catch((err) => { - this.logSvc.logInfo('Error populating bookmarks menu.'); - throw err; - }); - } - - // Populate mobile bookmarks - let populateMobile = this.$q.resolve(0); - if (mobileContainer) { - populateMobile = browser.bookmarks - .getSubTree(mobileBookmarksId) - .then(() => { - return this.createNativeBookmarkTree(mobileBookmarksId, mobileContainer.children); - }) - .catch((err) => { - this.logSvc.logInfo('Error populating mobile bookmarks.'); - throw err; - }); - } - - // Populate other bookmarks - let populateOther = this.$q.resolve(0); - if (otherContainer) { - populateOther = browser.bookmarks - .getSubTree(otherBookmarksId) - .then(() => { - return this.createNativeBookmarkTree(otherBookmarksId, otherContainer.children); - }) - .catch((err) => { - this.logSvc.logInfo('Error populating other bookmarks.'); - throw err; - }); - } - - // Populate bookmarks toolbar if enabled - const populateToolbar = this.$q((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, populateMobile, populateOther, populateToolbar]); - }) - .then((totals) => { - // Move native unsupported containers into the correct order - return this.reorderUnsupportedContainers().then(() => { - return totals.filter(Boolean).reduce((a, b) => a + b, 0); - }); - }); - } - createNativeSeparator(parentId: string): ng.IPromise { const newSeparator: NativeBookmarks.CreateDetails = { parentId, @@ -244,30 +61,6 @@ export default class FirefoxBookmarkService extends WebExtBookmarkService { }); } - ensureContainersExist(bookmarks: Bookmark[]): Bookmark[] { - if (angular.isUndefined(bookmarks)) { - return; - } - - // Add supported containers - const bookmarksToReturn = angular.copy(bookmarks); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Menu, bookmarksToReturn, true); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Mobile, bookmarksToReturn, true); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarksToReturn, true); - this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarksToReturn, true); - - // Return sorted containers - return bookmarksToReturn.sort((x, y) => { - if (x.title < y.title) { - return -1; - } - if (x.title > y.title) { - return 1; - } - return 0; - }); - } - fixMultipleMoveOldIndexes(): void { const processBatch = (batch) => { // Adjust oldIndexes if bookmarks moved to different parent or to higher indexes @@ -314,214 +107,28 @@ export default class FirefoxBookmarkService extends WebExtBookmarkService { } } - getNativeBookmarksAsBookmarks(): ng.IPromise { - let allNativeBookmarks = []; - - // Get native container ids - return this.getNativeContainerIds() - .then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Get menu bookmarks - const getMenuBookmarks = - menuBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(menuBookmarksId).then((subTree) => { - const menuBookmarks = subTree[0]; - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(menuBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks(menuBookmarks.children); - }); - - // Get mobile bookmarks - const getMobileBookmarks = - mobileBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(mobileBookmarksId).then((subTree) => { - const mobileBookmarks = subTree[0]; - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(mobileBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks(mobileBookmarks.children); - }); - - // Get other bookmarks - const getOtherBookmarks = - otherBookmarksId === undefined - ? Promise.resolve(undefined) - : browser.bookmarks.getSubTree(otherBookmarksId).then((subTree) => { - const otherBookmarks = subTree[0]; - if (otherBookmarks.children.length === 0) { - return; - } - - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(otherBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - - // Convert native bookmarks sub tree to bookmarks - const bookmarks = this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks(otherBookmarks.children); - - // Remove any unsupported container folders present - const bookmarksWithoutContainers = bookmarks.filter((x) => { - return !this.unsupportedContainers.find((y) => { - return y === x.title; - }); - }); - return bookmarksWithoutContainers; - }); - - // Get toolbar bookmarks if enabled - const getToolbarBookmarks = - toolbarBookmarksId === undefined - ? this.$q.resolve(undefined) - : browser.bookmarks.getSubTree(toolbarBookmarksId).then((results) => { - const toolbarBookmarks = results[0]; - return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { - if (syncBookmarksToolbar && toolbarBookmarks.children.length > 0) { - // Add all bookmarks into flat array - this.bookmarkHelperSvc.eachBookmark(toolbarBookmarks.children, (bookmark) => { - allNativeBookmarks.push(bookmark); - }); - return this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks(toolbarBookmarks.children); - } - }); - }); - - return this.$q.all([getMenuBookmarks, getMobileBookmarks, getOtherBookmarks, getToolbarBookmarks]); - }) - .then((results) => { - const menuBookmarks = results[0]; - const mobileBookmarks = results[1]; - const otherBookmarks = results[2]; - const toolbarBookmarks = results[3]; - const bookmarks: Bookmark[] = []; - - // Add other container if bookmarks present - const otherContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks, true); - if (otherBookmarks?.length > 0) { - otherContainer.children = otherBookmarks; - } - - // Add toolbar container if bookmarks present - const toolbarContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Toolbar, bookmarks, true); - if (toolbarBookmarks?.length > 0) { - toolbarContainer.children = toolbarBookmarks; - } - - // Add menu container if bookmarks present - const menuContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Menu, bookmarks, true); - if (menuBookmarks?.length > 0) { - menuContainer.children = menuBookmarks; - } - - // Add mobile container if bookmarks present - const mobileContainer = this.bookmarkHelperSvc.getContainer(BookmarkContainer.Mobile, bookmarks, true); - if (mobileBookmarks?.length > 0) { - mobileContainer.children = mobileBookmarks; - } - - // Filter containers from flat array of bookmarks - [otherContainer, toolbarContainer, menuContainer, mobileContainer].forEach((container) => { - if (!container) { - return; - } - - allNativeBookmarks = allNativeBookmarks.filter((bookmark) => { - return bookmark.title !== container.title; - }); - }); - - // Sort by date added asc - allNativeBookmarks = allNativeBookmarks.sort((x, y) => { - return x.dateAdded - y.dateAdded; - }); - - // Iterate native bookmarks to add unique bookmark ids in correct order - allNativeBookmarks.forEach((nativeBookmark) => { - this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { - if ( - !bookmark.id && - ((!nativeBookmark.url && bookmark.title === nativeBookmark.title) || - (nativeBookmark.url && bookmark.url === nativeBookmark.url)) - ) { - bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); - } - }); - }); - - // Find and fix any bookmarks missing ids - this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { - if (!bookmark.id) { - bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); - } - }); - - return bookmarks; - }); + supportedContainersInfo = new Map([ + [BookmarkContainer.Menu, { id: 'menu________', throwIfNotFound: true }], + [BookmarkContainer.Other, { id: 'unfiled_____', throwIfNotFound: true }], + [BookmarkContainer.Toolbar, { id: 'toolbar_____', throwIfNotFound: true }], + [BookmarkContainer.Mobile, { id: 'mobile______', throwIfNotFound: true }] + ]); + + getNativeContainerInfo(containerName: BookmarkContainer): ng.IPromise<{ id?: string; throwIfNotFound: boolean }> { + return browser.bookmarks.getTree().then((tree) => { + const baseInfo = this.supportedContainersInfo.get(containerName); + let info: { id?: string; throwIfNotFound: boolean }; + if (baseInfo && tree[0].children!.find((x) => x.id === baseInfo.id)) { + info = { ...baseInfo }; // make a copy + } else { + info = { id: undefined, throwIfNotFound: false }; + } + return info; + }); } - getNativeContainerIds(): ng.IPromise> { - return this.utilitySvc - .isSyncEnabled() - .then((syncEnabled) => (syncEnabled ? this.bookmarkHelperSvc.getCachedBookmarks() : undefined)) - .then((bookmarks) => { - // Initialise container ids object using containers defined in bookmarks - const containerIds = new Map(); - if (!angular.isUndefined(bookmarks)) { - bookmarks.forEach((x) => { - containerIds.set(x.title as BookmarkContainer, undefined); - }); - } - - // Populate container ids - return browser.bookmarks.getTree().then((tree) => { - // Get the root child nodes - const menuBookmarksNode = tree[0].children.find((x) => { - return x.id === 'menu________'; - }); - const mobileBookmarksNode = tree[0].children.find((x) => { - return x.id === 'mobile______'; - }); - const otherBookmarksNode = tree[0].children.find((x) => { - return x.id === 'unfiled_____'; - }); - const toolbarBookmarksNode = tree[0].children.find((x) => { - return x.id === 'toolbar_____'; - }); - - // Throw an error if a native container is not found - if (!menuBookmarksNode || !mobileBookmarksNode || !otherBookmarksNode || !toolbarBookmarksNode) { - if (!menuBookmarksNode) { - this.logSvc.logWarning('Missing container: menu bookmarks'); - } - if (!mobileBookmarksNode) { - this.logSvc.logWarning('Missing container: mobile bookmarks'); - } - if (!otherBookmarksNode) { - this.logSvc.logWarning('Missing container: other bookmarks'); - } - if (!toolbarBookmarksNode) { - this.logSvc.logWarning('Missing container: toolbar bookmarks'); - } - throw new Exceptions.ContainerNotFoundException(); - } - - // Add container ids to result - containerIds.set(BookmarkContainer.Menu, menuBookmarksNode.id); - containerIds.set(BookmarkContainer.Mobile, mobileBookmarksNode.id); - containerIds.set(BookmarkContainer.Other, otherBookmarksNode.id); - containerIds.set(BookmarkContainer.Toolbar, toolbarBookmarksNode.id); - return containerIds; - }); - }); + getDefaultNativeContainerCandidates(): BookmarkContainer[] { + return [BookmarkContainer.Other]; } processNativeBookmarkEventsQueue(): void { diff --git a/src/modules/webext/shared/webext-bookmark/NativeContainersInfo.ts b/src/modules/webext/shared/webext-bookmark/NativeContainersInfo.ts new file mode 100644 index 000000000..e0f52663b --- /dev/null +++ b/src/modules/webext/shared/webext-bookmark/NativeContainersInfo.ts @@ -0,0 +1,5 @@ +import { BookmarkContainer } from '../../../shared/bookmark/bookmark.enum'; + +export class NativeContainersInfo extends Map { + defaultNativeContainerId: string; +} diff --git a/src/modules/webext/shared/webext-bookmark/webext-bookmark.service.ts b/src/modules/webext/shared/webext-bookmark/webext-bookmark.service.ts index f595b06cf..45649e9af 100644 --- a/src/modules/webext/shared/webext-bookmark/webext-bookmark.service.ts +++ b/src/modules/webext/shared/webext-bookmark/webext-bookmark.service.ts @@ -7,6 +7,7 @@ import { Bookmark, BookmarkChange, BookmarkMetadata, + BookmarkService, ModifyNativeBookmarkChangeData, MoveNativeBookmarkChangeData, OnChildrenReorderedReorderInfoType, @@ -27,9 +28,10 @@ import SyncEngineService from '../../../shared/sync/sync-engine/sync-engine.serv import UtilityService from '../../../shared/utility/utility.service'; import { BookmarkIdMapping } from '../bookmark-id-mapper/bookmark-id-mapper.interface'; import BookmarkIdMapperService from '../bookmark-id-mapper/bookmark-id-mapper.service'; +import { NativeContainersInfo } from './NativeContainersInfo'; @autobind -export default abstract class WebExtBookmarkService { +export default abstract class WebExtBookmarkService implements BookmarkService { $injector: ng.auto.IInjectorService; $q: ng.IQService; $timeout: ng.ITimeoutService; @@ -44,7 +46,6 @@ export default abstract class WebExtBookmarkService { nativeBookmarkEventsQueue: any[] = []; processNativeBookmarkEventsTimeout: ng.IPromise; - unsupportedContainers = []; static $inject = [ '$injector', @@ -111,105 +112,71 @@ export default abstract class WebExtBookmarkService { ): BookmarkIdMapping[] => { return nativeBookmarks.reduce((acc, val, index) => { // Create mapping for the current node - const mapping = this.bookmarkIdMapperSvc.createMapping(syncedBookmarks[index].id, val.id); + const mapping = this.bookmarkIdMapperSvc.createMapping(syncedBookmarks[index].id as number, val.id); acc.push(mapping); // Process child nodes - return val.children?.length ? acc.concat(mapIds(val.children, syncedBookmarks[index].children)) : acc; + return val.children?.length + ? acc.concat(mapIds(val.children, syncedBookmarks[index].children as Bookmark[])) + : acc; }, [] as BookmarkIdMapping[]); }; // Get native container ids return this.getNativeContainerIds() .then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - // Map menu bookmarks - const getMenuBookmarks = - menuBookmarksId == null - ? this.$q.resolve([] as BookmarkIdMapping[]) - : browser.bookmarks.getSubTree(menuBookmarksId).then((subTree) => { - const menuBookmarks = subTree[0]; - if (!menuBookmarks.children?.length) { - return [] as BookmarkIdMapping[]; - } - - // Map ids between nodes and synced container children - const menuBookmarksContainer = bookmarks.find((x) => { - return x.title === BookmarkContainer.Menu; - }); - return menuBookmarksContainer?.children?.length - ? mapIds(menuBookmarks.children, menuBookmarksContainer.children) - : ([] as BookmarkIdMapping[]); - }); + // Get whether syncBookmarksToolbar + return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { + const getBookmarkPromises = new Array>(); + + // eslint-disable-next-line no-restricted-syntax + for (const containerEnumVal of Object.keys(BookmarkContainer)) { + const containerName = BookmarkContainer[containerEnumVal]; + // Get native bookmark node id + const nativeBookmarkNodeId = nativeContainerIds.get(containerName); + + if (containerName === BookmarkContainer.Toolbar) { + if (!syncBookmarksToolbar) { + this.logSvc.logInfo('Not mapping toolbar'); + // eslint-disable-next-line no-continue + continue; + } + } - // Map mobile bookmarks - const getMobileBookmarks = - mobileBookmarksId == null - ? this.$q.resolve([] as BookmarkIdMapping[]) - : browser.bookmarks.getSubTree(mobileBookmarksId).then((subTree) => { - const mobileBookmarks = subTree[0]; - if (!mobileBookmarks.children?.length) { + // Map bookmarks of that type + if (nativeBookmarkNodeId) { + const getBookmarkPromise = browser.bookmarks.getSubTree(nativeBookmarkNodeId).then((subTree) => { + const bookmarksNode = subTree[0]; + if (!bookmarksNode.children?.length) { return [] as BookmarkIdMapping[]; } - // Map ids between nodes and synced container children - const mobileBookmarksContainer = bookmarks.find((x) => { - return x.title === BookmarkContainer.Mobile; - }); - return mobileBookmarksContainer?.children?.length - ? mapIds(mobileBookmarks.children, mobileBookmarksContainer.children) - : ([] as BookmarkIdMapping[]); - }); - - // Map other bookmarks - const getOtherBookmarks = - otherBookmarksId == null - ? this.$q.resolve([] as BookmarkIdMapping[]) - : browser.bookmarks.getSubTree(otherBookmarksId).then((subTree) => { - const otherBookmarks = subTree[0]; - if (!otherBookmarks.children?.length) { - return [] as BookmarkIdMapping[]; + let bookmarksNodeChildren: NativeBookmarks.BookmarkTreeNode[]; + // Skip over any unsupported container mount-point bookmark folders present, + // if we are now "in" the platform-default bookmark node + // The skipped bookmarks will be processed in this loop for their own nativeContainerIds entry + if (nativeBookmarkNodeId === nativeContainerIds.defaultNativeContainerId) { + bookmarksNodeChildren = bookmarksNode.children.filter( + (x) => !this.getUnsupportedContainers().includes(x.title as BookmarkContainer) + ); + } else { + bookmarksNodeChildren = bookmarksNode.children; } - // Remove any unsupported container folders present - const nodes = otherBookmarks.children.filter((x) => !this.unsupportedContainers.includes(x.title)); - // Map ids between nodes and synced container children - const otherBookmarksContainer = bookmarks.find((x) => { - return x.title === BookmarkContainer.Other; + const container = bookmarks.find((x) => { + return x.title === containerName; }); - return otherBookmarksContainer?.children?.length - ? mapIds(nodes, otherBookmarksContainer.children) + return container?.children?.length + ? mapIds(bookmarksNodeChildren, container.children) : ([] as BookmarkIdMapping[]); }); + getBookmarkPromises.push(getBookmarkPromise); + } + } - // Map toolbar bookmarks if enabled - const getToolbarBookmarks = - toolbarBookmarksId == null - ? this.$q.resolve([] as BookmarkIdMapping[]) - : browser.bookmarks.getSubTree(toolbarBookmarksId).then((results) => { - return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { - const toolbarBookmarks = results[0]; - - if (!syncBookmarksToolbar || !toolbarBookmarks.children?.length) { - return [] as BookmarkIdMapping[]; - } - - // Map ids between nodes and synced container children - const toolbarBookmarksContainer = bookmarks.find((x) => { - return x.title === BookmarkContainer.Toolbar; - }); - return toolbarBookmarksContainer?.children?.length - ? mapIds(toolbarBookmarks.children, toolbarBookmarksContainer.children) - : ([] as BookmarkIdMapping[]); - }); - }); - - return this.$q.all([getMenuBookmarks, getMobileBookmarks, getOtherBookmarks, getToolbarBookmarks]); + return this.$q.all(getBookmarkPromises); + }); }) .then((results) => { // Combine all mappings @@ -225,7 +192,7 @@ export default abstract class WebExtBookmarkService { checkIfBookmarkChangeShouldBeSynced(changedBookmark: Bookmark, bookmarks: Bookmark[]): ng.IPromise { return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { // If container is Toolbar, check if Toolbar sync is disabled - const container = this.bookmarkHelperSvc.getContainerByBookmarkId(changedBookmark.id, bookmarks); + const container = this.bookmarkHelperSvc.getContainerByBookmarkId(changedBookmark.id as number, bookmarks); if (!container) { throw new Exceptions.ContainerNotFoundException(); } @@ -248,7 +215,61 @@ export default abstract class WebExtBookmarkService { }); } - abstract clearNativeBookmarks(): ng.IPromise; + clearNativeBookmarks(): ng.IPromise { + // Get native container ids + return this.getNativeContainerIds() + .then((nativeContainerIds) => { + // Get whether syncBookmarksToolbar + return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { + const clearPromises = []; + + // eslint-disable-next-line no-restricted-syntax + for (const containerEnumVal of Object.keys(BookmarkContainer)) { + const containerName = BookmarkContainer[containerEnumVal]; + // Get native bookmark node id + const nativeBookmarkNodeId = nativeContainerIds.get(containerName); + + if (containerName === BookmarkContainer.Toolbar) { + if (!syncBookmarksToolbar) { + this.logSvc.logInfo('Not clearing toolbar'); + // eslint-disable-next-line no-continue + continue; + } + } + + // Clear bookmarks of that type + if (nativeBookmarkNodeId) { + const clearPromise = browser.bookmarks + .getChildren(nativeBookmarkNodeId) + .then((children) => { + // TODO: alternatively the other way arround... do not clear unsupported containers bookmark nodes but clear the default bookmark node completely + // Do not remove the bookmark-folders that server as mount-point for unsupported containers + if (nativeBookmarkNodeId === nativeContainerIds.defaultNativeContainerId) { + children = children.filter( + (x) => !this.getUnsupportedContainers().includes(x.title as BookmarkContainer) + ); + } + return this.$q.all( + children.map((child) => { + return this.removeNativeBookmarks(child.id); + }) + ); + }) + .catch((err) => { + this.logSvc.logWarning(`Error clearing ${containerEnumVal} bookmarks`); + throw err; + }); + clearPromises.push(clearPromise); + } + } + + return this.$q.all(clearPromises).then(() => {}); + }); + }) + .catch((err) => { + throw new Exceptions.FailedRemoveNativeBookmarksException(undefined, err); + }); + } convertNativeBookmarkToBookmark( nativeBookmark: NativeBookmarks.BookmarkTreeNode, @@ -283,14 +304,14 @@ export default abstract class WebExtBookmarkService { countNativeContainersBeforeIndex(parentId: string, index: number): ng.IPromise { // Get native container ids return this.getNativeContainerIds().then((nativeContainerIds) => { - // No containers to adjust for if parent is not other bookmarks - if (parentId !== nativeContainerIds.get(BookmarkContainer.Other)) { + // No containers to adjust for if parent is not platform-default bookmarks node + if (parentId !== nativeContainerIds.defaultNativeContainerId) { return 0; } // Get parent bookmark and count containers return browser.bookmarks.getSubTree(parentId).then((subTree) => { - const numContainers = subTree[0].children.filter((child, childIndex) => { + const numContainers = subTree[0].children!.filter((child, childIndex) => { return childIndex < index && Array.from(nativeContainerIds.values()).includes(child.id); }).length; return numContainers; @@ -311,8 +332,8 @@ export default abstract class WebExtBookmarkService { createNativeBookmark( parentId: string, - title: string, - url: string, + title?: string, + url?: string, index?: number ): ng.IPromise { const nativeBookmarkInfo: NativeBookmarks.CreateDetails = { @@ -332,16 +353,72 @@ export default abstract class WebExtBookmarkService { }); } - abstract createNativeBookmarksFromBookmarks(bookmarks: Bookmark[]): ng.IPromise; + createNativeBookmarksFromBookmarks(bookmarks: Bookmark[]): ng.IPromise { + // Get native container ids + return this.getNativeContainerIds() + .then((nativeContainerIds) => { + // Get whether syncBookmarksToolbar + return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { + const populatePromises: ng.IPromise[] = []; + + // eslint-disable-next-line no-restricted-syntax + for (const containerEnumVal of Object.keys(BookmarkContainer)) { + const containerName = BookmarkContainer[containerEnumVal]; + // Get container + const container = this.bookmarkHelperSvc.getContainer(containerName, bookmarks); + // Get native bookmark node id + const nativeBookmarkNodeId = nativeContainerIds.get(containerName); + + if (containerName === BookmarkContainer.Toolbar) { + if (!syncBookmarksToolbar) { + this.logSvc.logInfo('Not populating toolbar'); + // eslint-disable-next-line no-continue + continue; + } + } - createNativeBookmarkTree( - parentId: string, - bookmarks: Bookmark[], - nativeToolbarContainerId?: string - ): ng.IPromise { + // Populate bookmarks for the container + if (container) { + let parentNodeId: string; + let childrenToCreate: Bookmark[]; + if (nativeBookmarkNodeId) { + // this is a natively supported container + parentNodeId = nativeBookmarkNodeId; + childrenToCreate = container.children!; + } else { + // there is no nativeContainerId -> it's not a natively supported container + // -> create it's mount-point bookmark folder now + parentNodeId = nativeContainerIds.defaultNativeContainerId; + childrenToCreate = [container]; + } + const populatePromise = browser.bookmarks + .getSubTree(parentNodeId) + .then(() => { + return this.createNativeBookmarkTree(parentNodeId, childrenToCreate); + }) + .catch((err) => { + this.logSvc.logInfo(`Error populating ${containerEnumVal}.`); + throw err; + }); + populatePromises.push(populatePromise); + } + } + + return this.$q.all(populatePromises); + }); + }) + .then((totals) => { + // Move native unsupported containers into the correct order + return this.reorderUnsupportedContainers().then(() => { + return totals.reduce((a, b) => a + b, 0); + }); + }); + } + + createNativeBookmarkTree(parentId: string, bookmarks: Bookmark[]): ng.IPromise { let processError: Error; let total = 0; - const createRecursive = (id: string, bookmarksToCreate: Bookmark[] = [], toolbarId: string) => { + const createRecursive = (id: string, bookmarksToCreate: Bookmark[] = []) => { const createChildBookmarksPromises = []; // Create bookmarks at the top level of the supplied array @@ -354,13 +431,11 @@ export default abstract class WebExtBookmarkService { } return this.bookmarkHelperSvc.getBookmarkType(bookmark) === BookmarkType.Separator - ? this.createNativeSeparator(id, toolbarId).then(() => {}) + ? this.createNativeSeparator(id).then(() => {}) : this.createNativeBookmark(id, bookmark.title, bookmark.url).then((newNativeBookmark) => { // If the bookmark has children, recurse if (bookmark.children?.length) { - createChildBookmarksPromises.push( - createRecursive(newNativeBookmark.id, bookmark.children, toolbarId) - ); + createChildBookmarksPromises.push(createRecursive(newNativeBookmark.id, bookmark.children)); } }); }); @@ -374,43 +449,47 @@ export default abstract class WebExtBookmarkService { throw err; }); }; - return createRecursive(parentId, bookmarks, nativeToolbarContainerId).then(() => total); + return createRecursive(parentId, bookmarks).then(() => total); } - abstract createNativeSeparator( - parentId: string, - nativeToolbarContainerId: string - ): ng.IPromise; + abstract createNativeSeparator(parentId: string): ng.IPromise; abstract disableEventListeners(): ng.IPromise; abstract enableEventListeners(): ng.IPromise; - abstract ensureContainersExist(bookmarks: Bookmark[]): Bookmark[]; + ensureContainersExist(bookmarks: Bookmark[]): Bookmark[] { + if (angular.isUndefined(bookmarks)) { + return undefined!; + } - getContainerNameFromNativeId(nativeBookmarkId: string): ng.IPromise { - return this.getNativeContainerIds().then((nativeContainerIds) => { - const menuBookmarksId = nativeContainerIds.get(BookmarkContainer.Menu); - const mobileBookmarksId = nativeContainerIds.get(BookmarkContainer.Mobile); - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - const toolbarBookmarksId = nativeContainerIds.get(BookmarkContainer.Toolbar); - - const nativeContainers = [ - { nativeId: otherBookmarksId, containerName: BookmarkContainer.Other }, - { nativeId: toolbarBookmarksId, containerName: BookmarkContainer.Toolbar } - ]; - - if (menuBookmarksId) { - nativeContainers.push({ nativeId: menuBookmarksId, containerName: BookmarkContainer.Menu }); - } + // Add supported containers + const bookmarksToReturn = angular.copy(bookmarks); + this.getSupportedContainers().forEach((element) => { + this.bookmarkHelperSvc.getContainer(element, bookmarksToReturn, true); + }); - if (mobileBookmarksId) { - nativeContainers.push({ nativeId: mobileBookmarksId, containerName: BookmarkContainer.Mobile }); + // Return sorted containers + return bookmarksToReturn.sort((x, y) => { + if (x.title! < y.title!) { + return -1; } + if (x.title! > y.title!) { + return 1; + } + return 0; + }); + } + + getContainerNameFromNativeId(nativeBookmarkId: string): ng.IPromise { + if (angular.isUndefined(nativeBookmarkId)) return this.$q.resolve(''); - // Check if the native bookmark id resolves to a container - const result = nativeContainers.find((x) => x.nativeId === nativeBookmarkId); - return result ? result.containerName : ''; + return this.getNativeContainerIds().then((nativeContainerIds) => { + // eslint-disable-next-line no-restricted-syntax + for (const [containerName, nativeBookmarkNodeId] of nativeContainerIds.entries()) { + if (nativeBookmarkNodeId === nativeBookmarkId) return containerName; + } + return ''; }); } @@ -436,28 +515,257 @@ export default abstract class WebExtBookmarkService { }); } - abstract getNativeBookmarksAsBookmarks(): ng.IPromise; + getNativeBookmarksAsBookmarks(): ng.IPromise { + let allNativeBookmarks: NativeBookmarks.BookmarkTreeNode[] = []; + + // Get native container ids + return this.getNativeContainerIds().then((nativeContainerIds) => { + // Get whether syncBookmarksToolbar + return this.settingsSvc.syncBookmarksToolbar().then((syncBookmarksToolbar) => { + const getBookmarkPromises = new Array]>>(); + + // eslint-disable-next-line no-restricted-syntax + for (const containerEnumVal of Object.keys(BookmarkContainer)) { + const containerName: BookmarkContainer = BookmarkContainer[containerEnumVal]; + // Get native bookmark node id + const nativeBookmarkNodeId = nativeContainerIds.get(containerName); + + if (containerName === BookmarkContainer.Toolbar) { + if (!syncBookmarksToolbar) { + // skip + // eslint-disable-next-line no-continue + continue; + } + } + + // Map bookmarks of that type + if (nativeBookmarkNodeId) { + const getBookmarkPromise: Promise<[BookmarkContainer, Array]> = browser.bookmarks + .getSubTree(nativeBookmarkNodeId) + // eslint-disable-next-line @typescript-eslint/no-loop-func + .then((subTree) => { + const bookmarksNode = subTree[0]; + + if (!bookmarksNode.children?.length) { + return [containerName, [] as Bookmark[]]; + } + + let bookmarksNodeChildren: NativeBookmarks.BookmarkTreeNode[]; + // Skip over any unsupported container mount-point bookmark folders present, + // if we are now "in" the platform-default bookmark node. + // The skipped bookmarks will be processed in this loop for their own nativeContainerIds entry + if (nativeBookmarkNodeId === nativeContainerIds.defaultNativeContainerId) { + bookmarksNodeChildren = bookmarksNode.children.filter( + (x) => !this.getUnsupportedContainers().includes(x.title as BookmarkContainer) + ); + } else { + bookmarksNodeChildren = bookmarksNode.children; + } + + // Add all native bookmarks (except the "unsupported containers" mount-point folders) into flat array + this.bookmarkHelperSvc.eachBookmark(bookmarksNodeChildren, (bookmark) => { + allNativeBookmarks.push(bookmark); + }); + + // Return all native bookmarks (except the "unsupported containers" mount-point folders) + // converted to "our" bookmarks. + const convertedBookmarks = this.bookmarkHelperSvc.getNativeBookmarksAsBookmarks( + this.getNativeBookmarksWithSeparators(bookmarksNodeChildren) + ); + return [containerName, convertedBookmarks]; + }); + + getBookmarkPromises.push(getBookmarkPromise); + } + } + + return this.$q.all(getBookmarkPromises).then((containerBookmarksPairArray) => { + const bookmarks: Bookmark[] = []; + + containerBookmarksPairArray.forEach((tuple) => { + const [containerName, convertedBookmarks] = tuple; + + const container = this.bookmarkHelperSvc.getContainer(containerName, bookmarks, true); + if (convertedBookmarks.length > 0) { + container.children = convertedBookmarks; + } + + // Michal Kotoun's note: this is probably a no-op, since we only added to allNativeBookmarks: + // 1) children of non-default containers + // 2) children of default container except for virtual/unsupportedContainers + // Filter containers from flat array of bookmarks + allNativeBookmarks = allNativeBookmarks.filter((bookmark) => { + return bookmark.title !== container.title; + }); + }); + + // Sort by date added asc + allNativeBookmarks = allNativeBookmarks.sort((x, y) => { + return x.dateAdded - y.dateAdded; + }); + + // Iterate native bookmarks to add unique bookmark ids in correct order + allNativeBookmarks.forEach((nativeBookmark) => { + this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { + if ( + !bookmark.id && + ((!nativeBookmark.url && bookmark.title === nativeBookmark.title) || + (nativeBookmark.url && bookmark.url === nativeBookmark.url)) + ) { + bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); + } + }); + }); + + // Find and fix any bookmarks missing ids + this.bookmarkHelperSvc.eachBookmark(bookmarks, (bookmark) => { + if (!bookmark.id) { + bookmark.id = this.bookmarkHelperSvc.getNewBookmarkId(bookmarks); + } + }); + + return bookmarks; + }); + }); + }); + } + + // no-op by default (in Firefox) -> maybe a slight reformatting to move the bookmark.type = Separator assignment in chromium would be nice! + getNativeBookmarksWithSeparators( + nativeBookmarks: NativeBookmarks.BookmarkTreeNode[] + ): NativeBookmarks.BookmarkTreeNode[] { + return nativeBookmarks; + } + + /** + * to be overridden; used in getNativeContainerIds() + * + * id: the native id, if it is supported \ + * throwIfNotFound: whether getNativeContainerIds should throw an exception, when the id is undefined + */ + abstract getNativeContainerInfo( + containerName: BookmarkContainer + ): ng.IPromise<{ id?: string; throwIfNotFound: boolean }>; + + /** + * to be overridden; used in getNativeContainerIds() + */ + abstract getDefaultNativeContainerCandidates(): BookmarkContainer[]; + + unsupportedNativeContainerCache: BookmarkContainer[]; + supportedNativeContainerCache: BookmarkContainer[]; + supportedNativeContainerIdsCache: Map; + + /** wrapper for unsupportedNativeContainerCache */ + getUnsupportedContainers(): BookmarkContainer[] { + return this.unsupportedNativeContainerCache; + } + + /** wrapper for supportedNativeContainerCache */ + getSupportedContainers(): BookmarkContainer[] { + return this.supportedNativeContainerCache; + } + + /** + * must be called before any getSupportedContainers() / getUnsupportedContainers() calls + */ + identifySupportedContainers(): ng.IPromise { + let promise: ng.IPromise; + if (this.supportedNativeContainerCache === undefined) { + // initialize + this.supportedNativeContainerCache = []; + this.supportedNativeContainerIdsCache = new Map(); + + const promises = Object.values(BookmarkContainer).map((containerName) => { + return this.getNativeContainerInfo(containerName).then((info) => { + if (info.id) { + // add to supported cache + this.supportedNativeContainerCache.push(containerName); + this.supportedNativeContainerIdsCache.set(containerName, info.id); + } else { + this.logSvc.logWarning(`Missing container for: ${containerName}`); + if (info.throwIfNotFound) { + throw new Exceptions.ContainerNotFoundException(); + } + } + }); + }); + promise = this.$q.all(promises).then(() => { + this.unsupportedNativeContainerCache = Object.values(BookmarkContainer).filter( + (bc) => !this.supportedNativeContainerCache.includes(bc) + ); + }); + return promise; + } + // else + return this.$q.resolve(); + } + + /** + * Returns the mapping of BookmarkContainer to native BookmarkTreeNode ids. + * For natively supported containers, their ids are returned. + * For (natively) unsupported containers, it returns the id the bookmark-folder they are mapped to - the detection is based on: + * 1) they are children of the browser-default container; + * 2) the name of the folder equals to the name of the bookmark container. + */ + getNativeContainerIds(): ng.IPromise { + return this.identifySupportedContainers().then(() => { + const containerIds = new NativeContainersInfo(this.supportedNativeContainerIdsCache); + + // Throw an error if a default container is not found + let defaultNativeContainerId: string | undefined; + // eslint-disable-next-line no-restricted-syntax + for (const candidate of this.getDefaultNativeContainerCandidates()) { + defaultNativeContainerId = containerIds.get(candidate); + if (defaultNativeContainerId) break; + } + + if (!defaultNativeContainerId) { + // could not find a default container to create folders to mount natively unsupported containers into + throw new Exceptions.ContainerNotFoundException(); + } + containerIds.defaultNativeContainerId = defaultNativeContainerId; - abstract getNativeContainerIds(): ng.IPromise>; + // if all BookmarkContainer have now associated IDs, return + if (!Object.values(BookmarkContainer).find((containerName) => containerIds.get(containerName) === undefined)) { + return containerIds; + } + + return browser.bookmarks.getTree().then((tree) => { + const defaultBookmarksNode = tree[0].children!.find((x) => { + return x.id === defaultNativeContainerId; + })!; + // eslint-disable-next-line no-restricted-syntax + for (const containerName of Object.values(BookmarkContainer)) { + if (!containerIds.get(containerName)) { + const mountPointNode = defaultBookmarksNode.children!.find((x) => x.title === containerName); + if (mountPointNode) containerIds.set(containerName, mountPointNode.id); + } + } + return containerIds; + }); + }); + } - getSupportedUrl(url: string): string { + getSupportedUrl(url?: string): string { if (angular.isUndefined(url ?? undefined)) { return ''; } + url = url!; // If url is not supported, use new tab url instead let returnUrl = url; if (!this.platformSvc.urlIsSupported(url)) { this.logSvc.logInfo(`Bookmark url unsupported: ${url}`); - returnUrl = this.platformSvc.getNewTabUrl(); + returnUrl = this.platformSvc.getNewTabUrl!(); } return returnUrl; } - isNativeBookmarkInToolbarContainer(nativeBookmark: NativeBookmarks.BookmarkTreeNode): ng.IPromise { + isNativeBookmarkIdOfToolbarContainer(nativeBookmarkId?: string): ng.IPromise { return this.getNativeContainerIds().then((nativeContainerIds) => { - return nativeBookmark.parentId === nativeContainerIds.get(BookmarkContainer.Toolbar); + return nativeBookmarkId === nativeContainerIds.get(BookmarkContainer.Toolbar); }); } @@ -534,6 +842,7 @@ export default abstract class WebExtBookmarkService { return container.id as number; } + // Michal Kotoun's notes: what is the use-case for this??? If getContainerNameFromNativeId() can't return a result, I doubt this would either... // Get the synced parent id from id mappings and retrieve the synced parent bookmark return this.bookmarkIdMapperSvc.get(changeData.nativeBookmark.parentId).then((idMapping) => { if (!idMapping) { @@ -617,11 +926,10 @@ export default abstract class WebExtBookmarkService { } processChangeTypeAddOnNativeBookmarks(id: number, createInfo: BookmarkMetadata): ng.IPromise { - // Create native bookmark in other bookmarks container + // Create native bookmark in platform default bookmarks container return this.getNativeContainerIds() .then((nativeContainerIds) => { - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - return this.createNativeBookmark(otherBookmarksId, createInfo.title, createInfo.url); + return this.createNativeBookmark(nativeContainerIds.defaultNativeContainerId, createInfo.title, createInfo.url); }) .then((newNativeBookmark) => { // Add id mapping for new bookmark @@ -680,18 +988,20 @@ export default abstract class WebExtBookmarkService { processChangeTypeMoveOnBookmarks( bookmarks: Bookmark[], changeData: MoveNativeBookmarkChangeData - ): ng.IPromise { + ): ng.IPromise { // Get native container ids - return this.getNativeContainerIds().then((nativeContainerIds) => { - // Check if container was moved to a different folder - if (Array.from(nativeContainerIds.values()).includes(undefined)) { - throw new Exceptions.ContainerChangedException(); - } + return Promise.all([ + // TODO: use this.$q.all ? + this.wasContainerChanged(), + this.getNativeContainerIds() + ]).then(([changedBookmarkIsContainer, nativeContainerIds]) => { + // Check if container was changed + if (changedBookmarkIsContainer) throw new Exceptions.ContainerChangedException(); return browser.bookmarks.get(changeData.id).then((results) => { // If container moved to a different position in same folder, skip sync const movedBookmark = results[0]; - if (Array.from(nativeContainerIds.values()).includes(movedBookmark.id)) { + if ([...nativeContainerIds.values()].includes(movedBookmark.id)) { return; } @@ -882,15 +1192,23 @@ export default abstract class WebExtBookmarkService { const currentEvent = this.nativeBookmarkEventsQueue.shift(); switch (currentEvent.changeType) { case BookmarkChangeType.Add: - return this.syncNativeBookmarkCreated(...currentEvent.eventArgs); + return this.syncNativeBookmarkCreated( + ...(currentEvent.eventArgs as [string, NativeBookmarks.BookmarkTreeNode]) + ); case BookmarkChangeType.ChildrenReordered: - return this.syncNativeBookmarkChildrenReordered(...currentEvent.eventArgs); + return this.syncNativeBookmarkChildrenReordered( + ...(currentEvent.eventArgs as [string, OnChildrenReorderedReorderInfoType]) + ); case BookmarkChangeType.Remove: - return this.syncNativeBookmarkRemoved(...currentEvent.eventArgs); + return this.syncNativeBookmarkRemoved( + ...(currentEvent.eventArgs as [string, NativeBookmarks.OnRemovedRemoveInfoType]) + ); case BookmarkChangeType.Move: - return this.syncNativeBookmarkMoved(...currentEvent.eventArgs); + return this.syncNativeBookmarkMoved( + ...(currentEvent.eventArgs as [string, NativeBookmarks.OnMovedMoveInfoType]) + ); case BookmarkChangeType.Modify: - return this.syncNativeBookmarkChanged(...currentEvent.eventArgs); + return this.syncNativeBookmarkChanged(...(currentEvent.eventArgs as [string])); default: throw new Exceptions.AmbiguousSyncRequestException(); } @@ -956,7 +1274,7 @@ export default abstract class WebExtBookmarkService { reorderUnsupportedContainers(): ng.IPromise { // Get unsupported containers - return this.$q.all(this.unsupportedContainers.map(this.getNativeBookmarkByTitle)).then((results) => { + return this.$q.all(this.getUnsupportedContainers().map(this.getNativeBookmarkByTitle)).then((results) => { return this.$q .all( results @@ -986,12 +1304,9 @@ export default abstract class WebExtBookmarkService { }); } - abstract syncNativeBookmarkChanged(id?: string): ng.IPromise; + abstract syncNativeBookmarkChanged(id: string): ng.IPromise; - syncNativeBookmarkChildrenReordered( - id?: string, - reorderInfo?: OnChildrenReorderedReorderInfoType - ): ng.IPromise { + syncNativeBookmarkChildrenReordered(id: string, reorderInfo: OnChildrenReorderedReorderInfoType): ng.IPromise { // Create change info const data: ReorderNativeBookmarkChangeData = { childIds: reorderInfo.childIds, @@ -1007,11 +1322,11 @@ export default abstract class WebExtBookmarkService { return this.$q.resolve(); } - abstract syncNativeBookmarkCreated(id?: string, nativeBookmark?: NativeBookmarks.BookmarkTreeNode): ng.IPromise; + abstract syncNativeBookmarkCreated(id: string, nativeBookmark: NativeBookmarks.BookmarkTreeNode): ng.IPromise; - abstract syncNativeBookmarkMoved(id?: string, moveInfo?: NativeBookmarks.OnMovedMoveInfoType): ng.IPromise; + abstract syncNativeBookmarkMoved(id: string, moveInfo: NativeBookmarks.OnMovedMoveInfoType): ng.IPromise; - syncNativeBookmarkRemoved(id?: string, removeInfo?: NativeBookmarks.OnRemovedRemoveInfoType): ng.IPromise { + syncNativeBookmarkRemoved(id: string, removeInfo: NativeBookmarks.OnRemovedRemoveInfoType): ng.IPromise { // Create change info const data: RemoveNativeBookmarkChangeData = { nativeBookmark: { @@ -1029,24 +1344,35 @@ export default abstract class WebExtBookmarkService { return this.$q.resolve(); } - wasContainerChanged(changedNativeBookmark: NativeBookmarks.BookmarkTreeNode): ng.IPromise { - return this.getNativeContainerIds().then((nativeContainerIds) => { - // If parent is not other bookmarks, no container was changed - const otherBookmarksId = nativeContainerIds.get(BookmarkContainer.Other); - if ((changedNativeBookmark as NativeBookmarks.BookmarkTreeNode).parentId !== otherBookmarksId) { + wasContainerChanged(changedNativeBookmark?: NativeBookmarks.BookmarkTreeNode): ng.IPromise { + return Promise.all([ + // TODO: use this.$q.all ? + this.getNativeContainerIds(), + this.utilitySvc + .isSyncEnabled() + .then((syncEnabled) => (syncEnabled ? this.bookmarkHelperSvc.getCachedBookmarks() : undefined)) + ]).then(([nativeContainerIds, bookmarks]) => { + // If parent is not browser-default container, no (natively unsupported) container was changed + const defaultNativeContainerId = nativeContainerIds.defaultNativeContainerId; + if (angular.isDefined(changedNativeBookmark) && changedNativeBookmark.parentId !== defaultNativeContainerId) { return false; } - // If any native container ids are undefined, container was removed - if (Array.from(nativeContainerIds.values()).includes(undefined)) { - return true; + if (!angular.isUndefined(bookmarks)) { + // if a container is present in the sync-data, but its native bookmark node was not found in getNativeContainerIds() + // the previously-existing node was either removed, renamed or otherwise modified + const nativeBookmarkNodeDisappeared = + bookmarks.findIndex((b) => angular.isUndefined(nativeContainerIds.get(b.title as BookmarkContainer))) >= 0; + if (nativeBookmarkNodeDisappeared) return true; } return browser.bookmarks - .getChildren(otherBookmarksId) + .getChildren(defaultNativeContainerId) .then((children) => { - // Get all native bookmarks in other bookmarks that are unsupported containers and check for duplicates - const containers = children.filter((x) => this.unsupportedContainers.includes(x.title)).map((x) => x.title); + // Get all native bookmarks - in platform-default bookmarks node - that are unsupported containers and check for duplicates + const containers = children + .filter((x) => this.getUnsupportedContainers().includes(x.title as BookmarkContainer)) + .map((x) => x.title); return containers.length !== new Set(containers).size; }) .catch((err) => { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 000000000..c8d06e998 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "strictNullChecks": false + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1ba4d291c..a6caac5c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "resolveJsonModule": true, "sourceMap": true, "strictFunctionTypes": true, + //"strictNullChecks": true, "target": "ES2018" }, "exclude": ["node_modules"], diff --git a/webpack/base.config.js b/webpack/base.config.js index 7d8909401..0788e20c4 100644 --- a/webpack/base.config.js +++ b/webpack/base.config.js @@ -5,7 +5,13 @@ module.exports = { devtool: 'inline-cheap-module-source-map', module: { rules: [ - { test: /\.ts$/, loader: 'ts-loader' }, + { + test: /\.ts$/, + loader: 'ts-loader', + options: { + configFile: 'tsconfig.build.json' + } + }, { test: /\.(sa|sc|c)ss$/, use: [