From 0d1b213b9667e01defc798e8a950d42c0579c47c Mon Sep 17 00:00:00 2001 From: Natalino Picone Date: Sun, 15 Mar 2026 00:00:20 +0100 Subject: [PATCH] WIP manifest v3 update --- src/modules/shared/alert/alert.service.ts | 2 + .../chromium-background.module.ts | 46 ++- .../firefox-background.module.ts | 53 +-- .../webext-platform.service.ts | 29 +- .../webext/webext-background/angular-shims.ts | 315 ++++++++++++++++++ .../webext-background/background-container.ts | 248 ++++++++++++++ .../webext-background.service.ts | 14 +- webpack/chromium.config.js | 45 +++ webpack/firefox.config.js | 46 ++- 9 files changed, 732 insertions(+), 66 deletions(-) create mode 100644 src/modules/webext/webext-background/angular-shims.ts create mode 100644 src/modules/webext/webext-background/background-container.ts diff --git a/src/modules/shared/alert/alert.service.ts b/src/modules/shared/alert/alert.service.ts index 6da525aef..b802bf98e 100644 --- a/src/modules/shared/alert/alert.service.ts +++ b/src/modules/shared/alert/alert.service.ts @@ -4,6 +4,7 @@ import { Alert } from './alert.interface'; @Injectable('AlertService') export class AlertService { private _currentAlert: Alert | undefined; + onAlertChanged?: (alert: Alert) => void; get currentAlert(): Alert | undefined { return this._currentAlert; @@ -11,6 +12,7 @@ export class AlertService { set currentAlert(value: Alert) { this._currentAlert = value; + this.onAlertChanged?.(value); } clearCurrentAlert(): void { diff --git a/src/modules/webext/chromium/chromium-background/chromium-background.module.ts b/src/modules/webext/chromium/chromium-background/chromium-background.module.ts index 9af7c8a8c..8f6f2fb39 100644 --- a/src/modules/webext/chromium/chromium-background/chromium-background.module.ts +++ b/src/modules/webext/chromium/chromium-background/chromium-background.module.ts @@ -1,16 +1,40 @@ -import angular from 'angular'; -import { NgModule } from 'angular-ts-decorators'; -import { WebExtBackgroundModule } from '../../webext-background/webext-background.module'; +/** + * Chromium background entry point for MV3 service worker. + * Replaces the AngularJS bootstrap with a manual DI container. + */ + +import browser from 'webextension-polyfill'; +import { WebExtV160UpgradeProviderService } from '../../shared/webext-upgrade/webext-v1.6.0-upgrade-provider.service'; +import { setupAngularShim } from '../../webext-background/angular-shims'; +import { createBackgroundContainer } from '../../webext-background/background-container'; import { ChromiumBookmarkService } from '../shared/chromium-bookmark/chromium-bookmark.service'; import { ChromiumPlatformService } from '../shared/chromium-platform/chromium-platform.service'; -@NgModule({ - id: 'ChromiumBackgroundModule', - imports: [WebExtBackgroundModule], - providers: [ChromiumBookmarkService, ChromiumPlatformService] -}) -class ChromiumBackgroundModule {} +// Set up angular shim before any service code runs +setupAngularShim(); + +// Mark this as the background context +// eslint-disable-next-line no-undef, no-restricted-globals +(self as any).__xbs_isBackground = true; + +// Create the DI container with Chromium-specific services +const { backgroundSvc } = createBackgroundContainer({ + BookmarkServiceClass: ChromiumBookmarkService, + PlatformServiceClass: ChromiumPlatformService, + UpgradeProviderServiceClass: WebExtV160UpgradeProviderService +}); + +// Register event handlers synchronously (required for MV3 service workers) +let startupInitiated = false; + +browser.runtime.onInstalled.addListener((details) => { + if (startupInitiated) return; + startupInitiated = true; + backgroundSvc.onInstall(details.reason); +}); -angular.element(document).ready(() => { - angular.bootstrap(document, [(ChromiumBackgroundModule as NgModule).module.name]); +browser.runtime.onStartup.addListener(() => { + if (startupInitiated) return; + startupInitiated = true; + backgroundSvc.init(); }); diff --git a/src/modules/webext/firefox/firefox-background/firefox-background.module.ts b/src/modules/webext/firefox/firefox-background/firefox-background.module.ts index 1f13fa813..7423ffb7e 100644 --- a/src/modules/webext/firefox/firefox-background/firefox-background.module.ts +++ b/src/modules/webext/firefox/firefox-background/firefox-background.module.ts @@ -1,37 +1,40 @@ -import angular from 'angular'; -import { NgModule } from 'angular-ts-decorators'; +/** + * Firefox background entry point for MV3 background scripts. + * Replaces the AngularJS bootstrap with a manual DI container. + */ + import browser from 'webextension-polyfill'; -import { WebExtBackgroundModule } from '../../webext-background/webext-background.module'; +import { WebExtV160UpgradeProviderService } from '../../shared/webext-upgrade/webext-v1.6.0-upgrade-provider.service'; +import { setupAngularShim } from '../../webext-background/angular-shims'; +import { createBackgroundContainer } from '../../webext-background/background-container'; import { FirefoxBookmarkService } from '../shared/firefox-bookmark/firefox-bookmark.service'; import { FirefoxPlatformService } from '../shared/firefox-platform/firefox-platform.service'; -@NgModule({ - id: 'FirefoxBackgroundModule', - imports: [WebExtBackgroundModule], - providers: [FirefoxBookmarkService, FirefoxPlatformService] -}) -class FirefoxBackgroundModule {} +// Set up angular shim before any service code runs +setupAngularShim(); -(FirefoxBackgroundModule as NgModule).module.config([ - '$compileProvider', - '$httpProvider', - ($compileProvider: ng.ICompileProvider, $httpProvider: ng.IHttpProvider) => { - $compileProvider.debugInfoEnabled(false); - $httpProvider.interceptors.push('ApiRequestInterceptorFactory'); - } -]); +// Mark this as the background context +// eslint-disable-next-line no-undef, no-restricted-globals +(self as any).__xbs_isBackground = true; -angular.element(document).ready(() => { - angular.bootstrap(document, [(FirefoxBackgroundModule as NgModule).module.name]); +// Create the DI container with Firefox-specific services +const { backgroundSvc } = createBackgroundContainer({ + BookmarkServiceClass: FirefoxBookmarkService, + PlatformServiceClass: FirefoxPlatformService, + UpgradeProviderServiceClass: WebExtV160UpgradeProviderService }); -// Set synchronous event handlers +// Register event handlers synchronously (required for MV3 background scripts) +let startupInitiated = false; + browser.runtime.onInstalled.addListener((details) => { - // Store event details as element data - const element = document.querySelector('#install'); - angular.element(element).data('details', details); - (document.querySelector('#install') as HTMLButtonElement).click(); + if (startupInitiated) return; + startupInitiated = true; + backgroundSvc.onInstall(details.reason); }); + browser.runtime.onStartup.addListener(() => { - (document.querySelector('#startup') as HTMLButtonElement).click(); + if (startupInitiated) return; + startupInitiated = true; + backgroundSvc.init(); }); diff --git a/src/modules/webext/shared/webext-platform/webext-platform.service.ts b/src/modules/webext/shared/webext-platform/webext-platform.service.ts index 54a4e3407..fe2bd01c9 100644 --- a/src/modules/webext/shared/webext-platform/webext-platform.service.ts +++ b/src/modules/webext/shared/webext-platform/webext-platform.service.ts @@ -1,4 +1,3 @@ -import angular from 'angular'; import { boundMethod } from 'autobind-decorator'; import * as detectBrowser from 'detect-browser'; import browser, { Tabs } from 'webextension-polyfill'; @@ -87,7 +86,7 @@ export abstract class WebExtPlatformService implements PlatformService { platformName = ''; get backgroundSvc(): WebExtBackgroundService { - if (angular.isUndefined(this._backgroundSvc)) { + if (this._backgroundSvc === undefined) { this._backgroundSvc = this.$injector.get('WebExtBackgroundService'); } return this._backgroundSvc as WebExtBackgroundService; @@ -165,7 +164,7 @@ export abstract class WebExtPlatformService implements PlatformService { i18nStr = browser.i18n.getMessage(`${i18nObj.key}_Default`); } - if (angular.isUndefined(i18nStr ?? undefined)) { + if ((i18nStr ?? undefined) === undefined) { throw new I18nError('I18n string has no value'); } @@ -308,7 +307,7 @@ export abstract class WebExtPlatformService implements PlatformService { const iconUpdated = this.$q.defer(); const titleUpdated = this.$q.defer(); - browser.browserAction.getTitle({}).then((currentTitle) => { + (browser.action || browser.browserAction).getTitle({}).then((currentTitle) => { // Don't do anything if browser action title hasn't changed if (newTitle === currentTitle) { return resolve(); @@ -317,14 +316,14 @@ export abstract class WebExtPlatformService implements PlatformService { // Set a delay if finished syncing to prevent flickering when executing many syncs if (currentTitle.indexOf(syncingTitle) > 0 && newTitle.indexOf(syncedTitle)) { this.refreshInterfaceTimeout = this.$timeout(() => { - browser.browserAction.setIcon({ path: iconPath }); - browser.browserAction.setTitle({ title: newTitle }); + (browser.action || browser.browserAction).setIcon({ path: iconPath }); + (browser.action || browser.browserAction).setTitle({ title: newTitle }); }, 350); iconUpdated.resolve(); titleUpdated.resolve(); } else { - browser.browserAction.setIcon({ path: iconPath }).then(iconUpdated.resolve); - browser.browserAction.setTitle({ title: newTitle }).then(titleUpdated.resolve); + (browser.action || browser.browserAction).setIcon({ path: iconPath }).then(iconUpdated.resolve); + (browser.action || browser.browserAction).setTitle({ title: newTitle }).then(titleUpdated.resolve); } this.$q.all([iconUpdated, titleUpdated]).then(resolve).catch(reject); @@ -333,17 +332,13 @@ export abstract class WebExtPlatformService implements PlatformService { } sendMessage(message: Message): ng.IPromise { - // If background module loaded use browser API to send the message - let module: ng.IModule | undefined; - try { - module = angular.module('WebExtBackgroundModule'); - } catch (err) {} - + // If running in background context, call service directly; otherwise use browser messaging API let promise: ng.IPromise; - if (angular.isUndefined(module)) { - promise = browser.runtime.sendMessage(message); - } else { + // eslint-disable-next-line no-undef, no-restricted-globals + if ((self as any).__xbs_isBackground) { promise = this.backgroundSvc.onMessage(message); + } else { + promise = browser.runtime.sendMessage(message); } return promise.catch((err: Error) => { diff --git a/src/modules/webext/webext-background/angular-shims.ts b/src/modules/webext/webext-background/angular-shims.ts new file mode 100644 index 000000000..98cce58f7 --- /dev/null +++ b/src/modules/webext/webext-background/angular-shims.ts @@ -0,0 +1,315 @@ +/* eslint-disable no-console */ + +/** + * Lightweight shims for AngularJS services used by shared code, + * allowing them to run in a service worker context without the full AngularJS framework. + */ + +import Globals from '../../shared/global-shared.constants'; + +// --- $q shim: wraps native Promise to satisfy ng.IQService interface --- + +interface Deferred { + promise: Promise; + resolve: (value?: T | PromiseLike) => void; + reject: (reason?: any) => void; +} + +const $qFactory = (): ng.IQService => { + const $q: any = ( + resolver: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void + ): Promise => { + return new Promise(resolver); + }; + + $q.defer = (): Deferred => { + let resolve: any; + let reject: any; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; + + $q.resolve = (value?: T | PromiseLike): Promise => Promise.resolve(value); + $q.reject = (reason?: any): Promise => Promise.reject(reason); + $q.when = (value?: T | PromiseLike): Promise => Promise.resolve(value); + $q.all = (promises: any): Promise => { + if (Array.isArray(promises)) { + return Promise.all(promises); + } + // Handle object of promises + const keys = Object.keys(promises); + return Promise.all(keys.map((k) => promises[k])).then((values) => { + const result: any = {}; + keys.forEach((k, i) => { + result[k] = values[i]; + }); + return result; + }); + }; + + return $q as ng.IQService; +}; + +// --- $timeout shim: wraps setTimeout --- + +const $timeoutFactory = (): ng.ITimeoutService => { + const $timeout: any = (fn: () => any, delay = 0): Promise => { + let timeoutId: ReturnType; + const promise: any = new Promise((resolve) => { + timeoutId = setTimeout(() => { + resolve(fn()); + }, delay); + }); + promise.$$timeoutId = timeoutId; + return promise; + }; + + $timeout.cancel = (promise: any): boolean => { + if (promise && promise.$$timeoutId != null) { + clearTimeout(promise.$$timeoutId); + return true; + } + return false; + }; + + return $timeout as ng.ITimeoutService; +}; + +// --- $interval shim: wraps setInterval --- + +const $intervalFactory = (): ng.IIntervalService => { + const $interval: any = (fn: () => any, delay = 0, count = 0): Promise => { + let intervalId: ReturnType; + let iterations = 0; + const promise: any = new Promise((resolve) => { + intervalId = setInterval(() => { + fn(); + iterations += 1; + if (count > 0 && iterations >= count) { + clearInterval(intervalId); + resolve(); + } + }, delay); + }); + promise.$$intervalId = intervalId; + return promise; + }; + + $interval.cancel = (promise: any): boolean => { + if (promise && promise.$$intervalId != null) { + clearInterval(promise.$$intervalId); + return true; + } + return false; + }; + + return $interval as ng.IIntervalService; +}; + +// --- $http shim: wraps fetch() API --- + +const $httpFactory = (): ng.IHttpService => { + const applyInterceptors = (config: ng.IRequestConfig): ng.IRequestConfig => { + // Apply Accept-Version header (same as ApiRequestInterceptorFactory) + if (config.url !== Globals.ReleaseLatestUrl) { + config.headers = config.headers || {}; + config.headers['Accept-Version'] = Globals.MinApiVersion; + } + // Set default timeout + config.timeout = config.timeout || 12000; + return config; + }; + + const doRequest = (reqConfig: ng.IRequestConfig): Promise> => { + const config = applyInterceptors(reqConfig); + + const controller = new AbortController(); + let timeoutId: ReturnType; + if (typeof config.timeout === 'number' && config.timeout > 0) { + timeoutId = setTimeout(() => controller.abort(), config.timeout); + } + + const fetchOptions: { method: string; headers: Record; signal: AbortSignal; body?: string } = { + method: config.method || 'GET', + headers: (config.headers as Record) || {}, + signal: controller.signal + }; + + if (config.data) { + fetchOptions.body = typeof config.data === 'string' ? config.data : JSON.stringify(config.data); + if (!fetchOptions.headers['Content-Type']) { + fetchOptions.headers['Content-Type'] = 'application/json'; + } + } + + return fetch(config.url, fetchOptions) + .then((response) => { + if (timeoutId) clearTimeout(timeoutId); + const xhrStatus = response.ok ? 'complete' : 'error'; + return response.text().then((text) => { + let data: T; + try { + data = JSON.parse(text); + } catch { + data = text as any; + } + const httpResponse: ng.IHttpResponse = { + data, + status: response.status, + statusText: response.statusText, + headers: ((name?: string) => { + if (name) return response.headers.get(name); + const headers: any = {}; + response.headers.forEach((v, k) => { + headers[k] = v; + }); + return headers; + }) as any, + config, + xhrStatus + }; + + if (!response.ok) { + return Promise.reject(httpResponse); + } + return httpResponse; + }); + }) + .catch((err) => { + if (timeoutId) clearTimeout(timeoutId); + if (err && err.status) { + // Already an httpResponse-shaped error + return Promise.reject(err); + } + // Network or abort error + const xhrStatus = err?.name === 'AbortError' ? 'timeout' : 'error'; + const httpResponse: ng.IHttpResponse = { + data: null as any, + status: 0, + statusText: '', + headers: (() => null) as any, + config, + xhrStatus + }; + return Promise.reject(httpResponse); + }); + }; + + const $http: any = (config: ng.IRequestConfig) => doRequest(config); + $http.get = (url: string, config?: ng.IRequestConfig) => doRequest({ ...config, method: 'GET', url }); + $http.post = (url: string, data?: any, config?: ng.IRequestConfig) => + doRequest({ ...config, method: 'POST', url, data }); + $http.put = (url: string, data?: any, config?: ng.IRequestConfig) => + doRequest({ ...config, method: 'PUT', url, data }); + $http.delete = (url: string, config?: ng.IRequestConfig) => doRequest({ ...config, method: 'DELETE', url }); + + return $http as ng.IHttpService; +}; + +// --- $log shim: maps to console --- + +const $logFactory = (): ng.ILogService => { + return { + debug: console.debug.bind(console), + error: console.error.bind(console), + info: console.info.bind(console), + log: console.log.bind(console), + warn: console.warn.bind(console) + } as ng.ILogService; +}; + +// --- $injector shim: simple Map-based service locator --- + +const $injectorFactory = (): ng.auto.IInjectorService => { + const services = new Map(); + + const injector: any = { + get: (name: string): any => { + const svc = services.get(name); + if (!svc) { + throw new Error(`Service '${name}' not registered in background injector`); + } + return svc; + }, + has: (name: string): boolean => services.has(name), + register: (name: string, instance: any): void => { + services.set(name, instance); + } + }; + + return injector; +}; + +// --- angular global shim --- + +const setupAngularShim = (): void => { + const angularShim: any = { + isUndefined: (value: any): boolean => value === undefined, + isString: (value: any): boolean => typeof value === 'string', + isNumber: (value: any): boolean => typeof value === 'number', + isObject: (value: any): boolean => value !== null && typeof value === 'object', + isArray: Array.isArray, + copy: (source: T): T => { + if (source === null || source === undefined) return source; + return JSON.parse(JSON.stringify(source)); + }, + equals: (a: any, b: any): boolean => JSON.stringify(a) === JSON.stringify(b), + noop: () => {}, + element: () => { + throw new Error('angular.element() is not available in service worker context'); + }, + module: () => { + throw new Error('angular.module() is not available in service worker context'); + }, + bootstrap: () => { + throw new Error('angular.bootstrap() is not available in service worker context'); + } + }; + + // eslint-disable-next-line no-undef, no-restricted-globals + (self as any).angular = angularShim; +}; + +// --- $rootScope shim: no-op for background context --- + +const $rootScopeFactory = (): ng.IRootScopeService => { + return { + $broadcast: () => ({}), + $on: () => () => {}, + $emit: () => ({}), + $apply: (fn?: any) => { + if (typeof fn === 'function') fn(); + }, + $digest: () => {}, + $watch: () => () => {} + } as any; +}; + +// --- $location shim: stub for background context --- + +const $locationFactory = (): ng.ILocationService => { + return { + path: () => '', + url: () => '', + absUrl: () => '', + hash: () => '', + search: () => ({}) + } as any; +}; + +// --- Export factory functions --- + +export { + $httpFactory, + $injectorFactory, + $intervalFactory, + $locationFactory, + $logFactory, + $qFactory, + $rootScopeFactory, + $timeoutFactory, + setupAngularShim +}; diff --git a/src/modules/webext/webext-background/background-container.ts b/src/modules/webext/webext-background/background-container.ts new file mode 100644 index 000000000..8bce09030 --- /dev/null +++ b/src/modules/webext/webext-background/background-container.ts @@ -0,0 +1,248 @@ +/** + * Manual DI container for the background context (service worker). + * Instantiates all services in dependency order without AngularJS. + */ + +import { AlertService } from '../../shared/alert/alert.service'; +import { ApiXbrowsersyncService } from '../../shared/api/api-xbrowsersync/api-xbrowsersync.service'; +import { BackupRestoreService } from '../../shared/backup-restore/backup-restore.service'; +import { BookmarkService } from '../../shared/bookmark/bookmark.interface'; +import { BookmarkHelperService } from '../../shared/bookmark/bookmark-helper/bookmark-helper.service'; +import { CryptoService } from '../../shared/crypto/crypto.service'; +import { ExceptionHandler } from '../../shared/errors/errors.interface'; +import { ExceptionHandlerService } from '../../shared/errors/exception-handler/exception-handler.service'; +import { PlatformService } from '../../shared/global-shared.interface'; +import { LogService } from '../../shared/log/log.service'; +import { NetworkService } from '../../shared/network/network.service'; +import { SettingsService } from '../../shared/settings/settings.service'; +import { BookmarkSyncProviderService } from '../../shared/sync/bookmark-sync-provider/bookmark-sync-provider.service'; +import { SyncService } from '../../shared/sync/sync.service'; +import { TelemetryService } from '../../shared/telemetry/telemetry.service'; +import { UpgradeService } from '../../shared/upgrade/upgrade.service'; +import { UtilityService } from '../../shared/utility/utility.service'; +import { WorkingService } from '../../shared/working/working.service'; +import { BookmarkIdMapperService } from '../shared/bookmark-id-mapper/bookmark-id-mapper.service'; +import { WebExtStoreService } from '../shared/webext-store/webext-store.service'; +import { + $httpFactory, + $injectorFactory, + $intervalFactory, + $locationFactory, + $logFactory, + $qFactory, + $rootScopeFactory, + $timeoutFactory +} from './angular-shims'; +import { WebExtBackgroundService } from './webext-background.service'; + +export interface BackgroundPlatformConfig { + BookmarkServiceClass: new (...args: any[]) => BookmarkService; + PlatformServiceClass: new (...args: any[]) => PlatformService; + UpgradeProviderServiceClass: new (...args: any[]) => any; +} + +export interface BackgroundContainer { + backgroundSvc: WebExtBackgroundService; + alertSvc: AlertService; + injector: ng.auto.IInjectorService; +} + +export const createBackgroundContainer = (config: BackgroundPlatformConfig): BackgroundContainer => { + // Create AngularJS shims + const $q = $qFactory(); + const $timeout = $timeoutFactory(); + const $interval = $intervalFactory(); + const $http = $httpFactory(); + const $log = $logFactory(); + const $rootScope = $rootScopeFactory(); + const $location = $locationFactory(); + const injector = $injectorFactory() as any; + + // Register shims in injector + injector.register('$q', $q); + injector.register('$timeout', $timeout); + injector.register('$interval', $interval); + injector.register('$http', $http); + injector.register('$log', $log); + injector.register('$rootScope', $rootScope); + injector.register('$location', $location); + injector.register('$injector', injector); + + // --- Instantiate services in dependency order --- + + // 1. No-dependency services + const alertSvc = new AlertService(); + const workingSvc = new WorkingService(); + injector.register('AlertService', alertSvc); + injector.register('WorkingService', workingSvc); + + // 2. LogService (depends on $injector, $log - uses lazy $q and StoreService) + const logSvc = new LogService(injector, $log); + injector.register('LogService', logSvc); + + // 3. NetworkService (depends on $q) + const networkSvc = new NetworkService($q); + injector.register('NetworkService', networkSvc); + + // 4. StoreService (WebExtStoreService depends on $q) + const storeSvc = new WebExtStoreService($q); + injector.register('StoreService', storeSvc); + + // 5. ExceptionHandlerService (depends on $injector, AlertService, LogService) + const exceptionHandlerSvc = new ExceptionHandlerService(injector, alertSvc, logSvc); + const $exceptionHandler = exceptionHandlerSvc.handleError as ExceptionHandler; + injector.register('ExceptionHandler', exceptionHandlerSvc); + injector.register('$exceptionHandler', $exceptionHandler); + + // 6. UtilityService (depends on $exceptionHandler, $http, $injector, $location, $q, $rootScope, LogService, NetworkService, StoreService) + const utilitySvc = new UtilityService( + $exceptionHandler, + $http, + injector, + $location, + $q, + $rootScope, + logSvc, + networkSvc, + storeSvc + ); + injector.register('UtilityService', utilitySvc); + + // 7. CryptoService (depends on $q, LogService, StoreService, UtilityService) + const cryptoSvc = new CryptoService($q, logSvc, storeSvc, utilitySvc); + injector.register('CryptoService', cryptoSvc); + + // 8. BookmarkHelperService (depends on $injector, $q, CryptoService, StoreService, UtilityService) + const bookmarkHelperSvc = new BookmarkHelperService(injector, $q, cryptoSvc, storeSvc, utilitySvc); + injector.register('BookmarkHelperService', bookmarkHelperSvc); + + // 9. SettingsService (depends on LogService, StoreService) + const settingsSvc = new SettingsService(logSvc, storeSvc); + injector.register('SettingsService', settingsSvc); + + // 10. BookmarkIdMapperService (depends on $q, StoreService) + const bookmarkIdMapperSvc = new BookmarkIdMapperService($q, storeSvc); + injector.register('BookmarkIdMapperService', bookmarkIdMapperSvc); + + // 11. ApiXbrowsersyncService (depends on $injector, $http, $q, NetworkService, StoreService, UtilityService) + const apiSvc = new ApiXbrowsersyncService(injector, $http, $q, networkSvc, storeSvc, utilitySvc); + injector.register('ApiXbrowsersyncService', apiSvc); + + // 12. V160UpgradeProviderService (platform-specific, depends on $q, BookmarkHelperService, BookmarkService, PlatformService, StoreService, UtilityService) + // Defer creation until after BookmarkService and PlatformService are created + + // 13. BookmarkService (platform-specific) + // The constructor signatures vary by platform, so we use $inject to determine args + const BookmarkSvcClass = config.BookmarkServiceClass as any; + const bookmarkSvcInjectNames: string[] = BookmarkSvcClass.$inject || []; + const bookmarkSvcArgs = bookmarkSvcInjectNames.map((name: string) => injector.get(name)); + const bookmarkSvc = new BookmarkSvcClass(...bookmarkSvcArgs); + injector.register('BookmarkService', bookmarkSvc); + + // 14. PlatformService (platform-specific, depends on $injector, $interval, $q, $timeout, AlertService, BookmarkHelperService, BookmarkIdMapperService, LogService, StoreService, UtilityService, WorkingService) + const PlatformSvcClass = config.PlatformServiceClass as any; + const platformSvcInjectNames: string[] = PlatformSvcClass.$inject || []; + const platformSvcArgs = platformSvcInjectNames.map((name: string) => injector.get(name)); + const platformSvc = new PlatformSvcClass(...platformSvcArgs); + injector.register('PlatformService', platformSvc); + + // 15. V160UpgradeProviderService (now that BookmarkService and PlatformService exist) + const UpgradeProviderSvcClass = config.UpgradeProviderServiceClass as any; + const upgradeProviderInjectNames: string[] = UpgradeProviderSvcClass.$inject || []; + const upgradeProviderArgs = upgradeProviderInjectNames.map((name: string) => injector.get(name)); + const v160UpgradeProviderSvc = new UpgradeProviderSvcClass(...upgradeProviderArgs); + injector.register('V160UpgradeProviderService', v160UpgradeProviderSvc); + + // 16. UpgradeService (depends on $q, LogService, PlatformService, StoreService, UtilityService, V160UpgradeProviderService) + const upgradeSvc = new UpgradeService($q, logSvc, platformSvc, storeSvc, utilitySvc, v160UpgradeProviderSvc); + injector.register('UpgradeService', upgradeSvc); + + // 17. BackupRestoreService (depends on $q, BookmarkService, LogService, PlatformService, StoreService, UpgradeService, UtilityService) + const backupRestoreSvc = new BackupRestoreService( + $q, + bookmarkSvc, + logSvc, + platformSvc, + storeSvc, + upgradeSvc, + utilitySvc + ); + injector.register('BackupRestoreService', backupRestoreSvc); + + // 18. BookmarkSyncProviderService (depends on $q, BookmarkHelperService, BookmarkService, CryptoService, LogService, NetworkService, PlatformService, SettingsService, StoreService, UpgradeService, UtilityService) + const bookmarkSyncProviderSvc = new BookmarkSyncProviderService( + $q, + bookmarkHelperSvc, + bookmarkSvc, + cryptoSvc, + logSvc, + networkSvc, + platformSvc, + settingsSvc, + storeSvc, + upgradeSvc, + utilitySvc + ); + injector.register('BookmarkSyncProviderService', bookmarkSyncProviderSvc); + + // 19. SyncService (depends on $exceptionHandler, $q, $timeout, BookmarkHelperService, BookmarkSyncProviderService, CryptoService, LogService, NetworkService, PlatformService, StoreService, UtilityService) + const syncSvc = new SyncService( + $exceptionHandler, + $q, + $timeout, + bookmarkHelperSvc, + bookmarkSyncProviderSvc, + cryptoSvc, + logSvc, + networkSvc, + platformSvc, + storeSvc, + utilitySvc + ); + injector.register('SyncService', syncSvc); + + // 20. TelemetryService (depends on $http, $q, LogService, NetworkService, PlatformService, SettingsService, StoreService, SyncService, UtilityService) + const telemetrySvc = new TelemetryService( + $http, + $q, + logSvc, + networkSvc, + platformSvc, + settingsSvc, + storeSvc, + syncSvc, + utilitySvc + ); + injector.register('TelemetryService', telemetrySvc); + + // 21. WebExtBackgroundService (depends on $exceptionHandler, $q, $timeout, AlertService, BackupRestoreService, BookmarkHelperService, BookmarkIdMapperService, BookmarkService, LogService, NetworkService, PlatformService, SettingsService, StoreService, SyncService, TelemetryService, UpgradeService, UtilityService) + const backgroundSvc = new WebExtBackgroundService( + $exceptionHandler, + $q, + $timeout, + alertSvc, + backupRestoreSvc, + bookmarkHelperSvc, + bookmarkIdMapperSvc, + bookmarkSvc, + logSvc, + networkSvc, + platformSvc, + settingsSvc, + storeSvc, + syncSvc, + telemetrySvc, + upgradeSvc, + utilitySvc + ); + injector.register('WebExtBackgroundService', backgroundSvc); + + // Set up alert observer: when alert changes, display notification + alertSvc.onAlertChanged = (alert) => { + if (alert) { + backgroundSvc.displayAlert(alert); + } + }; + + return { backgroundSvc, alertSvc, injector }; +}; diff --git a/src/modules/webext/webext-background/webext-background.service.ts b/src/modules/webext/webext-background/webext-background.service.ts index 09d2d5798..72f35b801 100644 --- a/src/modules/webext/webext-background/webext-background.service.ts +++ b/src/modules/webext/webext-background/webext-background.service.ts @@ -1,4 +1,3 @@ -import angular from 'angular'; import { Injectable } from 'angular-ts-decorators'; import { boundMethod } from 'autobind-decorator'; import browser, { Alarms, Downloads, Notifications } from 'webextension-polyfill'; @@ -25,8 +24,8 @@ import { SyncService } from '../../shared/sync/sync.service'; import { TelemetryService } from '../../shared/telemetry/telemetry.service'; import { UpgradeService } from '../../shared/upgrade/upgrade.service'; import { UtilityService } from '../../shared/utility/utility.service'; -import { ChromiumBookmarkService } from '../chromium/shared/chromium-bookmark/chromium-bookmark.service'; import { BookmarkIdMapperService } from '../shared/bookmark-id-mapper/bookmark-id-mapper.service'; +import { WebExtBookmarkService } from '../shared/webext-bookmark/webext-bookmark.service'; import { DownloadFileMessage, EnableAutoBackUpMessage, @@ -46,7 +45,7 @@ export class WebExtBackgroundService { backupRestoreSvc: BackupRestoreService; bookmarkIdMapperSvc: BookmarkIdMapperService; bookmarkHelperSvc: BookmarkHelperService; - bookmarkSvc: ChromiumBookmarkService; + bookmarkSvc: WebExtBookmarkService; logSvc: LogService; networkSvc: NetworkService; platformSvc: PlatformService; @@ -86,7 +85,7 @@ export class WebExtBackgroundService { BackupRestoreSvc: BackupRestoreService, BookmarkHelperSvc: BookmarkHelperService, BookmarkIdMapperSvc: BookmarkIdMapperService, - BookmarkSvc: ChromiumBookmarkService, + BookmarkSvc: WebExtBookmarkService, LogSvc: LogService, NetworkSvc: NetworkService, PlatformSvc: PlatformService, @@ -322,10 +321,9 @@ export class WebExtBackgroundService { } } - onInstall(event: InputEvent): void { + onInstall(reason?: string): void { // Check if fresh install needed - const details = angular.element(event.currentTarget as Element).data('details'); - (details?.reason === 'install' ? this.installExtension() : this.$q.resolve()).then(() => this.init()); + (reason === 'install' ? this.installExtension() : this.$q.resolve()).then(() => this.init()); } @boundMethod @@ -531,7 +529,7 @@ export class WebExtBackgroundService { runSyncBookmarksCommand(message: SyncBookmarksMessage): ng.IPromise { const { sync, runSync } = message; // If no sync has been provided, process current sync queue and check for updates - if (angular.isUndefined(sync)) { + if (sync === undefined) { return this.syncSvc.executeSync(); } return this.syncSvc.queueSync(sync, runSync); diff --git a/webpack/chromium.config.js b/webpack/chromium.config.js index 44e78e6e6..96dda443f 100644 --- a/webpack/chromium.config.js +++ b/webpack/chromium.config.js @@ -3,6 +3,51 @@ const WebExtConfig = require('./webext.config'); module.exports = (env, argv) => { const webExtConfig = WebExtConfig(env, argv); + + // Transform manifest to Manifest V3 for Chromium + const copyPlugin = webExtConfig.plugins.find((p) => p.constructor.name === 'CopyPlugin'); + const manifestPattern = copyPlugin.patterns.find((p) => p.from.indexOf('manifest.json') > -1); + const webExtTransform = manifestPattern.transform; + manifestPattern.transform = (buffer) => { + const webExtTransformResult = webExtTransform(buffer); + const manifest = JSON.parse(webExtTransformResult); + + // Upgrade to Manifest V3 + manifest.manifest_version = 3; + + // browser_action -> action + manifest.action = manifest.browser_action; + delete manifest.browser_action; + + // background page -> service worker + manifest.background = { + service_worker: 'assets/background.js' + }; + + // content_security_policy string -> object + manifest.content_security_policy = { + extension_pages: manifest.content_security_policy + }; + + // Move host patterns from optional_permissions to optional_host_permissions + manifest.optional_host_permissions = manifest.optional_permissions; + delete manifest.optional_permissions; + + // _execute_browser_action -> _execute_action + if (manifest.commands && manifest.commands._execute_browser_action) { + manifest.commands._execute_action = manifest.commands._execute_browser_action; + delete manifest.commands._execute_browser_action; + } + + return JSON.stringify(manifest, null, 2); + }; + + // Remove background.html copy for Chromium (MV3 uses service worker) + const bgHtmlIdx = copyPlugin.patterns.findIndex((p) => p.from && p.from.indexOf('background.html') > -1); + if (bgHtmlIdx > -1) { + copyPlugin.patterns.splice(bgHtmlIdx, 1); + } + return { ...webExtConfig, entry: { diff --git a/webpack/firefox.config.js b/webpack/firefox.config.js index e5d93dce6..122943c9a 100644 --- a/webpack/firefox.config.js +++ b/webpack/firefox.config.js @@ -4,23 +4,59 @@ const WebExtConfig = require('./webext.config'); module.exports = (env, argv) => { const webExtConfig = WebExtConfig(env, argv); - // Add Firefox browser_specific_settings entry to manifest + // Transform manifest to Manifest V3 and add Firefox browser_specific_settings const copyPlugin = webExtConfig.plugins.find((p) => p.constructor.name === 'CopyPlugin'); const manifestPattern = copyPlugin.patterns.find((p) => p.from.indexOf('manifest.json') > -1); - const webExtTransfrom = manifestPattern.transform; + const webExtTransform = manifestPattern.transform; manifestPattern.transform = (buffer) => { - const webExtTransfromResult = webExtTransfrom(buffer); - const manifest = JSON.parse(webExtTransfromResult); + const webExtTransformResult = webExtTransform(buffer); + const manifest = JSON.parse(webExtTransformResult); + + // Upgrade to Manifest V3 + manifest.manifest_version = 3; + + // browser_action -> action + manifest.action = manifest.browser_action; + delete manifest.browser_action; + + // background page -> scripts (Firefox MV3 uses background.scripts, not service_worker) + manifest.background = { + scripts: ['assets/background.js'] + }; + + // content_security_policy string -> object + manifest.content_security_policy = { + extension_pages: manifest.content_security_policy + }; + + // Move host patterns from optional_permissions to optional_host_permissions + manifest.optional_host_permissions = manifest.optional_permissions; + delete manifest.optional_permissions; + + // _execute_browser_action -> _execute_action + if (manifest.commands && manifest.commands._execute_browser_action) { + manifest.commands._execute_action = manifest.commands._execute_browser_action; + delete manifest.commands._execute_browser_action; + } + + // Firefox-specific settings manifest.browser_specific_settings = { gecko: { id: '{019b606a-6f61-4d01-af2a-cea528f606da}', - strict_min_version: '75.0', + strict_min_version: '109.0', update_url: 'https://xbrowsersync.github.io/app/firefox-versions.json' } }; + return JSON.stringify(manifest, null, 2); }; + // Remove background.html copy for Firefox (MV3 uses background scripts) + const bgHtmlIdx = copyPlugin.patterns.findIndex((p) => p.from && p.from.indexOf('background.html') > -1); + if (bgHtmlIdx > -1) { + copyPlugin.patterns.splice(bgHtmlIdx, 1); + } + return { ...webExtConfig, entry: {