1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +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>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>;
bgGetActiveUserServerConfig: () => Promise<ServerConfig>;
bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
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 { 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<chrome.runtime.MessageSender["frameId"], PageDetail>
>;
export type TabId = NonNullable<chrome.tabs.Tab["id"]>;
export type FrameId = NonNullable<chrome.runtime.MessageSender["frameId"]>;
type PageDetailsByFrame = Map<FrameId, PageDetail>;
export type PageDetailsForTab = Record<TabId, PageDetailsByFrame>;
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<chrome.runtime.MessageSender["frameId"], SubFrameOffsetData>
>;
type SubFrameOffsetsByFrame = Map<FrameId, SubFrameOffsetData>;
export type WebsiteIconData = {
imageEnabled: boolean;
image: string;
fallbackImage: string;
icon: string;
};
export type SubFrameOffsetsForTab = Record<TabId, SubFrameOffsetsByFrame>;
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<CSSStyleDeclaration>;
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<chrome.runtime.MessageSender["tab"]>;
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<chrome.runtime.Port["sender"]>;
};
};
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
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 { 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");
}
});
}
}
},
);

View File

@@ -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,

View File

@@ -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;

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 { 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);
}
}
}
}

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 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)) {

View File

@@ -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<CipherTypeValue> = {
id: string;
name: string;
type: CipherTypeValue;
reprompt: CipherRepromptType;
favorite: boolean;
icon: WebsiteIconData;
icon: CipherIconDetails;
};
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 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,

View File

@@ -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 */
}
}

View File

@@ -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<T> {
remotePort: MockMessagePort<T>;
postMessage(message: T, port?: MessagePort) {
this.remotePort.onmessage(
this.remotePort.onmessage?.(
new MessageEvent("message", {
data: message,
ports: port ? [port] : [],

View File

@@ -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;

View File

@@ -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
*/

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
*/
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;
}

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 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;
}

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 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
}

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 "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);
}
}
/**

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 { setElementStyles } from "../../../../utils";
@@ -14,8 +12,10 @@ export class AutofillInlineMenuContainer {
private readonly setElementStyles = setElementStyles;
private readonly extensionOriginsSet: Set<string>;
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<CSSStyleDeclaration> = {
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<AutofillInlineMenuContainerWindowMessage>) => {
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<AutofillInlineMenuContainerWindowMessage>) {
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<AutofillInlineMenuContainerWindowMessage>,
): 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<AutofillInlineMenuContainerWindowMessage>,
): boolean {
if (!this.inlineMenuPageIframe) {
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 { 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<string, string>;
private portKey: string;
protected windowMessageHandlers: AutofillInlineMenuPageElementWindowMessageHandlers;
/** Non-null asserted. */
protected messageOrigin!: string;
/** Non-null asserted. */
protected translations!: Record<string, string>;
/** Non-null asserted. */
private portKey!: string;
/** Non-null asserted. */
protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers;
constructor() {
super();

View File

@@ -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<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 { 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),
);
}

View File

@@ -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(

View File

@@ -26,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
destroyAutofillInlineMenuListeners: () => void;
getInlineMenuFormFieldData: ({
message,
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData | void>;
};
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 { 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<PageDetail[]>;
loadAutofillScriptsOnInstall: () => Promise<void>;
reloadAutofillScripts: () => Promise<void>;
injectAutofillScripts: (
/** Non-null asserted. */
collectPageDetailsFromTab$!: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
/** Non-null asserted. */
loadAutofillScriptsOnInstall!: () => Promise<void>;
/** Non-null asserted. */
reloadAutofillScripts!: () => Promise<void>;
/** Non-null asserted. */
injectAutofillScripts!: (
tab: chrome.tabs.Tab,
frameId?: number,
triggeringOnPageLoad?: boolean,
) => Promise<void>;
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
doAutoFillOnTab: (
/** Non-null asserted. */
getFormsWithPasswordFields!: (pageDetails: AutofillPageDetails) => FormData[];
/** Non-null asserted. */
doAutoFill!: (options: AutoFillOptions) => Promise<string | null>;
/** Non-null asserted. */
doAutoFillOnTab!: (
pageDetails: PageDetail[],
tab: chrome.tabs.Tab,
fromCommand: boolean,
autoSubmitLogin?: boolean,
) => Promise<string | null>;
doAutoFillActiveTab: (
/** Non-null asserted. */
doAutoFillActiveTab!: (
pageDetails: PageDetail[],
fromCommand: boolean,
cipherType?: CipherType,
) => Promise<string | null>;
setAutoFillOnPageLoadOrgPolicy: () => Promise<void>;
isPasswordRepromptRequired: (
/** Non-null asserted. */
setAutoFillOnPageLoadOrgPolicy!: () => Promise<void>;
/** Non-null asserted. */
isPasswordRepromptRequired!: (
cipher: CipherView,
tab: chrome.tabs.Tab,
action?: string,

View File

@@ -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,
},

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 { 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;
}
}

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 { 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)) {

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 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<string>): 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<string>();
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) : "";
}
/**

View File

@@ -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,
});
});
});

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 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<void>}
* @private
*/
private runFillScriptAction = (
[action, opid, value]: FillScript,
actionIndex: number,
): Promise<void> => {
private runFillScriptAction = ([action, opid, value]: FillScript): Promise<void> => {
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;
}

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 { 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<NodeList>(),
target: null,
target: mock<Node>(),
type: "attributes",
...customFields,
};