mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-17189] Remove LP fileless importer (#12918)
* Remove LP fileless importer - Remove content scripts - Remove additions to message handlers and notifcation queue - Remove UI elements for the importer - Remove the actual importer code - Remove unsued keys from en/messages.json Remove feature flag "browser-fileless- import" Update webpack.config and manifest files to no longer include content scripts * Move feature flag idp-auto-submit-login under autofill grouping --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
dafeb1492a
commit
9eecfbc8af
@@ -3425,38 +3425,6 @@
|
|||||||
"message": "Toggle collapse",
|
"message": "Toggle collapse",
|
||||||
"description": "Toggling an expand/collapse state."
|
"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": {
|
"aliasDomain": {
|
||||||
"message": "Alias domain"
|
"message": "Alias domain"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,16 +31,10 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage {
|
|||||||
type: "unlock";
|
type: "unlock";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddRequestFilelessImportQueueMessage extends NotificationQueueMessage {
|
|
||||||
type: "fileless-import";
|
|
||||||
importType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationQueueMessageItem =
|
type NotificationQueueMessageItem =
|
||||||
| AddLoginQueueMessage
|
| AddLoginQueueMessage
|
||||||
| AddChangePasswordQueueMessage
|
| AddChangePasswordQueueMessage
|
||||||
| AddUnlockVaultQueueMessage
|
| AddUnlockVaultQueueMessage;
|
||||||
| AddRequestFilelessImportQueueMessage;
|
|
||||||
|
|
||||||
type LockedVaultPendingNotificationsData = {
|
type LockedVaultPendingNotificationsData = {
|
||||||
commandToRetry: {
|
commandToRetry: {
|
||||||
@@ -122,7 +116,6 @@ export {
|
|||||||
AddChangePasswordQueueMessage,
|
AddChangePasswordQueueMessage,
|
||||||
AddLoginQueueMessage,
|
AddLoginQueueMessage,
|
||||||
AddUnlockVaultQueueMessage,
|
AddUnlockVaultQueueMessage,
|
||||||
AddRequestFilelessImportQueueMessage,
|
|
||||||
NotificationQueueMessageItem,
|
NotificationQueueMessageItem,
|
||||||
LockedVaultPendingNotificationsData,
|
LockedVaultPendingNotificationsData,
|
||||||
AdjustNotificationBarMessageData,
|
AdjustNotificationBarMessageData,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import { AutofillService } from "../services/abstractions/autofill.service";
|
|||||||
import {
|
import {
|
||||||
AddChangePasswordQueueMessage,
|
AddChangePasswordQueueMessage,
|
||||||
AddLoginQueueMessage,
|
AddLoginQueueMessage,
|
||||||
AddRequestFilelessImportQueueMessage,
|
|
||||||
AddUnlockVaultQueueMessage,
|
AddUnlockVaultQueueMessage,
|
||||||
ChangePasswordMessageData,
|
ChangePasswordMessageData,
|
||||||
AddLoginMessageData,
|
AddLoginMessageData,
|
||||||
@@ -201,11 +200,6 @@ export default class NotificationBackground {
|
|||||||
case NotificationQueueMessageType.AddLogin:
|
case NotificationQueueMessageType.AddLogin:
|
||||||
typeData.removeIndividualVault = await this.removeIndividualVault();
|
typeData.removeIndividualVault = await this.removeIndividualVault();
|
||||||
break;
|
break;
|
||||||
case NotificationQueueMessageType.RequestFilelessImport:
|
|
||||||
typeData.importType = (
|
|
||||||
notificationQueueMessage as AddRequestFilelessImportQueueMessage
|
|
||||||
).importType;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
|
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(
|
private async pushChangePasswordToQueue(
|
||||||
cipherId: string,
|
cipherId: string,
|
||||||
loginDomain: string,
|
loginDomain: string,
|
||||||
@@ -456,36 +431,6 @@ export default class NotificationBackground {
|
|||||||
await this.sendNotificationQueueMessage(tab, message);
|
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
|
* 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
|
* is locked, the message will be added to the notification queue and the unlock
|
||||||
|
|||||||
@@ -91,8 +91,6 @@ function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationTy
|
|||||||
return i18n.updateLoginPrompt;
|
return i18n.updateLoginPrompt;
|
||||||
case NotificationTypes.Unlock:
|
case NotificationTypes.Unlock:
|
||||||
return "";
|
return "";
|
||||||
case NotificationTypes.FilelessImport:
|
|
||||||
return "";
|
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const NotificationQueueMessageType = {
|
|||||||
AddLogin: "add",
|
AddLogin: "add",
|
||||||
ChangePassword: "change",
|
ChangePassword: "change",
|
||||||
UnlockVault: "unlock",
|
UnlockVault: "unlock",
|
||||||
RequestFilelessImport: "fileless-import",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type NotificationQueueMessageTypes =
|
type NotificationQueueMessageTypes =
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const NotificationTypes = {
|
|||||||
Add: "add",
|
Add: "add",
|
||||||
Change: "change",
|
Change: "change",
|
||||||
Unlock: "unlock",
|
Unlock: "unlock",
|
||||||
FilelessImport: "fileless-import",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||||
|
|||||||
@@ -55,14 +55,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="template-fileless-import">
|
|
||||||
<div class="inner-wrapper">
|
|
||||||
<div id="fileless-import-text" class="notification-body"></div>
|
|
||||||
<div id="fileless-import-buttons" class="notification-actions">
|
|
||||||
<button type="button" id="cancel-fileless-import" class="secondary"></button>
|
|
||||||
<button type="button" id="start-fileless-import" class="primary"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
|||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
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 { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
|
||||||
import { buildSvgDomElement } from "../utils";
|
import { buildSvgDomElement } from "../utils";
|
||||||
import { circleCheckIcon } from "../utils/svg-icons";
|
import { circleCheckIcon } from "../utils/svg-icons";
|
||||||
@@ -59,11 +58,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||||
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
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);
|
setupLogoLink(i18n);
|
||||||
@@ -107,22 +101,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
|
|
||||||
unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
|
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
|
// i18n for body content
|
||||||
const closeButton = document.getElementById("close-button");
|
const closeButton = document.getElementById("close-button");
|
||||||
closeButton.title = i18n.close;
|
closeButton.title = i18n.close;
|
||||||
@@ -134,8 +112,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
handleTypeChange();
|
handleTypeChange();
|
||||||
} else if (notificationType === "unlock") {
|
} else if (notificationType === "unlock") {
|
||||||
handleTypeUnlock();
|
handleTypeUnlock();
|
||||||
} else if (notificationType === "fileless-import") {
|
|
||||||
handleTypeFilelessImport();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeButton.addEventListener("click", (e) => {
|
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) {
|
function setContent(template: HTMLTemplateElement) {
|
||||||
const content = document.getElementById("content");
|
const content = document.getElementById("content");
|
||||||
while (content.firstChild) {
|
while (content.firstChild) {
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s
|
|||||||
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
|
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
|
||||||
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
||||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
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 { VaultFilterService } from "../vault/services/vault-filter.service";
|
||||||
|
|
||||||
import CommandsBackground from "./commands.background";
|
import CommandsBackground from "./commands.background";
|
||||||
@@ -393,7 +392,6 @@ export default class MainBackground {
|
|||||||
private notificationBackground: NotificationBackground;
|
private notificationBackground: NotificationBackground;
|
||||||
private overlayBackground: OverlayBackgroundInterface;
|
private overlayBackground: OverlayBackgroundInterface;
|
||||||
private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface;
|
private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface;
|
||||||
private filelessImporterBackground: FilelessImporterBackground;
|
|
||||||
private runtimeBackground: RuntimeBackground;
|
private runtimeBackground: RuntimeBackground;
|
||||||
private tabsBackground: TabsBackground;
|
private tabsBackground: TabsBackground;
|
||||||
private webRequestBackground: WebRequestBackground;
|
private webRequestBackground: WebRequestBackground;
|
||||||
@@ -1160,16 +1158,6 @@ export default class MainBackground {
|
|||||||
this.notificationBackground,
|
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.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
|
||||||
this.logService,
|
this.logService,
|
||||||
this.autofillService,
|
this.autofillService,
|
||||||
@@ -1296,7 +1284,6 @@ export default class MainBackground {
|
|||||||
await this.runtimeBackground.init();
|
await this.runtimeBackground.init();
|
||||||
await this.notificationBackground.init();
|
await this.notificationBackground.init();
|
||||||
this.overlayNotificationsBackground.init();
|
this.overlayNotificationsBackground.init();
|
||||||
this.filelessImporterBackground.init();
|
|
||||||
this.commandsBackground.init();
|
this.commandsBackground.init();
|
||||||
this.contextMenusBackground?.init();
|
this.contextMenusBackground?.init();
|
||||||
this.idleBackground.init();
|
this.idleBackground.init();
|
||||||
|
|||||||
@@ -29,12 +29,6 @@
|
|||||||
"matches": ["*://*/*", "file:///*"],
|
"matches": ["*://*/*", "file:///*"],
|
||||||
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],
|
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
},
|
|
||||||
{
|
|
||||||
"all_frames": false,
|
|
||||||
"js": ["content/lp-fileless-importer.js"],
|
|
||||||
"matches": ["https://lastpass.com/export.php"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
@@ -140,7 +134,6 @@
|
|||||||
},
|
},
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"content/fido2-page-script.js",
|
"content/fido2-page-script.js",
|
||||||
"content/lp-suppress-import-download.js",
|
|
||||||
"notification/bar.html",
|
"notification/bar.html",
|
||||||
"images/icon38.png",
|
"images/icon38.png",
|
||||||
"images/icon38_locked.png",
|
"images/icon38_locked.png",
|
||||||
|
|||||||
@@ -30,12 +30,6 @@
|
|||||||
"matches": ["*://*/*", "file:///*"],
|
"matches": ["*://*/*", "file:///*"],
|
||||||
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],
|
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
},
|
|
||||||
{
|
|
||||||
"all_frames": false,
|
|
||||||
"js": ["content/lp-fileless-importer.js"],
|
|
||||||
"matches": ["https://lastpass.com/export.php"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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<ConfigService>();
|
|
||||||
const domainSettingsService = mock<DomainSettingsService>();
|
|
||||||
const authService = mock<AuthService>();
|
|
||||||
const policyService = mock<PolicyService>();
|
|
||||||
const notificationBackground = mock<NotificationBackground>();
|
|
||||||
const importService = mock<ImportServiceAbstraction>();
|
|
||||||
const syncService = mock<SyncService>();
|
|
||||||
const platformUtilsService = mock<PlatformUtilsService>();
|
|
||||||
const logService = mock<LogService>();
|
|
||||||
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<Importer>();
|
|
||||||
jest
|
|
||||||
.spyOn(filelessImporterBackground["importService"], "getImporter")
|
|
||||||
.mockReturnValue(importer);
|
|
||||||
jest.spyOn(filelessImporterBackground["importService"], "import").mockResolvedValue(
|
|
||||||
mock<ImportResult>({
|
|
||||||
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<Importer>();
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<string> = 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<boolean> {
|
|
||||||
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;
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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<MutationObserver>());
|
|
||||||
|
|
||||||
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<MutationObserver>({ 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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -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",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
@@ -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 };
|
|
||||||
@@ -206,9 +206,7 @@ const mainConfig = {
|
|||||||
"overlay/list":
|
"overlay/list":
|
||||||
"./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
|
"./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
|
||||||
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.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/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: {
|
optimization: {
|
||||||
minimize: ENV !== "development",
|
minimize: ENV !== "development",
|
||||||
@@ -312,8 +310,6 @@ if (manifestVersion == 2) {
|
|||||||
// Manifest V2 background pages can be run through the regular build pipeline.
|
// Manifest V2 background pages can be run through the regular build pipeline.
|
||||||
// Since it's a standard webpage.
|
// Since it's a standard webpage.
|
||||||
mainConfig.entry.background = "./src/platform/background.ts";
|
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"] =
|
mainConfig.entry["content/fido2-page-script-append-mv2"] =
|
||||||
"./src/autofill/fido2/content/fido2-page-script-append.mv2.ts";
|
"./src/autofill/fido2/content/fido2-page-script-append.mv2.ts";
|
||||||
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] =
|
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] =
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum FeatureFlag {
|
|||||||
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
||||||
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||||
|
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||||
InlineMenuTotp = "inline-menu-totp",
|
InlineMenuTotp = "inline-menu-totp",
|
||||||
@@ -16,7 +17,6 @@ export enum FeatureFlag {
|
|||||||
NotificationRefresh = "notification-refresh",
|
NotificationRefresh = "notification-refresh",
|
||||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||||
|
|
||||||
BrowserFilelessImport = "browser-fileless-import",
|
|
||||||
ItemShare = "item-share",
|
ItemShare = "item-share",
|
||||||
GeneratorToolsModernization = "generator-tools-modernization",
|
GeneratorToolsModernization = "generator-tools-modernization",
|
||||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||||
@@ -27,7 +27,6 @@ export enum FeatureFlag {
|
|||||||
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
||||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
|
||||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||||
@@ -66,6 +65,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
||||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||||
|
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||||
[FeatureFlag.InlineMenuTotp]: FALSE,
|
[FeatureFlag.InlineMenuTotp]: FALSE,
|
||||||
@@ -73,7 +73,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||||
|
|
||||||
[FeatureFlag.BrowserFilelessImport]: FALSE,
|
|
||||||
[FeatureFlag.ItemShare]: FALSE,
|
[FeatureFlag.ItemShare]: FALSE,
|
||||||
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
||||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||||
@@ -84,7 +83,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
||||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
|
||||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user