1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-27569] Typing cleanup (#17087)

* typing cleanup

* additional cleanup

* more typing fixes

* revert notification background changes

* fix DOM query service breakage

* do not run a fill_by_opid action if there is a nullish or empty value attribute

* type cleanup

* cleanup per review suggestions

* remove unused flag check

* add non-null assertion signposts

* additional cleanup
This commit is contained in:
Jonathan Prusik
2025-11-07 11:34:08 -05:00
committed by GitHub
parent 146f612fbd
commit 0ef4964b2e
31 changed files with 449 additions and 335 deletions

View File

@@ -147,7 +147,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>; bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>; bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>; bgGetExcludedDomains: () => Promise<NeverDomains>;
bgGetActiveUserServerConfig: () => Promise<ServerConfig>; bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
getWebVaultUrlForNotification: () => Promise<string>; getWebVaultUrlForNotification: () => Promise<string>;
}; };

View File

@@ -1,19 +1,22 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InlineMenuFillType } from "../../enums/autofill-overlay.enum"; import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
import AutofillField from "../../models/autofill-field";
import AutofillPageDetails from "../../models/autofill-page-details"; import AutofillPageDetails from "../../models/autofill-page-details";
import { PageDetail } from "../../services/abstractions/autofill.service"; import { PageDetail } from "../../services/abstractions/autofill.service";
import { LockedVaultPendingNotificationsData } from "./notification.background"; import { LockedVaultPendingNotificationsData } from "./notification.background";
export type PageDetailsForTab = Record< export type TabId = NonNullable<chrome.tabs.Tab["id"]>;
chrome.runtime.MessageSender["tab"]["id"],
Map<chrome.runtime.MessageSender["frameId"], PageDetail> export type FrameId = NonNullable<chrome.runtime.MessageSender["frameId"]>;
>;
type PageDetailsByFrame = Map<FrameId, PageDetail>;
export type PageDetailsForTab = Record<TabId, PageDetailsByFrame>;
export type SubFrameOffsetData = { export type SubFrameOffsetData = {
top: number; top: number;
@@ -21,19 +24,14 @@ export type SubFrameOffsetData = {
url?: string; url?: string;
frameId?: number; frameId?: number;
parentFrameIds?: number[]; parentFrameIds?: number[];
isCrossOriginSubframe?: boolean;
isMainFrame?: boolean;
hasParentFrame?: boolean;
} | null; } | null;
export type SubFrameOffsetsForTab = Record< type SubFrameOffsetsByFrame = Map<FrameId, SubFrameOffsetData>;
chrome.runtime.MessageSender["tab"]["id"],
Map<chrome.runtime.MessageSender["frameId"], SubFrameOffsetData>
>;
export type WebsiteIconData = { export type SubFrameOffsetsForTab = Record<TabId, SubFrameOffsetsByFrame>;
imageEnabled: boolean;
image: string;
fallbackImage: string;
icon: string;
};
export type UpdateOverlayCiphersParams = { export type UpdateOverlayCiphersParams = {
updateAllCipherTypes: boolean; updateAllCipherTypes: boolean;
@@ -146,7 +144,7 @@ export type OverlayBackgroundExtensionMessage = {
isFieldCurrentlyFilling?: boolean; isFieldCurrentlyFilling?: boolean;
subFrameData?: SubFrameOffsetData; subFrameData?: SubFrameOffsetData;
focusedFieldData?: FocusedFieldData; focusedFieldData?: FocusedFieldData;
allFieldsRect?: any; allFieldsRect?: AutofillField[];
isOpeningFullInlineMenu?: boolean; isOpeningFullInlineMenu?: boolean;
styles?: Partial<CSSStyleDeclaration>; styles?: Partial<CSSStyleDeclaration>;
data?: LockedVaultPendingNotificationsData; data?: LockedVaultPendingNotificationsData;
@@ -155,13 +153,30 @@ export type OverlayBackgroundExtensionMessage = {
ToggleInlineMenuHiddenMessage & ToggleInlineMenuHiddenMessage &
UpdateInlineMenuVisibilityMessage; UpdateInlineMenuVisibilityMessage;
export type OverlayPortCommand =
| "fillCipher"
| "addNewVaultItem"
| "viewCipher"
| "redirectFocus"
| "updateHeight"
| "buttonClicked"
| "blurred"
| "updateColorScheme"
| "unlockVault"
| "refreshGeneratedPassword"
| "fillGeneratedPassword";
export type OverlayPortMessage = { export type OverlayPortMessage = {
[key: string]: any; command: OverlayPortCommand;
command: string; direction?: "up" | "down" | "left" | "right";
direction?: string;
inlineMenuCipherId?: string; inlineMenuCipherId?: string;
addNewCipherType?: CipherType; addNewCipherType?: CipherType;
usePasskey?: boolean; usePasskey?: boolean;
height?: number;
backgroundColorScheme?: "light" | "dark";
viewsCipherData?: InlineMenuCipherData;
loginUrl?: string;
fillGeneratedPassword?: boolean;
}; };
export type InlineMenuCipherData = { export type InlineMenuCipherData = {
@@ -170,7 +185,7 @@ export type InlineMenuCipherData = {
type: CipherType; type: CipherType;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
favorite: boolean; favorite: boolean;
icon: WebsiteIconData; icon: CipherIconDetails;
accountCreationFieldType?: string; accountCreationFieldType?: string;
login?: { login?: {
totp?: string; totp?: string;
@@ -201,9 +216,14 @@ export type BuildCipherDataParams = {
export type BackgroundMessageParam = { export type BackgroundMessageParam = {
message: OverlayBackgroundExtensionMessage; message: OverlayBackgroundExtensionMessage;
}; };
export type BackgroundSenderParam = { export type BackgroundSenderParam = {
sender: chrome.runtime.MessageSender; sender: chrome.runtime.MessageSender & {
tab: NonNullable<chrome.runtime.MessageSender["tab"]>;
frameId: FrameId;
};
}; };
export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
export type OverlayBackgroundExtensionMessageHandlers = { export type OverlayBackgroundExtensionMessageHandlers = {
@@ -253,9 +273,13 @@ export type OverlayBackgroundExtensionMessageHandlers = {
export type PortMessageParam = { export type PortMessageParam = {
message: OverlayPortMessage; message: OverlayPortMessage;
}; };
export type PortConnectionParam = { export type PortConnectionParam = {
port: chrome.runtime.Port; port: chrome.runtime.Port & {
sender: NonNullable<chrome.runtime.Port["sender"]>;
};
}; };
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
export type InlineMenuButtonPortMessageHandlers = { export type InlineMenuButtonPortMessageHandlers = {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler"; import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
@@ -17,9 +15,11 @@ export default class ContextMenusBackground {
return; return;
} }
this.contextMenus.onClicked.addListener((info, tab) => this.contextMenus.onClicked.addListener((info, tab) => {
this.contextMenuClickedHandler.run(info, tab), if (tab) {
); return this.contextMenuClickedHandler.run(info, tab);
}
});
BrowserApi.messageListener( BrowserApi.messageListener(
"contextmenus.background", "contextmenus.background",
@@ -28,18 +28,16 @@ export default class ContextMenusBackground {
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
) => { ) => {
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") { if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. const onClickData = msg.data.commandToRetry.message.contextMenuOnClickData;
// eslint-disable-next-line @typescript-eslint/no-floating-promises const senderTab = msg.data.commandToRetry.sender.tab;
this.contextMenuClickedHandler
.cipherAction( if (onClickData && senderTab) {
msg.data.commandToRetry.message.contextMenuOnClickData, void this.contextMenuClickedHandler.cipherAction(onClickData, senderTab).then(() => {
msg.data.commandToRetry.sender.tab, if (sender.tab) {
) void BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
.then(() => { }
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
}); });
}
} }
}, },
); );

View File

@@ -39,9 +39,7 @@ describe("TabsBackground", () => {
"handleWindowOnFocusChanged", "handleWindowOnFocusChanged",
); );
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void tabsBackground.init();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tabsBackground.init();
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith( expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
handleWindowOnFocusChangedSpy, handleWindowOnFocusChangedSpy,

View File

@@ -191,9 +191,11 @@ export class ContextMenuClickedHandler {
}); });
} else { } else {
this.copyToClipboard({ text: cipher.login.password, tab: tab }); this.copyToClipboard({ text: cipher.login.password, tab: tab });
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void this.eventCollectionService.collect(
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); EventType.Cipher_ClientCopiedPassword,
cipher.id,
);
} }
break; break;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -179,9 +177,11 @@ export class MainContextMenuHandler {
try { try {
const account = await firstValueFrom(this.accountService.activeAccount$); const account = await firstValueFrom(this.accountService.activeAccount$);
const hasPremium = await firstValueFrom( const hasPremium =
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), !!account?.id &&
); (await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
));
const isCardRestricted = ( const isCardRestricted = (
await firstValueFrom(this.restrictedItemTypesService.restricted$) await firstValueFrom(this.restrictedItemTypesService.restricted$)
@@ -198,14 +198,16 @@ export class MainContextMenuHandler {
if (requiresPremiumAccess && !hasPremium) { if (requiresPremiumAccess && !hasPremium) {
continue; continue;
} }
if (menuItem.id.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) { if (menuItem.id?.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) {
continue; continue;
} }
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] }); await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
} }
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} finally { } finally {
this.initRunning = false; this.initRunning = false;
} }
@@ -318,9 +320,11 @@ export class MainContextMenuHandler {
} }
const account = await firstValueFrom(this.accountService.activeAccount$); const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium =
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), !!account?.id &&
); (await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
));
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
await createChildItem(COPY_VERIFICATION_CODE_ID); await createChildItem(COPY_VERIFICATION_CODE_ID);
} }
@@ -333,7 +337,9 @@ export class MainContextMenuHandler {
await createChildItem(AUTOFILL_IDENTITY_ID); await createChildItem(AUTOFILL_IDENTITY_ID);
} }
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} }
} }
@@ -351,7 +357,11 @@ export class MainContextMenuHandler {
this.loadOptions( this.loadOptions(
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"), this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
NOOP_COMMAND_SUFFIX, NOOP_COMMAND_SUFFIX,
).catch((error) => this.logService.warning(error.message)); ).catch((error) => {
if (error instanceof Error) {
return this.logService.warning(error.message);
}
});
} }
} }
@@ -363,7 +373,9 @@ export class MainContextMenuHandler {
} }
} }
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} }
} }
@@ -373,7 +385,9 @@ export class MainContextMenuHandler {
await MainContextMenuHandler.create(menuItem); await MainContextMenuHandler.create(menuItem);
} }
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} }
} }
@@ -383,7 +397,9 @@ export class MainContextMenuHandler {
await MainContextMenuHandler.create(menuItem); await MainContextMenuHandler.create(menuItem);
} }
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} }
} }
@@ -395,7 +411,9 @@ export class MainContextMenuHandler {
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID); await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
} catch (error) { } catch (error) {
this.logService.warning(error.message); if (error instanceof Error) {
this.logService.warning(error.message);
}
} }
} }
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import AutofillPageDetails from "../models/autofill-page-details"; import AutofillPageDetails from "../models/autofill-page-details";
@@ -123,9 +121,9 @@ import {
* @param fillScript - The autofill script to use * @param fillScript - The autofill script to use
*/ */
function triggerAutoSubmitOnForm(fillScript: AutofillScript) { function triggerAutoSubmitOnForm(fillScript: AutofillScript) {
const formOpid = fillScript.autosubmit[0]; const formOpid = fillScript.autosubmit?.[0];
if (formOpid === null) { if (!formOpid) {
triggerAutoSubmitOnFormlessFields(fillScript); triggerAutoSubmitOnFormlessFields(fillScript);
return; return;
} }
@@ -159,8 +157,11 @@ import {
fillScript.script[fillScript.script.length - 1][1], fillScript.script[fillScript.script.length - 1][1],
); );
const lastFieldIsPasswordInput = const lastFieldIsPasswordInput = !!(
elementIsInputElement(currentElement) && currentElement.type === "password"; currentElement &&
elementIsInputElement(currentElement) &&
currentElement.type === "password"
);
while (currentElement && currentElement.tagName !== "HTML") { while (currentElement && currentElement.tagName !== "HTML") {
if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) { if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) {

View File

@@ -1,3 +1,5 @@
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
export const CipherTypes = { export const CipherTypes = {
Login: 1, Login: 1,
SecureNote: 2, SecureNote: 2,
@@ -22,20 +24,13 @@ export const OrganizationCategories = {
family: "family", family: "family",
} as const; } as const;
export type WebsiteIconData = {
imageEnabled: boolean;
image: string;
fallbackImage: string;
icon: string;
};
type BaseCipherData<CipherTypeValue> = { type BaseCipherData<CipherTypeValue> = {
id: string; id: string;
name: string; name: string;
type: CipherTypeValue; type: CipherTypeValue;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
favorite: boolean; favorite: boolean;
icon: WebsiteIconData; icon: CipherIconDetails;
}; };
export type CipherData = BaseCipherData<CipherType> & { export type CipherData = BaseCipherData<CipherType> & {

View File

@@ -1,43 +1,43 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
const inputTags = ["input", "textarea", "select"]; const inputTags = ["input", "textarea", "select"];
const labelTags = ["label", "span"]; const labelTags = ["label", "span"];
const attributes = ["id", "name", "label-aria", "placeholder"]; const attributeKeys = ["id", "name", "label-aria", "placeholder"];
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement"); const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique"); const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
let clickedEl: HTMLElement = null; let clickedElement: HTMLElement | null = null;
// Find the best attribute to be used as the Name for an element in a custom field. // Find the best attribute to be used as the Name for an element in a custom field.
function getClickedElementIdentifier() { function getClickedElementIdentifier() {
if (clickedEl == null) { if (clickedElement == null) {
return invalidElement; return invalidElement;
} }
const clickedTag = clickedEl.nodeName.toLowerCase(); const clickedTag = clickedElement.nodeName.toLowerCase();
let inputEl = null; let inputElement = null;
// Try to identify the input element (which may not be the clicked element) // Try to identify the input element (which may not be the clicked element)
if (labelTags.includes(clickedTag)) { if (labelTags.includes(clickedTag)) {
let inputId = null; let inputId;
if (clickedTag === "label") { if (clickedTag === "label") {
inputId = clickedEl.getAttribute("for"); inputId = clickedElement.getAttribute("for");
} else { } else {
inputId = clickedEl.closest("label")?.getAttribute("for"); inputId = clickedElement.closest("label")?.getAttribute("for");
} }
inputEl = document.getElementById(inputId); if (inputId) {
inputElement = document.getElementById(inputId);
}
} else { } else {
inputEl = clickedEl; inputElement = clickedElement;
} }
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) { if (inputElement == null || !inputTags.includes(inputElement.nodeName.toLowerCase())) {
return invalidElement; return invalidElement;
} }
for (const attr of attributes) { for (const attributeKey of attributeKeys) {
const attributeValue = inputEl.getAttribute(attr); const attributeValue = inputElement.getAttribute(attributeKey);
const selector = "[" + attr + '="' + attributeValue + '"]'; const selector = "[" + attributeKey + '="' + attributeValue + '"]';
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) { if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
return attributeValue; return attributeValue;
} }
@@ -45,14 +45,14 @@ function getClickedElementIdentifier() {
return noUniqueIdentifier; return noUniqueIdentifier;
} }
function isNullOrEmpty(s: string) { function isNullOrEmpty(s: string | null) {
return s == null || s === ""; return s == null || s === "";
} }
// We only have access to the element that's been clicked when the context menu is first opened. // We only have access to the element that's been clicked when the context menu is first opened.
// Remember it for use later. // Remember it for use later.
document.addEventListener("contextmenu", (event) => { document.addEventListener("contextmenu", (event) => {
clickedEl = event.target as HTMLElement; clickedElement = event.target as HTMLElement;
}); });
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked. // Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
@@ -62,9 +62,8 @@ chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => {
if (sendResponse) { if (sendResponse) {
sendResponse(identifier); sendResponse(identifier);
} }
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void chrome.runtime.sendMessage({
chrome.runtime.sendMessage({
command: "getClickedElementResponse", command: "getClickedElementResponse",
sender: "contextMenuHandler", sender: "contextMenuHandler",
identifier: identifier, identifier: identifier,

View File

@@ -267,9 +267,7 @@ import { Messenger } from "./messaging/messenger";
clearWaitForFocus(); clearWaitForFocus();
void messenger.destroy(); void messenger.destroy();
// FIXME: Remove when updating file. Eslint update } catch {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/** empty */ /** empty */
} }
} }

View File

@@ -31,9 +31,8 @@ describe("Messenger", () => {
it("should deliver message to B when sending request from A", () => { it("should deliver message to B when sending request from A", () => {
const request = createRequest(); const request = createRequest();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.request(request);
messengerA.request(request);
const received = handlerB.receive(); const received = handlerB.receive();
@@ -66,14 +65,13 @@ describe("Messenger", () => {
it("should deliver abort signal to B when requesting abort", () => { it("should deliver abort signal to B when requesting abort", () => {
const abortController = new AbortController(); const abortController = new AbortController();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.request(createRequest(), abortController.signal);
messengerA.request(createRequest(), abortController.signal);
abortController.abort(); abortController.abort();
const received = handlerB.receive(); const received = handlerB.receive();
expect(received[0].abortController.signal.aborted).toBe(true); expect(received[0].abortController?.signal.aborted).toBe(true);
}); });
describe("destroy", () => { describe("destroy", () => {
@@ -103,29 +101,25 @@ describe("Messenger", () => {
it("should dispatch the destroy event on messenger destruction", async () => { it("should dispatch the destroy event on messenger destruction", async () => {
const request = createRequest(); const request = createRequest();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.request(request);
messengerA.request(request);
const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent"); const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent");
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.destroy();
messengerA.destroy();
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event)); expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event));
}); });
it("should trigger onDestroyListener when the destroy event is dispatched", async () => { it("should trigger onDestroyListener when the destroy event is dispatched", async () => {
const request = createRequest(); const request = createRequest();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.request(request);
messengerA.request(request);
const onDestroyListener = jest.fn(); const onDestroyListener = jest.fn();
(messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener); (messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void messengerA.destroy();
messengerA.destroy();
expect(onDestroyListener).toHaveBeenCalled(); expect(onDestroyListener).toHaveBeenCalled();
const eventArg = onDestroyListener.mock.calls[0][0]; const eventArg = onDestroyListener.mock.calls[0][0];
@@ -213,7 +207,7 @@ class MockMessagePort<T> {
remotePort: MockMessagePort<T>; remotePort: MockMessagePort<T>;
postMessage(message: T, port?: MessagePort) { postMessage(message: T, port?: MessagePort) {
this.remotePort.onmessage( this.remotePort.onmessage?.(
new MessageEvent("message", { new MessageEvent("message", {
data: message, data: message,
ports: port ? [port] : [], ports: port ? [port] : [],

View File

@@ -155,9 +155,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
} }
static sendMessage(msg: BrowserFido2Message) { static sendMessage(msg: BrowserFido2Message) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void BrowserApi.sendMessage(BrowserFido2MessageName, msg);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
} }
static abortPopout(sessionId: string, fallbackRequested = false) { static abortPopout(sessionId: string, fallbackRequested = false) {
@@ -206,9 +204,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
fromEvent(abortController.signal, "abort") fromEvent(abortController.signal, "abort")
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(() => { .subscribe(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.close();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.close();
BrowserFido2UserInterfaceSession.sendMessage({ BrowserFido2UserInterfaceSession.sendMessage({
type: BrowserFido2MessageTypes.AbortRequest, type: BrowserFido2MessageTypes.AbortRequest,
sessionId: this.sessionId, sessionId: this.sessionId,
@@ -224,12 +220,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
) )
.subscribe((msg) => { .subscribe((msg) => {
if (msg.type === BrowserFido2MessageTypes.AbortResponse) { if (msg.type === BrowserFido2MessageTypes.AbortResponse) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.close();
// eslint-disable-next-line @typescript-eslint/no-floating-promises void this.abort(msg.fallbackRequested);
this.close();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.abort(msg.fallbackRequested);
} }
}); });
@@ -388,12 +380,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(() => { .subscribe(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.close();
// eslint-disable-next-line @typescript-eslint/no-floating-promises void this.abort(true);
this.close();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.abort(true);
}); });
await connectPromise; await connectPromise;

View File

@@ -1,6 +1,4 @@
import { FieldRect } from "../background/abstractions/overlay.background"; import { FieldRect } from "../background/abstractions/overlay.background";
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
import { import {
InlineMenuAccountCreationFieldTypes, InlineMenuAccountCreationFieldTypes,
@@ -13,34 +11,36 @@ import {
export default class AutofillField { export default class AutofillField {
[key: string]: any; [key: string]: any;
/** /**
* The unique identifier assigned to this field during collection of the page details * Non-null asserted. The unique identifier assigned to this field during collection of the page details
*/ */
opid: string; opid!: string;
/** /**
* Sequential number assigned to each element collected, based on its position in the DOM. * Non-null asserted. Sequential number assigned to each element collected, based on its position in the DOM.
* Used to do perform proximal checks for username and password fields on the DOM. * Used to do perform proximal checks for username and password fields on the DOM.
*/ */
elementNumber: number; elementNumber!: number;
/** /**
* Designates whether the field is viewable on the current part of the DOM that the user can see * Non-null asserted. Designates whether the field is viewable on the current part of the DOM that the user can see
*/ */
viewable: boolean; viewable!: boolean;
/** /**
* The HTML `id` attribute of the field * Non-null asserted. The HTML `id` attribute of the field
*/ */
htmlID: string | null; htmlID!: string | null;
/** /**
* The HTML `name` attribute of the field * Non-null asserted. The HTML `name` attribute of the field
*/ */
htmlName: string | null; htmlName!: string | null;
/** /**
* The HTML `class` attribute of the field * Non-null asserted. The HTML `class` attribute of the field
*/ */
htmlClass: string | null; htmlClass!: string | null;
tabindex: string | null; /** Non-null asserted. */
tabindex!: string | null;
title: string | null; /** Non-null asserted. */
title!: string | null;
/** /**
* The `tagName` for the field * The `tagName` for the field
*/ */

View File

@@ -1,28 +1,31 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
/** /**
* Represents an HTML form whose elements can be autofilled * Represents an HTML form whose elements can be autofilled
*/ */
export default class AutofillForm { export default class AutofillForm {
[key: string]: any; [key: string]: any;
/** /**
* The unique identifier assigned to this field during collection of the page details * Non-null asserted. The unique identifier assigned to this field during collection of the page details
*/ */
opid: string; opid!: string;
/** /**
* The HTML `name` attribute of the form field * Non-null asserted. The HTML `name` attribute of the form field
*/ */
htmlName: string; htmlName!: string;
/** /**
* The HTML `id` attribute of the form field * Non-null asserted. The HTML `id` attribute of the form field
*/ */
htmlID: string; htmlID!: string;
/** /**
* The HTML `action` attribute of the form field * Non-null asserted. The HTML `action` attribute of the form field
*/ */
htmlAction: string; htmlAction!: string;
/** /**
* The HTML `method` attribute of the form field * Non-null asserted. The HTML `method` attribute of the form field.
*/ */
htmlMethod: string; htmlMethod!: "get" | "post" | string;
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import AutofillField from "./autofill-field"; import AutofillField from "./autofill-field";
import AutofillForm from "./autofill-form"; import AutofillForm from "./autofill-form";
@@ -7,16 +5,20 @@ import AutofillForm from "./autofill-form";
* The details of a page that have been collected and can be used for autofill * The details of a page that have been collected and can be used for autofill
*/ */
export default class AutofillPageDetails { export default class AutofillPageDetails {
title: string; /** Non-null asserted. */
url: string; title!: string;
documentUrl: string; /** Non-null asserted. */
url!: string;
/** Non-null asserted. */
documentUrl!: string;
/** /**
* A collection of all of the forms in the page DOM, keyed by their `opid` * Non-null asserted. A collection of all of the forms in the page DOM, keyed by their `opid`
*/ */
forms: { [id: string]: AutofillForm }; forms!: { [id: string]: AutofillForm };
/** /**
* A collection of all the fields in the page DOM, keyed by their `opid` * Non-null asserted. A collection of all the fields in the page DOM, keyed by their `opid`
*/ */
fields: AutofillField[]; fields!: AutofillField[];
collectedTimestamp: number; /** Non-null asserted. */
collectedTimestamp!: number;
} }

View File

@@ -1,26 +1,33 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// String values affect code flow in autofill.ts and must not be changed
export type FillScriptActions = "click_on_opid" | "focus_by_opid" | "fill_by_opid";
export type FillScript = [action: FillScriptActions, opid: string, value?: string]; export type FillScript = [action: FillScriptActions, opid: string, value?: string];
export type AutofillScriptProperties = { export type AutofillScriptProperties = {
delay_between_operations?: number; delay_between_operations?: number;
}; };
export const FillScriptActionTypes = {
fill_by_opid: "fill_by_opid",
click_on_opid: "click_on_opid",
focus_by_opid: "focus_by_opid",
} as const;
// String values affect code flow in autofill.ts and must not be changed
export type FillScriptActions = keyof typeof FillScriptActionTypes;
export type AutofillInsertActions = { export type AutofillInsertActions = {
fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void; [FillScriptActionTypes.fill_by_opid]: ({ opid, value }: { opid: string; value: string }) => void;
click_on_opid: ({ opid }: { opid: string }) => void; [FillScriptActionTypes.click_on_opid]: ({ opid }: { opid: string }) => void;
focus_by_opid: ({ opid }: { opid: string }) => void; [FillScriptActionTypes.focus_by_opid]: ({ opid }: { opid: string }) => void;
}; };
export default class AutofillScript { export default class AutofillScript {
script: FillScript[] = []; script: FillScript[] = [];
properties: AutofillScriptProperties = {}; properties: AutofillScriptProperties = {};
metadata: any = {}; // Unused, not written or read /** Non-null asserted. */
autosubmit: string[]; // Appears to be unused, read but not written autosubmit!: string[] | null; // Appears to be unused, read but not written
savedUrls: string[]; /** Non-null asserted. */
untrustedIframe: boolean; savedUrls!: string[];
itemType: string; // Appears to be unused, read but not written /** Non-null asserted. */
untrustedIframe!: boolean;
/** Non-null asserted. */
itemType!: string; // Appears to be unused, read but not written
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import "@webcomponents/custom-elements"; import "@webcomponents/custom-elements";
import "lit/polyfill-support.js"; import "lit/polyfill-support.js";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@@ -103,7 +101,10 @@ export class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
*/ */
private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) { private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) {
const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']"); const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']");
colorSchemeMetaTag?.setAttribute("content", colorScheme);
if (colorSchemeMetaTag && colorScheme) {
colorSchemeMetaTag.setAttribute("content", colorScheme);
}
} }
/** /**

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import { setElementStyles } from "../../../../utils"; import { setElementStyles } from "../../../../utils";
@@ -14,8 +12,10 @@ export class AutofillInlineMenuContainer {
private readonly setElementStyles = setElementStyles; private readonly setElementStyles = setElementStyles;
private readonly extensionOriginsSet: Set<string>; private readonly extensionOriginsSet: Set<string>;
private port: chrome.runtime.Port | null = null; private port: chrome.runtime.Port | null = null;
private portName: string; /** Non-null asserted. */
private inlineMenuPageIframe: HTMLIFrameElement; private portName!: string;
/** Non-null asserted. */
private inlineMenuPageIframe!: HTMLIFrameElement;
private readonly iframeStyles: Partial<CSSStyleDeclaration> = { private readonly iframeStyles: Partial<CSSStyleDeclaration> = {
all: "initial", all: "initial",
position: "fixed", position: "fixed",
@@ -42,8 +42,10 @@ export class AutofillInlineMenuContainer {
tabIndex: "-1", tabIndex: "-1",
}; };
private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = { private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = {
initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message), initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) =>
initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message), this.handleInitInlineMenuIframe(message),
initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) =>
this.handleInitInlineMenuIframe(message),
}; };
constructor() { constructor() {
@@ -116,14 +118,20 @@ export class AutofillInlineMenuContainer {
* *
* @param event - The message event. * @param event - The message event.
*/ */
private handleWindowMessage = (event: MessageEvent) => { private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
const message = event.data; const message = event.data;
if (this.isForeignWindowMessage(event)) { if (this.isForeignWindowMessage(event)) {
return; return;
} }
if (this.windowMessageHandlers[message.command]) { if (
this.windowMessageHandlers[message.command](message); this.windowMessageHandlers[
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
]
) {
this.windowMessageHandlers[
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
](message);
return; return;
} }
@@ -142,8 +150,8 @@ export class AutofillInlineMenuContainer {
* *
* @param event - The message event. * @param event - The message event.
*/ */
private isForeignWindowMessage(event: MessageEvent) { private isForeignWindowMessage(event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) {
if (!event.data.portKey) { if (!event.data?.portKey) {
return true; return true;
} }
@@ -159,7 +167,9 @@ export class AutofillInlineMenuContainer {
* *
* @param event - The message event. * @param event - The message event.
*/ */
private isMessageFromParentWindow(event: MessageEvent): boolean { private isMessageFromParentWindow(
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
): boolean {
return globalThis.parent === event.source; return globalThis.parent === event.source;
} }
@@ -168,7 +178,9 @@ export class AutofillInlineMenuContainer {
* *
* @param event - The message event. * @param event - The message event.
*/ */
private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean { private isMessageFromInlineMenuPageIframe(
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
): boolean {
if (!this.inlineMenuPageIframe) { if (!this.inlineMenuPageIframe) {
return false; return false;
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum"; import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
@@ -10,10 +8,14 @@ import {
export class AutofillInlineMenuPageElement extends HTMLElement { export class AutofillInlineMenuPageElement extends HTMLElement {
protected shadowDom: ShadowRoot; protected shadowDom: ShadowRoot;
protected messageOrigin: string; /** Non-null asserted. */
protected translations: Record<string, string>; protected messageOrigin!: string;
private portKey: string; /** Non-null asserted. */
protected windowMessageHandlers: AutofillInlineMenuPageElementWindowMessageHandlers; protected translations!: Record<string, string>;
/** Non-null asserted. */
private portKey!: string;
/** Non-null asserted. */
protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers;
constructor() { constructor() {
super(); super();

View File

@@ -20,7 +20,7 @@ describe("OverlayNotificationsContentService", () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn()); jest.spyOn(utils, "sendExtensionMessage").mockImplementation(async () => null);
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window); jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn()); postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
domQueryService = mock<DomQueryService>(); domQueryService = mock<DomQueryService>();

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
@@ -69,7 +67,7 @@ export class Fido2UseBrowserLinkComponent {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
null, "",
this.i18nService.t("domainAddedToExcludedDomains", validDomain), this.i18nService.t("domainAddedToExcludedDomains", validDomain),
); );
} }

View File

@@ -155,13 +155,15 @@ export class AutofillComponent implements OnInit {
autofillOnPageLoadOptions: { name: string; value: boolean }[]; autofillOnPageLoadOptions: { name: string; value: boolean }[];
enableContextMenuItem: boolean = false; enableContextMenuItem: boolean = false;
enableAutoTotpCopy: boolean = false; enableAutoTotpCopy: boolean = false;
clearClipboard: ClearClipboardDelaySetting; /** Non-null asserted. */
clearClipboard!: ClearClipboardDelaySetting;
clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[]; clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[];
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[]; uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[];
showCardsCurrentTab: boolean = true; showCardsCurrentTab: boolean = true;
showIdentitiesCurrentTab: boolean = true; showIdentitiesCurrentTab: boolean = true;
autofillKeyboardHelperText: string; /** Non-null asserted. */
autofillKeyboardHelperText!: string;
accountSwitcherEnabled: boolean = false; accountSwitcherEnabled: boolean = false;
constructor( constructor(

View File

@@ -26,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
destroyAutofillInlineMenuListeners: () => void; destroyAutofillInlineMenuListeners: () => void;
getInlineMenuFormFieldData: ({ getInlineMenuFormFieldData: ({
message, message,
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>; }: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData | void>;
}; };
export interface AutofillOverlayContentService { export interface AutofillOverlayContentService {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service"; import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
@@ -64,29 +62,39 @@ export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND =
); );
export abstract class AutofillService { export abstract class AutofillService {
collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>; /** Non-null asserted. */
loadAutofillScriptsOnInstall: () => Promise<void>; collectPageDetailsFromTab$!: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
reloadAutofillScripts: () => Promise<void>; /** Non-null asserted. */
injectAutofillScripts: ( loadAutofillScriptsOnInstall!: () => Promise<void>;
/** Non-null asserted. */
reloadAutofillScripts!: () => Promise<void>;
/** Non-null asserted. */
injectAutofillScripts!: (
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
frameId?: number, frameId?: number,
triggeringOnPageLoad?: boolean, triggeringOnPageLoad?: boolean,
) => Promise<void>; ) => Promise<void>;
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[]; /** Non-null asserted. */
doAutoFill: (options: AutoFillOptions) => Promise<string | null>; getFormsWithPasswordFields!: (pageDetails: AutofillPageDetails) => FormData[];
doAutoFillOnTab: ( /** Non-null asserted. */
doAutoFill!: (options: AutoFillOptions) => Promise<string | null>;
/** Non-null asserted. */
doAutoFillOnTab!: (
pageDetails: PageDetail[], pageDetails: PageDetail[],
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
fromCommand: boolean, fromCommand: boolean,
autoSubmitLogin?: boolean, autoSubmitLogin?: boolean,
) => Promise<string | null>; ) => Promise<string | null>;
doAutoFillActiveTab: ( /** Non-null asserted. */
doAutoFillActiveTab!: (
pageDetails: PageDetail[], pageDetails: PageDetail[],
fromCommand: boolean, fromCommand: boolean,
cipherType?: CipherType, cipherType?: CipherType,
) => Promise<string | null>; ) => Promise<string | null>;
setAutoFillOnPageLoadOrgPolicy: () => Promise<void>; /** Non-null asserted. */
isPasswordRepromptRequired: ( setAutoFillOnPageLoadOrgPolicy!: () => Promise<void>;
/** Non-null asserted. */
isPasswordRepromptRequired!: (
cipher: CipherView, cipher: CipherView,
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
action?: string, action?: string,

View File

@@ -369,9 +369,7 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs"); jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void autofillService.reloadAutofillScripts();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
autofillService.reloadAutofillScripts();
expect(port1.disconnect).toHaveBeenCalled(); expect(port1.disconnect).toHaveBeenCalled();
expect(port2.disconnect).toHaveBeenCalled(); expect(port2.disconnect).toHaveBeenCalled();
@@ -680,7 +678,9 @@ describe("AutofillService", () => {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(error.message).toBe(nothingToAutofillError); if (error instanceof Error) {
expect(error.message).toBe(nothingToAutofillError);
}
} }
}); });
@@ -691,7 +691,9 @@ describe("AutofillService", () => {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(error.message).toBe(nothingToAutofillError); if (error instanceof Error) {
expect(error.message).toBe(nothingToAutofillError);
}
} }
}); });
@@ -702,7 +704,9 @@ describe("AutofillService", () => {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(error.message).toBe(nothingToAutofillError); if (error instanceof Error) {
expect(error.message).toBe(nothingToAutofillError);
}
} }
}); });
@@ -713,7 +717,9 @@ describe("AutofillService", () => {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(error.message).toBe(nothingToAutofillError); if (error instanceof Error) {
expect(error.message).toBe(nothingToAutofillError);
}
} }
}); });
@@ -727,7 +733,9 @@ describe("AutofillService", () => {
await autofillService.doAutoFill(autofillOptions); await autofillService.doAutoFill(autofillOptions);
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(error.message).toBe(didNotAutofillError); if (error instanceof Error) {
expect(error.message).toBe(didNotAutofillError);
}
} }
}); });
}); });
@@ -766,7 +774,6 @@ describe("AutofillService", () => {
{ {
command: "fillForm", command: "fillForm",
fillScript: { fillScript: {
metadata: {},
properties: { properties: {
delay_between_operations: 20, delay_between_operations: 20,
}, },
@@ -863,7 +870,9 @@ describe("AutofillService", () => {
expect(logService.info).toHaveBeenCalledWith( expect(logService.info).toHaveBeenCalledWith(
"Autofill on page load was blocked due to an untrusted iframe.", "Autofill on page load was blocked due to an untrusted iframe.",
); );
expect(error.message).toBe(didNotAutofillError); if (error instanceof Error) {
expect(error.message).toBe(didNotAutofillError);
}
} }
}); });
@@ -898,7 +907,10 @@ describe("AutofillService", () => {
} catch (error) { } catch (error) {
expect(autofillService["generateFillScript"]).toHaveBeenCalled(); expect(autofillService["generateFillScript"]).toHaveBeenCalled();
expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled(); expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled();
expect(error.message).toBe(didNotAutofillError);
if (error instanceof Error) {
expect(error.message).toBe(didNotAutofillError);
}
} }
}); });
@@ -1370,7 +1382,10 @@ describe("AutofillService", () => {
triggerTestFailure(); triggerTestFailure();
} catch (error) { } catch (error) {
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled(); expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
expect(error.message).toBe("No tab found.");
if (error instanceof Error) {
expect(error.message).toBe("No tab found.");
}
} }
}); });
@@ -1610,7 +1625,6 @@ describe("AutofillService", () => {
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith(
{ {
metadata: {},
properties: {}, properties: {},
script: [ script: [
["click_on_opid", "username-field"], ["click_on_opid", "username-field"],
@@ -1648,7 +1662,6 @@ describe("AutofillService", () => {
expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith(
{ {
metadata: {},
properties: {}, properties: {},
script: [ script: [
["click_on_opid", "username-field"], ["click_on_opid", "username-field"],
@@ -1686,7 +1699,6 @@ describe("AutofillService", () => {
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith(
{ {
metadata: {},
properties: {}, properties: {},
script: [ script: [
["click_on_opid", "username-field"], ["click_on_opid", "username-field"],
@@ -2279,7 +2291,7 @@ describe("AutofillService", () => {
); );
expect(value).toStrictEqual({ expect(value).toStrictEqual({
autosubmit: null, autosubmit: null,
metadata: {}, itemType: "",
properties: { delay_between_operations: 20 }, properties: { delay_between_operations: 20 },
savedUrls: ["https://www.example.com"], savedUrls: ["https://www.example.com"],
script: [ script: [
@@ -2294,7 +2306,6 @@ describe("AutofillService", () => {
["fill_by_opid", "password", "password"], ["fill_by_opid", "password", "password"],
["focus_by_opid", "password"], ["focus_by_opid", "password"],
], ],
itemType: "",
untrustedIframe: false, untrustedIframe: false,
}); });
}); });
@@ -2364,11 +2375,10 @@ describe("AutofillService", () => {
describe("given an invalid autofill field", () => { describe("given an invalid autofill field", () => {
const unmodifiedFillScriptValues: AutofillScript = { const unmodifiedFillScriptValues: AutofillScript = {
autosubmit: null, autosubmit: null,
metadata: {}, itemType: "",
properties: { delay_between_operations: 20 }, properties: { delay_between_operations: 20 },
savedUrls: [], savedUrls: [],
script: [], script: [],
itemType: "",
untrustedIframe: false, untrustedIframe: false,
}; };
@@ -2555,7 +2565,6 @@ describe("AutofillService", () => {
expect(value).toStrictEqual({ expect(value).toStrictEqual({
autosubmit: null, autosubmit: null,
itemType: "", itemType: "",
metadata: {},
properties: { properties: {
delay_between_operations: 20, delay_between_operations: 20,
}, },

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
import { FillableFormFieldElement, FormFieldElement } from "../types"; import { FillableFormFieldElement, FormFieldElement } from "../types";
@@ -202,7 +200,7 @@ class DomElementVisibilityService implements DomElementVisibilityServiceInterfac
const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label"); const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label");
return targetElementLabelsSet.has(closestParentLabel); return closestParentLabel ? targetElementLabelsSet.has(closestParentLabel) : false;
} }
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS, MAX_DEEP_QUERY_RECURSION_DEPTH } from "@bitwarden/common/autofill/constants"; import { EVENTS, MAX_DEEP_QUERY_RECURSION_DEPTH } from "@bitwarden/common/autofill/constants";
import { nodeIsElement } from "../utils"; import { nodeIsElement } from "../utils";
@@ -7,7 +5,8 @@ import { nodeIsElement } from "../utils";
import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service"; import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service";
export class DomQueryService implements DomQueryServiceInterface { export class DomQueryService implements DomQueryServiceInterface {
private pageContainsShadowDom: boolean; /** Non-null asserted. */
private pageContainsShadowDom!: boolean;
private ignoredTreeWalkerNodes = new Set([ private ignoredTreeWalkerNodes = new Set([
"svg", "svg",
"script", "script",
@@ -217,13 +216,12 @@ export class DomQueryService implements DomQueryServiceInterface {
if ((chrome as any).dom?.openOrClosedShadowRoot) { if ((chrome as any).dom?.openOrClosedShadowRoot) {
try { try {
return (chrome as any).dom.openOrClosedShadowRoot(node); return (chrome as any).dom.openOrClosedShadowRoot(node);
// FIXME: Remove when updating file. Eslint update } catch {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return null; return null;
} }
} }
// Firefox-specific equivalent of `openOrClosedShadowRoot`
return (node as any).openOrClosedShadowRoot; return (node as any).openOrClosedShadowRoot;
} }
@@ -276,7 +274,7 @@ export class DomQueryService implements DomQueryServiceInterface {
? NodeFilter.FILTER_REJECT ? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT, : NodeFilter.FILTER_ACCEPT,
); );
let currentNode = treeWalker?.currentNode; let currentNode: Node | null = treeWalker?.currentNode;
while (currentNode) { while (currentNode) {
if (filterCallback(currentNode)) { if (filterCallback(currentNode)) {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils"; import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
@@ -162,12 +160,14 @@ export class InlineMenuFieldQualificationService
private isExplicitIdentityEmailField(field: AutofillField): boolean { private isExplicitIdentityEmailField(field: AutofillField): boolean {
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
if (!matchFieldAttributeValues[attrIndex]) { const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
if (!attributeValueToMatch) {
continue; continue;
} }
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) { for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) { if (this.newEmailFieldKeywords.has(attributeValueToMatch)) {
return true; return true;
} }
} }
@@ -210,10 +210,7 @@ export class InlineMenuFieldQualificationService
} }
constructor() { constructor() {
void Promise.all([ void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => {
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
sendExtensionMessage("getUserPremiumStatus"),
]).then(([fieldQualificationFlag, premiumStatus]) => {
this.premiumEnabled = !!premiumStatus?.result; this.premiumEnabled = !!premiumStatus?.result;
}); });
} }
@@ -263,7 +260,13 @@ export class InlineMenuFieldQualificationService
return true; return true;
} }
const parentForm = pageDetails.forms[field.form]; let parentForm;
const fieldForm = field.form;
if (fieldForm) {
parentForm = pageDetails.forms[fieldForm];
}
// If the field does not have a parent form // If the field does not have a parent form
if (!parentForm) { if (!parentForm) {
@@ -321,7 +324,13 @@ export class InlineMenuFieldQualificationService
return false; return false;
} }
const parentForm = pageDetails.forms[field.form]; let parentForm;
const fieldForm = field.form;
if (fieldForm) {
parentForm = pageDetails.forms[fieldForm];
}
if (!parentForm) { if (!parentForm) {
// If the field does not have a parent form, but we can identify that the page contains at least // If the field does not have a parent form, but we can identify that the page contains at least
@@ -374,7 +383,13 @@ export class InlineMenuFieldQualificationService
field: AutofillField, field: AutofillField,
pageDetails: AutofillPageDetails, pageDetails: AutofillPageDetails,
): boolean { ): boolean {
const parentForm = pageDetails.forms[field.form]; let parentForm;
const fieldForm = field.form;
if (fieldForm) {
parentForm = pageDetails.forms[fieldForm];
}
// If the provided field is set with an autocomplete value of "current-password", we should assume that // If the provided field is set with an autocomplete value of "current-password", we should assume that
// the page developer intends for this field to be interpreted as a password field for a login form. // the page developer intends for this field to be interpreted as a password field for a login form.
@@ -476,7 +491,13 @@ export class InlineMenuFieldQualificationService
// If the field is not explicitly set as a username field, we need to qualify // If the field is not explicitly set as a username field, we need to qualify
// the field based on the other fields that are present on the page. // the field based on the other fields that are present on the page.
const parentForm = pageDetails.forms[field.form]; let parentForm;
const fieldForm = field.form;
if (fieldForm) {
parentForm = pageDetails.forms[fieldForm];
}
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField); const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
if (this.isNewsletterForm(parentForm)) { if (this.isNewsletterForm(parentForm)) {
@@ -919,8 +940,10 @@ export class InlineMenuFieldQualificationService
* @param field - The field to validate * @param field - The field to validate
*/ */
isUsernameField = (field: AutofillField): boolean => { isUsernameField = (field: AutofillField): boolean => {
const fieldType = field.type;
if ( if (
!this.usernameFieldTypes.has(field.type) || !fieldType ||
!this.usernameFieldTypes.has(fieldType) ||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) || this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
this.fieldHasDisqualifyingAttributeValue(field) this.fieldHasDisqualifyingAttributeValue(field)
) { ) {
@@ -1026,7 +1049,13 @@ export class InlineMenuFieldQualificationService
const testedValues = [field.htmlID, field.htmlName, field.placeholder]; const testedValues = [field.htmlID, field.htmlName, field.placeholder];
for (let i = 0; i < testedValues.length; i++) { for (let i = 0; i < testedValues.length; i++) {
if (this.valueIsLikePassword(testedValues[i])) { const attributeValueToMatch = testedValues[i];
if (!attributeValueToMatch) {
continue;
}
if (this.valueIsLikePassword(attributeValueToMatch)) {
return true; return true;
} }
} }
@@ -1101,7 +1130,9 @@ export class InlineMenuFieldQualificationService
* @param excludedTypes - The set of excluded types * @param excludedTypes - The set of excluded types
*/ */
private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean { private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean {
if (excludedTypes.has(field.type)) { const fieldType = field.type;
if (fieldType && excludedTypes.has(fieldType)) {
return true; return true;
} }
@@ -1116,12 +1147,14 @@ export class InlineMenuFieldQualificationService
private isSearchField(field: AutofillField): boolean { private isSearchField(field: AutofillField): boolean {
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
if (!matchFieldAttributeValues[attrIndex]) { const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
if (!attributeValueToMatch) {
continue; continue;
} }
// Separate camel case words and case them to lower case values // Separate camel case words and case them to lower case values
const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex] const camelCaseSeparatedFieldAttribute = attributeValueToMatch
.replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/([a-z])([A-Z])/g, "$1 $2")
.toLowerCase(); .toLowerCase();
// Split the attribute by non-alphabetical characters to get the keywords // Split the attribute by non-alphabetical characters to get the keywords
@@ -1168,7 +1201,7 @@ export class InlineMenuFieldQualificationService
this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(",")); this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(","));
} }
return this.submitButtonKeywordsMap.get(element); return this.submitButtonKeywordsMap.get(element) || "";
} }
/** /**
@@ -1222,8 +1255,9 @@ export class InlineMenuFieldQualificationService
]; ];
const keywordsSet = new Set<string>(); const keywordsSet = new Set<string>();
for (let i = 0; i < keywords.length; i++) { for (let i = 0; i < keywords.length; i++) {
if (keywords[i] && typeof keywords[i] === "string") { const attributeValue = keywords[i];
let keywordEl = keywords[i].toLowerCase(); if (attributeValue && typeof attributeValue === "string") {
let keywordEl = attributeValue.toLowerCase();
keywordsSet.add(keywordEl); keywordsSet.add(keywordEl);
// Remove hyphens from all potential keywords, we want to treat these as a single word. // Remove hyphens from all potential keywords, we want to treat these as a single word.
@@ -1253,7 +1287,7 @@ export class InlineMenuFieldQualificationService
} }
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData); const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet; return mapValues ? (returnStringValue ? mapValues.stringValue : mapValues.keywordsSet) : "";
} }
/** /**

View File

@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; import AutofillScript, { FillScript, FillScriptActionTypes } from "../models/autofill-script";
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
@@ -94,14 +94,13 @@ describe("InsertAutofillContentService", () => {
); );
fillScript = { fillScript = {
script: [ script: [
["click_on_opid", "username"], [FillScriptActionTypes.click_on_opid, "username"],
["focus_by_opid", "username"], [FillScriptActionTypes.focus_by_opid, "username"],
["fill_by_opid", "username", "test"], [FillScriptActionTypes.fill_by_opid, "username", "test"],
], ],
properties: { properties: {
delay_between_operations: 20, delay_between_operations: 20,
}, },
metadata: {},
autosubmit: [], autosubmit: [],
savedUrls: ["https://bitwarden.com"], savedUrls: ["https://bitwarden.com"],
untrustedIframe: false, untrustedIframe: false,
@@ -221,17 +220,14 @@ describe("InsertAutofillContentService", () => {
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
1, 1,
fillScript.script[0], fillScript.script[0],
0,
); );
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
2, 2,
fillScript.script[1], fillScript.script[1],
1,
); );
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
3, 3,
fillScript.script[2], fillScript.script[2],
2,
); );
}); });
}); });
@@ -376,42 +372,62 @@ describe("InsertAutofillContentService", () => {
}); });
it("returns early if no opid is provided", async () => { it("returns early if no opid is provided", async () => {
const action = "fill_by_opid"; const action = FillScriptActionTypes.fill_by_opid;
const opid = ""; const opid = "";
const value = "value"; const value = "value";
const scriptAction: FillScript = [action, opid, value]; const scriptAction: FillScript = [action, opid, value];
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action); jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
await insertAutofillContentService["runFillScriptAction"](scriptAction, 0); await insertAutofillContentService["runFillScriptAction"](scriptAction);
jest.advanceTimersByTime(20); jest.advanceTimersByTime(20);
expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled(); expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled();
}); });
describe("given a valid fill script action and opid", () => { describe("given a valid fill script action and opid", () => {
const fillScriptActions: FillScriptActions[] = [ it(`triggers a fill_by_opid action`, () => {
"fill_by_opid", const action = FillScriptActionTypes.fill_by_opid;
"click_on_opid", const opid = "opid";
"focus_by_opid", const value = "value";
]; const scriptAction: FillScript = [action, opid, value];
fillScriptActions.forEach((action) => { jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
it(`triggers a ${action} action`, () => {
const opid = "opid";
const value = "value";
const scriptAction: FillScript = [action, opid, value];
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void insertAutofillContentService["runFillScriptAction"](scriptAction);
// eslint-disable-next-line @typescript-eslint/no-floating-promises jest.advanceTimersByTime(20);
insertAutofillContentService["runFillScriptAction"](scriptAction, 0);
jest.advanceTimersByTime(20);
expect( expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
insertAutofillContentService["autofillInsertActions"][action], opid,
).toHaveBeenCalledWith({ value,
opid, });
value, });
});
it(`triggers a click_on_opid action`, () => {
const action = FillScriptActionTypes.click_on_opid;
const opid = "opid";
const value = "value";
const scriptAction: FillScript = [action, opid, value];
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
void insertAutofillContentService["runFillScriptAction"](scriptAction);
jest.advanceTimersByTime(20);
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
opid,
});
});
it(`triggers a focus_by_opid action`, () => {
const action = FillScriptActionTypes.focus_by_opid;
const opid = "opid";
const value = "value";
const scriptAction: FillScript = [action, opid, value];
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
void insertAutofillContentService["runFillScriptAction"](scriptAction);
jest.advanceTimersByTime(20);
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
opid,
}); });
}); });
}); });

View File

@@ -1,8 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants"; import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script"; import AutofillScript, {
AutofillInsertActions,
FillScript,
FillScriptActionTypes,
} from "../models/autofill-script";
import { FormFieldElement } from "../types"; import { FormFieldElement } from "../types";
import { import {
currentlyInSandboxedIframe, currentlyInSandboxedIframe,
@@ -50,7 +52,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
} }
for (let index = 0; index < fillScript.script.length; index++) { for (let index = 0; index < fillScript.script.length; index++) {
await this.runFillScriptAction(fillScript.script[index], index); await this.runFillScriptAction(fillScript.script[index]);
} }
} }
@@ -116,25 +118,26 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
/** /**
* Runs the autofill action based on the action type and the opid. * Runs the autofill action based on the action type and the opid.
* Each action is subsequently delayed by 20 milliseconds. * Each action is subsequently delayed by 20 milliseconds.
* @param {"click_on_opid" | "focus_by_opid" | "fill_by_opid"} action * @param {FillScript} [action, opid, value]
* @param {string} opid
* @param {string} value
* @param {number} actionIndex
* @returns {Promise<void>} * @returns {Promise<void>}
* @private * @private
*/ */
private runFillScriptAction = ( private runFillScriptAction = ([action, opid, value]: FillScript): Promise<void> => {
[action, opid, value]: FillScript,
actionIndex: number,
): Promise<void> => {
if (!opid || !this.autofillInsertActions[action]) { if (!opid || !this.autofillInsertActions[action]) {
return; return Promise.resolve();
} }
const delayActionsInMilliseconds = 20; const delayActionsInMilliseconds = 20;
return new Promise((resolve) => return new Promise((resolve) =>
setTimeout(() => { setTimeout(() => {
this.autofillInsertActions[action]({ opid, value }); if (action === FillScriptActionTypes.fill_by_opid && !!value?.length) {
this.autofillInsertActions.fill_by_opid({ opid, value });
} else if (action === FillScriptActionTypes.click_on_opid) {
this.autofillInsertActions.click_on_opid({ opid });
} else if (action === FillScriptActionTypes.focus_by_opid) {
this.autofillInsertActions.focus_by_opid({ opid });
}
resolve(); resolve();
}, delayActionsInMilliseconds), }, delayActionsInMilliseconds),
); );
@@ -158,7 +161,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
*/ */
private handleClickOnFieldByOpidAction(opid: string) { private handleClickOnFieldByOpidAction(opid: string) {
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
this.triggerClickOnElement(element);
if (element) {
this.triggerClickOnElement(element);
}
} }
/** /**
@@ -171,6 +177,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
private handleFocusOnFieldByOpidAction(opid: string) { private handleFocusOnFieldByOpidAction(opid: string) {
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
if (!element) {
return;
}
if (document.activeElement === element) { if (document.activeElement === element) {
element.blur(); element.blur();
} }
@@ -187,6 +197,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
* @private * @private
*/ */
private insertValueIntoField(element: FormFieldElement | null, value: string) { private insertValueIntoField(element: FormFieldElement | null, value: string) {
if (!element || !value) {
return;
}
const elementCanBeReadonly = const elementCanBeReadonly =
elementIsInputElement(element) || elementIsTextAreaElement(element); elementIsInputElement(element) || elementIsTextAreaElement(element);
const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element); const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element);
@@ -195,8 +209,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value); const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value);
if ( if (
!element ||
!value ||
elementAlreadyHasTheValue || elementAlreadyHasTheValue ||
(elementCanBeReadonly && element.readOnly) || (elementCanBeReadonly && element.readOnly) ||
(elementCanBeFilled && element.disabled) (elementCanBeFilled && element.disabled)
@@ -298,7 +310,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
* @private * @private
*/ */
private triggerClickOnElement(element?: HTMLElement): void { private triggerClickOnElement(element?: HTMLElement): void {
if (typeof element?.click !== TYPE_CHECK.FUNCTION) { if (!element || typeof element.click !== TYPE_CHECK.FUNCTION) {
return; return;
} }
@@ -313,7 +325,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
* @private * @private
*/ */
private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void { private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void {
if (typeof element?.focus !== TYPE_CHECK.FUNCTION) { if (!element || typeof element.focus !== TYPE_CHECK.FUNCTION) {
return; return;
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@@ -144,7 +142,6 @@ export function createAutofillScriptMock(
return { return {
autosubmit: null, autosubmit: null,
metadata: {},
properties: { properties: {
delay_between_operations: 20, delay_between_operations: 20,
}, },
@@ -299,7 +296,7 @@ export function createMutationRecordMock(customFields = {}): MutationRecord {
oldValue: "default-oldValue", oldValue: "default-oldValue",
previousSibling: null, previousSibling: null,
removedNodes: mock<NodeList>(), removedNodes: mock<NodeList>(),
target: null, target: mock<Node>(),
type: "attributes", type: "attributes",
...customFields, ...customFields,
}; };