mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[PM-934] Autofill not working until page has been refreshed (#6826)
* [PM-934] Autofill not working until page has been refreshed * [PM-934] Adjusting cleanup of the messages_handler script * [PM-934] Fixing small issue found within collection of page details * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Addressing concenrs brought up during code review * [PM-934] Applying re-set changes to the autofill overlay implementation on reset of the extension * [PM-934] Applying jest tests to added logic within AutofillOverlayContent service * [PM-934] Fixing typo present in tabs background listener * [PM-934] Finishing up jest tests for updated implementation * [PM-934] Incorporating methodology for ensuring the autofill overlay updates to reflect user settings within existing tabs * [PM-934] Refining implementation to ensure we do not unnecessarily re-inject content scripts when the autofill overlay settings change * [PM-934] Working through jest tests for added implementation details * [PM-934] Working through jest tests for added implementation details * [PM-934] Finalizing jest tests for implemented logic * [PM-5035] Refactoring method structure
This commit is contained in:
parent
7051f255ed
commit
bf60711efe
@ -673,7 +673,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
*/
|
*/
|
||||||
private setupExtensionMessageListeners() {
|
private setupExtensionMessageListeners() {
|
||||||
BrowserApi.messageListener("overlay.background", this.handleExtensionMessage);
|
BrowserApi.messageListener("overlay.background", this.handleExtensionMessage);
|
||||||
chrome.runtime.onConnect.addListener(this.handlePortOnConnect);
|
BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +10,10 @@ import {
|
|||||||
settingsServiceFactory,
|
settingsServiceFactory,
|
||||||
SettingsServiceInitOptions,
|
SettingsServiceInitOptions,
|
||||||
} from "../../../background/service-factories/settings-service.factory";
|
} from "../../../background/service-factories/settings-service.factory";
|
||||||
|
import {
|
||||||
|
configServiceFactory,
|
||||||
|
ConfigServiceInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/config-service.factory";
|
||||||
import {
|
import {
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
@ -43,7 +47,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
|
|||||||
EventCollectionServiceInitOptions &
|
EventCollectionServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
SettingsServiceInitOptions &
|
SettingsServiceInitOptions &
|
||||||
UserVerificationServiceInitOptions;
|
UserVerificationServiceInitOptions &
|
||||||
|
ConfigServiceInitOptions;
|
||||||
|
|
||||||
export function autofillServiceFactory(
|
export function autofillServiceFactory(
|
||||||
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
|
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
|
||||||
@ -62,6 +67,7 @@ export function autofillServiceFactory(
|
|||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await settingsServiceFactory(cache, opts),
|
await settingsServiceFactory(cache, opts),
|
||||||
await userVerificationServiceFactory(cache, opts),
|
await userVerificationServiceFactory(cache, opts),
|
||||||
|
await configServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import OverlayBackground from "./overlay.background";
|
|||||||
import TabsBackground from "./tabs.background";
|
import TabsBackground from "./tabs.background";
|
||||||
|
|
||||||
describe("TabsBackground", () => {
|
describe("TabsBackground", () => {
|
||||||
let tabsBackgorund: TabsBackground;
|
let tabsBackground: TabsBackground;
|
||||||
const mainBackground = mock<MainBackground>({
|
const mainBackground = mock<MainBackground>({
|
||||||
messagingService: {
|
messagingService: {
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
@ -25,7 +25,7 @@ describe("TabsBackground", () => {
|
|||||||
const overlayBackground = mock<OverlayBackground>();
|
const overlayBackground = mock<OverlayBackground>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tabsBackgorund = new TabsBackground(mainBackground, notificationBackground, overlayBackground);
|
tabsBackground = new TabsBackground(mainBackground, notificationBackground, overlayBackground);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -35,11 +35,11 @@ describe("TabsBackground", () => {
|
|||||||
describe("init", () => {
|
describe("init", () => {
|
||||||
it("sets up a window on focusChanged listener", () => {
|
it("sets up a window on focusChanged listener", () => {
|
||||||
const handleWindowOnFocusChangedSpy = jest.spyOn(
|
const handleWindowOnFocusChangedSpy = jest.spyOn(
|
||||||
tabsBackgorund as any,
|
tabsBackground as any,
|
||||||
"handleWindowOnFocusChanged",
|
"handleWindowOnFocusChanged",
|
||||||
);
|
);
|
||||||
|
|
||||||
tabsBackgorund.init();
|
tabsBackground.init();
|
||||||
|
|
||||||
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
||||||
handleWindowOnFocusChangedSpy,
|
handleWindowOnFocusChangedSpy,
|
||||||
@ -49,7 +49,7 @@ describe("TabsBackground", () => {
|
|||||||
|
|
||||||
describe("tab event listeners", () => {
|
describe("tab event listeners", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tabsBackgorund.init();
|
tabsBackground["setupTabEventListeners"]();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("window onFocusChanged event", () => {
|
describe("window onFocusChanged event", () => {
|
||||||
@ -64,7 +64,7 @@ describe("TabsBackground", () => {
|
|||||||
triggerWindowOnFocusedChangedEvent(10);
|
triggerWindowOnFocusedChangedEvent(10);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(tabsBackgorund["focusedWindowId"]).toBe(10);
|
expect(tabsBackground["focusedWindowId"]).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the current tab data", async () => {
|
it("updates the current tab data", async () => {
|
||||||
@ -144,7 +144,7 @@ describe("TabsBackground", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mainBackground.onUpdatedRan = false;
|
mainBackground.onUpdatedRan = false;
|
||||||
tabsBackgorund["focusedWindowId"] = focusedWindowId;
|
tabsBackground["focusedWindowId"] = focusedWindowId;
|
||||||
tab = mock<chrome.tabs.Tab>({
|
tab = mock<chrome.tabs.Tab>({
|
||||||
windowId: focusedWindowId,
|
windowId: focusedWindowId,
|
||||||
active: true,
|
active: true,
|
||||||
|
@ -20,6 +20,14 @@ export default class TabsBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateCurrentTabData();
|
||||||
|
this.setupTabEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the tab and window event listeners.
|
||||||
|
*/
|
||||||
|
private setupTabEventListeners() {
|
||||||
chrome.windows.onFocusChanged.addListener(this.handleWindowOnFocusChanged);
|
chrome.windows.onFocusChanged.addListener(this.handleWindowOnFocusChanged);
|
||||||
chrome.tabs.onActivated.addListener(this.handleTabOnActivated);
|
chrome.tabs.onActivated.addListener(this.handleTabOnActivated);
|
||||||
chrome.tabs.onReplaced.addListener(this.handleTabOnReplaced);
|
chrome.tabs.onReplaced.addListener(this.handleTabOnReplaced);
|
||||||
@ -33,7 +41,7 @@ export default class TabsBackground {
|
|||||||
* @param windowId - The ID of the window that was focused.
|
* @param windowId - The ID of the window that was focused.
|
||||||
*/
|
*/
|
||||||
private handleWindowOnFocusChanged = async (windowId: number) => {
|
private handleWindowOnFocusChanged = async (windowId: number) => {
|
||||||
if (!windowId) {
|
if (windowId == null || windowId < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +124,10 @@ export default class TabsBackground {
|
|||||||
* for the current tab. Also updates the overlay ciphers.
|
* for the current tab. Also updates the overlay ciphers.
|
||||||
*/
|
*/
|
||||||
private updateCurrentTabData = async () => {
|
private updateCurrentTabData = async () => {
|
||||||
await this.main.refreshBadge();
|
await Promise.all([
|
||||||
await this.main.refreshMenu();
|
this.main.refreshBadge(),
|
||||||
await this.overlayBackground.updateOverlayCiphers();
|
this.main.refreshMenu(),
|
||||||
|
this.overlayBackground.updateOverlayCiphers(),
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ type AutofillExtensionMessage = {
|
|||||||
direction?: "previous" | "next";
|
direction?: "previous" | "next";
|
||||||
isOpeningFullOverlay?: boolean;
|
isOpeningFullOverlay?: boolean;
|
||||||
forceCloseOverlay?: boolean;
|
forceCloseOverlay?: boolean;
|
||||||
|
autofillOverlayVisibility?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,10 +35,12 @@ type AutofillExtensionMessageHandlers = {
|
|||||||
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
|
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
bgUnlockPopoutOpened: () => void;
|
bgUnlockPopoutOpened: () => void;
|
||||||
bgVaultItemRepromptPopoutOpened: () => void;
|
bgVaultItemRepromptPopoutOpened: () => void;
|
||||||
|
updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AutofillInit {
|
interface AutofillInit {
|
||||||
init(): void;
|
init(): void;
|
||||||
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };
|
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };
|
||||||
|
@ -6,7 +6,7 @@ import { flushPromises, sendExtensionRuntimeMessage } from "../jest/testing-util
|
|||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import AutofillScript from "../models/autofill-script";
|
import AutofillScript from "../models/autofill-script";
|
||||||
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
||||||
import { RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
import { AutofillOverlayVisibility, RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
||||||
|
|
||||||
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
||||||
import AutofillInit from "./autofill-init";
|
import AutofillInit from "./autofill-init";
|
||||||
@ -16,6 +16,11 @@ describe("AutofillInit", () => {
|
|||||||
const autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
const autofillOverlayContentService = mock<AutofillOverlayContentService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
chrome.runtime.connect = jest.fn().mockReturnValue({
|
||||||
|
onDisconnect: {
|
||||||
|
addListener: jest.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
autofillInit = new AutofillInit(autofillOverlayContentService);
|
autofillInit = new AutofillInit(autofillOverlayContentService);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -477,6 +482,57 @@ describe("AutofillInit", () => {
|
|||||||
expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
|
expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("updateAutofillOverlayVisibility", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
autofillInit["autofillOverlayContentService"].autofillOverlayVisibility =
|
||||||
|
AutofillOverlayVisibility.OnButtonClick;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => {
|
||||||
|
sendExtensionRuntimeMessage({
|
||||||
|
command: "updateAutofillOverlayVisibility",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
||||||
|
AutofillOverlayVisibility.OnButtonClick,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the overlay visibility value", () => {
|
||||||
|
const message = {
|
||||||
|
command: "updateAutofillOverlayVisibility",
|
||||||
|
data: {
|
||||||
|
autofillOverlayVisibility: AutofillOverlayVisibility.Off,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sendExtensionRuntimeMessage(message);
|
||||||
|
|
||||||
|
expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual(
|
||||||
|
message.data.autofillOverlayVisibility,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("destroy", () => {
|
||||||
|
it("removes the extension message listeners", () => {
|
||||||
|
autofillInit.destroy();
|
||||||
|
|
||||||
|
expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith(
|
||||||
|
autofillInit["handleExtensionMessage"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("destroys the collectAutofillContentService", () => {
|
||||||
|
jest.spyOn(autofillInit["collectAutofillContentService"], "destroy");
|
||||||
|
|
||||||
|
autofillInit.destroy();
|
||||||
|
|
||||||
|
expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -26,6 +26,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
|
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
|
||||||
bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(),
|
bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(),
|
||||||
bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(),
|
bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(),
|
||||||
|
updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,6 +215,19 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the autofill overlay visibility.
|
||||||
|
*
|
||||||
|
* @param data - Contains the autoFillOverlayVisibility value
|
||||||
|
*/
|
||||||
|
private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) {
|
||||||
|
if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the extension message listeners for the content script.
|
* Sets up the extension message listeners for the content script.
|
||||||
*/
|
*/
|
||||||
@ -247,6 +261,16 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles destroying the autofill init content script. Removes all
|
||||||
|
* listeners, timeouts, and object instances to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
chrome.runtime.onMessage.removeListener(this.handleExtensionMessage);
|
||||||
|
this.collectAutofillContentService.destroy();
|
||||||
|
this.autofillOverlayContentService?.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutofillInit;
|
export default AutofillInit;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", loadAutofiller);
|
document.addEventListener("DOMContentLoaded", loadAutofiller);
|
||||||
} else {
|
} else {
|
||||||
@ -8,27 +10,30 @@ function loadAutofiller() {
|
|||||||
let pageHref: string = null;
|
let pageHref: string = null;
|
||||||
let filledThisHref = false;
|
let filledThisHref = false;
|
||||||
let delayFillTimeout: number;
|
let delayFillTimeout: number;
|
||||||
|
let doFillInterval: NodeJS.Timeout;
|
||||||
const activeUserIdKey = "activeUserId";
|
const handleExtensionDisconnect = () => {
|
||||||
let activeUserId: string;
|
clearDoFillInterval();
|
||||||
|
clearDelayFillTimeout();
|
||||||
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
|
};
|
||||||
if (obj == null || obj[activeUserIdKey] == null) {
|
const handleExtensionMessage = (message: any) => {
|
||||||
return;
|
if (message.command === "fillForm" && pageHref === message.url) {
|
||||||
}
|
|
||||||
activeUserId = obj[activeUserIdKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.storage.local.get(activeUserId, (obj: any) => {
|
|
||||||
if (obj?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
|
|
||||||
setInterval(() => doFillIfNeeded(), 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
||||||
if (msg.command === "fillForm" && pageHref === msg.url) {
|
|
||||||
filledThisHref = true;
|
filledThisHref = true;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setupExtensionEventListeners();
|
||||||
|
triggerUserFillOnLoad();
|
||||||
|
|
||||||
|
async function triggerUserFillOnLoad() {
|
||||||
|
const activeUserIdKey = "activeUserId";
|
||||||
|
const userKeyStorage = await getFromLocalStorage(activeUserIdKey);
|
||||||
|
const activeUserId = userKeyStorage[activeUserIdKey];
|
||||||
|
const activeUserStorage = await getFromLocalStorage(activeUserId);
|
||||||
|
if (activeUserStorage?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
|
||||||
|
clearDoFillInterval();
|
||||||
|
doFillInterval = setInterval(() => doFillIfNeeded(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function doFillIfNeeded(force = false) {
|
function doFillIfNeeded(force = false) {
|
||||||
if (force || pageHref !== window.location.href) {
|
if (force || pageHref !== window.location.href) {
|
||||||
@ -36,9 +41,7 @@ function loadAutofiller() {
|
|||||||
// Some websites are slow and rendering all page content. Try to fill again later
|
// Some websites are slow and rendering all page content. Try to fill again later
|
||||||
// if we haven't already.
|
// if we haven't already.
|
||||||
filledThisHref = false;
|
filledThisHref = false;
|
||||||
if (delayFillTimeout != null) {
|
clearDelayFillTimeout();
|
||||||
window.clearTimeout(delayFillTimeout);
|
|
||||||
}
|
|
||||||
delayFillTimeout = window.setTimeout(() => {
|
delayFillTimeout = window.setTimeout(() => {
|
||||||
if (!filledThisHref) {
|
if (!filledThisHref) {
|
||||||
doFillIfNeeded(true);
|
doFillIfNeeded(true);
|
||||||
@ -55,4 +58,21 @@ function loadAutofiller() {
|
|||||||
chrome.runtime.sendMessage(msg);
|
chrome.runtime.sendMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearDoFillInterval() {
|
||||||
|
if (doFillInterval) {
|
||||||
|
window.clearInterval(doFillInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDelayFillTimeout() {
|
||||||
|
if (delayFillTimeout) {
|
||||||
|
window.clearTimeout(delayFillTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupExtensionEventListeners() {
|
||||||
|
setupExtensionDisconnectAction(handleExtensionDisconnect);
|
||||||
|
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
||||||
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
|
|
||||||
import AutofillInit from "./autofill-init";
|
import AutofillInit from "./autofill-init";
|
||||||
|
|
||||||
@ -6,6 +7,8 @@ import AutofillInit from "./autofill-init";
|
|||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
const autofillOverlayContentService = new AutofillOverlayContentService();
|
const autofillOverlayContentService = new AutofillOverlayContentService();
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService);
|
windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService);
|
||||||
|
setupAutofillInitDisconnectAction(windowContext);
|
||||||
|
|
||||||
windowContext.bitwardenAutofillInit.init();
|
windowContext.bitwardenAutofillInit.init();
|
||||||
}
|
}
|
||||||
})(window);
|
})(window);
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
|
import { setupAutofillInitDisconnectAction } from "../utils";
|
||||||
|
|
||||||
import AutofillInit from "./autofill-init";
|
import AutofillInit from "./autofill-init";
|
||||||
|
|
||||||
(function (windowContext) {
|
(function (windowContext) {
|
||||||
if (!windowContext.bitwardenAutofillInit) {
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
windowContext.bitwardenAutofillInit = new AutofillInit();
|
windowContext.bitwardenAutofillInit = new AutofillInit();
|
||||||
|
setupAutofillInitDisconnectAction(windowContext);
|
||||||
|
|
||||||
windowContext.bitwardenAutofillInit.init();
|
windowContext.bitwardenAutofillInit.init();
|
||||||
}
|
}
|
||||||
})(window);
|
})(window);
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
window.addEventListener(
|
import { setupExtensionDisconnectAction } from "../utils";
|
||||||
"message",
|
|
||||||
(event) => {
|
const forwardCommands = [
|
||||||
|
"bgUnlockPopoutOpened",
|
||||||
|
"addToLockedVaultPendingNotifications",
|
||||||
|
"unlockCompleted",
|
||||||
|
"addedCipher",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles sending extension messages to the background
|
||||||
|
* script based on window messages from the page.
|
||||||
|
*
|
||||||
|
* @param event - Window message event
|
||||||
|
*/
|
||||||
|
const handleWindowMessage = (event: MessageEvent) => {
|
||||||
if (event.source !== window) {
|
if (event.source !== window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -23,19 +36,30 @@ window.addEventListener(
|
|||||||
referrer: event.source.location.hostname,
|
referrer: event.source.location.hostname,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const forwardCommands = [
|
/**
|
||||||
"bgUnlockPopoutOpened",
|
* Handles forwarding any commands that need to trigger
|
||||||
"addToLockedVaultPendingNotifications",
|
* an action from one service of the extension background
|
||||||
"unlockCompleted",
|
* to another.
|
||||||
"addedCipher",
|
*
|
||||||
];
|
* @param message - Message from the extension
|
||||||
|
*/
|
||||||
chrome.runtime.onMessage.addListener((event) => {
|
const handleExtensionMessage = (message: any) => {
|
||||||
if (forwardCommands.includes(event.command)) {
|
if (forwardCommands.includes(message.command)) {
|
||||||
chrome.runtime.sendMessage(event);
|
chrome.runtime.sendMessage(message);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cleaning up any event listeners that were
|
||||||
|
* added to the window or extension.
|
||||||
|
*/
|
||||||
|
const handleExtensionDisconnect = () => {
|
||||||
|
window.removeEventListener("message", handleWindowMessage);
|
||||||
|
chrome.runtime.onMessage.removeListener(handleExtensionMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleWindowMessage, false);
|
||||||
|
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||||
|
setupExtensionDisconnectAction(handleExtensionDisconnect);
|
||||||
|
@ -4,6 +4,7 @@ import AddLoginRuntimeMessage from "../notification/models/add-login-runtime-mes
|
|||||||
import ChangePasswordRuntimeMessage from "../notification/models/change-password-runtime-message";
|
import ChangePasswordRuntimeMessage from "../notification/models/change-password-runtime-message";
|
||||||
import { FormData } from "../services/abstractions/autofill.service";
|
import { FormData } from "../services/abstractions/autofill.service";
|
||||||
import { GlobalSettings, UserSettings } from "../types";
|
import { GlobalSettings, UserSettings } from "../types";
|
||||||
|
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
|
||||||
|
|
||||||
interface HTMLElementWithFormOpId extends HTMLElement {
|
interface HTMLElementWithFormOpId extends HTMLElement {
|
||||||
formOpId: string;
|
formOpId: string;
|
||||||
@ -122,6 +123,8 @@ async function loadNotificationBar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupExtensionDisconnectAction(handleExtensionDisconnection);
|
||||||
|
|
||||||
if (!showNotificationBar) {
|
if (!showNotificationBar) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -999,11 +1002,23 @@ async function loadNotificationBar() {
|
|||||||
return theEl === document;
|
return theEl === document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleExtensionDisconnection(port: chrome.runtime.Port) {
|
||||||
|
closeBar(false);
|
||||||
|
clearTimeout(domObservationCollectTimeoutId);
|
||||||
|
clearTimeout(collectPageDetailsTimeoutId);
|
||||||
|
clearTimeout(handlePageChangeTimeoutId);
|
||||||
|
observer?.disconnect();
|
||||||
|
observer = null;
|
||||||
|
watchedForms.forEach((wf: WatchedForm) => {
|
||||||
|
const form = wf.formEl;
|
||||||
|
form.removeEventListener("submit", formSubmitted, false);
|
||||||
|
const submitButton = getSubmitButton(
|
||||||
|
form,
|
||||||
|
unionSets(logInButtonNames, changePasswordButtonNames),
|
||||||
|
);
|
||||||
|
submitButton?.removeEventListener("click", formSubmitted, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// End Helper Functions
|
// End Helper Functions
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
5
apps/browser/src/autofill/enums/autofill-port.enums.ts
Normal file
5
apps/browser/src/autofill/enums/autofill-port.enums.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const AutofillPort = {
|
||||||
|
InjectedScript: "autofill-injected-script-port",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export { AutofillPort };
|
@ -1,5 +1,5 @@
|
|||||||
import { EVENTS } from "../../constants";
|
import { EVENTS } from "../../constants";
|
||||||
import { setElementStyles } from "../../utils/utils";
|
import { setElementStyles } from "../../utils";
|
||||||
import {
|
import {
|
||||||
BackgroundPortMessageHandlers,
|
BackgroundPortMessageHandlers,
|
||||||
AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
|
AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
|
||||||
@ -166,9 +166,10 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
|||||||
|
|
||||||
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
|
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
|
||||||
globalThis.removeEventListener("message", this.handleWindowMessage);
|
globalThis.removeEventListener("message", this.handleWindowMessage);
|
||||||
this.port.onMessage.removeListener(this.handlePortMessage);
|
this.unobserveIframe();
|
||||||
this.port.onDisconnect.removeListener(this.handlePortDisconnect);
|
this.port?.onMessage.removeListener(this.handlePortMessage);
|
||||||
this.port.disconnect();
|
this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
|
||||||
|
this.port?.disconnect();
|
||||||
this.port = null;
|
this.port = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -369,7 +370,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
|||||||
* Unobserves the iframe element for mutations to its style attribute.
|
* Unobserves the iframe element for mutations to its style attribute.
|
||||||
*/
|
*/
|
||||||
private unobserveIframe() {
|
private unobserveIframe() {
|
||||||
this.iframeMutationObserver.disconnect();
|
this.iframeMutationObserver?.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,8 +3,8 @@ import "lit/polyfill-support.js";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
import { EVENTS } from "../../../constants";
|
import { EVENTS } from "../../../constants";
|
||||||
|
import { buildSvgDomElement } from "../../../utils";
|
||||||
import { logoIcon, logoLockedIcon } from "../../../utils/svg-icons";
|
import { logoIcon, logoLockedIcon } from "../../../utils/svg-icons";
|
||||||
import { buildSvgDomElement } from "../../../utils/utils";
|
|
||||||
import {
|
import {
|
||||||
InitAutofillOverlayButtonMessage,
|
InitAutofillOverlayButtonMessage,
|
||||||
OverlayButtonWindowMessageHandlers,
|
OverlayButtonWindowMessageHandlers,
|
||||||
|
@ -4,8 +4,8 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
|||||||
|
|
||||||
import { OverlayCipherData } from "../../../background/abstractions/overlay.background";
|
import { OverlayCipherData } from "../../../background/abstractions/overlay.background";
|
||||||
import { EVENTS } from "../../../constants";
|
import { EVENTS } from "../../../constants";
|
||||||
|
import { buildSvgDomElement } from "../../../utils";
|
||||||
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../utils/svg-icons";
|
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../utils/svg-icons";
|
||||||
import { buildSvgDomElement } from "../../../utils/utils";
|
|
||||||
import {
|
import {
|
||||||
InitAutofillOverlayListMessage,
|
InitAutofillOverlayListMessage,
|
||||||
OverlayListWindowMessageHandlers,
|
OverlayListWindowMessageHandlers,
|
||||||
|
@ -10,6 +10,7 @@ import { UriMatchType } from "@bitwarden/common/vault/enums";
|
|||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { flagEnabled } from "../../../platform/flags";
|
import { flagEnabled } from "../../../platform/flags";
|
||||||
|
import { AutofillService } from "../../services/abstractions/autofill.service";
|
||||||
import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum";
|
import { AutofillOverlayVisibility } from "../../utils/autofill-overlay.enum";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -35,6 +36,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
|
private autofillService: AutofillService,
|
||||||
) {
|
) {
|
||||||
this.autoFillOverlayVisibilityOptions = [
|
this.autoFillOverlayVisibilityOptions = [
|
||||||
{
|
{
|
||||||
@ -86,7 +88,10 @@ export class AutofillComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAutoFillOverlayVisibility() {
|
async updateAutoFillOverlayVisibility() {
|
||||||
|
const previousAutoFillOverlayVisibility =
|
||||||
|
await this.settingsService.getAutoFillOverlayVisibility();
|
||||||
await this.settingsService.setAutoFillOverlayVisibility(this.autoFillOverlayVisibility);
|
await this.settingsService.setAutoFillOverlayVisibility(this.autoFillOverlayVisibility);
|
||||||
|
await this.handleUpdatingAutofillOverlayContentScripts(previousAutoFillOverlayVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAutoFillOnPageLoad() {
|
async updateAutoFillOnPageLoad() {
|
||||||
@ -144,4 +149,25 @@ export class AutofillComponent implements OnInit {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
BrowserApi.createNewTab(this.disablePasswordManagerLink);
|
BrowserApi.createNewTab(this.disablePasswordManagerLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleUpdatingAutofillOverlayContentScripts(
|
||||||
|
previousAutoFillOverlayVisibility: number,
|
||||||
|
) {
|
||||||
|
const autofillOverlayPreviouslyDisabled =
|
||||||
|
previousAutoFillOverlayVisibility === AutofillOverlayVisibility.Off;
|
||||||
|
const autofillOverlayCurrentlyDisabled =
|
||||||
|
this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off;
|
||||||
|
|
||||||
|
if (!autofillOverlayPreviouslyDisabled && !autofillOverlayCurrentlyDisabled) {
|
||||||
|
const tabs = await BrowserApi.tabsQuery({});
|
||||||
|
tabs.forEach((tab) =>
|
||||||
|
BrowserApi.tabSendMessageData(tab, "updateAutofillOverlayVisibility", {
|
||||||
|
autofillOverlayVisibility: this.autoFillOverlayVisibility,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.autofillService.reloadAutofillScripts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ interface AutofillOverlayContentService {
|
|||||||
isCurrentlyFilling: boolean;
|
isCurrentlyFilling: boolean;
|
||||||
isOverlayCiphersPopulated: boolean;
|
isOverlayCiphersPopulated: boolean;
|
||||||
pageDetailsUpdateRequired: boolean;
|
pageDetailsUpdateRequired: boolean;
|
||||||
|
autofillOverlayVisibility: number;
|
||||||
init(): void;
|
init(): void;
|
||||||
setupAutofillOverlayListenerOnField(
|
setupAutofillOverlayListenerOnField(
|
||||||
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
autofillFieldElement: ElementWithOpId<FormFieldElement>,
|
||||||
@ -27,6 +28,7 @@ interface AutofillOverlayContentService {
|
|||||||
redirectOverlayFocusOut(direction: "previous" | "next"): void;
|
redirectOverlayFocusOut(direction: "previous" | "next"): void;
|
||||||
focusMostRecentOverlayField(): void;
|
focusMostRecentOverlayField(): void;
|
||||||
blurMostRecentOverlayField(): void;
|
blurMostRecentOverlayField(): void;
|
||||||
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OpenAutofillOverlayOptions, AutofillOverlayContentService };
|
export { OpenAutofillOverlayOptions, AutofillOverlayContentService };
|
||||||
|
@ -44,10 +44,12 @@ export interface GenerateFillScriptOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AutofillService {
|
export abstract class AutofillService {
|
||||||
|
loadAutofillScriptsOnInstall: () => Promise<void>;
|
||||||
|
reloadAutofillScripts: () => Promise<void>;
|
||||||
injectAutofillScripts: (
|
injectAutofillScripts: (
|
||||||
sender: chrome.runtime.MessageSender,
|
tab: chrome.tabs.Tab,
|
||||||
autofillV2?: boolean,
|
frameId?: number,
|
||||||
autofillOverlay?: boolean,
|
triggeringOnPageLoad?: boolean,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
||||||
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
||||||
|
@ -22,6 +22,7 @@ interface CollectAutofillContentService {
|
|||||||
filterCallback: CallableFunction,
|
filterCallback: CallableFunction,
|
||||||
isObservingShadowRoot?: boolean,
|
isObservingShadowRoot?: boolean,
|
||||||
): Node[];
|
): Node[];
|
||||||
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1609,4 +1609,100 @@ describe("AutofillOverlayContentService", () => {
|
|||||||
expect(autofillOverlayContentService["removeAutofillOverlay"]).toHaveBeenCalled();
|
expect(autofillOverlayContentService["removeAutofillOverlay"]).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("destroy", () => {
|
||||||
|
let autofillFieldElement: ElementWithOpId<FormFieldElement>;
|
||||||
|
let autofillFieldData: AutofillField;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<form id="validFormId">
|
||||||
|
<input type="text" id="username-field" placeholder="username" />
|
||||||
|
<input type="password" id="password-field" placeholder="password" />
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
autofillFieldElement = document.getElementById(
|
||||||
|
"username-field",
|
||||||
|
) as ElementWithOpId<FormFieldElement>;
|
||||||
|
autofillFieldElement.opid = "op-1";
|
||||||
|
autofillFieldData = createAutofillFieldMock({
|
||||||
|
opid: "username-field",
|
||||||
|
form: "validFormId",
|
||||||
|
placeholder: "username",
|
||||||
|
elementNumber: 1,
|
||||||
|
});
|
||||||
|
autofillOverlayContentService.setupAutofillOverlayListenerOnField(
|
||||||
|
autofillFieldElement,
|
||||||
|
autofillFieldData,
|
||||||
|
);
|
||||||
|
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disconnects all mutation observers", () => {
|
||||||
|
autofillOverlayContentService["setupMutationObserver"]();
|
||||||
|
jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect");
|
||||||
|
jest.spyOn(autofillOverlayContentService["documentElementMutationObserver"], "disconnect");
|
||||||
|
|
||||||
|
autofillOverlayContentService.destroy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
autofillOverlayContentService["documentElementMutationObserver"].disconnect,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
autofillOverlayContentService["bodyElementMutationObserver"].disconnect,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the user interaction event timeout", () => {
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout");
|
||||||
|
|
||||||
|
autofillOverlayContentService.destroy();
|
||||||
|
|
||||||
|
expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("de-registers all global event listeners", () => {
|
||||||
|
jest.spyOn(globalThis.document, "removeEventListener");
|
||||||
|
jest.spyOn(globalThis, "removeEventListener");
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners");
|
||||||
|
|
||||||
|
autofillOverlayContentService.destroy();
|
||||||
|
|
||||||
|
expect(globalThis.document.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.VISIBILITYCHANGE,
|
||||||
|
autofillOverlayContentService["handleVisibilityChangeEvent"],
|
||||||
|
);
|
||||||
|
expect(globalThis.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.FOCUSOUT,
|
||||||
|
autofillOverlayContentService["handleFormFieldBlurEvent"],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
autofillOverlayContentService["removeOverlayRepositionEventListeners"],
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("de-registers any event listeners that are attached to the form field elements", () => {
|
||||||
|
jest.spyOn(autofillOverlayContentService as any, "removeCachedFormFieldEventListeners");
|
||||||
|
jest.spyOn(autofillFieldElement, "removeEventListener");
|
||||||
|
jest.spyOn(autofillOverlayContentService["formFieldElements"], "delete");
|
||||||
|
|
||||||
|
autofillOverlayContentService.destroy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
autofillOverlayContentService["removeCachedFormFieldEventListeners"],
|
||||||
|
).toHaveBeenCalledWith(autofillFieldElement);
|
||||||
|
expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.BLUR,
|
||||||
|
autofillOverlayContentService["handleFormFieldBlurEvent"],
|
||||||
|
);
|
||||||
|
expect(autofillFieldElement.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
EVENTS.KEYUP,
|
||||||
|
autofillOverlayContentService["handleFormFieldKeyupEvent"],
|
||||||
|
);
|
||||||
|
expect(autofillOverlayContentService["formFieldElements"].delete).toHaveBeenCalledWith(
|
||||||
|
autofillFieldElement,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,16 +10,12 @@ import AutofillField from "../models/autofill-field";
|
|||||||
import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
|
import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
|
||||||
import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
|
import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
|
||||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
|
import { generateRandomCustomElementName, sendExtensionMessage, setElementStyles } from "../utils";
|
||||||
import {
|
import {
|
||||||
AutofillOverlayElement,
|
AutofillOverlayElement,
|
||||||
RedirectFocusDirection,
|
RedirectFocusDirection,
|
||||||
AutofillOverlayVisibility,
|
AutofillOverlayVisibility,
|
||||||
} from "../utils/autofill-overlay.enum";
|
} from "../utils/autofill-overlay.enum";
|
||||||
import {
|
|
||||||
generateRandomCustomElementName,
|
|
||||||
sendExtensionMessage,
|
|
||||||
setElementStyles,
|
|
||||||
} from "../utils/utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
||||||
@ -32,9 +28,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
isCurrentlyFilling = false;
|
isCurrentlyFilling = false;
|
||||||
isOverlayCiphersPopulated = false;
|
isOverlayCiphersPopulated = false;
|
||||||
pageDetailsUpdateRequired = false;
|
pageDetailsUpdateRequired = false;
|
||||||
|
autofillOverlayVisibility: number;
|
||||||
private readonly findTabs = tabbable;
|
private readonly findTabs = tabbable;
|
||||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||||
private autofillOverlayVisibility: number;
|
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
|
||||||
private userFilledFields: Record<string, FillableFormFieldElement> = {};
|
private userFilledFields: Record<string, FillableFormFieldElement> = {};
|
||||||
private authStatus: AuthenticationStatus;
|
private authStatus: AuthenticationStatus;
|
||||||
private focusableElements: FocusableElement[] = [];
|
private focusableElements: FocusableElement[] = [];
|
||||||
@ -47,6 +44,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
private userInteractionEventTimeout: NodeJS.Timeout;
|
private userInteractionEventTimeout: NodeJS.Timeout;
|
||||||
private overlayElementsMutationObserver: MutationObserver;
|
private overlayElementsMutationObserver: MutationObserver;
|
||||||
private bodyElementMutationObserver: MutationObserver;
|
private bodyElementMutationObserver: MutationObserver;
|
||||||
|
private documentElementMutationObserver: MutationObserver;
|
||||||
private mutationObserverIterations = 0;
|
private mutationObserverIterations = 0;
|
||||||
private mutationObserverIterationsResetTimeout: NodeJS.Timeout;
|
private mutationObserverIterationsResetTimeout: NodeJS.Timeout;
|
||||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||||
@ -86,6 +84,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.formFieldElements.add(formFieldElement);
|
||||||
|
|
||||||
if (!this.autofillOverlayVisibility) {
|
if (!this.autofillOverlayVisibility) {
|
||||||
await this.getAutofillOverlayVisibility();
|
await this.getAutofillOverlayVisibility();
|
||||||
}
|
}
|
||||||
@ -901,10 +901,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
this.handleBodyElementMutationObserverUpdate,
|
this.handleBodyElementMutationObserverUpdate,
|
||||||
);
|
);
|
||||||
|
|
||||||
const documentElementMutationObserver = new MutationObserver(
|
this.documentElementMutationObserver = new MutationObserver(
|
||||||
this.handleDocumentElementMutationObserverUpdate,
|
this.handleDocumentElementMutationObserverUpdate,
|
||||||
);
|
);
|
||||||
documentElementMutationObserver.observe(globalThis.document.documentElement, {
|
this.documentElementMutationObserver.observe(globalThis.document.documentElement, {
|
||||||
childList: true,
|
childList: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1117,6 +1117,28 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
const documentRoot = element.getRootNode() as ShadowRoot | Document;
|
const documentRoot = element.getRootNode() as ShadowRoot | Document;
|
||||||
return documentRoot?.activeElement;
|
return documentRoot?.activeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the autofill overlay content service. This method will
|
||||||
|
* disconnect the mutation observers and remove all event listeners.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
this.documentElementMutationObserver?.disconnect();
|
||||||
|
this.clearUserInteractionEventTimeout();
|
||||||
|
this.formFieldElements.forEach((formFieldElement) => {
|
||||||
|
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||||
|
formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||||
|
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||||
|
this.formFieldElements.delete(formFieldElement);
|
||||||
|
});
|
||||||
|
globalThis.document.removeEventListener(
|
||||||
|
EVENTS.VISIBILITYCHANGE,
|
||||||
|
this.handleVisibilityChangeEvent,
|
||||||
|
);
|
||||||
|
globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
||||||
|
this.removeAutofillOverlay();
|
||||||
|
this.removeOverlayRepositionEventListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutofillOverlayContentService;
|
export default AutofillOverlayContentService;
|
||||||
|
@ -2,7 +2,9 @@ import { mock, mockReset } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||||
import {
|
import {
|
||||||
@ -24,6 +26,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
|||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
||||||
|
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||||
import {
|
import {
|
||||||
createAutofillFieldMock,
|
createAutofillFieldMock,
|
||||||
createAutofillPageDetailsMock,
|
createAutofillPageDetailsMock,
|
||||||
@ -54,6 +57,7 @@ describe("AutofillService", () => {
|
|||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const settingsService = mock<SettingsService>();
|
const settingsService = mock<SettingsService>();
|
||||||
const userVerificationService = mock<UserVerificationService>();
|
const userVerificationService = mock<UserVerificationService>();
|
||||||
|
const configService = mock<ConfigService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
autofillService = new AutofillService(
|
autofillService = new AutofillService(
|
||||||
@ -64,6 +68,7 @@ describe("AutofillService", () => {
|
|||||||
logService,
|
logService,
|
||||||
settingsService,
|
settingsService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,6 +77,72 @@ describe("AutofillService", () => {
|
|||||||
mockReset(cipherService);
|
mockReset(cipherService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("loadAutofillScriptsOnInstall", () => {
|
||||||
|
let tab1: chrome.tabs.Tab;
|
||||||
|
let tab2: chrome.tabs.Tab;
|
||||||
|
let tab3: chrome.tabs.Tab;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tab1 = createChromeTabMock({ id: 1, url: "https://some-url.com" });
|
||||||
|
tab2 = createChromeTabMock({ id: 2, url: "http://some-url.com" });
|
||||||
|
tab3 = createChromeTabMock({ id: 3, url: "chrome-extension://some-extension-route" });
|
||||||
|
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([tab1, tab2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("queries all browser tabs and injects the autofill scripts into them", async () => {
|
||||||
|
jest.spyOn(autofillService, "injectAutofillScripts");
|
||||||
|
|
||||||
|
await autofillService.loadAutofillScriptsOnInstall();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
|
||||||
|
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab1, 0, false);
|
||||||
|
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab2, 0, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips injecting scripts into tabs that do not have an http(s) protocol", async () => {
|
||||||
|
jest.spyOn(autofillService, "injectAutofillScripts");
|
||||||
|
|
||||||
|
await autofillService.loadAutofillScriptsOnInstall();
|
||||||
|
|
||||||
|
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
|
||||||
|
expect(autofillService.injectAutofillScripts).not.toHaveBeenCalledWith(tab3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up an extension runtime onConnect listener", async () => {
|
||||||
|
await autofillService.loadAutofillScriptsOnInstall();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reloadAutofillScripts", () => {
|
||||||
|
it("disconnects and removes all autofill script ports", () => {
|
||||||
|
const port1 = mock<chrome.runtime.Port>({
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
});
|
||||||
|
const port2 = mock<chrome.runtime.Port>({
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
});
|
||||||
|
autofillService["autofillScriptPortsSet"] = new Set([port1, port2]);
|
||||||
|
|
||||||
|
autofillService.reloadAutofillScripts();
|
||||||
|
|
||||||
|
expect(port1.disconnect).toHaveBeenCalled();
|
||||||
|
expect(port2.disconnect).toHaveBeenCalled();
|
||||||
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-injects the autofill scripts in all tabs", () => {
|
||||||
|
autofillService["autofillScriptPortsSet"] = new Set([mock<chrome.runtime.Port>()]);
|
||||||
|
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
||||||
|
|
||||||
|
autofillService.reloadAutofillScripts();
|
||||||
|
|
||||||
|
expect(autofillService["injectAutofillScriptsInAllTabs"]).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("injectAutofillScripts", () => {
|
describe("injectAutofillScripts", () => {
|
||||||
const autofillV1Script = "autofill.js";
|
const autofillV1Script = "autofill.js";
|
||||||
const autofillV2BootstrapScript = "bootstrap-autofill.js";
|
const autofillV2BootstrapScript = "bootstrap-autofill.js";
|
||||||
@ -83,12 +154,12 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tabMock = createChromeTabMock();
|
tabMock = createChromeTabMock();
|
||||||
sender = { tab: tabMock };
|
sender = { tab: tabMock, frameId: 1 };
|
||||||
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
||||||
await autofillService.injectAutofillScripts(sender);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
|
||||||
|
|
||||||
[autofillV1Script, ...defaultAutofillScripts].forEach((scriptName) => {
|
[autofillV1Script, ...defaultAutofillScripts].forEach((scriptName) => {
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
@ -105,7 +176,11 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will inject the bootstrap-autofill script if the enableAutofillV2 flag is set", async () => {
|
it("will inject the bootstrap-autofill script if the enableAutofillV2 flag is set", async () => {
|
||||||
await autofillService.injectAutofillScripts(sender, true);
|
jest
|
||||||
|
.spyOn(configService, "getFeatureFlag")
|
||||||
|
.mockImplementation((flag) => Promise.resolve(flag === FeatureFlag.AutofillV2));
|
||||||
|
|
||||||
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
|
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
file: `content/${autofillV2BootstrapScript}`,
|
file: `content/${autofillV2BootstrapScript}`,
|
||||||
@ -120,11 +195,16 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will inject the bootstrap-autofill-overlay script if the enableAutofillOverlay flag is set and the user has the autofill overlay enabled", async () => {
|
it("will inject the bootstrap-autofill-overlay script if the enableAutofillOverlay flag is set and the user has the autofill overlay enabled", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(configService, "getFeatureFlag")
|
||||||
|
.mockImplementation((flag) =>
|
||||||
|
Promise.resolve(flag === FeatureFlag.AutofillOverlay || flag === FeatureFlag.AutofillV2),
|
||||||
|
);
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender, true, true);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
|
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
file: `content/${autofillOverlayBootstrapScript}`,
|
file: `content/${autofillOverlayBootstrapScript}`,
|
||||||
@ -144,18 +224,25 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will inject the bootstrap-autofill script if the enableAutofillOverlay flag is set but the user does not have the autofill overlay enabled", async () => {
|
it("will inject the bootstrap-autofill script if the enableAutofillOverlay flag is set but the user does not have the autofill overlay enabled", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(configService, "getFeatureFlag")
|
||||||
|
.mockImplementation((flag) =>
|
||||||
|
Promise.resolve(flag === FeatureFlag.AutofillOverlay || flag === FeatureFlag.AutofillV2),
|
||||||
|
);
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender, true, true);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
|
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
file: `content/${autofillV2BootstrapScript}`,
|
file: `content/${autofillV2BootstrapScript}`,
|
||||||
|
frameId: sender.frameId,
|
||||||
...defaultExecuteScriptOptions,
|
...defaultExecuteScriptOptions,
|
||||||
});
|
});
|
||||||
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
|
||||||
file: `content/${autofillV1Script}`,
|
file: `content/${autofillV1Script}`,
|
||||||
|
frameId: sender.frameId,
|
||||||
...defaultExecuteScriptOptions,
|
...defaultExecuteScriptOptions,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -4436,4 +4523,58 @@ describe("AutofillService", () => {
|
|||||||
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(false);
|
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handleInjectedScriptPortConnection", () => {
|
||||||
|
it("ignores port connections that do not have the correct port name", () => {
|
||||||
|
const port = mock<chrome.runtime.Port>({
|
||||||
|
name: "some-invalid-port-name",
|
||||||
|
onDisconnect: { addListener: jest.fn() },
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
autofillService["handleInjectedScriptPortConnection"](port);
|
||||||
|
|
||||||
|
expect(port.onDisconnect.addListener).not.toHaveBeenCalled();
|
||||||
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds the connect port to the set of injected script ports and sets up an onDisconnect listener", () => {
|
||||||
|
const port = mock<chrome.runtime.Port>({
|
||||||
|
name: AutofillPort.InjectedScript,
|
||||||
|
onDisconnect: { addListener: jest.fn() },
|
||||||
|
}) as any;
|
||||||
|
jest.spyOn(autofillService as any, "handleInjectScriptPortOnDisconnect");
|
||||||
|
|
||||||
|
autofillService["handleInjectedScriptPortConnection"](port);
|
||||||
|
|
||||||
|
expect(port.onDisconnect.addListener).toHaveBeenCalledWith(
|
||||||
|
autofillService["handleInjectScriptPortOnDisconnect"],
|
||||||
|
);
|
||||||
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleInjectScriptPortOnDisconnect", () => {
|
||||||
|
it("ignores port disconnections that do not have the correct port name", () => {
|
||||||
|
autofillService["autofillScriptPortsSet"].add(mock<chrome.runtime.Port>());
|
||||||
|
|
||||||
|
autofillService["handleInjectScriptPortOnDisconnect"](
|
||||||
|
mock<chrome.runtime.Port>({
|
||||||
|
name: "some-invalid-port-name",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the port from the set of injected script ports", () => {
|
||||||
|
const port = mock<chrome.runtime.Port>({
|
||||||
|
name: AutofillPort.InjectedScript,
|
||||||
|
}) as any;
|
||||||
|
autofillService["autofillScriptPortsSet"].add(port);
|
||||||
|
|
||||||
|
autofillService["handleInjectScriptPortOnDisconnect"](port);
|
||||||
|
|
||||||
|
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,8 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
|||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
@ -13,6 +15,7 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
|||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||||
|
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||||
import AutofillField from "../models/autofill-field";
|
import AutofillField from "../models/autofill-field";
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import AutofillScript from "../models/autofill-script";
|
import AutofillScript from "../models/autofill-script";
|
||||||
@ -35,6 +38,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
|
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
|
||||||
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
|
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
|
||||||
private currentlyOpeningPasswordRepromptPopout = false;
|
private currentlyOpeningPasswordRepromptPopout = false;
|
||||||
|
private autofillScriptPortsSet = new Set<chrome.runtime.Port>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
@ -44,23 +48,54 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
|
private configService: ConfigServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers on installation of the extension Handles injecting
|
||||||
|
* content scripts into all tabs that are currently open, and
|
||||||
|
* sets up a listener to ensure content scripts can identify
|
||||||
|
* if the extension context has been disconnected.
|
||||||
|
*/
|
||||||
|
async loadAutofillScriptsOnInstall() {
|
||||||
|
BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection);
|
||||||
|
|
||||||
|
this.injectAutofillScriptsInAllTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a complete reload of all autofill scripts on tabs open within
|
||||||
|
* the user's browsing session. This is done by first disconnecting all
|
||||||
|
* existing autofill content script ports, which cleans up existing object
|
||||||
|
* instances, and then re-injecting the autofill scripts into all tabs.
|
||||||
|
*/
|
||||||
|
async reloadAutofillScripts() {
|
||||||
|
this.autofillScriptPortsSet.forEach((port) => {
|
||||||
|
port.disconnect();
|
||||||
|
this.autofillScriptPortsSet.delete(port);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.injectAutofillScriptsInAllTabs();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injects the autofill scripts into the current tab and all frames
|
* Injects the autofill scripts into the current tab and all frames
|
||||||
* found within the tab. Temporarily, will conditionally inject
|
* found within the tab. Temporarily, will conditionally inject
|
||||||
* the refactor of the core autofill script if the feature flag
|
* the refactor of the core autofill script if the feature flag
|
||||||
* is enabled.
|
* is enabled.
|
||||||
* @param {chrome.runtime.MessageSender} sender
|
* @param {chrome.tabs.Tab} tab
|
||||||
* @param {boolean} autofillV2
|
* @param {number} frameId
|
||||||
* @param {boolean} autofillOverlay
|
* @param {boolean} triggeringOnPageLoad
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async injectAutofillScripts(
|
async injectAutofillScripts(
|
||||||
sender: chrome.runtime.MessageSender,
|
tab: chrome.tabs.Tab,
|
||||||
autofillV2 = false,
|
frameId = 0,
|
||||||
autofillOverlay = false,
|
triggeringOnPageLoad = true,
|
||||||
) {
|
): Promise<void> {
|
||||||
|
const autofillV2 = await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillV2);
|
||||||
|
const autofillOverlay = await this.configService.getFeatureFlag<boolean>(
|
||||||
|
FeatureFlag.AutofillOverlay,
|
||||||
|
);
|
||||||
let mainAutofillScript = "autofill.js";
|
let mainAutofillScript = "autofill.js";
|
||||||
|
|
||||||
const isUsingAutofillOverlay =
|
const isUsingAutofillOverlay =
|
||||||
@ -73,20 +108,24 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
: "bootstrap-autofill.js";
|
: "bootstrap-autofill.js";
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectedScripts = [
|
const injectedScripts = [mainAutofillScript];
|
||||||
mainAutofillScript,
|
if (triggeringOnPageLoad) {
|
||||||
"autofiller.js",
|
injectedScripts.push("autofiller.js");
|
||||||
"notificationBar.js",
|
}
|
||||||
"contextMenuHandler.js",
|
injectedScripts.push("notificationBar.js", "contextMenuHandler.js");
|
||||||
];
|
|
||||||
|
|
||||||
for (const injectedScript of injectedScripts) {
|
for (const injectedScript of injectedScripts) {
|
||||||
await BrowserApi.executeScriptInTab(sender.tab.id, {
|
await BrowserApi.executeScriptInTab(tab.id, {
|
||||||
file: `content/${injectedScript}`,
|
file: `content/${injectedScript}`,
|
||||||
frameId: sender.frameId,
|
frameId,
|
||||||
runAt: "document_start",
|
runAt: "document_start",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await BrowserApi.executeScriptInTab(tab.id, {
|
||||||
|
file: "content/message_handler.js",
|
||||||
|
runAt: "document_start",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1877,4 +1916,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming long-lived connections from injected autofill scripts.
|
||||||
|
* Stores the port in a set to facilitate disconnecting ports if the extension
|
||||||
|
* needs to re-inject the autofill scripts.
|
||||||
|
*
|
||||||
|
* @param port - The port that was connected
|
||||||
|
*/
|
||||||
|
private handleInjectedScriptPortConnection = (port: chrome.runtime.Port) => {
|
||||||
|
if (port.name !== AutofillPort.InjectedScript) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autofillScriptPortsSet.add(port);
|
||||||
|
port.onDisconnect.addListener(this.handleInjectScriptPortOnDisconnect);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles disconnecting ports that relate to injected autofill scripts.
|
||||||
|
|
||||||
|
* @param port - The port that was disconnected
|
||||||
|
*/
|
||||||
|
private handleInjectScriptPortOnDisconnect = (port: chrome.runtime.Port) => {
|
||||||
|
if (port.name !== AutofillPort.InjectedScript) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autofillScriptPortsSet.delete(port);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries all open tabs in the user's browsing session
|
||||||
|
* and injects the autofill scripts into the page.
|
||||||
|
*/
|
||||||
|
private async injectAutofillScriptsInAllTabs() {
|
||||||
|
const tabs = await BrowserApi.tabsQuery({});
|
||||||
|
for (let index = 0; index < tabs.length; index++) {
|
||||||
|
const tab = tabs[index];
|
||||||
|
if (tab.url?.startsWith("http")) {
|
||||||
|
this.injectAutofillScripts(tab, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1249,6 +1249,17 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
|||||||
|
|
||||||
return attributeValue;
|
return attributeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the CollectAutofillContentService. Clears all
|
||||||
|
* timeouts and disconnects the mutation observer.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.updateAutofillElementsAfterMutationTimeout) {
|
||||||
|
clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
|
||||||
|
}
|
||||||
|
this.mutationObserver?.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CollectAutofillContentService;
|
export default CollectAutofillContentService;
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||||
|
import { triggerPortOnDisconnectEvent } from "../jest/testing-utils";
|
||||||
|
|
||||||
import { logoIcon, logoLockedIcon } from "./svg-icons";
|
import { logoIcon, logoLockedIcon } from "./svg-icons";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildSvgDomElement,
|
buildSvgDomElement,
|
||||||
generateRandomCustomElementName,
|
generateRandomCustomElementName,
|
||||||
sendExtensionMessage,
|
sendExtensionMessage,
|
||||||
setElementStyles,
|
setElementStyles,
|
||||||
} from "./utils";
|
getFromLocalStorage,
|
||||||
|
setupExtensionDisconnectAction,
|
||||||
|
setupAutofillInitDisconnectAction,
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
describe("buildSvgDomElement", () => {
|
describe("buildSvgDomElement", () => {
|
||||||
it("returns an SVG DOM element", () => {
|
it("returns an SVG DOM element", () => {
|
||||||
@ -116,3 +123,107 @@ describe("setElementStyles", () => {
|
|||||||
expect(testDiv.style.cssText).toEqual(expectedCSSRuleString);
|
expect(testDiv.style.cssText).toEqual(expectedCSSRuleString);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFromLocalStorage", () => {
|
||||||
|
it("returns a promise with the storage object pulled from the extension storage api", async () => {
|
||||||
|
const localStorage: Record<string, any> = {
|
||||||
|
testValue: "test",
|
||||||
|
another: "another",
|
||||||
|
};
|
||||||
|
jest.spyOn(chrome.storage.local, "get").mockImplementation((keys, callback) => {
|
||||||
|
const localStorageObject: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (typeof keys === "string") {
|
||||||
|
localStorageObject[keys] = localStorage[keys];
|
||||||
|
} else if (Array.isArray(keys)) {
|
||||||
|
for (const key of keys) {
|
||||||
|
localStorageObject[key] = localStorage[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(localStorageObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnValue = await getFromLocalStorage("testValue");
|
||||||
|
|
||||||
|
expect(chrome.storage.local.get).toHaveBeenCalled();
|
||||||
|
expect(returnValue).toEqual({ testValue: "test" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setupExtensionDisconnectAction", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("connects a port to the extension background and sets up an onDisconnect listener", () => {
|
||||||
|
const onDisconnectCallback = jest.fn();
|
||||||
|
let port: chrome.runtime.Port;
|
||||||
|
jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||||
|
port = {
|
||||||
|
onDisconnect: {
|
||||||
|
addListener: onDisconnectCallback,
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as chrome.runtime.Port;
|
||||||
|
|
||||||
|
return port;
|
||||||
|
});
|
||||||
|
|
||||||
|
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||||
|
|
||||||
|
expect(chrome.runtime.connect).toHaveBeenCalledWith({
|
||||||
|
name: AutofillPort.InjectedScript,
|
||||||
|
});
|
||||||
|
expect(port.onDisconnect.addListener).toHaveBeenCalledWith(expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setupAutofillInitDisconnectAction", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips setting up the extension disconnect action if the bitwardenAutofillInit object is not populated", () => {
|
||||||
|
const onDisconnectCallback = jest.fn();
|
||||||
|
window.bitwardenAutofillInit = undefined;
|
||||||
|
const portConnectSpy = jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
onDisconnect: {
|
||||||
|
addListener: onDisconnectCallback,
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as chrome.runtime.Port;
|
||||||
|
});
|
||||||
|
|
||||||
|
setupAutofillInitDisconnectAction(window);
|
||||||
|
|
||||||
|
expect(portConnectSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("destroys the autofill init instance when the port is disconnected", () => {
|
||||||
|
let port: chrome.runtime.Port;
|
||||||
|
const autofillInitDestroy: CallableFunction = jest.fn();
|
||||||
|
window.bitwardenAutofillInit = {
|
||||||
|
destroy: autofillInitDestroy,
|
||||||
|
} as any;
|
||||||
|
jest.spyOn(chrome.runtime, "connect").mockImplementation(() => {
|
||||||
|
port = {
|
||||||
|
onDisconnect: {
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as chrome.runtime.Port;
|
||||||
|
|
||||||
|
return port;
|
||||||
|
});
|
||||||
|
|
||||||
|
setupAutofillInitDisconnectAction(window);
|
||||||
|
triggerPortOnDisconnectEvent(port as chrome.runtime.Port);
|
||||||
|
|
||||||
|
expect(chrome.runtime.connect).toHaveBeenCalled();
|
||||||
|
expect(port.onDisconnect.addListener).toHaveBeenCalled();
|
||||||
|
expect(autofillInitDestroy).toHaveBeenCalled();
|
||||||
|
expect(window.bitwardenAutofillInit).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a random string of characters that formatted as a custom element name.
|
* Generates a random string of characters that formatted as a custom element name.
|
||||||
*/
|
*/
|
||||||
@ -103,9 +105,57 @@ function setElementStyles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data from local storage based on the keys provided.
|
||||||
|
*
|
||||||
|
* @param keys - String or array of strings of keys to get from local storage
|
||||||
|
*/
|
||||||
|
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a long-lived connection with the extension background
|
||||||
|
* and triggers an onDisconnect event if the extension context
|
||||||
|
* is invalidated.
|
||||||
|
*
|
||||||
|
* @param callback - Callback function to run when the extension disconnects
|
||||||
|
*/
|
||||||
|
function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) {
|
||||||
|
const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript });
|
||||||
|
const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => {
|
||||||
|
callback(disconnectedPort);
|
||||||
|
port.onDisconnect.removeListener(onDisconnectCallback);
|
||||||
|
};
|
||||||
|
port.onDisconnect.addListener(onDisconnectCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles setup of the extension disconnect action for the autofill init class
|
||||||
|
* in both instances where the overlay might or might not be initialized.
|
||||||
|
*
|
||||||
|
* @param windowContext - The global window context
|
||||||
|
*/
|
||||||
|
function setupAutofillInitDisconnectAction(windowContext: Window) {
|
||||||
|
if (!windowContext.bitwardenAutofillInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDisconnectCallback = () => {
|
||||||
|
windowContext.bitwardenAutofillInit.destroy();
|
||||||
|
delete windowContext.bitwardenAutofillInit;
|
||||||
|
};
|
||||||
|
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
generateRandomCustomElementName,
|
generateRandomCustomElementName,
|
||||||
buildSvgDomElement,
|
buildSvgDomElement,
|
||||||
sendExtensionMessage,
|
sendExtensionMessage,
|
||||||
setElementStyles,
|
setElementStyles,
|
||||||
|
getFromLocalStorage,
|
||||||
|
setupExtensionDisconnectAction,
|
||||||
|
setupAutofillInitDisconnectAction,
|
||||||
};
|
};
|
@ -566,6 +566,7 @@ export default class MainBackground {
|
|||||||
this.logService,
|
this.logService,
|
||||||
this.settingsService,
|
this.settingsService,
|
||||||
this.userVerificationService,
|
this.userVerificationService,
|
||||||
|
this.configService,
|
||||||
);
|
);
|
||||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@ -97,9 +96,9 @@ export default class RuntimeBackground {
|
|||||||
await closeUnlockPopout();
|
await closeUnlockPopout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.notificationsService.updateConnection(msg.command === "loggedIn");
|
||||||
await this.main.refreshBadge();
|
await this.main.refreshBadge();
|
||||||
await this.main.refreshMenu(false);
|
await this.main.refreshMenu(false);
|
||||||
this.notificationsService.updateConnection(msg.command === "unlocked");
|
|
||||||
this.systemService.cancelProcessReload();
|
this.systemService.cancelProcessReload();
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
@ -133,11 +132,7 @@ export default class RuntimeBackground {
|
|||||||
await this.main.openPopup();
|
await this.main.openPopup();
|
||||||
break;
|
break;
|
||||||
case "triggerAutofillScriptInjection":
|
case "triggerAutofillScriptInjection":
|
||||||
await this.autofillService.injectAutofillScripts(
|
await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
sender,
|
|
||||||
await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillV2),
|
|
||||||
await this.configService.getFeatureFlag<boolean>(FeatureFlag.AutofillOverlay),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case "bgCollectPageDetails":
|
case "bgCollectPageDetails":
|
||||||
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
|
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
|
||||||
@ -325,6 +320,8 @@ export default class RuntimeBackground {
|
|||||||
|
|
||||||
private async checkOnInstalled() {
|
private async checkOnInstalled() {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
this.autofillService.loadAutofillScriptsOnInstall();
|
||||||
|
|
||||||
if (this.onInstalledReason != null) {
|
if (this.onInstalledReason != null) {
|
||||||
if (this.onInstalledReason === "install") {
|
if (this.onInstalledReason === "install") {
|
||||||
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
||||||
|
@ -24,12 +24,6 @@
|
|||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"all_frames": false,
|
|
||||||
"js": ["content/message_handler.js"],
|
|
||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"css": ["content/autofill.css"],
|
"css": ["content/autofill.css"],
|
||||||
|
@ -23,11 +23,12 @@ const runtime = {
|
|||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
},
|
},
|
||||||
sendMessage: jest.fn(),
|
sendMessage: jest.fn(),
|
||||||
getManifest: jest.fn(),
|
getManifest: jest.fn(() => ({ version: 2 })),
|
||||||
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
|
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
|
||||||
connect: jest.fn(),
|
connect: jest.fn(),
|
||||||
onConnect: {
|
onConnect: {
|
||||||
addListener: jest.fn(),
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user