diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 912d9657124..e50a317e8a7 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -147,7 +147,7 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetEnableChangedPasswordPrompt: () => Promise; bgGetEnableAddedLoginPrompt: () => Promise; bgGetExcludedDomains: () => Promise; - bgGetActiveUserServerConfig: () => Promise; + bgGetActiveUserServerConfig: () => Promise; getWebVaultUrlForNotification: () => Promise; }; diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 6067d563db2..18cf1d20446 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -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 { 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 { InlineMenuFillType } from "../../enums/autofill-overlay.enum"; +import AutofillField from "../../models/autofill-field"; import AutofillPageDetails from "../../models/autofill-page-details"; import { PageDetail } from "../../services/abstractions/autofill.service"; import { LockedVaultPendingNotificationsData } from "./notification.background"; -export type PageDetailsForTab = Record< - chrome.runtime.MessageSender["tab"]["id"], - Map ->; +export type TabId = NonNullable; + +export type FrameId = NonNullable; + +type PageDetailsByFrame = Map; + +export type PageDetailsForTab = Record; export type SubFrameOffsetData = { top: number; @@ -21,19 +24,14 @@ export type SubFrameOffsetData = { url?: string; frameId?: number; parentFrameIds?: number[]; + isCrossOriginSubframe?: boolean; + isMainFrame?: boolean; + hasParentFrame?: boolean; } | null; -export type SubFrameOffsetsForTab = Record< - chrome.runtime.MessageSender["tab"]["id"], - Map ->; +type SubFrameOffsetsByFrame = Map; -export type WebsiteIconData = { - imageEnabled: boolean; - image: string; - fallbackImage: string; - icon: string; -}; +export type SubFrameOffsetsForTab = Record; export type UpdateOverlayCiphersParams = { updateAllCipherTypes: boolean; @@ -146,7 +144,7 @@ export type OverlayBackgroundExtensionMessage = { isFieldCurrentlyFilling?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; - allFieldsRect?: any; + allFieldsRect?: AutofillField[]; isOpeningFullInlineMenu?: boolean; styles?: Partial; data?: LockedVaultPendingNotificationsData; @@ -155,13 +153,30 @@ export type OverlayBackgroundExtensionMessage = { ToggleInlineMenuHiddenMessage & UpdateInlineMenuVisibilityMessage; +export type OverlayPortCommand = + | "fillCipher" + | "addNewVaultItem" + | "viewCipher" + | "redirectFocus" + | "updateHeight" + | "buttonClicked" + | "blurred" + | "updateColorScheme" + | "unlockVault" + | "refreshGeneratedPassword" + | "fillGeneratedPassword"; + export type OverlayPortMessage = { - [key: string]: any; - command: string; - direction?: string; + command: OverlayPortCommand; + direction?: "up" | "down" | "left" | "right"; inlineMenuCipherId?: string; addNewCipherType?: CipherType; usePasskey?: boolean; + height?: number; + backgroundColorScheme?: "light" | "dark"; + viewsCipherData?: InlineMenuCipherData; + loginUrl?: string; + fillGeneratedPassword?: boolean; }; export type InlineMenuCipherData = { @@ -170,7 +185,7 @@ export type InlineMenuCipherData = { type: CipherType; reprompt: CipherRepromptType; favorite: boolean; - icon: WebsiteIconData; + icon: CipherIconDetails; accountCreationFieldType?: string; login?: { totp?: string; @@ -201,9 +216,14 @@ export type BuildCipherDataParams = { export type BackgroundMessageParam = { message: OverlayBackgroundExtensionMessage; }; + export type BackgroundSenderParam = { - sender: chrome.runtime.MessageSender; + sender: chrome.runtime.MessageSender & { + tab: NonNullable; + frameId: FrameId; + }; }; + export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; export type OverlayBackgroundExtensionMessageHandlers = { @@ -253,9 +273,13 @@ export type OverlayBackgroundExtensionMessageHandlers = { export type PortMessageParam = { message: OverlayPortMessage; }; + export type PortConnectionParam = { - port: chrome.runtime.Port; + port: chrome.runtime.Port & { + sender: NonNullable; + }; }; + export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; export type InlineMenuButtonPortMessageHandlers = { diff --git a/apps/browser/src/autofill/background/context-menus.background.ts b/apps/browser/src/autofill/background/context-menus.background.ts index 0db2fd59af3..8c99c0b065e 100644 --- a/apps/browser/src/autofill/background/context-menus.background.ts +++ b/apps/browser/src/autofill/background/context-menus.background.ts @@ -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 { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler"; @@ -17,9 +15,11 @@ export default class ContextMenusBackground { return; } - this.contextMenus.onClicked.addListener((info, tab) => - this.contextMenuClickedHandler.run(info, tab), - ); + this.contextMenus.onClicked.addListener((info, tab) => { + if (tab) { + return this.contextMenuClickedHandler.run(info, tab); + } + }); BrowserApi.messageListener( "contextmenus.background", @@ -28,18 +28,16 @@ export default class ContextMenusBackground { sender: chrome.runtime.MessageSender, ) => { 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.contextMenuClickedHandler - .cipherAction( - msg.data.commandToRetry.message.contextMenuOnClickData, - msg.data.commandToRetry.sender.tab, - ) - .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"); + const onClickData = msg.data.commandToRetry.message.contextMenuOnClickData; + const senderTab = msg.data.commandToRetry.sender.tab; + + if (onClickData && senderTab) { + void this.contextMenuClickedHandler.cipherAction(onClickData, senderTab).then(() => { + if (sender.tab) { + void BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); + } }); + } } }, ); diff --git a/apps/browser/src/autofill/background/tabs.background.spec.ts b/apps/browser/src/autofill/background/tabs.background.spec.ts index 635ab8504a1..7bfa3b83c16 100644 --- a/apps/browser/src/autofill/background/tabs.background.spec.ts +++ b/apps/browser/src/autofill/background/tabs.background.spec.ts @@ -39,9 +39,7 @@ describe("TabsBackground", () => { "handleWindowOnFocusChanged", ); - // 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 - tabsBackground.init(); + void tabsBackground.init(); expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith( handleWindowOnFocusChangedSpy, diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index c33cb6a4371..6f0979d4fd5 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -191,9 +191,11 @@ export class ContextMenuClickedHandler { }); } else { 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 - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); + + void this.eventCollectionService.collect( + EventType.Cipher_ClientCopiedPassword, + cipher.id, + ); } break; diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index 00ff55f5517..5a47975684c 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -179,9 +177,11 @@ export class MainContextMenuHandler { try { const account = await firstValueFrom(this.accountService.activeAccount$); - const hasPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - ); + const hasPremium = + !!account?.id && + (await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + )); const isCardRestricted = ( await firstValueFrom(this.restrictedItemTypesService.restricted$) @@ -198,14 +198,16 @@ export class MainContextMenuHandler { if (requiresPremiumAccess && !hasPremium) { continue; } - if (menuItem.id.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) { + if (menuItem.id?.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) { continue; } await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] }); } } catch (error) { - this.logService.warning(error.message); + if (error instanceof Error) { + this.logService.warning(error.message); + } } finally { this.initRunning = false; } @@ -318,9 +320,11 @@ export class MainContextMenuHandler { } const account = await firstValueFrom(this.accountService.activeAccount$); - const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - ); + const canAccessPremium = + !!account?.id && + (await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + )); if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { await createChildItem(COPY_VERIFICATION_CODE_ID); } @@ -333,7 +337,9 @@ export class MainContextMenuHandler { await createChildItem(AUTOFILL_IDENTITY_ID); } } 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.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"), 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) { - 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); } } 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); } } 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); } catch (error) { - this.logService.warning(error.message); + if (error instanceof Error) { + this.logService.warning(error.message); + } } } } diff --git a/apps/browser/src/autofill/content/auto-submit-login.ts b/apps/browser/src/autofill/content/auto-submit-login.ts index ca5c8ebee80..511d35d7a49 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.ts @@ -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 AutofillPageDetails from "../models/autofill-page-details"; @@ -123,9 +121,9 @@ import { * @param fillScript - The autofill script to use */ function triggerAutoSubmitOnForm(fillScript: AutofillScript) { - const formOpid = fillScript.autosubmit[0]; + const formOpid = fillScript.autosubmit?.[0]; - if (formOpid === null) { + if (!formOpid) { triggerAutoSubmitOnFormlessFields(fillScript); return; } @@ -159,8 +157,11 @@ import { fillScript.script[fillScript.script.length - 1][1], ); - const lastFieldIsPasswordInput = - elementIsInputElement(currentElement) && currentElement.type === "password"; + const lastFieldIsPasswordInput = !!( + currentElement && + elementIsInputElement(currentElement) && + currentElement.type === "password" + ); while (currentElement && currentElement.tagName !== "HTML") { if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) { diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index 590311682bf..f8b5d2b85bf 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -1,3 +1,5 @@ +import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon"; + export const CipherTypes = { Login: 1, SecureNote: 2, @@ -22,20 +24,13 @@ export const OrganizationCategories = { family: "family", } as const; -export type WebsiteIconData = { - imageEnabled: boolean; - image: string; - fallbackImage: string; - icon: string; -}; - type BaseCipherData = { id: string; name: string; type: CipherTypeValue; reprompt: CipherRepromptType; favorite: boolean; - icon: WebsiteIconData; + icon: CipherIconDetails; }; export type CipherData = BaseCipherData & { diff --git a/apps/browser/src/autofill/content/context-menu-handler.ts b/apps/browser/src/autofill/content/context-menu-handler.ts index 82cf95afc81..d3926d57c9a 100644 --- a/apps/browser/src/autofill/content/context-menu-handler.ts +++ b/apps/browser/src/autofill/content/context-menu-handler.ts @@ -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 labelTags = ["label", "span"]; -const attributes = ["id", "name", "label-aria", "placeholder"]; +const attributeKeys = ["id", "name", "label-aria", "placeholder"]; const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement"); 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. function getClickedElementIdentifier() { - if (clickedEl == null) { + if (clickedElement == null) { return invalidElement; } - const clickedTag = clickedEl.nodeName.toLowerCase(); - let inputEl = null; + const clickedTag = clickedElement.nodeName.toLowerCase(); + let inputElement = null; // Try to identify the input element (which may not be the clicked element) if (labelTags.includes(clickedTag)) { - let inputId = null; + let inputId; if (clickedTag === "label") { - inputId = clickedEl.getAttribute("for"); + inputId = clickedElement.getAttribute("for"); } else { - inputId = clickedEl.closest("label")?.getAttribute("for"); + inputId = clickedElement.closest("label")?.getAttribute("for"); } - inputEl = document.getElementById(inputId); + if (inputId) { + inputElement = document.getElementById(inputId); + } } else { - inputEl = clickedEl; + inputElement = clickedElement; } - if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) { + if (inputElement == null || !inputTags.includes(inputElement.nodeName.toLowerCase())) { return invalidElement; } - for (const attr of attributes) { - const attributeValue = inputEl.getAttribute(attr); - const selector = "[" + attr + '="' + attributeValue + '"]'; + for (const attributeKey of attributeKeys) { + const attributeValue = inputElement.getAttribute(attributeKey); + const selector = "[" + attributeKey + '="' + attributeValue + '"]'; if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) { return attributeValue; } @@ -45,14 +45,14 @@ function getClickedElementIdentifier() { return noUniqueIdentifier; } -function isNullOrEmpty(s: string) { +function isNullOrEmpty(s: string | null) { return s == null || s === ""; } // We only have access to the element that's been clicked when the context menu is first opened. // Remember it for use later. 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. @@ -62,9 +62,8 @@ chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => { if (sendResponse) { 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 - chrome.runtime.sendMessage({ + + void chrome.runtime.sendMessage({ command: "getClickedElementResponse", sender: "contextMenuHandler", identifier: identifier, diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts index 5b9ea5e5b27..1cd614a9516 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts @@ -267,9 +267,7 @@ import { Messenger } from "./messaging/messenger"; clearWaitForFocus(); void messenger.destroy(); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { /** empty */ } } diff --git a/apps/browser/src/autofill/fido2/content/messaging/messenger.spec.ts b/apps/browser/src/autofill/fido2/content/messaging/messenger.spec.ts index 5283c60882d..1aa8c27c0ae 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/messenger.spec.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/messenger.spec.ts @@ -31,9 +31,8 @@ describe("Messenger", () => { it("should deliver message to B when sending request from A", () => { 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 - messengerA.request(request); + + void messengerA.request(request); const received = handlerB.receive(); @@ -66,14 +65,13 @@ describe("Messenger", () => { it("should deliver abort signal to B when requesting abort", () => { 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 - messengerA.request(createRequest(), abortController.signal); + + void messengerA.request(createRequest(), abortController.signal); abortController.abort(); const received = handlerB.receive(); - expect(received[0].abortController.signal.aborted).toBe(true); + expect(received[0].abortController?.signal.aborted).toBe(true); }); describe("destroy", () => { @@ -103,29 +101,25 @@ describe("Messenger", () => { it("should dispatch the destroy event on messenger destruction", async () => { 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 - messengerA.request(request); + + void messengerA.request(request); 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 - messengerA.destroy(); + + void messengerA.destroy(); expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event)); }); it("should trigger onDestroyListener when the destroy event is dispatched", async () => { 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 - messengerA.request(request); + + void messengerA.request(request); const onDestroyListener = jest.fn(); (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 - messengerA.destroy(); + + void messengerA.destroy(); expect(onDestroyListener).toHaveBeenCalled(); const eventArg = onDestroyListener.mock.calls[0][0]; @@ -213,7 +207,7 @@ class MockMessagePort { remotePort: MockMessagePort; postMessage(message: T, port?: MessagePort) { - this.remotePort.onmessage( + this.remotePort.onmessage?.( new MessageEvent("message", { data: message, ports: port ? [port] : [], diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 5e523a1a48d..5818bbf8d82 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -155,9 +155,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi } 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.sendMessage(BrowserFido2MessageName, msg); + void BrowserApi.sendMessage(BrowserFido2MessageName, msg); } static abortPopout(sessionId: string, fallbackRequested = false) { @@ -206,9 +204,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi fromEvent(abortController.signal, "abort") .pipe(takeUntil(this.destroy$)) .subscribe(() => { - // 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.close(); + void this.close(); BrowserFido2UserInterfaceSession.sendMessage({ type: BrowserFido2MessageTypes.AbortRequest, sessionId: this.sessionId, @@ -224,12 +220,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ) .subscribe((msg) => { 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - 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); + void this.close(); + void this.abort(msg.fallbackRequested); } }); @@ -388,12 +380,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi takeUntil(this.destroy$), ) .subscribe(() => { - // 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.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); + void this.close(); + void this.abort(true); }); await connectPromise; diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts index 1a8c3bb875b..9d2cf3773d4 100644 --- a/apps/browser/src/autofill/models/autofill-field.ts +++ b/apps/browser/src/autofill/models/autofill-field.ts @@ -1,6 +1,4 @@ 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 { InlineMenuAccountCreationFieldTypes, @@ -13,34 +11,36 @@ import { export default class AutofillField { [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. */ - 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 */ diff --git a/apps/browser/src/autofill/models/autofill-form.ts b/apps/browser/src/autofill/models/autofill-form.ts index d335a81b3c4..e9161620527 100644 --- a/apps/browser/src/autofill/models/autofill-form.ts +++ b/apps/browser/src/autofill/models/autofill-form.ts @@ -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 */ export default class AutofillForm { [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; } diff --git a/apps/browser/src/autofill/models/autofill-page-details.ts b/apps/browser/src/autofill/models/autofill-page-details.ts index c32dfed4e43..ca8c66a3152 100644 --- a/apps/browser/src/autofill/models/autofill-page-details.ts +++ b/apps/browser/src/autofill/models/autofill-page-details.ts @@ -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 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 */ export default class AutofillPageDetails { - title: string; - url: string; - documentUrl: string; + /** Non-null asserted. */ + title!: 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[]; - collectedTimestamp: number; + fields!: AutofillField[]; + /** Non-null asserted. */ + collectedTimestamp!: number; } diff --git a/apps/browser/src/autofill/models/autofill-script.ts b/apps/browser/src/autofill/models/autofill-script.ts index 1da05e07308..43c85c58c9a 100644 --- a/apps/browser/src/autofill/models/autofill-script.ts +++ b/apps/browser/src/autofill/models/autofill-script.ts @@ -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 AutofillScriptProperties = { 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 = { - fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void; - click_on_opid: ({ opid }: { opid: string }) => void; - focus_by_opid: ({ opid }: { opid: string }) => void; + [FillScriptActionTypes.fill_by_opid]: ({ opid, value }: { opid: string; value: string }) => void; + [FillScriptActionTypes.click_on_opid]: ({ opid }: { opid: string }) => void; + [FillScriptActionTypes.focus_by_opid]: ({ opid }: { opid: string }) => void; }; export default class AutofillScript { script: FillScript[] = []; properties: AutofillScriptProperties = {}; - metadata: any = {}; // Unused, not written or read - autosubmit: string[]; // Appears to be unused, read but not written - savedUrls: string[]; - untrustedIframe: boolean; - itemType: string; // Appears to be unused, read but not written + /** Non-null asserted. */ + autosubmit!: string[] | null; // Appears to be unused, read but not written + /** Non-null asserted. */ + savedUrls!: string[]; + /** Non-null asserted. */ + untrustedIframe!: boolean; + /** Non-null asserted. */ + itemType!: string; // Appears to be unused, read but not written } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.ts index 4f497172b39..414673a9b81 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.ts @@ -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 "lit/polyfill-support.js"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -103,7 +101,10 @@ export class AutofillInlineMenuButton extends AutofillInlineMenuPageElement { */ private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) { const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']"); - colorSchemeMetaTag?.setAttribute("content", colorScheme); + + if (colorSchemeMetaTag && colorScheme) { + colorSchemeMetaTag.setAttribute("content", colorScheme); + } } /** diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index 663eae9144a..6d85982a1ac 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -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 { setElementStyles } from "../../../../utils"; @@ -14,8 +12,10 @@ export class AutofillInlineMenuContainer { private readonly setElementStyles = setElementStyles; private readonly extensionOriginsSet: Set; private port: chrome.runtime.Port | null = null; - private portName: string; - private inlineMenuPageIframe: HTMLIFrameElement; + /** Non-null asserted. */ + private portName!: string; + /** Non-null asserted. */ + private inlineMenuPageIframe!: HTMLIFrameElement; private readonly iframeStyles: Partial = { all: "initial", position: "fixed", @@ -42,8 +42,10 @@ export class AutofillInlineMenuContainer { tabIndex: "-1", }; private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = { - initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message), - initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message), + initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) => + this.handleInitInlineMenuIframe(message), + initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) => + this.handleInitInlineMenuIframe(message), }; constructor() { @@ -116,14 +118,20 @@ export class AutofillInlineMenuContainer { * * @param event - The message event. */ - private handleWindowMessage = (event: MessageEvent) => { + private handleWindowMessage = (event: MessageEvent) => { const message = event.data; if (this.isForeignWindowMessage(event)) { return; } - if (this.windowMessageHandlers[message.command]) { - this.windowMessageHandlers[message.command](message); + if ( + this.windowMessageHandlers[ + message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers + ] + ) { + this.windowMessageHandlers[ + message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers + ](message); return; } @@ -142,8 +150,8 @@ export class AutofillInlineMenuContainer { * * @param event - The message event. */ - private isForeignWindowMessage(event: MessageEvent) { - if (!event.data.portKey) { + private isForeignWindowMessage(event: MessageEvent) { + if (!event.data?.portKey) { return true; } @@ -159,7 +167,9 @@ export class AutofillInlineMenuContainer { * * @param event - The message event. */ - private isMessageFromParentWindow(event: MessageEvent): boolean { + private isMessageFromParentWindow( + event: MessageEvent, + ): boolean { return globalThis.parent === event.source; } @@ -168,7 +178,9 @@ export class AutofillInlineMenuContainer { * * @param event - The message event. */ - private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean { + private isMessageFromInlineMenuPageIframe( + event: MessageEvent, + ): boolean { if (!this.inlineMenuPageIframe) { return false; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts index 950676cf202..89f44a6a80d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts @@ -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 { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum"; @@ -10,10 +8,14 @@ import { export class AutofillInlineMenuPageElement extends HTMLElement { protected shadowDom: ShadowRoot; - protected messageOrigin: string; - protected translations: Record; - private portKey: string; - protected windowMessageHandlers: AutofillInlineMenuPageElementWindowMessageHandlers; + /** Non-null asserted. */ + protected messageOrigin!: string; + /** Non-null asserted. */ + protected translations!: Record; + /** Non-null asserted. */ + private portKey!: string; + /** Non-null asserted. */ + protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers; constructor() { super(); diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts index 45d29c1cda9..d5e8c559326 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts @@ -20,7 +20,7 @@ describe("OverlayNotificationsContentService", () => { beforeEach(() => { jest.useFakeTimers(); - jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn()); + jest.spyOn(utils, "sendExtensionMessage").mockImplementation(async () => null); jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window); postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn()); domQueryService = mock(); diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index f4c4c871478..9ccbce4d8e6 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -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 { Component } from "@angular/core"; import { firstValueFrom } from "rxjs"; @@ -69,7 +67,7 @@ export class Fido2UseBrowserLinkComponent { this.platformUtilsService.showToast( "success", - null, + "", this.i18nService.t("domainAddedToExcludedDomains", validDomain), ); } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 62e5ba3a151..49be3104dc1 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -155,13 +155,15 @@ export class AutofillComponent implements OnInit { autofillOnPageLoadOptions: { name: string; value: boolean }[]; enableContextMenuItem: boolean = false; enableAutoTotpCopy: boolean = false; - clearClipboard: ClearClipboardDelaySetting; + /** Non-null asserted. */ + clearClipboard!: ClearClipboardDelaySetting; clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[]; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[]; showCardsCurrentTab: boolean = true; showIdentitiesCurrentTab: boolean = true; - autofillKeyboardHelperText: string; + /** Non-null asserted. */ + autofillKeyboardHelperText!: string; accountSwitcherEnabled: boolean = false; constructor( diff --git a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts index 56c2d1704d2..e1d24159664 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts @@ -26,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = { destroyAutofillInlineMenuListeners: () => void; getInlineMenuFormFieldData: ({ message, - }: AutofillExtensionMessageParam) => Promise; + }: AutofillExtensionMessageParam) => Promise; }; export interface AutofillOverlayContentService { diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 09e22e278be..13a00fb573f 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -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 { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service"; @@ -64,29 +62,39 @@ export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND = ); export abstract class AutofillService { - collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable; - loadAutofillScriptsOnInstall: () => Promise; - reloadAutofillScripts: () => Promise; - injectAutofillScripts: ( + /** Non-null asserted. */ + collectPageDetailsFromTab$!: (tab: chrome.tabs.Tab) => Observable; + /** Non-null asserted. */ + loadAutofillScriptsOnInstall!: () => Promise; + /** Non-null asserted. */ + reloadAutofillScripts!: () => Promise; + /** Non-null asserted. */ + injectAutofillScripts!: ( tab: chrome.tabs.Tab, frameId?: number, triggeringOnPageLoad?: boolean, ) => Promise; - getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[]; - doAutoFill: (options: AutoFillOptions) => Promise; - doAutoFillOnTab: ( + /** Non-null asserted. */ + getFormsWithPasswordFields!: (pageDetails: AutofillPageDetails) => FormData[]; + /** Non-null asserted. */ + doAutoFill!: (options: AutoFillOptions) => Promise; + /** Non-null asserted. */ + doAutoFillOnTab!: ( pageDetails: PageDetail[], tab: chrome.tabs.Tab, fromCommand: boolean, autoSubmitLogin?: boolean, ) => Promise; - doAutoFillActiveTab: ( + /** Non-null asserted. */ + doAutoFillActiveTab!: ( pageDetails: PageDetail[], fromCommand: boolean, cipherType?: CipherType, ) => Promise; - setAutoFillOnPageLoadOrgPolicy: () => Promise; - isPasswordRepromptRequired: ( + /** Non-null asserted. */ + setAutoFillOnPageLoadOrgPolicy!: () => Promise; + /** Non-null asserted. */ + isPasswordRepromptRequired!: ( cipher: CipherView, tab: chrome.tabs.Tab, action?: string, diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 77e8c661d08..bfeaa360a39 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -369,9 +369,7 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs"); 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - autofillService.reloadAutofillScripts(); + void autofillService.reloadAutofillScripts(); expect(port1.disconnect).toHaveBeenCalled(); expect(port2.disconnect).toHaveBeenCalled(); @@ -680,7 +678,9 @@ describe("AutofillService", () => { await autofillService.doAutoFill(autofillOptions); triggerTestFailure(); } 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); triggerTestFailure(); } 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); triggerTestFailure(); } 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); triggerTestFailure(); } 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); triggerTestFailure(); } catch (error) { - expect(error.message).toBe(didNotAutofillError); + if (error instanceof Error) { + expect(error.message).toBe(didNotAutofillError); + } } }); }); @@ -766,7 +774,6 @@ describe("AutofillService", () => { { command: "fillForm", fillScript: { - metadata: {}, properties: { delay_between_operations: 20, }, @@ -863,7 +870,9 @@ describe("AutofillService", () => { expect(logService.info).toHaveBeenCalledWith( "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) { expect(autofillService["generateFillScript"]).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(); } catch (error) { 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( { - metadata: {}, properties: {}, script: [ ["click_on_opid", "username-field"], @@ -1648,7 +1662,6 @@ describe("AutofillService", () => { expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith( { - metadata: {}, properties: {}, script: [ ["click_on_opid", "username-field"], @@ -1686,7 +1699,6 @@ describe("AutofillService", () => { expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith( { - metadata: {}, properties: {}, script: [ ["click_on_opid", "username-field"], @@ -2279,7 +2291,7 @@ describe("AutofillService", () => { ); expect(value).toStrictEqual({ autosubmit: null, - metadata: {}, + itemType: "", properties: { delay_between_operations: 20 }, savedUrls: ["https://www.example.com"], script: [ @@ -2294,7 +2306,6 @@ describe("AutofillService", () => { ["fill_by_opid", "password", "password"], ["focus_by_opid", "password"], ], - itemType: "", untrustedIframe: false, }); }); @@ -2364,11 +2375,10 @@ describe("AutofillService", () => { describe("given an invalid autofill field", () => { const unmodifiedFillScriptValues: AutofillScript = { autosubmit: null, - metadata: {}, + itemType: "", properties: { delay_between_operations: 20 }, savedUrls: [], script: [], - itemType: "", untrustedIframe: false, }; @@ -2555,7 +2565,6 @@ describe("AutofillService", () => { expect(value).toStrictEqual({ autosubmit: null, itemType: "", - metadata: {}, properties: { delay_between_operations: 20, }, diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index bd75cb55ba5..21f024a510c 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -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 { FillableFormFieldElement, FormFieldElement } from "../types"; @@ -202,7 +200,7 @@ class DomElementVisibilityService implements DomElementVisibilityServiceInterfac const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label"); - return targetElementLabelsSet.has(closestParentLabel); + return closestParentLabel ? targetElementLabelsSet.has(closestParentLabel) : false; } } diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index b681e8e9fbb..1b0c5681ff0 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -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 { nodeIsElement } from "../utils"; @@ -7,7 +5,8 @@ import { nodeIsElement } from "../utils"; import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service"; export class DomQueryService implements DomQueryServiceInterface { - private pageContainsShadowDom: boolean; + /** Non-null asserted. */ + private pageContainsShadowDom!: boolean; private ignoredTreeWalkerNodes = new Set([ "svg", "script", @@ -217,13 +216,12 @@ export class DomQueryService implements DomQueryServiceInterface { if ((chrome as any).dom?.openOrClosedShadowRoot) { try { return (chrome as any).dom.openOrClosedShadowRoot(node); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { + } catch { return null; } } + // Firefox-specific equivalent of `openOrClosedShadowRoot` return (node as any).openOrClosedShadowRoot; } @@ -276,7 +274,7 @@ export class DomQueryService implements DomQueryServiceInterface { ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, ); - let currentNode = treeWalker?.currentNode; + let currentNode: Node | null = treeWalker?.currentNode; while (currentNode) { if (filterCallback(currentNode)) { diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index ed8e41df8ba..f7c46a9fa77 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -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 AutofillPageDetails from "../models/autofill-page-details"; import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils"; @@ -162,12 +160,14 @@ export class InlineMenuFieldQualificationService private isExplicitIdentityEmailField(field: AutofillField): boolean { const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { - if (!matchFieldAttributeValues[attrIndex]) { + const attributeValueToMatch = matchFieldAttributeValues[attrIndex]; + + if (!attributeValueToMatch) { continue; } for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) { - if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) { + if (this.newEmailFieldKeywords.has(attributeValueToMatch)) { return true; } } @@ -210,10 +210,7 @@ export class InlineMenuFieldQualificationService } constructor() { - void Promise.all([ - sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"), - sendExtensionMessage("getUserPremiumStatus"), - ]).then(([fieldQualificationFlag, premiumStatus]) => { + void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => { this.premiumEnabled = !!premiumStatus?.result; }); } @@ -263,7 +260,13 @@ export class InlineMenuFieldQualificationService 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 (!parentForm) { @@ -321,7 +324,13 @@ export class InlineMenuFieldQualificationService return false; } - const parentForm = pageDetails.forms[field.form]; + let parentForm; + + const fieldForm = field.form; + + if (fieldForm) { + parentForm = pageDetails.forms[fieldForm]; + } if (!parentForm) { // 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, pageDetails: AutofillPageDetails, ): 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 // 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 // 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); if (this.isNewsletterForm(parentForm)) { @@ -919,8 +940,10 @@ export class InlineMenuFieldQualificationService * @param field - The field to validate */ isUsernameField = (field: AutofillField): boolean => { + const fieldType = field.type; if ( - !this.usernameFieldTypes.has(field.type) || + !fieldType || + !this.usernameFieldTypes.has(fieldType) || this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) || this.fieldHasDisqualifyingAttributeValue(field) ) { @@ -1026,7 +1049,13 @@ export class InlineMenuFieldQualificationService const testedValues = [field.htmlID, field.htmlName, field.placeholder]; 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; } } @@ -1101,7 +1130,9 @@ export class InlineMenuFieldQualificationService * @param excludedTypes - The set of excluded types */ private isExcludedFieldType(field: AutofillField, excludedTypes: Set): boolean { - if (excludedTypes.has(field.type)) { + const fieldType = field.type; + + if (fieldType && excludedTypes.has(fieldType)) { return true; } @@ -1116,12 +1147,14 @@ export class InlineMenuFieldQualificationService private isSearchField(field: AutofillField): boolean { const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { - if (!matchFieldAttributeValues[attrIndex]) { + const attributeValueToMatch = matchFieldAttributeValues[attrIndex]; + + if (!attributeValueToMatch) { continue; } // 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") .toLowerCase(); // 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(",")); } - return this.submitButtonKeywordsMap.get(element); + return this.submitButtonKeywordsMap.get(element) || ""; } /** @@ -1222,8 +1255,9 @@ export class InlineMenuFieldQualificationService ]; const keywordsSet = new Set(); for (let i = 0; i < keywords.length; i++) { - if (keywords[i] && typeof keywords[i] === "string") { - let keywordEl = keywords[i].toLowerCase(); + const attributeValue = keywords[i]; + if (attributeValue && typeof attributeValue === "string") { + let keywordEl = attributeValue.toLowerCase(); keywordsSet.add(keywordEl); // 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); - return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet; + return mapValues ? (returnStringValue ? mapValues.stringValue : mapValues.keywordsSet) : ""; } /** diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 63cd4b534fb..1f2b23021f4 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; 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 { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; @@ -94,14 +94,13 @@ describe("InsertAutofillContentService", () => { ); fillScript = { script: [ - ["click_on_opid", "username"], - ["focus_by_opid", "username"], - ["fill_by_opid", "username", "test"], + [FillScriptActionTypes.click_on_opid, "username"], + [FillScriptActionTypes.focus_by_opid, "username"], + [FillScriptActionTypes.fill_by_opid, "username", "test"], ], properties: { delay_between_operations: 20, }, - metadata: {}, autosubmit: [], savedUrls: ["https://bitwarden.com"], untrustedIframe: false, @@ -221,17 +220,14 @@ describe("InsertAutofillContentService", () => { expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 1, fillScript.script[0], - 0, ); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 2, fillScript.script[1], - 1, ); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 3, fillScript.script[2], - 2, ); }); }); @@ -376,42 +372,62 @@ describe("InsertAutofillContentService", () => { }); it("returns early if no opid is provided", async () => { - const action = "fill_by_opid"; + const action = FillScriptActionTypes.fill_by_opid; const opid = ""; const value = "value"; const scriptAction: FillScript = [action, opid, value]; jest.spyOn(insertAutofillContentService["autofillInsertActions"], action); - await insertAutofillContentService["runFillScriptAction"](scriptAction, 0); + await insertAutofillContentService["runFillScriptAction"](scriptAction); jest.advanceTimersByTime(20); expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled(); }); describe("given a valid fill script action and opid", () => { - const fillScriptActions: FillScriptActions[] = [ - "fill_by_opid", - "click_on_opid", - "focus_by_opid", - ]; - fillScriptActions.forEach((action) => { - it(`triggers a ${action} action`, () => { - const opid = "opid"; - const value = "value"; - const scriptAction: FillScript = [action, opid, value]; - jest.spyOn(insertAutofillContentService["autofillInsertActions"], action); + it(`triggers a fill_by_opid action`, () => { + const action = FillScriptActionTypes.fill_by_opid; + 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - insertAutofillContentService["runFillScriptAction"](scriptAction, 0); - jest.advanceTimersByTime(20); + void insertAutofillContentService["runFillScriptAction"](scriptAction); + jest.advanceTimersByTime(20); - expect( - insertAutofillContentService["autofillInsertActions"][action], - ).toHaveBeenCalledWith({ - opid, - value, - }); + expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({ + 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, }); }); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index 6c951afc1a0..4b7f699fecb 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -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 AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script"; +import AutofillScript, { + AutofillInsertActions, + FillScript, + FillScriptActionTypes, +} from "../models/autofill-script"; import { FormFieldElement } from "../types"; import { currentlyInSandboxedIframe, @@ -50,7 +52,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf } 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. * Each action is subsequently delayed by 20 milliseconds. - * @param {"click_on_opid" | "focus_by_opid" | "fill_by_opid"} action - * @param {string} opid - * @param {string} value - * @param {number} actionIndex + * @param {FillScript} [action, opid, value] * @returns {Promise} * @private */ - private runFillScriptAction = ( - [action, opid, value]: FillScript, - actionIndex: number, - ): Promise => { + private runFillScriptAction = ([action, opid, value]: FillScript): Promise => { if (!opid || !this.autofillInsertActions[action]) { - return; + return Promise.resolve(); } const delayActionsInMilliseconds = 20; return new Promise((resolve) => 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(); }, delayActionsInMilliseconds), ); @@ -158,7 +161,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf */ private handleClickOnFieldByOpidAction(opid: string) { 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) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + if (!element) { + return; + } + if (document.activeElement === element) { element.blur(); } @@ -187,6 +197,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf * @private */ private insertValueIntoField(element: FormFieldElement | null, value: string) { + if (!element || !value) { + return; + } + const elementCanBeReadonly = elementIsInputElement(element) || elementIsTextAreaElement(element); const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element); @@ -195,8 +209,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value); if ( - !element || - !value || elementAlreadyHasTheValue || (elementCanBeReadonly && element.readOnly) || (elementCanBeFilled && element.disabled) @@ -298,7 +310,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf * @private */ private triggerClickOnElement(element?: HTMLElement): void { - if (typeof element?.click !== TYPE_CHECK.FUNCTION) { + if (!element || typeof element.click !== TYPE_CHECK.FUNCTION) { return; } @@ -313,7 +325,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf * @private */ private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void { - if (typeof element?.focus !== TYPE_CHECK.FUNCTION) { + if (!element || typeof element.focus !== TYPE_CHECK.FUNCTION) { return; } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index d1e127227c6..3714ef2105b 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -144,7 +142,6 @@ export function createAutofillScriptMock( return { autosubmit: null, - metadata: {}, properties: { delay_between_operations: 20, }, @@ -299,7 +296,7 @@ export function createMutationRecordMock(customFields = {}): MutationRecord { oldValue: "default-oldValue", previousSibling: null, removedNodes: mock(), - target: null, + target: mock(), type: "attributes", ...customFields, };