diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 51e1203673..ecb47843df 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3425,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ed9d8e6d84..1b98928311 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -31,16 +31,10 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { type: "unlock"; } -interface AddRequestFilelessImportQueueMessage extends NotificationQueueMessage { - type: "fileless-import"; - importType?: string; -} - type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordQueueMessage - | AddUnlockVaultQueueMessage - | AddRequestFilelessImportQueueMessage; + | AddUnlockVaultQueueMessage; type LockedVaultPendingNotificationsData = { commandToRetry: { @@ -122,7 +116,6 @@ export { AddChangePasswordQueueMessage, AddLoginQueueMessage, AddUnlockVaultQueueMessage, - AddRequestFilelessImportQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, AdjustNotificationBarMessageData, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 5c6ff3c2c8..0175b27bd6 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -37,7 +37,6 @@ import { AutofillService } from "../services/abstractions/autofill.service"; import { AddChangePasswordQueueMessage, AddLoginQueueMessage, - AddRequestFilelessImportQueueMessage, AddUnlockVaultQueueMessage, ChangePasswordMessageData, AddLoginMessageData, @@ -201,11 +200,6 @@ export default class NotificationBackground { case NotificationQueueMessageType.AddLogin: typeData.removeIndividualVault = await this.removeIndividualVault(); break; - case NotificationQueueMessageType.RequestFilelessImport: - typeData.importType = ( - notificationQueueMessage as AddRequestFilelessImportQueueMessage - ).importType; - break; } await BrowserApi.tabSendMessageData(tab, "openNotificationBar", { @@ -399,25 +393,6 @@ export default class NotificationBackground { } } - /** - * Sets up a notification to request a fileless import when the user - * attempts to trigger an import from a third party website. - * - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.getAuthStatus(); - if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { - return; - } - - const loginDomain = Utils.getDomain(tab.url); - if (loginDomain) { - await this.pushRequestFilelessImportToQueue(loginDomain, tab, importType); - } - } - private async pushChangePasswordToQueue( cipherId: string, loginDomain: string, @@ -456,36 +431,6 @@ export default class NotificationBackground { await this.sendNotificationQueueMessage(tab, message); } - /** - * Pushes a request to start a fileless import to the notification queue. - * This will display a notification bar to the user, prompting them to - * start the import. - * - * @param loginDomain - The domain of the tab that we are sending the notification to - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - private async pushRequestFilelessImportToQueue( - loginDomain: string, - tab: chrome.tabs.Tab, - importType?: string, - ) { - this.removeTabFromNotificationQueue(tab); - const launchTimestamp = new Date().getTime(); - const message: AddRequestFilelessImportQueueMessage = { - type: NotificationQueueMessageType.RequestFilelessImport, - domain: loginDomain, - tab, - launchTimestamp, - expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds - wasVaultLocked: false, - importType, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); - this.removeTabFromNotificationQueue(tab); - } - /** * Saves a cipher based on the message sent from the notification bar. If the vault * is locked, the message will be added to the notification queue and the unlock diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 0cce066cf3..8bd07ab829 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -91,8 +91,6 @@ function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationTy return i18n.updateLoginPrompt; case NotificationTypes.Unlock: return ""; - case NotificationTypes.FilelessImport: - return ""; default: return undefined; } diff --git a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts index 1f5abac92b..5a7b8fa990 100644 --- a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts +++ b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts @@ -2,7 +2,6 @@ const NotificationQueueMessageType = { AddLogin: "add", ChangePassword: "change", UnlockVault: "unlock", - RequestFilelessImport: "fileless-import", } as const; type NotificationQueueMessageTypes = diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 2e38adacb3..53948a26a2 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -4,7 +4,6 @@ const NotificationTypes = { Add: "add", Change: "change", Unlock: "unlock", - FilelessImport: "fileless-import", } as const; type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 6b0e76b516..b686e1ec2f 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -55,14 +55,4 @@ - - diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 2c0ebe8e8e..3fc61c448f 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -4,7 +4,6 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -59,11 +58,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), - filelessImport: chrome.i18n.getMessage("filelessImport"), - lpFilelessImport: chrome.i18n.getMessage("lpFilelessImport"), - cancelFilelessImport: chrome.i18n.getMessage("no"), - lpCancelFilelessImport: chrome.i18n.getMessage("lpCancelFilelessImport"), - startFilelessImport: chrome.i18n.getMessage("startFilelessImport"), }; setupLogoLink(i18n); @@ -107,22 +101,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc; - // i18n for "Fileless Import" (fileless-import) template - const isLpImport = initData.importType === FilelessImportType.LP; - const importTemplate = document.getElementById("template-fileless-import") as HTMLTemplateElement; - - const startImportButton = importTemplate.content.getElementById("start-fileless-import"); - startImportButton.textContent = i18n.startFilelessImport; - - const cancelImportButton = importTemplate.content.getElementById("cancel-fileless-import"); - cancelImportButton.textContent = isLpImport - ? i18n.lpCancelFilelessImport - : i18n.cancelFilelessImport; - - importTemplate.content.getElementById("fileless-import-text").textContent = isLpImport - ? i18n.lpFilelessImport - : i18n.filelessImport; - // i18n for body content const closeButton = document.getElementById("close-button"); closeButton.title = i18n.close; @@ -134,8 +112,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { handleTypeChange(); } else if (notificationType === "unlock") { handleTypeUnlock(); - } else if (notificationType === "fileless-import") { - handleTypeFilelessImport(); } closeButton.addEventListener("click", (e) => { @@ -249,59 +225,6 @@ function handleTypeUnlock() { }); } -/** - * Sets up a port to communicate with the fileless importer content script. - * This connection to the background script is used to trigger the action of - * downloading the CSV file from the LP importer or importing the data into - * the Bitwarden vault. - */ -function handleTypeFilelessImport() { - const importType = notificationBarIframeInitData.importType; - const port = chrome.runtime.connect({ name: FilelessImportPort.NotificationBar }); - setContent(document.getElementById("template-fileless-import") as HTMLTemplateElement); - - const startFilelessImportButton = document.getElementById("start-fileless-import"); - const startFilelessImport = () => { - port.postMessage({ command: "startFilelessImport", importType }); - document.getElementById("fileless-import-buttons").textContent = - chrome.i18n.getMessage("importing"); - startFilelessImportButton.removeEventListener("click", startFilelessImport); - }; - startFilelessImportButton.addEventListener("click", startFilelessImport); - - const cancelFilelessImportButton = document.getElementById("cancel-fileless-import"); - cancelFilelessImportButton.addEventListener("click", () => { - port.postMessage({ command: "cancelFilelessImport", importType }); - }); - - const handlePortMessage = (msg: any) => { - if (msg.command !== "filelessImportCompleted" && msg.command !== "filelessImportFailed") { - return; - } - - port.disconnect(); - - const filelessImportButtons = document.getElementById("fileless-import-buttons"); - const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper"); - - if (msg.command === "filelessImportCompleted") { - filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported"); - filelessImportButtons.prepend(buildSvgDomElement(circleCheckIcon)); - filelessImportButtons.classList.add("success-message"); - notificationBarOuterWrapper.classList.add("success-event"); - adjustHeight(); - return; - } - - filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed"); - filelessImportButtons.classList.add("error-message"); - notificationBarOuterWrapper.classList.add("error-event"); - adjustHeight(); - logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`); - }; - port.onMessage.addListener(handlePortMessage); -} - function setContent(template: HTMLTemplateElement) { const content = document.getElementById("content"); while (content.firstChild) { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5d09122bbd..3a60cc5210 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -268,7 +268,6 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; -import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -393,7 +392,6 @@ export default class MainBackground { private notificationBackground: NotificationBackground; private overlayBackground: OverlayBackgroundInterface; private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface; - private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; @@ -1160,16 +1158,6 @@ export default class MainBackground { this.notificationBackground, ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - this.scriptInjectorService, - ); - this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( this.logService, this.autofillService, @@ -1296,7 +1284,6 @@ export default class MainBackground { await this.runtimeBackground.init(); await this.notificationBackground.init(); this.overlayNotificationsBackground.init(); - this.filelessImporterBackground.init(); this.commandsBackground.init(); this.contextMenusBackground?.init(); this.idleBackground.init(); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 86ea0eebbc..016bf6dfe4 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -29,12 +29,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { @@ -140,7 +134,6 @@ }, "web_accessible_resources": [ "content/fido2-page-script.js", - "content/lp-suppress-import-download.js", "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index ae7c888eb9..104036140b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -30,12 +30,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts deleted file mode 100644 index 2ade5bf767..0000000000 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; - -type FilelessImportPortMessage = { - command?: string; - importType?: FilelessImportTypeKeys; - data?: string; -}; - -type FilelessImportPortMessageHandlerParams = { - message: FilelessImportPortMessage; - port: chrome.runtime.Port; -}; - -type ImportNotificationMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - cancelFilelessImport: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; -}; - -type LpImporterMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - displayLpImportNotification: ({ port }: { port: chrome.runtime.Port }) => void; - startLpImport: ({ message }: { message: FilelessImportPortMessage }) => void; -}; - -interface FilelessImporterBackground { - init(): void; -} - -export { - FilelessImportPortMessage, - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground, -}; diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts deleted file mode 100644 index 409fac9790..0000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { - flushPromises, - sendPortMessage, - triggerRuntimeOnConnectEvent, -} from "../../autofill/spec/testing-utils"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; - -import FilelessImporterBackground from "./fileless-importer.background"; - -describe("FilelessImporterBackground ", () => { - let filelessImporterBackground: FilelessImporterBackground; - const configService = mock(); - const domainSettingsService = mock(); - const authService = mock(); - const policyService = mock(); - const notificationBackground = mock(); - const importService = mock(); - const syncService = mock(); - const platformUtilsService = mock(); - const logService = mock(); - let scriptInjectorService: BrowserScriptInjectorService; - let tabMock: chrome.tabs.Tab; - - beforeEach(() => { - domainSettingsService.blockedInteractionsUris$ = of({}); - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(true)); - scriptInjectorService = new BrowserScriptInjectorService( - domainSettingsService, - platformUtilsService, - logService, - ); - filelessImporterBackground = new FilelessImporterBackground( - configService, - authService, - policyService, - notificationBackground, - importService, - syncService, - scriptInjectorService, - ); - filelessImporterBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sets up the port message listeners on initialization of the class", () => { - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function)); - }); - }); - - describe("handle ports onConnect", () => { - let lpImporterPort: chrome.runtime.Port; - let manifestVersionSpy: jest.SpyInstance; - let executeScriptInTabSpy: jest.SpyInstance; - - beforeEach(() => { - lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - tabMock = lpImporterPort.sender.tab; - jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); - manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest.spyOn(filelessImporterBackground as any, "removeIndividualVault"); - }); - - it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => { - const port = createPortSpyMock("some-other-port"); - - triggerRuntimeOnConnectEvent(port); - await flushPromises(); - - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's auth status is not unlocked", async () => { - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Locked); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => { - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the feature flag is turned off", async () => { - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(false); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is enabled", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }); - }); - - it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(3); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, - { world: "MAIN" }, - ); - }); - - it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => { - policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false)); - manifestVersionSpy.mockReturnValue(2); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - frameId: 0, - }); - }); - }); - - describe("port messages", () => { - let notificationPort: chrome.runtime.Port; - let lpImporterPort: chrome.runtime.Port; - - beforeEach(async () => { - policyService.policyAppliesToActiveUser$.mockImplementation(() => of(false)); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar)); - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter)); - await flushPromises(); - notificationPort = filelessImporterBackground["importNotificationsPort"]; - lpImporterPort = filelessImporterBackground["lpImporterPort"]; - }); - - it("skips handling a message if a message handler is not associated with the port message command", () => { - sendPortMessage(notificationPort, { command: "commandNotFound" }); - - expect(chrome.tabs.sendMessage).not.toHaveBeenCalled(); - }); - - describe("import notification port messages", () => { - describe("startFilelessImport", () => { - it("sends a message to start the LastPass fileless import within the content script", () => { - sendPortMessage(notificationPort, { - command: "startFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "startLpFilelessImport", - }); - }); - }); - - describe("cancelFilelessImport", () => { - it("sends a message to close the notification bar", async () => { - sendPortMessage(notificationPort, { command: "cancelFilelessImport" }); - - expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( - notificationPort.sender.tab.id, - { - command: "closeNotificationBar", - }, - null, - expect.anything(), - ); - expect(lpImporterPort.postMessage).not.toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - }); - - it("sends a message to trigger a download of the LP importer CSV", () => { - sendPortMessage(notificationPort, { - command: "cancelFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - expect(lpImporterPort.disconnect).toHaveBeenCalled(); - }); - }); - }); - - describe("lp importer port messages", () => { - describe("displayLpImportNotification", () => { - it("creates a request fileless import notification", async () => { - jest.spyOn(filelessImporterBackground["notificationBackground"], "requestFilelessImport"); - - sendPortMessage(lpImporterPort, { - command: "displayLpImportNotification", - }); - await flushPromises(); - - expect( - filelessImporterBackground["notificationBackground"].requestFilelessImport, - ).toHaveBeenCalledWith(lpImporterPort.sender.tab, FilelessImportType.LP); - }); - }); - - describe("startLpImport", () => { - it("ignores the message if the message does not contain data", () => { - sendPortMessage(lpImporterPort, { - command: "startLpImport", - }); - - expect(filelessImporterBackground["importService"].import).not.toHaveBeenCalled(); - }); - - it("triggers the import of the LastPass vault", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest.spyOn(filelessImporterBackground["importService"], "import").mockResolvedValue( - mock({ - success: true, - }), - ); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect(filelessImporterBackground["importService"].import).toHaveBeenCalledWith( - importer, - data, - null, - null, - false, - ); - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportCompleted" }); - expect(filelessImporterBackground["syncService"].fullSync).toHaveBeenCalledWith(true); - }); - - it("posts a failed message if the import fails", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest - .spyOn(filelessImporterBackground["importService"], "import") - .mockImplementation(() => { - throw new Error("error"); - }); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportFailed" }); - }); - }); - }); - }); - - describe("handleImporterPortDisconnect", () => { - it("resets the port properties to null", () => { - const lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - const notificationPort = createPortSpyMock(FilelessImportPort.NotificationBar); - filelessImporterBackground["lpImporterPort"] = lpImporterPort; - filelessImporterBackground["importNotificationsPort"] = notificationPort; - - filelessImporterBackground["handleImporterPortDisconnect"](lpImporterPort); - - expect(filelessImporterBackground["lpImporterPort"]).toBeNull(); - expect(filelessImporterBackground["importNotificationsPort"]).not.toBeNull(); - - filelessImporterBackground["handleImporterPortDisconnect"](notificationPort); - - expect(filelessImporterBackground["importNotificationsPort"]).toBeNull(); - }); - }); -}); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts deleted file mode 100644 index 21d597ec8a..0000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ /dev/null @@ -1,265 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; -import { - FilelessImportPort, - FilelessImportType, - FilelessImportTypeKeys, -} from "../enums/fileless-import.enums"; - -import { - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground as FilelessImporterBackgroundInterface, - FilelessImportPortMessage, -} from "./abstractions/fileless-importer.background"; - -class FilelessImporterBackground implements FilelessImporterBackgroundInterface { - private static readonly filelessImporterPortNames: Set = new Set([ - FilelessImportPort.LpImporter, - FilelessImportPort.NotificationBar, - ]); - private importNotificationsPort: chrome.runtime.Port; - private lpImporterPort: chrome.runtime.Port; - private readonly importNotificationsPortMessageHandlers: ImportNotificationMessageHandlers = { - startFilelessImport: ({ message }) => this.startFilelessImport(message.importType), - cancelFilelessImport: ({ message, port }) => - this.cancelFilelessImport(message.importType, port.sender), - }; - private readonly lpImporterPortMessageHandlers: LpImporterMessageHandlers = { - displayLpImportNotification: ({ port }) => - this.displayFilelessImportNotification(port.sender.tab, FilelessImportType.LP), - startLpImport: ({ message }) => this.triggerLpImport(message.data), - }; - - /** - * Creates a new instance of the fileless importer background logic. - * - * @param configService - Identifies if the feature flag is enabled. - * @param authService - Verifies if the auth status of the user. - * @param policyService - Identifies if the user account has a policy that disables personal ownership. - * @param notificationBackground - Used to inject the notification bar into the tab. - * @param importService - Used to import the export data into the vault. - * @param syncService - Used to trigger a full sync after the import is completed. - * @param scriptInjectorService - Used to inject content scripts that initialize the import process - */ - constructor( - private configService: ConfigService, - private authService: AuthService, - private policyService: PolicyService, - private notificationBackground: NotificationBackground, - private importService: ImportServiceAbstraction, - private syncService: SyncService, - private scriptInjectorService: ScriptInjectorService, - ) {} - - /** - * Initializes the fileless importer background logic. - */ - init() { - this.setupPortMessageListeners(); - } - - /** - * Starts an import of the export data pulled from the tab. - * - * @param importType - The type of import to start. Identifies the used content script. - */ - private startFilelessImport(importType: FilelessImportTypeKeys) { - if (importType === FilelessImportType.LP) { - this.lpImporterPort?.postMessage({ command: "startLpFilelessImport" }); - } - } - - /** - * Cancels an import of the export data pulled from the tab. This closes any - * existing notifications that are present in the tab, and triggers importer - * specific behavior based on the import type. - * - * @param importType - The type of import to cancel. Identifies the used content script. - * @param sender - The sender of the message. - */ - private async cancelFilelessImport( - importType: FilelessImportTypeKeys, - sender: chrome.runtime.MessageSender, - ) { - if (importType === FilelessImportType.LP) { - this.triggerLpImporterCsvDownload(); - } - - await BrowserApi.tabSendMessage(sender.tab, { command: "closeNotificationBar" }); - } - - /** - * Injects the notification bar into the passed tab. - * - * @param tab - * @param importType - */ - private async displayFilelessImportNotification(tab: chrome.tabs.Tab, importType: string) { - await this.notificationBackground.requestFilelessImport(tab, importType); - } - - /** - * Triggers the download of the CSV file from the LP importer. This is triggered - * when the user opts to not save the export to Bitwarden within the notification bar. - */ - private triggerLpImporterCsvDownload() { - this.lpImporterPort?.postMessage({ command: "triggerCsvDownload" }); - this.lpImporterPort?.disconnect(); - } - - /** - * Completes the import process for the LP importer. This is triggered when the - * user opts to save the export to Bitwarden within the notification bar. - * - * @param data - The export data to import. - * @param sender - The sender of the message. - */ - private async triggerLpImport(data: string) { - if (!data) { - return; - } - - const promptForPassword_callback = async () => ""; - const importer = this.importService.getImporter( - "lastpasscsv", - promptForPassword_callback, - null, - ); - - try { - const result = await this.importService.import(importer, data, null, null, false); - if (result.success) { - this.importNotificationsPort?.postMessage({ command: "filelessImportCompleted" }); - await this.syncService.fullSync(true); - } - } catch (error) { - this.importNotificationsPort?.postMessage({ - command: "filelessImportFailed", - importErrorMessage: Object.values(error).length - ? error - : chrome.i18n.getMessage("importNetworkError"), - }); - } - } - - /** - * Identifies if the user account has a policy that disables personal ownership. - */ - private async removeIndividualVault(): Promise { - return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ); - } - - /** - * Sets up onConnect listeners for the extension. - */ - private setupPortMessageListeners() { - chrome.runtime.onConnect.addListener(this.handlePortOnConnect); - } - - /** - * Handles connections from content scripts that affect the fileless importer behavior. - * Is used to facilitate the passing of data and user actions to enact the import - * of web content to the Bitwarden vault. Along with this, a check is made to ensure - * that the feature flag is enabled and the user is authenticated. - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - if (!FilelessImporterBackground.filelessImporterPortNames.has(port.name)) { - return; - } - - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BrowserFilelessImport, - ); - const userAuthStatus = await this.authService.getAuthStatus(); - const removeIndividualVault = await this.removeIndividualVault(); - const filelessImportEnabled = - filelessImportFeatureFlagEnabled && - userAuthStatus === AuthenticationStatus.Unlocked && - !removeIndividualVault; - port.postMessage({ command: "verifyFeatureFlag", filelessImportEnabled }); - - if (!filelessImportEnabled) { - return; - } - - port.onMessage.addListener(this.handleImporterPortMessage); - port.onDisconnect.addListener(this.handleImporterPortDisconnect); - - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = port; - await this.scriptInjectorService.inject({ - tabId: port.sender.tab.id, - injectDetails: { runAt: "document_start" }, - mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, - }); - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = port; - break; - } - }; - - /** - * Handles messages that are sent from fileless importer content scripts. - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handleImporterPortMessage = ( - message: FilelessImportPortMessage, - port: chrome.runtime.Port, - ) => { - let handler: CallableFunction | undefined; - - switch (port.name) { - case FilelessImportPort.LpImporter: - handler = this.lpImporterPortMessageHandlers[message.command]; - break; - case FilelessImportPort.NotificationBar: - handler = this.importNotificationsPortMessageHandlers[message.command]; - break; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; - - /** - * Handles disconnections from fileless importer content scripts. - * @param port - The port that was disconnected. - */ - private handleImporterPortDisconnect = (port: chrome.runtime.Port) => { - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = null; - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = null; - break; - } - }; -} - -export default FilelessImporterBackground; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts deleted file mode 100644 index 898ee1205a..0000000000 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Mv2ScriptInjectionDetails, - Mv3ScriptInjectionDetails, -} from "../../platform/services/abstractions/script-injector.service"; - -type FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: Mv2ScriptInjectionDetails; - mv3: Mv3ScriptInjectionDetails; - }; -}; - -const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: { - file: "content/lp-suppress-import-download-script-append-mv2.js", - }, - mv3: { - file: "content/lp-suppress-import-download.js", - world: "MAIN", - }, - }, -} as const; - -export { FilelessImporterInjectedScriptsConfig }; diff --git a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts b/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts deleted file mode 100644 index 018ea2c8d9..0000000000 --- a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts +++ /dev/null @@ -1,25 +0,0 @@ -type LpFilelessImporterMessage = { - command?: string; - data?: string; - filelessImportEnabled?: boolean; -}; - -type LpFilelessImporterMessageHandlerParams = { - message: LpFilelessImporterMessage; - port: chrome.runtime.Port; -}; - -type LpFilelessImporterMessageHandlers = { - [key: string]: ({ message, port }: LpFilelessImporterMessageHandlerParams) => void; - verifyFeatureFlag: ({ message }: { message: LpFilelessImporterMessage }) => void; - triggerCsvDownload: () => void; - startLpFilelessImport: () => void; -}; - -interface LpFilelessImporter { - init(): void; - handleFeatureFlagVerification(message: LpFilelessImporterMessage): void; - triggerCsvDownload(): void; -} - -export { LpFilelessImporterMessage, LpFilelessImporterMessageHandlers, LpFilelessImporter }; diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts deleted file mode 100644 index 21fa44b8d3..0000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { sendPortMessage } from "../../autofill/spec/testing-utils"; -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { LpFilelessImporter } from "./abstractions/lp-fileless-importer"; - -describe("LpFilelessImporter", () => { - let lpFilelessImporter: LpFilelessImporter & { [key: string]: any }; - const portSpy: chrome.runtime.Port = createPortSpyMock(FilelessImportPort.LpImporter); - chrome.runtime.connect = jest.fn(() => portSpy); - - beforeEach(() => { - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-fileless-importer"); - lpFilelessImporter = (globalThis as any).lpFilelessImporter; - }); - - afterEach(() => { - (globalThis as any).lpFilelessImporter = undefined; - jest.clearAllMocks(); - jest.resetModules(); - Object.defineProperty(document, "readyState", { - value: "complete", - writable: true, - }); - }); - - describe("init", () => { - it("sets up the port connection with the background script", () => { - lpFilelessImporter.init(); - - expect(chrome.runtime.connect).toHaveBeenCalledWith({ - name: FilelessImportPort.LpImporter, - }); - }); - }); - - describe("handleFeatureFlagVerification", () => { - it("disconnects the message port when the fileless import feature is disabled", () => { - lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: false }); - - expect(portSpy.disconnect).toHaveBeenCalled(); - }); - - it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => { - Object.defineProperty(document, "readyState", { - value: "loading", - writable: true, - }); - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(document.addEventListener).toHaveBeenCalledWith( - "DOMContentLoaded", - (lpFilelessImporter as any).loadImporter, - ); - }); - - it("sets up a mutation observer to watch the document body for injection of the export content", () => { - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - jest.spyOn(window, "MutationObserver").mockImplementationOnce(() => mock()); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(window.MutationObserver).toHaveBeenCalledWith( - (lpFilelessImporter as any).handleMutation, - ); - expect((lpFilelessImporter as any).mutationObserver.observe).toHaveBeenCalledWith( - document.body, - { childList: true, subtree: true }, - ); - }); - }); - - describe("triggerCsvDownload", () => { - it("posts a window message that triggers the download of the LastPass export", () => { - jest.spyOn(globalThis, "postMessage"); - - lpFilelessImporter.triggerCsvDownload(); - - expect(globalThis.postMessage).toHaveBeenCalledWith( - { command: "triggerCsvDownload" }, - "https://lastpass.com", - ); - }); - }); - - describe("handleMutation", () => { - beforeEach(() => { - lpFilelessImporter["mutationObserver"] = mock({ disconnect: jest.fn() }); - jest.spyOn(portSpy, "postMessage"); - }); - - it("ignores mutations that contain empty records", () => { - lpFilelessImporter["handleMutation"]([]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes in the mutation", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes with a tagname of `pre`", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "div" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain any textContent", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "pre" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain the expected header content", () => { - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "some other content" }] }, - ]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("will store the export data, display the import notification, and disconnect the mutation observer when the export data is appended", () => { - const observerDisconnectSpy = jest.spyOn( - lpFilelessImporter["mutationObserver"], - "disconnect", - ); - - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "url,username,password" }] }, - ]); - - expect(lpFilelessImporter["exportData"]).toEqual("url,username,password"); - expect(portSpy.postMessage).toHaveBeenCalledWith({ command: "displayLpImportNotification" }); - expect(observerDisconnectSpy).toHaveBeenCalled(); - }); - }); - - describe("handlePortMessage", () => { - it("ignores messages that are not registered with the portMessageHandlers", () => { - const message = { command: "unknownCommand" }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification"); - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).not.toHaveBeenCalled(); - expect(lpFilelessImporter.triggerCsvDownload).not.toHaveBeenCalled(); - }); - - it("handles the port message that verifies the fileless import feature flag", () => { - const message = { command: "verifyFeatureFlag", filelessImportEnabled: true }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification").mockImplementation(); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).toHaveBeenCalledWith(message); - }); - - it("handles the port message that triggers the LastPass csv download", () => { - const message = { command: "triggerCsvDownload" }; - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.triggerCsvDownload).toHaveBeenCalled(); - }); - - describe("handles the port message that triggers the LastPass fileless import", () => { - beforeEach(() => { - jest.spyOn(lpFilelessImporter as any, "postPortMessage"); - }); - - it("skips the import of the export data is not populated", () => { - const message = { command: "startLpFilelessImport" }; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).not.toHaveBeenCalled(); - }); - - it("starts the last pass fileless import", () => { - const message = { command: "startLpFilelessImport" }; - const exportData = "url,username,password"; - lpFilelessImporter["exportData"] = exportData; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).toHaveBeenCalledWith({ - command: "startLpImport", - data: exportData, - }); - }); - }); - }); -}); diff --git a/apps/browser/src/tools/content/lp-fileless-importer.ts b/apps/browser/src/tools/content/lp-fileless-importer.ts deleted file mode 100644 index 497a499b33..0000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.ts +++ /dev/null @@ -1,158 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { - LpFilelessImporter as LpFilelessImporterInterface, - LpFilelessImporterMessage, - LpFilelessImporterMessageHandlers, -} from "./abstractions/lp-fileless-importer"; - -class LpFilelessImporter implements LpFilelessImporterInterface { - private exportData: string; - private messagePort: chrome.runtime.Port; - private mutationObserver: MutationObserver; - private readonly portMessageHandlers: LpFilelessImporterMessageHandlers = { - verifyFeatureFlag: ({ message }) => this.handleFeatureFlagVerification(message), - triggerCsvDownload: () => this.triggerCsvDownload(), - startLpFilelessImport: () => this.startLpImport(), - }; - - /** - * Initializes the LP fileless importer. - */ - init() { - this.setupMessagePort(); - } - - /** - * Enacts behavior based on the feature flag verification message. If the feature flag is - * not enabled, the message port is disconnected. If the feature flag is enabled, the - * download of the CSV file is suppressed. - * - * @param message - The port message, contains the feature flag indicator. - */ - handleFeatureFlagVerification(message: LpFilelessImporterMessage) { - if (!message.filelessImportEnabled) { - this.messagePort?.disconnect(); - return; - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.loadImporter); - return; - } - - this.loadImporter(); - } - - /** - * Posts a message to the LP importer to trigger the download of the CSV file. - */ - triggerCsvDownload() { - this.postWindowMessage({ command: "triggerCsvDownload" }); - } - - /** - * Initializes the importing mechanism used to import the CSV file into Bitwarden. - * This is done by observing the DOM for the addition of the LP importer element. - */ - private loadImporter = () => { - this.mutationObserver = new MutationObserver(this.handleMutation); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true, - }); - }; - - /** - * Handles mutations that are observed by the mutation observer. When the exported data - * element is added to the DOM, the export data is extracted and the import prompt is - * displayed. - * - * @param mutations - The mutations that were observed. - */ - private handleMutation = (mutations: MutationRecord[]) => { - let textContent: string; - for (let index = 0; index < mutations?.length; index++) { - const mutation: MutationRecord = mutations[index]; - - textContent = Array.from(mutation.addedNodes) - .filter((node) => node.nodeName.toLowerCase() === "pre") - .map((node) => (node as HTMLPreElement).textContent?.trim()) - .find((text) => text?.indexOf("url,username,password") >= 0); - - if (textContent) { - break; - } - } - - if (textContent) { - this.exportData = textContent; - this.postPortMessage({ command: "displayLpImportNotification" }); - this.mutationObserver.disconnect(); - } - }; - - /** - * If the export data is present, sends a message to the background with - * the export data to start the import process. - */ - private startLpImport() { - if (!this.exportData) { - return; - } - - this.postPortMessage({ command: "startLpImport", data: this.exportData }); - this.messagePort?.disconnect(); - } - - /** - * Posts a message to the background script. - * - * @param message - The message to post. - */ - private postPortMessage(message: LpFilelessImporterMessage) { - this.messagePort?.postMessage(message); - } - - /** - * Posts a message to the global context of the page. - * - * @param message - The message to post. - */ - private postWindowMessage(message: LpFilelessImporterMessage) { - globalThis.postMessage(message, "https://lastpass.com"); - } - - /** - * Sets up the message port that is used to facilitate communication between the - * background script and the content script. - */ - private setupMessagePort() { - this.messagePort = chrome.runtime.connect({ name: FilelessImportPort.LpImporter }); - this.messagePort.onMessage.addListener(this.handlePortMessage); - } - - /** - * Handles messages that are sent from the background script. - * - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handlePortMessage = (message: LpFilelessImporterMessage, port: chrome.runtime.Port) => { - const handler = this.portMessageHandlers[message.command]; - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -(function () { - if (!(globalThis as any).lpFilelessImporter) { - (globalThis as any).lpFilelessImporter = new LpFilelessImporter(); - (globalThis as any).lpFilelessImporter.init(); - } -})(); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts deleted file mode 100644 index 8479235cc1..0000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -describe("LP Suppress Import Download for Manifest v2", () => { - it("appends the `lp-suppress-import-download.js` script to the document element", () => { - let createdScriptElement: HTMLScriptElement; - jest.spyOn(window.document, "createElement"); - jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => { - createdScriptElement = node as HTMLScriptElement; - return node; - }); - - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-suppress-import-download-script-append.mv2"); - - expect(window.document.createElement).toHaveBeenCalledWith("script"); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js"); - expect(window.document.documentElement.appendChild).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - ); - expect(createdScriptElement.src).toBe( - "chrome-extension://id/content/lp-suppress-import-download.js", - ); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts deleted file mode 100644 index cd641590ad..0000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This script handles injection of the LP suppress import download script into the document. - * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. - */ -(function (globalContext) { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js"); - globalContext.document.documentElement.appendChild(script); -})(window); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts deleted file mode 100644 index ff0ed38159..0000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils"; - -describe("LP Suppress Import Download", () => { - const downloadAttribute = "file.csv"; - const hrefAttribute = "https://example.com/file.csv"; - const overridenHrefAttribute = "javascript:void(0)"; - let anchor: HTMLAnchorElement; - - beforeEach(() => { - jest.spyOn(Element.prototype, "appendChild"); - jest.spyOn(window, "addEventListener"); - - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("./lp-suppress-import-download"); - - anchor = document.createElement("a"); - anchor.download = downloadAttribute; - anchor.href = hrefAttribute; - anchor.click = jest.fn(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - it("disables the automatic download anchor", () => { - document.body.appendChild(anchor); - - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - }); - - it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => { - window.document.createElement = jest.fn(() => anchor); - jest.spyOn(window, "removeEventListener"); - - document.body.appendChild(anchor); - - // Precondition - Ensure the anchor in the document has overridden href and download attributes - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalled(); - expect(anchor.href).toEqual(hrefAttribute); - expect(anchor.download).toEqual(downloadAttribute); - expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function)); - }); - - it("skips subsequent calls to trigger a CSVDownload", async () => { - window.document.createElement = jest.fn(() => anchor); - - document.body.appendChild(anchor); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalledTimes(1); - }); - - it("skips triggering the CSV download for window messages that do not have the correct command", () => { - document.body.appendChild(anchor); - - postWindowMessage({ command: "notTriggerCsvDownload" }); - - expect(anchor.click).not.toHaveBeenCalled(); - }); - - it("skips triggering the CSV download for window messages that do not have a data value", () => { - document.body.appendChild(anchor); - - postWindowMessage(null); - - expect(anchor.click).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.ts b/apps/browser/src/tools/content/lp-suppress-import-download.ts deleted file mode 100644 index 1d5d449d19..0000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.ts +++ /dev/null @@ -1,52 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -/** - * Handles intercepting the injection of the CSV download link, and ensures the - * download of the script is suppressed until the user opts to download the file. - * The download is triggered by a window message sent from the LpFilelessImporter - * content script. - */ -(function (globalContext) { - let csvDownload = ""; - let csvHref = ""; - let isCsvDownloadTriggered = false; - const defaultAppendChild = Element.prototype.appendChild; - Element.prototype.appendChild = function (newChild: Node) { - if (isAnchorElement(newChild) && newChild.download) { - csvDownload = newChild.download; - csvHref = newChild.href; - newChild.setAttribute("href", "javascript:void(0)"); - newChild.setAttribute("download", ""); - Element.prototype.appendChild = defaultAppendChild; - } - - return defaultAppendChild.call(this, newChild); - }; - - function isAnchorElement(node: Node): node is HTMLAnchorElement { - return node.nodeName.toLowerCase() === "a"; - } - - const handleWindowMessage = (event: MessageEvent) => { - const command = event.data?.command; - if ( - event.source !== globalContext || - command !== "triggerCsvDownload" || - isCsvDownloadTriggered - ) { - return; - } - - isCsvDownloadTriggered = true; - globalContext.removeEventListener("message", handleWindowMessage); - - const anchor = globalContext.document.createElement("a"); - anchor.setAttribute("href", csvHref); - anchor.setAttribute("download", csvDownload); - globalContext.document.body.appendChild(anchor); - anchor.click(); - globalContext.document.body.removeChild(anchor); - }; - - globalContext.addEventListener("message", handleWindowMessage); -})(window); diff --git a/apps/browser/src/tools/enums/fileless-import.enums.ts b/apps/browser/src/tools/enums/fileless-import.enums.ts deleted file mode 100644 index e20f4f1545..0000000000 --- a/apps/browser/src/tools/enums/fileless-import.enums.ts +++ /dev/null @@ -1,12 +0,0 @@ -const FilelessImportType = { - LP: "LP", -} as const; - -type FilelessImportTypeKeys = (typeof FilelessImportType)[keyof typeof FilelessImportType]; - -const FilelessImportPort = { - NotificationBar: "fileless-importer-notification-bar", - LpImporter: "lp-fileless-importer", -} as const; - -export { FilelessImportType, FilelessImportTypeKeys, FilelessImportPort }; diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 6ba74d7df4..bce41d64d1 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -206,9 +206,7 @@ const mainConfig = { "overlay/list": "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", - "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", - "content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts", }, optimization: { minimize: ENV !== "development", @@ -312,8 +310,6 @@ if (manifestVersion == 2) { // Manifest V2 background pages can be run through the regular build pipeline. // Since it's a standard webpage. mainConfig.entry.background = "./src/platform/background.ts"; - mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = - "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; mainConfig.entry["content/fido2-page-script-append-mv2"] = "./src/autofill/fido2/content/fido2-page-script-append.mv2.ts"; mainConfig.entry["content/fido2-page-script-delay-append-mv2"] = diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d008a09d66..2ac4f507cd 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,6 +9,7 @@ export enum FeatureFlag { DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", + IdpAutoSubmitLogin = "idp-auto-submit-login", InlineMenuFieldQualification = "inline-menu-field-qualification", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", InlineMenuTotp = "inline-menu-totp", @@ -16,7 +17,6 @@ export enum FeatureFlag { NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", - BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", @@ -27,7 +27,6 @@ export enum FeatureFlag { TwoFactorComponentRefactor = "two-factor-component-refactor", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", - IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", AccountDeprovisioning = "pm-10308-account-deprovisioning", SSHKeyVaultItem = "ssh-key-vault-item", @@ -66,6 +65,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, + [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.InlineMenuTotp]: FALSE, @@ -73,7 +73,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, - [FeatureFlag.BrowserFilelessImport]: FALSE, [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, @@ -84,7 +83,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.TwoFactorComponentRefactor]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, - [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE,