[PM-5876] Adjust the `LpFilelessImporter.supressDownload` method to inject through the `executeScript` API instead within manifest v3 (#7787)
* [PM-5876] Adjust LP Fileless Importer to Suppress Download with DOM Append in Manifest v3 * [PM-5876] Incorporating jest tests for affected logic * [PM-5876] Fixing jest test that leverages rxjs * [PM-5876] Updating documentation within BrowserApi.executeScriptInTab * [PM-5876] Implementing jest tests for the new LP suppress download content scripts * [PM-5876] Adding a change to webpack to ensure we do not package the mv2 side script for `lp-suppress-import-download.mv2.ts` if building the extension for mv3 * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing a configuration to feed the script injection of the Fileless Importer CSV download supression script
This commit is contained in:
parent
101e1a4f2b
commit
16c5fe65ca
|
@ -108,6 +108,7 @@
|
|||
},
|
||||
"web_accessible_resources": [
|
||||
"content/fido2/page-script.js",
|
||||
"content/lp-suppress-import-download.js",
|
||||
"notification/bar.html",
|
||||
"images/icon38.png",
|
||||
"images/icon38_locked.png",
|
||||
|
|
|
@ -31,6 +31,12 @@
|
|||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"all_frames": false,
|
||||
"js": ["content/lp-fileless-importer.js"],
|
||||
"matches": ["https://lastpass.com/export.php"],
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"all_frames": true,
|
||||
"css": ["content/autofill.css"],
|
||||
|
|
|
@ -320,6 +320,60 @@ describe("BrowserApi", () => {
|
|||
},
|
||||
files: [injectDetails.file],
|
||||
injectImmediately: true,
|
||||
world: "ISOLATED",
|
||||
});
|
||||
expect(result).toEqual(executeScriptResult);
|
||||
});
|
||||
|
||||
it("injects the script into a specified frameId when the extension is built for manifest v3", async () => {
|
||||
const tabId = 1;
|
||||
const frameId = 2;
|
||||
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
||||
file: "file.js",
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
frameId,
|
||||
});
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
||||
|
||||
await BrowserApi.executeScriptInTab(tabId, injectDetails);
|
||||
|
||||
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
||||
target: {
|
||||
tabId: tabId,
|
||||
allFrames: injectDetails.allFrames,
|
||||
frameIds: [frameId],
|
||||
},
|
||||
files: [injectDetails.file],
|
||||
injectImmediately: true,
|
||||
world: "ISOLATED",
|
||||
});
|
||||
});
|
||||
|
||||
it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => {
|
||||
const tabId = 1;
|
||||
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
||||
file: null,
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
frameId: null,
|
||||
});
|
||||
const scriptingApiDetails = { world: "MAIN" as chrome.scripting.ExecutionWorld };
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
||||
|
||||
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails, scriptingApiDetails);
|
||||
|
||||
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
||||
target: {
|
||||
tabId: tabId,
|
||||
allFrames: injectDetails.allFrames,
|
||||
frameIds: null,
|
||||
},
|
||||
files: null,
|
||||
injectImmediately: true,
|
||||
world: "MAIN",
|
||||
});
|
||||
expect(result).toEqual(executeScriptResult);
|
||||
});
|
||||
|
|
|
@ -475,12 +475,19 @@ export class BrowserApi {
|
|||
|
||||
/**
|
||||
* Extension API helper method used to execute a script in a tab.
|
||||
*
|
||||
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript
|
||||
* @param {number} tabId
|
||||
* @param {chrome.tabs.InjectDetails} details
|
||||
* @returns {Promise<unknown>}
|
||||
* @param tabId - The id of the tab to execute the script in.
|
||||
* @param details {@link "InjectDetails" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/extensionTypes/InjectDetails}
|
||||
* @param scriptingApiDetails {@link "ExecutionWorld" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld}
|
||||
*/
|
||||
static executeScriptInTab(tabId: number, details: chrome.tabs.InjectDetails) {
|
||||
static executeScriptInTab(
|
||||
tabId: number,
|
||||
details: chrome.tabs.InjectDetails,
|
||||
scriptingApiDetails?: {
|
||||
world: chrome.scripting.ExecutionWorld;
|
||||
},
|
||||
): Promise<unknown> {
|
||||
if (BrowserApi.manifestVersion === 3) {
|
||||
return chrome.scripting.executeScript({
|
||||
target: {
|
||||
|
@ -490,6 +497,7 @@ export class BrowserApi {
|
|||
},
|
||||
files: details.file ? [details.file] : null,
|
||||
injectImmediately: details.runAt === "document_start",
|
||||
world: scriptingApiDetails?.world || "ISOLATED",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums";
|
||||
|
||||
type SuppressDownloadScriptInjectionConfig = {
|
||||
file: string;
|
||||
scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld };
|
||||
};
|
||||
|
||||
type FilelessImportPortMessage = {
|
||||
command?: string;
|
||||
importType?: FilelessImportTypeKeys;
|
||||
|
@ -27,6 +32,7 @@ interface FilelessImporterBackground {
|
|||
}
|
||||
|
||||
export {
|
||||
SuppressDownloadScriptInjectionConfig,
|
||||
FilelessImportPortMessage,
|
||||
ImportNotificationMessageHandlers,
|
||||
LpImporterMessageHandlers,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
@ -14,10 +15,20 @@ import {
|
|||
sendPortMessage,
|
||||
triggerRuntimeOnConnectEvent,
|
||||
} from "../../autofill/spec/testing-utils";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums";
|
||||
|
||||
import FilelessImporterBackground from "./fileless-importer.background";
|
||||
|
||||
jest.mock("rxjs", () => {
|
||||
const rxjs = jest.requireActual("rxjs");
|
||||
const { firstValueFrom } = rxjs;
|
||||
return {
|
||||
...rxjs,
|
||||
firstValueFrom: jest.fn(firstValueFrom),
|
||||
};
|
||||
});
|
||||
|
||||
describe("FilelessImporterBackground ", () => {
|
||||
let filelessImporterBackground: FilelessImporterBackground;
|
||||
const configService = mock<ConfigService>();
|
||||
|
@ -51,14 +62,17 @@ describe("FilelessImporterBackground ", () => {
|
|||
|
||||
describe("handle ports onConnect", () => {
|
||||
let lpImporterPort: chrome.runtime.Port;
|
||||
let manifestVersionSpy: jest.SpyInstance;
|
||||
let executeScriptInTabSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
|
||||
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")
|
||||
.mockResolvedValue(false);
|
||||
jest.spyOn(filelessImporterBackground as any, "removeIndividualVault");
|
||||
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
|
||||
|
@ -83,9 +97,7 @@ describe("FilelessImporterBackground ", () => {
|
|||
});
|
||||
|
||||
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
|
||||
jest
|
||||
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
|
||||
.mockResolvedValue(true);
|
||||
(firstValueFrom as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||
await flushPromises();
|
||||
|
@ -117,6 +129,35 @@ describe("FilelessImporterBackground ", () => {
|
|||
filelessImportEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => {
|
||||
manifestVersionSpy.mockReturnValue(3);
|
||||
|
||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||
await flushPromises();
|
||||
|
||||
expect(executeScriptInTabSpy).toHaveBeenCalledWith(
|
||||
lpImporterPort.sender.tab.id,
|
||||
{ file: "content/lp-suppress-import-download.js", runAt: "document_start" },
|
||||
{ world: "MAIN" },
|
||||
);
|
||||
});
|
||||
|
||||
it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => {
|
||||
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",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("port messages", () => {
|
||||
|
@ -126,9 +167,7 @@ describe("FilelessImporterBackground ", () => {
|
|||
beforeEach(async () => {
|
||||
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
||||
jest
|
||||
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
|
||||
.mockResolvedValue(false);
|
||||
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
|
||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
|
||||
await flushPromises();
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core";
|
|||
|
||||
import NotificationBackground from "../../autofill/background/notification.background";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts";
|
||||
import {
|
||||
FilelessImportPort,
|
||||
FilelessImportType,
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
LpImporterMessageHandlers,
|
||||
FilelessImporterBackground as FilelessImporterBackgroundInterface,
|
||||
FilelessImportPortMessage,
|
||||
SuppressDownloadScriptInjectionConfig,
|
||||
} from "./abstractions/fileless-importer.background";
|
||||
|
||||
class FilelessImporterBackground implements FilelessImporterBackgroundInterface {
|
||||
|
@ -108,6 +110,23 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
|||
await this.notificationBackground.requestFilelessImport(tab, importType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the script used to suppress the download of the LP importer export file.
|
||||
*
|
||||
* @param sender - The sender of the message.
|
||||
* @param injectionConfig - The configuration for the injection.
|
||||
*/
|
||||
private async injectScriptConfig(
|
||||
sender: chrome.runtime.MessageSender,
|
||||
injectionConfig: SuppressDownloadScriptInjectionConfig,
|
||||
) {
|
||||
await BrowserApi.executeScriptInTab(
|
||||
sender.tab.id,
|
||||
{ file: injectionConfig.file, runAt: "document_start" },
|
||||
injectionConfig.scriptingApiDetails,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -200,6 +219,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
|||
switch (port.name) {
|
||||
case FilelessImportPort.LpImporter:
|
||||
this.lpImporterPort = port;
|
||||
await this.injectScriptConfig(
|
||||
port.sender,
|
||||
BrowserApi.manifestVersion === 3
|
||||
? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3
|
||||
: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2,
|
||||
);
|
||||
break;
|
||||
case FilelessImportPort.NotificationBar:
|
||||
this.importNotificationsPort = port;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background";
|
||||
|
||||
type FilelessImporterInjectedScriptsConfigurations = {
|
||||
LpSuppressImportDownload: {
|
||||
mv2: SuppressDownloadScriptInjectionConfig;
|
||||
mv3: SuppressDownloadScriptInjectionConfig;
|
||||
};
|
||||
};
|
||||
|
||||
const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = {
|
||||
LpSuppressImportDownload: {
|
||||
mv2: {
|
||||
file: "content/lp-suppress-import-download-script-append-mv2.js",
|
||||
},
|
||||
mv3: {
|
||||
file: "content/lp-suppress-import-download.js",
|
||||
scriptingApiDetails: { world: "MAIN" },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export { FilelessImporterInjectedScriptsConfig };
|
|
@ -43,20 +43,6 @@ describe("LpFilelessImporter", () => {
|
|||
expect(portSpy.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("injects a script element that suppresses the download of the LastPass export", () => {
|
||||
const script = document.createElement("script");
|
||||
jest.spyOn(document, "createElement").mockReturnValue(script);
|
||||
jest.spyOn(document.documentElement, "appendChild");
|
||||
|
||||
lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: true });
|
||||
|
||||
expect(document.createElement).toHaveBeenCalledWith("script");
|
||||
expect(document.documentElement.appendChild).toHaveBeenCalled();
|
||||
expect(script.textContent).toContain(
|
||||
"const defaultAppendChild = Element.prototype.appendChild;",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
|
|
|
@ -36,7 +36,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
|
|||
return;
|
||||
}
|
||||
|
||||
this.suppressDownload();
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", this.loadImporter);
|
||||
return;
|
||||
|
@ -52,46 +51,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
|
|||
this.postWindowMessage({ command: "triggerCsvDownload" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppresses the download of the CSV file by overriding the `download` attribute of the
|
||||
* anchor element that is created by the LP importer. This is done by injecting a script
|
||||
* into the page that overrides the `appendChild` method of the `Element` prototype.
|
||||
*/
|
||||
private suppressDownload() {
|
||||
const script = document.createElement("script");
|
||||
script.textContent = `
|
||||
let csvDownload = '';
|
||||
let csvHref = '';
|
||||
const defaultAppendChild = Element.prototype.appendChild;
|
||||
Element.prototype.appendChild = function (newChild) {
|
||||
if (newChild.nodeName.toLowerCase() === 'a' && 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);
|
||||
};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const command = event.data?.command;
|
||||
if (event.source !== window || command !== 'triggerCsvDownload') {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute('href', csvHref);
|
||||
anchor.setAttribute('download', csvDownload);
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
});
|
||||
`;
|
||||
document.documentElement.appendChild(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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;
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 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);
|
|
@ -0,0 +1,81 @@
|
|||
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");
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* 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);
|
|
@ -179,6 +179,7 @@ const mainConfig = {
|
|||
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
|
||||
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
|
||||
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
|
||||
"content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts",
|
||||
},
|
||||
optimization: {
|
||||
minimize: ENV !== "development",
|
||||
|
@ -276,6 +277,8 @@ 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";
|
||||
|
||||
configs.push(mainConfig);
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue