mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-27569] Typing cleanup (#17087)
* typing cleanup * additional cleanup * more typing fixes * revert notification background changes * fix DOM query service breakage * do not run a fill_by_opid action if there is a nullish or empty value attribute * type cleanup * cleanup per review suggestions * remove unused flag check * add non-null assertion signposts * additional cleanup
This commit is contained in:
@@ -147,7 +147,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
|||||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||||
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||||
bgGetActiveUserServerConfig: () => Promise<ServerConfig>;
|
bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
|
||||||
getWebVaultUrlForNotification: () => Promise<string>;
|
getWebVaultUrlForNotification: () => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
|
import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
|
||||||
|
import AutofillField from "../../models/autofill-field";
|
||||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||||
import { PageDetail } from "../../services/abstractions/autofill.service";
|
import { PageDetail } from "../../services/abstractions/autofill.service";
|
||||||
|
|
||||||
import { LockedVaultPendingNotificationsData } from "./notification.background";
|
import { LockedVaultPendingNotificationsData } from "./notification.background";
|
||||||
|
|
||||||
export type PageDetailsForTab = Record<
|
export type TabId = NonNullable<chrome.tabs.Tab["id"]>;
|
||||||
chrome.runtime.MessageSender["tab"]["id"],
|
|
||||||
Map<chrome.runtime.MessageSender["frameId"], PageDetail>
|
export type FrameId = NonNullable<chrome.runtime.MessageSender["frameId"]>;
|
||||||
>;
|
|
||||||
|
type PageDetailsByFrame = Map<FrameId, PageDetail>;
|
||||||
|
|
||||||
|
export type PageDetailsForTab = Record<TabId, PageDetailsByFrame>;
|
||||||
|
|
||||||
export type SubFrameOffsetData = {
|
export type SubFrameOffsetData = {
|
||||||
top: number;
|
top: number;
|
||||||
@@ -21,19 +24,14 @@ export type SubFrameOffsetData = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
frameId?: number;
|
frameId?: number;
|
||||||
parentFrameIds?: number[];
|
parentFrameIds?: number[];
|
||||||
|
isCrossOriginSubframe?: boolean;
|
||||||
|
isMainFrame?: boolean;
|
||||||
|
hasParentFrame?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type SubFrameOffsetsForTab = Record<
|
type SubFrameOffsetsByFrame = Map<FrameId, SubFrameOffsetData>;
|
||||||
chrome.runtime.MessageSender["tab"]["id"],
|
|
||||||
Map<chrome.runtime.MessageSender["frameId"], SubFrameOffsetData>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type WebsiteIconData = {
|
export type SubFrameOffsetsForTab = Record<TabId, SubFrameOffsetsByFrame>;
|
||||||
imageEnabled: boolean;
|
|
||||||
image: string;
|
|
||||||
fallbackImage: string;
|
|
||||||
icon: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateOverlayCiphersParams = {
|
export type UpdateOverlayCiphersParams = {
|
||||||
updateAllCipherTypes: boolean;
|
updateAllCipherTypes: boolean;
|
||||||
@@ -146,7 +144,7 @@ export type OverlayBackgroundExtensionMessage = {
|
|||||||
isFieldCurrentlyFilling?: boolean;
|
isFieldCurrentlyFilling?: boolean;
|
||||||
subFrameData?: SubFrameOffsetData;
|
subFrameData?: SubFrameOffsetData;
|
||||||
focusedFieldData?: FocusedFieldData;
|
focusedFieldData?: FocusedFieldData;
|
||||||
allFieldsRect?: any;
|
allFieldsRect?: AutofillField[];
|
||||||
isOpeningFullInlineMenu?: boolean;
|
isOpeningFullInlineMenu?: boolean;
|
||||||
styles?: Partial<CSSStyleDeclaration>;
|
styles?: Partial<CSSStyleDeclaration>;
|
||||||
data?: LockedVaultPendingNotificationsData;
|
data?: LockedVaultPendingNotificationsData;
|
||||||
@@ -155,13 +153,30 @@ export type OverlayBackgroundExtensionMessage = {
|
|||||||
ToggleInlineMenuHiddenMessage &
|
ToggleInlineMenuHiddenMessage &
|
||||||
UpdateInlineMenuVisibilityMessage;
|
UpdateInlineMenuVisibilityMessage;
|
||||||
|
|
||||||
|
export type OverlayPortCommand =
|
||||||
|
| "fillCipher"
|
||||||
|
| "addNewVaultItem"
|
||||||
|
| "viewCipher"
|
||||||
|
| "redirectFocus"
|
||||||
|
| "updateHeight"
|
||||||
|
| "buttonClicked"
|
||||||
|
| "blurred"
|
||||||
|
| "updateColorScheme"
|
||||||
|
| "unlockVault"
|
||||||
|
| "refreshGeneratedPassword"
|
||||||
|
| "fillGeneratedPassword";
|
||||||
|
|
||||||
export type OverlayPortMessage = {
|
export type OverlayPortMessage = {
|
||||||
[key: string]: any;
|
command: OverlayPortCommand;
|
||||||
command: string;
|
direction?: "up" | "down" | "left" | "right";
|
||||||
direction?: string;
|
|
||||||
inlineMenuCipherId?: string;
|
inlineMenuCipherId?: string;
|
||||||
addNewCipherType?: CipherType;
|
addNewCipherType?: CipherType;
|
||||||
usePasskey?: boolean;
|
usePasskey?: boolean;
|
||||||
|
height?: number;
|
||||||
|
backgroundColorScheme?: "light" | "dark";
|
||||||
|
viewsCipherData?: InlineMenuCipherData;
|
||||||
|
loginUrl?: string;
|
||||||
|
fillGeneratedPassword?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InlineMenuCipherData = {
|
export type InlineMenuCipherData = {
|
||||||
@@ -170,7 +185,7 @@ export type InlineMenuCipherData = {
|
|||||||
type: CipherType;
|
type: CipherType;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
icon: WebsiteIconData;
|
icon: CipherIconDetails;
|
||||||
accountCreationFieldType?: string;
|
accountCreationFieldType?: string;
|
||||||
login?: {
|
login?: {
|
||||||
totp?: string;
|
totp?: string;
|
||||||
@@ -201,9 +216,14 @@ export type BuildCipherDataParams = {
|
|||||||
export type BackgroundMessageParam = {
|
export type BackgroundMessageParam = {
|
||||||
message: OverlayBackgroundExtensionMessage;
|
message: OverlayBackgroundExtensionMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BackgroundSenderParam = {
|
export type BackgroundSenderParam = {
|
||||||
sender: chrome.runtime.MessageSender;
|
sender: chrome.runtime.MessageSender & {
|
||||||
|
tab: NonNullable<chrome.runtime.MessageSender["tab"]>;
|
||||||
|
frameId: FrameId;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
|
export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
|
||||||
|
|
||||||
export type OverlayBackgroundExtensionMessageHandlers = {
|
export type OverlayBackgroundExtensionMessageHandlers = {
|
||||||
@@ -253,9 +273,13 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
|||||||
export type PortMessageParam = {
|
export type PortMessageParam = {
|
||||||
message: OverlayPortMessage;
|
message: OverlayPortMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PortConnectionParam = {
|
export type PortConnectionParam = {
|
||||||
port: chrome.runtime.Port;
|
port: chrome.runtime.Port & {
|
||||||
|
sender: NonNullable<chrome.runtime.Port["sender"]>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
|
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
|
||||||
|
|
||||||
export type InlineMenuButtonPortMessageHandlers = {
|
export type InlineMenuButtonPortMessageHandlers = {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
|
import { ContextMenuClickedHandler } from "../browser/context-menu-clicked-handler";
|
||||||
|
|
||||||
@@ -17,9 +15,11 @@ export default class ContextMenusBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contextMenus.onClicked.addListener((info, tab) =>
|
this.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
this.contextMenuClickedHandler.run(info, tab),
|
if (tab) {
|
||||||
);
|
return this.contextMenuClickedHandler.run(info, tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
BrowserApi.messageListener(
|
BrowserApi.messageListener(
|
||||||
"contextmenus.background",
|
"contextmenus.background",
|
||||||
@@ -28,18 +28,16 @@ export default class ContextMenusBackground {
|
|||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) => {
|
) => {
|
||||||
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
|
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
const onClickData = msg.data.commandToRetry.message.contextMenuOnClickData;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
const senderTab = msg.data.commandToRetry.sender.tab;
|
||||||
this.contextMenuClickedHandler
|
|
||||||
.cipherAction(
|
if (onClickData && senderTab) {
|
||||||
msg.data.commandToRetry.message.contextMenuOnClickData,
|
void this.contextMenuClickedHandler.cipherAction(onClickData, senderTab).then(() => {
|
||||||
msg.data.commandToRetry.sender.tab,
|
if (sender.tab) {
|
||||||
)
|
void BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
|
||||||
.then(() => {
|
}
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ describe("TabsBackground", () => {
|
|||||||
"handleWindowOnFocusChanged",
|
"handleWindowOnFocusChanged",
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void tabsBackground.init();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
tabsBackground.init();
|
|
||||||
|
|
||||||
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
expect(chrome.windows.onFocusChanged.addListener).toHaveBeenCalledWith(
|
||||||
handleWindowOnFocusChangedSpy,
|
handleWindowOnFocusChangedSpy,
|
||||||
|
|||||||
@@ -191,9 +191,11 @@ export class ContextMenuClickedHandler {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void this.eventCollectionService.collect(
|
||||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
EventType.Cipher_ClientCopiedPassword,
|
||||||
|
cipher.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -179,9 +177,11 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
const hasPremium = await firstValueFrom(
|
const hasPremium =
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
!!account?.id &&
|
||||||
);
|
(await firstValueFrom(
|
||||||
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||||
|
));
|
||||||
|
|
||||||
const isCardRestricted = (
|
const isCardRestricted = (
|
||||||
await firstValueFrom(this.restrictedItemTypesService.restricted$)
|
await firstValueFrom(this.restrictedItemTypesService.restricted$)
|
||||||
@@ -198,14 +198,16 @@ export class MainContextMenuHandler {
|
|||||||
if (requiresPremiumAccess && !hasPremium) {
|
if (requiresPremiumAccess && !hasPremium) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (menuItem.id.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) {
|
if (menuItem.id?.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
|
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.initRunning = false;
|
this.initRunning = false;
|
||||||
}
|
}
|
||||||
@@ -318,9 +320,11 @@ export class MainContextMenuHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
const canAccessPremium = await firstValueFrom(
|
const canAccessPremium =
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
!!account?.id &&
|
||||||
);
|
(await firstValueFrom(
|
||||||
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||||
|
));
|
||||||
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
|
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
|
||||||
await createChildItem(COPY_VERIFICATION_CODE_ID);
|
await createChildItem(COPY_VERIFICATION_CODE_ID);
|
||||||
}
|
}
|
||||||
@@ -333,7 +337,9 @@ export class MainContextMenuHandler {
|
|||||||
await createChildItem(AUTOFILL_IDENTITY_ID);
|
await createChildItem(AUTOFILL_IDENTITY_ID);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +357,11 @@ export class MainContextMenuHandler {
|
|||||||
this.loadOptions(
|
this.loadOptions(
|
||||||
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
||||||
NOOP_COMMAND_SUFFIX,
|
NOOP_COMMAND_SUFFIX,
|
||||||
).catch((error) => this.logService.warning(error.message));
|
).catch((error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return this.logService.warning(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +373,9 @@ export class MainContextMenuHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +385,9 @@ export class MainContextMenuHandler {
|
|||||||
await MainContextMenuHandler.create(menuItem);
|
await MainContextMenuHandler.create(menuItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +397,9 @@ export class MainContextMenuHandler {
|
|||||||
await MainContextMenuHandler.create(menuItem);
|
await MainContextMenuHandler.create(menuItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +411,9 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
if (error instanceof Error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
@@ -123,9 +121,9 @@ import {
|
|||||||
* @param fillScript - The autofill script to use
|
* @param fillScript - The autofill script to use
|
||||||
*/
|
*/
|
||||||
function triggerAutoSubmitOnForm(fillScript: AutofillScript) {
|
function triggerAutoSubmitOnForm(fillScript: AutofillScript) {
|
||||||
const formOpid = fillScript.autosubmit[0];
|
const formOpid = fillScript.autosubmit?.[0];
|
||||||
|
|
||||||
if (formOpid === null) {
|
if (!formOpid) {
|
||||||
triggerAutoSubmitOnFormlessFields(fillScript);
|
triggerAutoSubmitOnFormlessFields(fillScript);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -159,8 +157,11 @@ import {
|
|||||||
fillScript.script[fillScript.script.length - 1][1],
|
fillScript.script[fillScript.script.length - 1][1],
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastFieldIsPasswordInput =
|
const lastFieldIsPasswordInput = !!(
|
||||||
elementIsInputElement(currentElement) && currentElement.type === "password";
|
currentElement &&
|
||||||
|
elementIsInputElement(currentElement) &&
|
||||||
|
currentElement.type === "password"
|
||||||
|
);
|
||||||
|
|
||||||
while (currentElement && currentElement.tagName !== "HTML") {
|
while (currentElement && currentElement.tagName !== "HTML") {
|
||||||
if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) {
|
if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||||
|
|
||||||
export const CipherTypes = {
|
export const CipherTypes = {
|
||||||
Login: 1,
|
Login: 1,
|
||||||
SecureNote: 2,
|
SecureNote: 2,
|
||||||
@@ -22,20 +24,13 @@ export const OrganizationCategories = {
|
|||||||
family: "family",
|
family: "family",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type WebsiteIconData = {
|
|
||||||
imageEnabled: boolean;
|
|
||||||
image: string;
|
|
||||||
fallbackImage: string;
|
|
||||||
icon: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BaseCipherData<CipherTypeValue> = {
|
type BaseCipherData<CipherTypeValue> = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: CipherTypeValue;
|
type: CipherTypeValue;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
icon: WebsiteIconData;
|
icon: CipherIconDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CipherData = BaseCipherData<CipherType> & {
|
export type CipherData = BaseCipherData<CipherType> & {
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
const inputTags = ["input", "textarea", "select"];
|
const inputTags = ["input", "textarea", "select"];
|
||||||
const labelTags = ["label", "span"];
|
const labelTags = ["label", "span"];
|
||||||
const attributes = ["id", "name", "label-aria", "placeholder"];
|
const attributeKeys = ["id", "name", "label-aria", "placeholder"];
|
||||||
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
|
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
|
||||||
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
|
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
|
||||||
|
|
||||||
let clickedEl: HTMLElement = null;
|
let clickedElement: HTMLElement | null = null;
|
||||||
|
|
||||||
// Find the best attribute to be used as the Name for an element in a custom field.
|
// Find the best attribute to be used as the Name for an element in a custom field.
|
||||||
function getClickedElementIdentifier() {
|
function getClickedElementIdentifier() {
|
||||||
if (clickedEl == null) {
|
if (clickedElement == null) {
|
||||||
return invalidElement;
|
return invalidElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickedTag = clickedEl.nodeName.toLowerCase();
|
const clickedTag = clickedElement.nodeName.toLowerCase();
|
||||||
let inputEl = null;
|
let inputElement = null;
|
||||||
|
|
||||||
// Try to identify the input element (which may not be the clicked element)
|
// Try to identify the input element (which may not be the clicked element)
|
||||||
if (labelTags.includes(clickedTag)) {
|
if (labelTags.includes(clickedTag)) {
|
||||||
let inputId = null;
|
let inputId;
|
||||||
if (clickedTag === "label") {
|
if (clickedTag === "label") {
|
||||||
inputId = clickedEl.getAttribute("for");
|
inputId = clickedElement.getAttribute("for");
|
||||||
} else {
|
} else {
|
||||||
inputId = clickedEl.closest("label")?.getAttribute("for");
|
inputId = clickedElement.closest("label")?.getAttribute("for");
|
||||||
}
|
}
|
||||||
|
|
||||||
inputEl = document.getElementById(inputId);
|
if (inputId) {
|
||||||
|
inputElement = document.getElementById(inputId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
inputEl = clickedEl;
|
inputElement = clickedElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) {
|
if (inputElement == null || !inputTags.includes(inputElement.nodeName.toLowerCase())) {
|
||||||
return invalidElement;
|
return invalidElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attr of attributes) {
|
for (const attributeKey of attributeKeys) {
|
||||||
const attributeValue = inputEl.getAttribute(attr);
|
const attributeValue = inputElement.getAttribute(attributeKey);
|
||||||
const selector = "[" + attr + '="' + attributeValue + '"]';
|
const selector = "[" + attributeKey + '="' + attributeValue + '"]';
|
||||||
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
|
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
|
||||||
return attributeValue;
|
return attributeValue;
|
||||||
}
|
}
|
||||||
@@ -45,14 +45,14 @@ function getClickedElementIdentifier() {
|
|||||||
return noUniqueIdentifier;
|
return noUniqueIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNullOrEmpty(s: string) {
|
function isNullOrEmpty(s: string | null) {
|
||||||
return s == null || s === "";
|
return s == null || s === "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only have access to the element that's been clicked when the context menu is first opened.
|
// We only have access to the element that's been clicked when the context menu is first opened.
|
||||||
// Remember it for use later.
|
// Remember it for use later.
|
||||||
document.addEventListener("contextmenu", (event) => {
|
document.addEventListener("contextmenu", (event) => {
|
||||||
clickedEl = event.target as HTMLElement;
|
clickedElement = event.target as HTMLElement;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
|
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
|
||||||
@@ -62,9 +62,8 @@ chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => {
|
|||||||
if (sendResponse) {
|
if (sendResponse) {
|
||||||
sendResponse(identifier);
|
sendResponse(identifier);
|
||||||
}
|
}
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void chrome.runtime.sendMessage({
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
command: "getClickedElementResponse",
|
command: "getClickedElementResponse",
|
||||||
sender: "contextMenuHandler",
|
sender: "contextMenuHandler",
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
|
|||||||
@@ -267,9 +267,7 @@ import { Messenger } from "./messaging/messenger";
|
|||||||
|
|
||||||
clearWaitForFocus();
|
clearWaitForFocus();
|
||||||
void messenger.destroy();
|
void messenger.destroy();
|
||||||
// FIXME: Remove when updating file. Eslint update
|
} catch {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (e) {
|
|
||||||
/** empty */
|
/** empty */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ describe("Messenger", () => {
|
|||||||
|
|
||||||
it("should deliver message to B when sending request from A", () => {
|
it("should deliver message to B when sending request from A", () => {
|
||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.request(request);
|
||||||
messengerA.request(request);
|
|
||||||
|
|
||||||
const received = handlerB.receive();
|
const received = handlerB.receive();
|
||||||
|
|
||||||
@@ -66,14 +65,13 @@ describe("Messenger", () => {
|
|||||||
|
|
||||||
it("should deliver abort signal to B when requesting abort", () => {
|
it("should deliver abort signal to B when requesting abort", () => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.request(createRequest(), abortController.signal);
|
||||||
messengerA.request(createRequest(), abortController.signal);
|
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
|
||||||
const received = handlerB.receive();
|
const received = handlerB.receive();
|
||||||
|
|
||||||
expect(received[0].abortController.signal.aborted).toBe(true);
|
expect(received[0].abortController?.signal.aborted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
@@ -103,29 +101,25 @@ describe("Messenger", () => {
|
|||||||
|
|
||||||
it("should dispatch the destroy event on messenger destruction", async () => {
|
it("should dispatch the destroy event on messenger destruction", async () => {
|
||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.request(request);
|
||||||
messengerA.request(request);
|
|
||||||
|
|
||||||
const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent");
|
const dispatchEventSpy = jest.spyOn((messengerA as any).onDestroy, "dispatchEvent");
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.destroy();
|
||||||
messengerA.destroy();
|
|
||||||
|
|
||||||
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event));
|
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(Event));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger onDestroyListener when the destroy event is dispatched", async () => {
|
it("should trigger onDestroyListener when the destroy event is dispatched", async () => {
|
||||||
const request = createRequest();
|
const request = createRequest();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.request(request);
|
||||||
messengerA.request(request);
|
|
||||||
|
|
||||||
const onDestroyListener = jest.fn();
|
const onDestroyListener = jest.fn();
|
||||||
(messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener);
|
(messengerA as any).onDestroy.addEventListener("destroy", onDestroyListener);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void messengerA.destroy();
|
||||||
messengerA.destroy();
|
|
||||||
|
|
||||||
expect(onDestroyListener).toHaveBeenCalled();
|
expect(onDestroyListener).toHaveBeenCalled();
|
||||||
const eventArg = onDestroyListener.mock.calls[0][0];
|
const eventArg = onDestroyListener.mock.calls[0][0];
|
||||||
@@ -213,7 +207,7 @@ class MockMessagePort<T> {
|
|||||||
remotePort: MockMessagePort<T>;
|
remotePort: MockMessagePort<T>;
|
||||||
|
|
||||||
postMessage(message: T, port?: MessagePort) {
|
postMessage(message: T, port?: MessagePort) {
|
||||||
this.remotePort.onmessage(
|
this.remotePort.onmessage?.(
|
||||||
new MessageEvent("message", {
|
new MessageEvent("message", {
|
||||||
data: message,
|
data: message,
|
||||||
ports: port ? [port] : [],
|
ports: port ? [port] : [],
|
||||||
|
|||||||
@@ -155,9 +155,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
static sendMessage(msg: BrowserFido2Message) {
|
static sendMessage(msg: BrowserFido2Message) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void BrowserApi.sendMessage(BrowserFido2MessageName, msg);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static abortPopout(sessionId: string, fallbackRequested = false) {
|
static abortPopout(sessionId: string, fallbackRequested = false) {
|
||||||
@@ -206,9 +204,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
fromEvent(abortController.signal, "abort")
|
fromEvent(abortController.signal, "abort")
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void this.close();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.close();
|
|
||||||
BrowserFido2UserInterfaceSession.sendMessage({
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
type: BrowserFido2MessageTypes.AbortRequest,
|
type: BrowserFido2MessageTypes.AbortRequest,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
@@ -224,12 +220,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
)
|
)
|
||||||
.subscribe((msg) => {
|
.subscribe((msg) => {
|
||||||
if (msg.type === BrowserFido2MessageTypes.AbortResponse) {
|
if (msg.type === BrowserFido2MessageTypes.AbortResponse) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void this.close();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void this.abort(msg.fallbackRequested);
|
||||||
this.close();
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.abort(msg.fallbackRequested);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -388,12 +380,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void this.close();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
void this.abort(true);
|
||||||
this.close();
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.abort(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await connectPromise;
|
await connectPromise;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { FieldRect } from "../background/abstractions/overlay.background";
|
import { FieldRect } from "../background/abstractions/overlay.background";
|
||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
||||||
import {
|
import {
|
||||||
InlineMenuAccountCreationFieldTypes,
|
InlineMenuAccountCreationFieldTypes,
|
||||||
@@ -13,34 +11,36 @@ import {
|
|||||||
export default class AutofillField {
|
export default class AutofillField {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
/**
|
/**
|
||||||
* The unique identifier assigned to this field during collection of the page details
|
* Non-null asserted. The unique identifier assigned to this field during collection of the page details
|
||||||
*/
|
*/
|
||||||
opid: string;
|
opid!: string;
|
||||||
/**
|
/**
|
||||||
* Sequential number assigned to each element collected, based on its position in the DOM.
|
* Non-null asserted. Sequential number assigned to each element collected, based on its position in the DOM.
|
||||||
* Used to do perform proximal checks for username and password fields on the DOM.
|
* Used to do perform proximal checks for username and password fields on the DOM.
|
||||||
*/
|
*/
|
||||||
elementNumber: number;
|
elementNumber!: number;
|
||||||
/**
|
/**
|
||||||
* Designates whether the field is viewable on the current part of the DOM that the user can see
|
* Non-null asserted. Designates whether the field is viewable on the current part of the DOM that the user can see
|
||||||
*/
|
*/
|
||||||
viewable: boolean;
|
viewable!: boolean;
|
||||||
/**
|
/**
|
||||||
* The HTML `id` attribute of the field
|
* Non-null asserted. The HTML `id` attribute of the field
|
||||||
*/
|
*/
|
||||||
htmlID: string | null;
|
htmlID!: string | null;
|
||||||
/**
|
/**
|
||||||
* The HTML `name` attribute of the field
|
* Non-null asserted. The HTML `name` attribute of the field
|
||||||
*/
|
*/
|
||||||
htmlName: string | null;
|
htmlName!: string | null;
|
||||||
/**
|
/**
|
||||||
* The HTML `class` attribute of the field
|
* Non-null asserted. The HTML `class` attribute of the field
|
||||||
*/
|
*/
|
||||||
htmlClass: string | null;
|
htmlClass!: string | null;
|
||||||
|
|
||||||
tabindex: string | null;
|
/** Non-null asserted. */
|
||||||
|
tabindex!: string | null;
|
||||||
|
|
||||||
title: string | null;
|
/** Non-null asserted. */
|
||||||
|
title!: string | null;
|
||||||
/**
|
/**
|
||||||
* The `tagName` for the field
|
* The `tagName` for the field
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
/**
|
/**
|
||||||
* Represents an HTML form whose elements can be autofilled
|
* Represents an HTML form whose elements can be autofilled
|
||||||
*/
|
*/
|
||||||
export default class AutofillForm {
|
export default class AutofillForm {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unique identifier assigned to this field during collection of the page details
|
* Non-null asserted. The unique identifier assigned to this field during collection of the page details
|
||||||
*/
|
*/
|
||||||
opid: string;
|
opid!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML `name` attribute of the form field
|
* Non-null asserted. The HTML `name` attribute of the form field
|
||||||
*/
|
*/
|
||||||
htmlName: string;
|
htmlName!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML `id` attribute of the form field
|
* Non-null asserted. The HTML `id` attribute of the form field
|
||||||
*/
|
*/
|
||||||
htmlID: string;
|
htmlID!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML `action` attribute of the form field
|
* Non-null asserted. The HTML `action` attribute of the form field
|
||||||
*/
|
*/
|
||||||
htmlAction: string;
|
htmlAction!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML `method` attribute of the form field
|
* Non-null asserted. The HTML `method` attribute of the form field.
|
||||||
*/
|
*/
|
||||||
htmlMethod: string;
|
htmlMethod!: "get" | "post" | string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import AutofillField from "./autofill-field";
|
import AutofillField from "./autofill-field";
|
||||||
import AutofillForm from "./autofill-form";
|
import AutofillForm from "./autofill-form";
|
||||||
|
|
||||||
@@ -7,16 +5,20 @@ import AutofillForm from "./autofill-form";
|
|||||||
* The details of a page that have been collected and can be used for autofill
|
* The details of a page that have been collected and can be used for autofill
|
||||||
*/
|
*/
|
||||||
export default class AutofillPageDetails {
|
export default class AutofillPageDetails {
|
||||||
title: string;
|
/** Non-null asserted. */
|
||||||
url: string;
|
title!: string;
|
||||||
documentUrl: string;
|
/** Non-null asserted. */
|
||||||
|
url!: string;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
documentUrl!: string;
|
||||||
/**
|
/**
|
||||||
* A collection of all of the forms in the page DOM, keyed by their `opid`
|
* Non-null asserted. A collection of all of the forms in the page DOM, keyed by their `opid`
|
||||||
*/
|
*/
|
||||||
forms: { [id: string]: AutofillForm };
|
forms!: { [id: string]: AutofillForm };
|
||||||
/**
|
/**
|
||||||
* A collection of all the fields in the page DOM, keyed by their `opid`
|
* Non-null asserted. A collection of all the fields in the page DOM, keyed by their `opid`
|
||||||
*/
|
*/
|
||||||
fields: AutofillField[];
|
fields!: AutofillField[];
|
||||||
collectedTimestamp: number;
|
/** Non-null asserted. */
|
||||||
|
collectedTimestamp!: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
// String values affect code flow in autofill.ts and must not be changed
|
|
||||||
export type FillScriptActions = "click_on_opid" | "focus_by_opid" | "fill_by_opid";
|
|
||||||
|
|
||||||
export type FillScript = [action: FillScriptActions, opid: string, value?: string];
|
export type FillScript = [action: FillScriptActions, opid: string, value?: string];
|
||||||
|
|
||||||
export type AutofillScriptProperties = {
|
export type AutofillScriptProperties = {
|
||||||
delay_between_operations?: number;
|
delay_between_operations?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FillScriptActionTypes = {
|
||||||
|
fill_by_opid: "fill_by_opid",
|
||||||
|
click_on_opid: "click_on_opid",
|
||||||
|
focus_by_opid: "focus_by_opid",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// String values affect code flow in autofill.ts and must not be changed
|
||||||
|
export type FillScriptActions = keyof typeof FillScriptActionTypes;
|
||||||
|
|
||||||
export type AutofillInsertActions = {
|
export type AutofillInsertActions = {
|
||||||
fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void;
|
[FillScriptActionTypes.fill_by_opid]: ({ opid, value }: { opid: string; value: string }) => void;
|
||||||
click_on_opid: ({ opid }: { opid: string }) => void;
|
[FillScriptActionTypes.click_on_opid]: ({ opid }: { opid: string }) => void;
|
||||||
focus_by_opid: ({ opid }: { opid: string }) => void;
|
[FillScriptActionTypes.focus_by_opid]: ({ opid }: { opid: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AutofillScript {
|
export default class AutofillScript {
|
||||||
script: FillScript[] = [];
|
script: FillScript[] = [];
|
||||||
properties: AutofillScriptProperties = {};
|
properties: AutofillScriptProperties = {};
|
||||||
metadata: any = {}; // Unused, not written or read
|
/** Non-null asserted. */
|
||||||
autosubmit: string[]; // Appears to be unused, read but not written
|
autosubmit!: string[] | null; // Appears to be unused, read but not written
|
||||||
savedUrls: string[];
|
/** Non-null asserted. */
|
||||||
untrustedIframe: boolean;
|
savedUrls!: string[];
|
||||||
itemType: string; // Appears to be unused, read but not written
|
/** Non-null asserted. */
|
||||||
|
untrustedIframe!: boolean;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
itemType!: string; // Appears to be unused, read but not written
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import "@webcomponents/custom-elements";
|
import "@webcomponents/custom-elements";
|
||||||
import "lit/polyfill-support.js";
|
import "lit/polyfill-support.js";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -103,7 +101,10 @@ export class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
|
|||||||
*/
|
*/
|
||||||
private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) {
|
private updatePageColorScheme({ colorScheme }: AutofillInlineMenuButtonMessage) {
|
||||||
const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']");
|
const colorSchemeMetaTag = globalThis.document.querySelector("meta[name='color-scheme']");
|
||||||
colorSchemeMetaTag?.setAttribute("content", colorScheme);
|
|
||||||
|
if (colorSchemeMetaTag && colorScheme) {
|
||||||
|
colorSchemeMetaTag.setAttribute("content", colorScheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import { setElementStyles } from "../../../../utils";
|
import { setElementStyles } from "../../../../utils";
|
||||||
@@ -14,8 +12,10 @@ export class AutofillInlineMenuContainer {
|
|||||||
private readonly setElementStyles = setElementStyles;
|
private readonly setElementStyles = setElementStyles;
|
||||||
private readonly extensionOriginsSet: Set<string>;
|
private readonly extensionOriginsSet: Set<string>;
|
||||||
private port: chrome.runtime.Port | null = null;
|
private port: chrome.runtime.Port | null = null;
|
||||||
private portName: string;
|
/** Non-null asserted. */
|
||||||
private inlineMenuPageIframe: HTMLIFrameElement;
|
private portName!: string;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
private inlineMenuPageIframe!: HTMLIFrameElement;
|
||||||
private readonly iframeStyles: Partial<CSSStyleDeclaration> = {
|
private readonly iframeStyles: Partial<CSSStyleDeclaration> = {
|
||||||
all: "initial",
|
all: "initial",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
@@ -42,8 +42,10 @@ export class AutofillInlineMenuContainer {
|
|||||||
tabIndex: "-1",
|
tabIndex: "-1",
|
||||||
};
|
};
|
||||||
private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = {
|
private readonly windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = {
|
||||||
initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message),
|
initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) =>
|
||||||
initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message),
|
this.handleInitInlineMenuIframe(message),
|
||||||
|
initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) =>
|
||||||
|
this.handleInitInlineMenuIframe(message),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -116,14 +118,20 @@ export class AutofillInlineMenuContainer {
|
|||||||
*
|
*
|
||||||
* @param event - The message event.
|
* @param event - The message event.
|
||||||
*/
|
*/
|
||||||
private handleWindowMessage = (event: MessageEvent) => {
|
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
if (this.isForeignWindowMessage(event)) {
|
if (this.isForeignWindowMessage(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.windowMessageHandlers[message.command]) {
|
if (
|
||||||
this.windowMessageHandlers[message.command](message);
|
this.windowMessageHandlers[
|
||||||
|
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
this.windowMessageHandlers[
|
||||||
|
message.command as keyof AutofillInlineMenuContainerWindowMessageHandlers
|
||||||
|
](message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +150,8 @@ export class AutofillInlineMenuContainer {
|
|||||||
*
|
*
|
||||||
* @param event - The message event.
|
* @param event - The message event.
|
||||||
*/
|
*/
|
||||||
private isForeignWindowMessage(event: MessageEvent) {
|
private isForeignWindowMessage(event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) {
|
||||||
if (!event.data.portKey) {
|
if (!event.data?.portKey) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +167,9 @@ export class AutofillInlineMenuContainer {
|
|||||||
*
|
*
|
||||||
* @param event - The message event.
|
* @param event - The message event.
|
||||||
*/
|
*/
|
||||||
private isMessageFromParentWindow(event: MessageEvent): boolean {
|
private isMessageFromParentWindow(
|
||||||
|
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
|
||||||
|
): boolean {
|
||||||
return globalThis.parent === event.source;
|
return globalThis.parent === event.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +178,9 @@ export class AutofillInlineMenuContainer {
|
|||||||
*
|
*
|
||||||
* @param event - The message event.
|
* @param event - The message event.
|
||||||
*/
|
*/
|
||||||
private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean {
|
private isMessageFromInlineMenuPageIframe(
|
||||||
|
event: MessageEvent<AutofillInlineMenuContainerWindowMessage>,
|
||||||
|
): boolean {
|
||||||
if (!this.inlineMenuPageIframe) {
|
if (!this.inlineMenuPageIframe) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
|
import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
|
||||||
@@ -10,10 +8,14 @@ import {
|
|||||||
|
|
||||||
export class AutofillInlineMenuPageElement extends HTMLElement {
|
export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||||
protected shadowDom: ShadowRoot;
|
protected shadowDom: ShadowRoot;
|
||||||
protected messageOrigin: string;
|
/** Non-null asserted. */
|
||||||
protected translations: Record<string, string>;
|
protected messageOrigin!: string;
|
||||||
private portKey: string;
|
/** Non-null asserted. */
|
||||||
protected windowMessageHandlers: AutofillInlineMenuPageElementWindowMessageHandlers;
|
protected translations!: Record<string, string>;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
private portKey!: string;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
protected windowMessageHandlers!: AutofillInlineMenuPageElementWindowMessageHandlers;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe("OverlayNotificationsContentService", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn());
|
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(async () => null);
|
||||||
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
|
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
|
||||||
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
|
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
|
||||||
domQueryService = mock<DomQueryService>();
|
domQueryService = mock<DomQueryService>();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
@@ -69,7 +67,7 @@ export class Fido2UseBrowserLinkComponent {
|
|||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
"",
|
||||||
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
|
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,13 +155,15 @@ export class AutofillComponent implements OnInit {
|
|||||||
autofillOnPageLoadOptions: { name: string; value: boolean }[];
|
autofillOnPageLoadOptions: { name: string; value: boolean }[];
|
||||||
enableContextMenuItem: boolean = false;
|
enableContextMenuItem: boolean = false;
|
||||||
enableAutoTotpCopy: boolean = false;
|
enableAutoTotpCopy: boolean = false;
|
||||||
clearClipboard: ClearClipboardDelaySetting;
|
/** Non-null asserted. */
|
||||||
|
clearClipboard!: ClearClipboardDelaySetting;
|
||||||
clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[];
|
clearClipboardOptions: { name: string; value: ClearClipboardDelaySetting }[];
|
||||||
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||||
uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[];
|
uriMatchOptions: { name: string; value: UriMatchStrategySetting; disabled?: boolean }[];
|
||||||
showCardsCurrentTab: boolean = true;
|
showCardsCurrentTab: boolean = true;
|
||||||
showIdentitiesCurrentTab: boolean = true;
|
showIdentitiesCurrentTab: boolean = true;
|
||||||
autofillKeyboardHelperText: string;
|
/** Non-null asserted. */
|
||||||
|
autofillKeyboardHelperText!: string;
|
||||||
accountSwitcherEnabled: boolean = false;
|
accountSwitcherEnabled: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
|
|||||||
destroyAutofillInlineMenuListeners: () => void;
|
destroyAutofillInlineMenuListeners: () => void;
|
||||||
getInlineMenuFormFieldData: ({
|
getInlineMenuFormFieldData: ({
|
||||||
message,
|
message,
|
||||||
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
|
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData | void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AutofillOverlayContentService {
|
export interface AutofillOverlayContentService {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
||||||
@@ -64,29 +62,39 @@ export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND =
|
|||||||
);
|
);
|
||||||
|
|
||||||
export abstract class AutofillService {
|
export abstract class AutofillService {
|
||||||
collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
/** Non-null asserted. */
|
||||||
loadAutofillScriptsOnInstall: () => Promise<void>;
|
collectPageDetailsFromTab$!: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
||||||
reloadAutofillScripts: () => Promise<void>;
|
/** Non-null asserted. */
|
||||||
injectAutofillScripts: (
|
loadAutofillScriptsOnInstall!: () => Promise<void>;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
reloadAutofillScripts!: () => Promise<void>;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
injectAutofillScripts!: (
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
frameId?: number,
|
frameId?: number,
|
||||||
triggeringOnPageLoad?: boolean,
|
triggeringOnPageLoad?: boolean,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
|
/** Non-null asserted. */
|
||||||
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
|
getFormsWithPasswordFields!: (pageDetails: AutofillPageDetails) => FormData[];
|
||||||
doAutoFillOnTab: (
|
/** Non-null asserted. */
|
||||||
|
doAutoFill!: (options: AutoFillOptions) => Promise<string | null>;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
doAutoFillOnTab!: (
|
||||||
pageDetails: PageDetail[],
|
pageDetails: PageDetail[],
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
fromCommand: boolean,
|
fromCommand: boolean,
|
||||||
autoSubmitLogin?: boolean,
|
autoSubmitLogin?: boolean,
|
||||||
) => Promise<string | null>;
|
) => Promise<string | null>;
|
||||||
doAutoFillActiveTab: (
|
/** Non-null asserted. */
|
||||||
|
doAutoFillActiveTab!: (
|
||||||
pageDetails: PageDetail[],
|
pageDetails: PageDetail[],
|
||||||
fromCommand: boolean,
|
fromCommand: boolean,
|
||||||
cipherType?: CipherType,
|
cipherType?: CipherType,
|
||||||
) => Promise<string | null>;
|
) => Promise<string | null>;
|
||||||
setAutoFillOnPageLoadOrgPolicy: () => Promise<void>;
|
/** Non-null asserted. */
|
||||||
isPasswordRepromptRequired: (
|
setAutoFillOnPageLoadOrgPolicy!: () => Promise<void>;
|
||||||
|
/** Non-null asserted. */
|
||||||
|
isPasswordRepromptRequired!: (
|
||||||
cipher: CipherView,
|
cipher: CipherView,
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
action?: string,
|
action?: string,
|
||||||
|
|||||||
@@ -369,9 +369,7 @@ describe("AutofillService", () => {
|
|||||||
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void autofillService.reloadAutofillScripts();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
autofillService.reloadAutofillScripts();
|
|
||||||
|
|
||||||
expect(port1.disconnect).toHaveBeenCalled();
|
expect(port1.disconnect).toHaveBeenCalled();
|
||||||
expect(port2.disconnect).toHaveBeenCalled();
|
expect(port2.disconnect).toHaveBeenCalled();
|
||||||
@@ -680,7 +678,9 @@ describe("AutofillService", () => {
|
|||||||
await autofillService.doAutoFill(autofillOptions);
|
await autofillService.doAutoFill(autofillOptions);
|
||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(nothingToAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(nothingToAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -691,7 +691,9 @@ describe("AutofillService", () => {
|
|||||||
await autofillService.doAutoFill(autofillOptions);
|
await autofillService.doAutoFill(autofillOptions);
|
||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(nothingToAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(nothingToAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -702,7 +704,9 @@ describe("AutofillService", () => {
|
|||||||
await autofillService.doAutoFill(autofillOptions);
|
await autofillService.doAutoFill(autofillOptions);
|
||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(nothingToAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(nothingToAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -713,7 +717,9 @@ describe("AutofillService", () => {
|
|||||||
await autofillService.doAutoFill(autofillOptions);
|
await autofillService.doAutoFill(autofillOptions);
|
||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(nothingToAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(nothingToAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -727,7 +733,9 @@ describe("AutofillService", () => {
|
|||||||
await autofillService.doAutoFill(autofillOptions);
|
await autofillService.doAutoFill(autofillOptions);
|
||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(didNotAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(didNotAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -766,7 +774,6 @@ describe("AutofillService", () => {
|
|||||||
{
|
{
|
||||||
command: "fillForm",
|
command: "fillForm",
|
||||||
fillScript: {
|
fillScript: {
|
||||||
metadata: {},
|
|
||||||
properties: {
|
properties: {
|
||||||
delay_between_operations: 20,
|
delay_between_operations: 20,
|
||||||
},
|
},
|
||||||
@@ -863,7 +870,9 @@ describe("AutofillService", () => {
|
|||||||
expect(logService.info).toHaveBeenCalledWith(
|
expect(logService.info).toHaveBeenCalledWith(
|
||||||
"Autofill on page load was blocked due to an untrusted iframe.",
|
"Autofill on page load was blocked due to an untrusted iframe.",
|
||||||
);
|
);
|
||||||
expect(error.message).toBe(didNotAutofillError);
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(didNotAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -898,7 +907,10 @@ describe("AutofillService", () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(autofillService["generateFillScript"]).toHaveBeenCalled();
|
expect(autofillService["generateFillScript"]).toHaveBeenCalled();
|
||||||
expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled();
|
expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled();
|
||||||
expect(error.message).toBe(didNotAutofillError);
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe(didNotAutofillError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1370,7 +1382,10 @@ describe("AutofillService", () => {
|
|||||||
triggerTestFailure();
|
triggerTestFailure();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
|
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
|
||||||
expect(error.message).toBe("No tab found.");
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
expect(error.message).toBe("No tab found.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1610,7 +1625,6 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith(
|
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
metadata: {},
|
|
||||||
properties: {},
|
properties: {},
|
||||||
script: [
|
script: [
|
||||||
["click_on_opid", "username-field"],
|
["click_on_opid", "username-field"],
|
||||||
@@ -1648,7 +1662,6 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith(
|
expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
metadata: {},
|
|
||||||
properties: {},
|
properties: {},
|
||||||
script: [
|
script: [
|
||||||
["click_on_opid", "username-field"],
|
["click_on_opid", "username-field"],
|
||||||
@@ -1686,7 +1699,6 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith(
|
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
metadata: {},
|
|
||||||
properties: {},
|
properties: {},
|
||||||
script: [
|
script: [
|
||||||
["click_on_opid", "username-field"],
|
["click_on_opid", "username-field"],
|
||||||
@@ -2279,7 +2291,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
expect(value).toStrictEqual({
|
expect(value).toStrictEqual({
|
||||||
autosubmit: null,
|
autosubmit: null,
|
||||||
metadata: {},
|
itemType: "",
|
||||||
properties: { delay_between_operations: 20 },
|
properties: { delay_between_operations: 20 },
|
||||||
savedUrls: ["https://www.example.com"],
|
savedUrls: ["https://www.example.com"],
|
||||||
script: [
|
script: [
|
||||||
@@ -2294,7 +2306,6 @@ describe("AutofillService", () => {
|
|||||||
["fill_by_opid", "password", "password"],
|
["fill_by_opid", "password", "password"],
|
||||||
["focus_by_opid", "password"],
|
["focus_by_opid", "password"],
|
||||||
],
|
],
|
||||||
itemType: "",
|
|
||||||
untrustedIframe: false,
|
untrustedIframe: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2364,11 +2375,10 @@ describe("AutofillService", () => {
|
|||||||
describe("given an invalid autofill field", () => {
|
describe("given an invalid autofill field", () => {
|
||||||
const unmodifiedFillScriptValues: AutofillScript = {
|
const unmodifiedFillScriptValues: AutofillScript = {
|
||||||
autosubmit: null,
|
autosubmit: null,
|
||||||
metadata: {},
|
itemType: "",
|
||||||
properties: { delay_between_operations: 20 },
|
properties: { delay_between_operations: 20 },
|
||||||
savedUrls: [],
|
savedUrls: [],
|
||||||
script: [],
|
script: [],
|
||||||
itemType: "",
|
|
||||||
untrustedIframe: false,
|
untrustedIframe: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2555,7 +2565,6 @@ describe("AutofillService", () => {
|
|||||||
expect(value).toStrictEqual({
|
expect(value).toStrictEqual({
|
||||||
autosubmit: null,
|
autosubmit: null,
|
||||||
itemType: "",
|
itemType: "",
|
||||||
metadata: {},
|
|
||||||
properties: {
|
properties: {
|
||||||
delay_between_operations: 20,
|
delay_between_operations: 20,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
|
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
|
||||||
import { FillableFormFieldElement, FormFieldElement } from "../types";
|
import { FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
|
|
||||||
@@ -202,7 +200,7 @@ class DomElementVisibilityService implements DomElementVisibilityServiceInterfac
|
|||||||
|
|
||||||
const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label");
|
const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label");
|
||||||
|
|
||||||
return targetElementLabelsSet.has(closestParentLabel);
|
return closestParentLabel ? targetElementLabelsSet.has(closestParentLabel) : false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { EVENTS, MAX_DEEP_QUERY_RECURSION_DEPTH } from "@bitwarden/common/autofill/constants";
|
import { EVENTS, MAX_DEEP_QUERY_RECURSION_DEPTH } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import { nodeIsElement } from "../utils";
|
import { nodeIsElement } from "../utils";
|
||||||
@@ -7,7 +5,8 @@ import { nodeIsElement } from "../utils";
|
|||||||
import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service";
|
import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service";
|
||||||
|
|
||||||
export class DomQueryService implements DomQueryServiceInterface {
|
export class DomQueryService implements DomQueryServiceInterface {
|
||||||
private pageContainsShadowDom: boolean;
|
/** Non-null asserted. */
|
||||||
|
private pageContainsShadowDom!: boolean;
|
||||||
private ignoredTreeWalkerNodes = new Set([
|
private ignoredTreeWalkerNodes = new Set([
|
||||||
"svg",
|
"svg",
|
||||||
"script",
|
"script",
|
||||||
@@ -217,13 +216,12 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
if ((chrome as any).dom?.openOrClosedShadowRoot) {
|
if ((chrome as any).dom?.openOrClosedShadowRoot) {
|
||||||
try {
|
try {
|
||||||
return (chrome as any).dom.openOrClosedShadowRoot(node);
|
return (chrome as any).dom.openOrClosedShadowRoot(node);
|
||||||
// FIXME: Remove when updating file. Eslint update
|
} catch {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Firefox-specific equivalent of `openOrClosedShadowRoot`
|
||||||
return (node as any).openOrClosedShadowRoot;
|
return (node as any).openOrClosedShadowRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +274,7 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
? NodeFilter.FILTER_REJECT
|
? NodeFilter.FILTER_REJECT
|
||||||
: NodeFilter.FILTER_ACCEPT,
|
: NodeFilter.FILTER_ACCEPT,
|
||||||
);
|
);
|
||||||
let currentNode = treeWalker?.currentNode;
|
let currentNode: Node | null = treeWalker?.currentNode;
|
||||||
|
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
if (filterCallback(currentNode)) {
|
if (filterCallback(currentNode)) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import AutofillField from "../models/autofill-field";
|
import AutofillField from "../models/autofill-field";
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
|
import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
|
||||||
@@ -162,12 +160,14 @@ export class InlineMenuFieldQualificationService
|
|||||||
private isExplicitIdentityEmailField(field: AutofillField): boolean {
|
private isExplicitIdentityEmailField(field: AutofillField): boolean {
|
||||||
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
||||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||||
if (!matchFieldAttributeValues[attrIndex]) {
|
const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
|
||||||
|
|
||||||
|
if (!attributeValueToMatch) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
|
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
|
||||||
if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) {
|
if (this.newEmailFieldKeywords.has(attributeValueToMatch)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,10 +210,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
void Promise.all([
|
void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => {
|
||||||
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
|
|
||||||
sendExtensionMessage("getUserPremiumStatus"),
|
|
||||||
]).then(([fieldQualificationFlag, premiumStatus]) => {
|
|
||||||
this.premiumEnabled = !!premiumStatus?.result;
|
this.premiumEnabled = !!premiumStatus?.result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -263,7 +260,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentForm = pageDetails.forms[field.form];
|
let parentForm;
|
||||||
|
|
||||||
|
const fieldForm = field.form;
|
||||||
|
|
||||||
|
if (fieldForm) {
|
||||||
|
parentForm = pageDetails.forms[fieldForm];
|
||||||
|
}
|
||||||
|
|
||||||
// If the field does not have a parent form
|
// If the field does not have a parent form
|
||||||
if (!parentForm) {
|
if (!parentForm) {
|
||||||
@@ -321,7 +324,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentForm = pageDetails.forms[field.form];
|
let parentForm;
|
||||||
|
|
||||||
|
const fieldForm = field.form;
|
||||||
|
|
||||||
|
if (fieldForm) {
|
||||||
|
parentForm = pageDetails.forms[fieldForm];
|
||||||
|
}
|
||||||
|
|
||||||
if (!parentForm) {
|
if (!parentForm) {
|
||||||
// If the field does not have a parent form, but we can identify that the page contains at least
|
// If the field does not have a parent form, but we can identify that the page contains at least
|
||||||
@@ -374,7 +383,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
field: AutofillField,
|
field: AutofillField,
|
||||||
pageDetails: AutofillPageDetails,
|
pageDetails: AutofillPageDetails,
|
||||||
): boolean {
|
): boolean {
|
||||||
const parentForm = pageDetails.forms[field.form];
|
let parentForm;
|
||||||
|
|
||||||
|
const fieldForm = field.form;
|
||||||
|
|
||||||
|
if (fieldForm) {
|
||||||
|
parentForm = pageDetails.forms[fieldForm];
|
||||||
|
}
|
||||||
|
|
||||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||||
@@ -476,7 +491,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
// If the field is not explicitly set as a username field, we need to qualify
|
// If the field is not explicitly set as a username field, we need to qualify
|
||||||
// the field based on the other fields that are present on the page.
|
// the field based on the other fields that are present on the page.
|
||||||
const parentForm = pageDetails.forms[field.form];
|
let parentForm;
|
||||||
|
|
||||||
|
const fieldForm = field.form;
|
||||||
|
|
||||||
|
if (fieldForm) {
|
||||||
|
parentForm = pageDetails.forms[fieldForm];
|
||||||
|
}
|
||||||
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
|
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
|
||||||
|
|
||||||
if (this.isNewsletterForm(parentForm)) {
|
if (this.isNewsletterForm(parentForm)) {
|
||||||
@@ -919,8 +940,10 @@ export class InlineMenuFieldQualificationService
|
|||||||
* @param field - The field to validate
|
* @param field - The field to validate
|
||||||
*/
|
*/
|
||||||
isUsernameField = (field: AutofillField): boolean => {
|
isUsernameField = (field: AutofillField): boolean => {
|
||||||
|
const fieldType = field.type;
|
||||||
if (
|
if (
|
||||||
!this.usernameFieldTypes.has(field.type) ||
|
!fieldType ||
|
||||||
|
!this.usernameFieldTypes.has(fieldType) ||
|
||||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
||||||
this.fieldHasDisqualifyingAttributeValue(field)
|
this.fieldHasDisqualifyingAttributeValue(field)
|
||||||
) {
|
) {
|
||||||
@@ -1026,7 +1049,13 @@ export class InlineMenuFieldQualificationService
|
|||||||
|
|
||||||
const testedValues = [field.htmlID, field.htmlName, field.placeholder];
|
const testedValues = [field.htmlID, field.htmlName, field.placeholder];
|
||||||
for (let i = 0; i < testedValues.length; i++) {
|
for (let i = 0; i < testedValues.length; i++) {
|
||||||
if (this.valueIsLikePassword(testedValues[i])) {
|
const attributeValueToMatch = testedValues[i];
|
||||||
|
|
||||||
|
if (!attributeValueToMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.valueIsLikePassword(attributeValueToMatch)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1101,7 +1130,9 @@ export class InlineMenuFieldQualificationService
|
|||||||
* @param excludedTypes - The set of excluded types
|
* @param excludedTypes - The set of excluded types
|
||||||
*/
|
*/
|
||||||
private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean {
|
private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean {
|
||||||
if (excludedTypes.has(field.type)) {
|
const fieldType = field.type;
|
||||||
|
|
||||||
|
if (fieldType && excludedTypes.has(fieldType)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,12 +1147,14 @@ export class InlineMenuFieldQualificationService
|
|||||||
private isSearchField(field: AutofillField): boolean {
|
private isSearchField(field: AutofillField): boolean {
|
||||||
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
||||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||||
if (!matchFieldAttributeValues[attrIndex]) {
|
const attributeValueToMatch = matchFieldAttributeValues[attrIndex];
|
||||||
|
|
||||||
|
if (!attributeValueToMatch) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate camel case words and case them to lower case values
|
// Separate camel case words and case them to lower case values
|
||||||
const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex]
|
const camelCaseSeparatedFieldAttribute = attributeValueToMatch
|
||||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
// Split the attribute by non-alphabetical characters to get the keywords
|
// Split the attribute by non-alphabetical characters to get the keywords
|
||||||
@@ -1168,7 +1201,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(","));
|
this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.submitButtonKeywordsMap.get(element);
|
return this.submitButtonKeywordsMap.get(element) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1222,8 +1255,9 @@ export class InlineMenuFieldQualificationService
|
|||||||
];
|
];
|
||||||
const keywordsSet = new Set<string>();
|
const keywordsSet = new Set<string>();
|
||||||
for (let i = 0; i < keywords.length; i++) {
|
for (let i = 0; i < keywords.length; i++) {
|
||||||
if (keywords[i] && typeof keywords[i] === "string") {
|
const attributeValue = keywords[i];
|
||||||
let keywordEl = keywords[i].toLowerCase();
|
if (attributeValue && typeof attributeValue === "string") {
|
||||||
|
let keywordEl = attributeValue.toLowerCase();
|
||||||
keywordsSet.add(keywordEl);
|
keywordsSet.add(keywordEl);
|
||||||
|
|
||||||
// Remove hyphens from all potential keywords, we want to treat these as a single word.
|
// Remove hyphens from all potential keywords, we want to treat these as a single word.
|
||||||
@@ -1253,7 +1287,7 @@ export class InlineMenuFieldQualificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
|
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
|
||||||
return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet;
|
return mapValues ? (returnStringValue ? mapValues.stringValue : mapValues.keywordsSet) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script";
|
import AutofillScript, { FillScript, FillScriptActionTypes } from "../models/autofill-script";
|
||||||
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
|
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
|
||||||
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
|
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
|
||||||
|
|
||||||
@@ -94,14 +94,13 @@ describe("InsertAutofillContentService", () => {
|
|||||||
);
|
);
|
||||||
fillScript = {
|
fillScript = {
|
||||||
script: [
|
script: [
|
||||||
["click_on_opid", "username"],
|
[FillScriptActionTypes.click_on_opid, "username"],
|
||||||
["focus_by_opid", "username"],
|
[FillScriptActionTypes.focus_by_opid, "username"],
|
||||||
["fill_by_opid", "username", "test"],
|
[FillScriptActionTypes.fill_by_opid, "username", "test"],
|
||||||
],
|
],
|
||||||
properties: {
|
properties: {
|
||||||
delay_between_operations: 20,
|
delay_between_operations: 20,
|
||||||
},
|
},
|
||||||
metadata: {},
|
|
||||||
autosubmit: [],
|
autosubmit: [],
|
||||||
savedUrls: ["https://bitwarden.com"],
|
savedUrls: ["https://bitwarden.com"],
|
||||||
untrustedIframe: false,
|
untrustedIframe: false,
|
||||||
@@ -221,17 +220,14 @@ describe("InsertAutofillContentService", () => {
|
|||||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
fillScript.script[0],
|
fillScript.script[0],
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
fillScript.script[1],
|
fillScript.script[1],
|
||||||
1,
|
|
||||||
);
|
);
|
||||||
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith(
|
||||||
3,
|
3,
|
||||||
fillScript.script[2],
|
fillScript.script[2],
|
||||||
2,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -376,42 +372,62 @@ describe("InsertAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns early if no opid is provided", async () => {
|
it("returns early if no opid is provided", async () => {
|
||||||
const action = "fill_by_opid";
|
const action = FillScriptActionTypes.fill_by_opid;
|
||||||
const opid = "";
|
const opid = "";
|
||||||
const value = "value";
|
const value = "value";
|
||||||
const scriptAction: FillScript = [action, opid, value];
|
const scriptAction: FillScript = [action, opid, value];
|
||||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||||
|
|
||||||
await insertAutofillContentService["runFillScriptAction"](scriptAction, 0);
|
await insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||||
jest.advanceTimersByTime(20);
|
jest.advanceTimersByTime(20);
|
||||||
|
|
||||||
expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled();
|
expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("given a valid fill script action and opid", () => {
|
describe("given a valid fill script action and opid", () => {
|
||||||
const fillScriptActions: FillScriptActions[] = [
|
it(`triggers a fill_by_opid action`, () => {
|
||||||
"fill_by_opid",
|
const action = FillScriptActionTypes.fill_by_opid;
|
||||||
"click_on_opid",
|
const opid = "opid";
|
||||||
"focus_by_opid",
|
const value = "value";
|
||||||
];
|
const scriptAction: FillScript = [action, opid, value];
|
||||||
fillScriptActions.forEach((action) => {
|
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||||
it(`triggers a ${action} action`, () => {
|
|
||||||
const opid = "opid";
|
|
||||||
const value = "value";
|
|
||||||
const scriptAction: FillScript = [action, opid, value];
|
|
||||||
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
jest.advanceTimersByTime(20);
|
||||||
insertAutofillContentService["runFillScriptAction"](scriptAction, 0);
|
|
||||||
jest.advanceTimersByTime(20);
|
|
||||||
|
|
||||||
expect(
|
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||||
insertAutofillContentService["autofillInsertActions"][action],
|
opid,
|
||||||
).toHaveBeenCalledWith({
|
value,
|
||||||
opid,
|
});
|
||||||
value,
|
});
|
||||||
});
|
|
||||||
|
it(`triggers a click_on_opid action`, () => {
|
||||||
|
const action = FillScriptActionTypes.click_on_opid;
|
||||||
|
const opid = "opid";
|
||||||
|
const value = "value";
|
||||||
|
const scriptAction: FillScript = [action, opid, value];
|
||||||
|
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||||
|
|
||||||
|
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||||
|
jest.advanceTimersByTime(20);
|
||||||
|
|
||||||
|
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||||
|
opid,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`triggers a focus_by_opid action`, () => {
|
||||||
|
const action = FillScriptActionTypes.focus_by_opid;
|
||||||
|
const opid = "opid";
|
||||||
|
const value = "value";
|
||||||
|
const scriptAction: FillScript = [action, opid, value];
|
||||||
|
jest.spyOn(insertAutofillContentService["autofillInsertActions"], action);
|
||||||
|
|
||||||
|
void insertAutofillContentService["runFillScriptAction"](scriptAction);
|
||||||
|
jest.advanceTimersByTime(20);
|
||||||
|
|
||||||
|
expect(insertAutofillContentService["autofillInsertActions"][action]).toHaveBeenCalledWith({
|
||||||
|
opid,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
|
import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script";
|
import AutofillScript, {
|
||||||
|
AutofillInsertActions,
|
||||||
|
FillScript,
|
||||||
|
FillScriptActionTypes,
|
||||||
|
} from "../models/autofill-script";
|
||||||
import { FormFieldElement } from "../types";
|
import { FormFieldElement } from "../types";
|
||||||
import {
|
import {
|
||||||
currentlyInSandboxedIframe,
|
currentlyInSandboxedIframe,
|
||||||
@@ -50,7 +52,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let index = 0; index < fillScript.script.length; index++) {
|
for (let index = 0; index < fillScript.script.length; index++) {
|
||||||
await this.runFillScriptAction(fillScript.script[index], index);
|
await this.runFillScriptAction(fillScript.script[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,25 +118,26 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
/**
|
/**
|
||||||
* Runs the autofill action based on the action type and the opid.
|
* Runs the autofill action based on the action type and the opid.
|
||||||
* Each action is subsequently delayed by 20 milliseconds.
|
* Each action is subsequently delayed by 20 milliseconds.
|
||||||
* @param {"click_on_opid" | "focus_by_opid" | "fill_by_opid"} action
|
* @param {FillScript} [action, opid, value]
|
||||||
* @param {string} opid
|
|
||||||
* @param {string} value
|
|
||||||
* @param {number} actionIndex
|
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private runFillScriptAction = (
|
private runFillScriptAction = ([action, opid, value]: FillScript): Promise<void> => {
|
||||||
[action, opid, value]: FillScript,
|
|
||||||
actionIndex: number,
|
|
||||||
): Promise<void> => {
|
|
||||||
if (!opid || !this.autofillInsertActions[action]) {
|
if (!opid || !this.autofillInsertActions[action]) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const delayActionsInMilliseconds = 20;
|
const delayActionsInMilliseconds = 20;
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.autofillInsertActions[action]({ opid, value });
|
if (action === FillScriptActionTypes.fill_by_opid && !!value?.length) {
|
||||||
|
this.autofillInsertActions.fill_by_opid({ opid, value });
|
||||||
|
} else if (action === FillScriptActionTypes.click_on_opid) {
|
||||||
|
this.autofillInsertActions.click_on_opid({ opid });
|
||||||
|
} else if (action === FillScriptActionTypes.focus_by_opid) {
|
||||||
|
this.autofillInsertActions.focus_by_opid({ opid });
|
||||||
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
}, delayActionsInMilliseconds),
|
}, delayActionsInMilliseconds),
|
||||||
);
|
);
|
||||||
@@ -158,7 +161,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
*/
|
*/
|
||||||
private handleClickOnFieldByOpidAction(opid: string) {
|
private handleClickOnFieldByOpidAction(opid: string) {
|
||||||
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
||||||
this.triggerClickOnElement(element);
|
|
||||||
|
if (element) {
|
||||||
|
this.triggerClickOnElement(element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,6 +177,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
private handleFocusOnFieldByOpidAction(opid: string) {
|
private handleFocusOnFieldByOpidAction(opid: string) {
|
||||||
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (document.activeElement === element) {
|
if (document.activeElement === element) {
|
||||||
element.blur();
|
element.blur();
|
||||||
}
|
}
|
||||||
@@ -187,6 +197,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private insertValueIntoField(element: FormFieldElement | null, value: string) {
|
private insertValueIntoField(element: FormFieldElement | null, value: string) {
|
||||||
|
if (!element || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const elementCanBeReadonly =
|
const elementCanBeReadonly =
|
||||||
elementIsInputElement(element) || elementIsTextAreaElement(element);
|
elementIsInputElement(element) || elementIsTextAreaElement(element);
|
||||||
const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element);
|
const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element);
|
||||||
@@ -195,8 +209,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value);
|
const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!element ||
|
|
||||||
!value ||
|
|
||||||
elementAlreadyHasTheValue ||
|
elementAlreadyHasTheValue ||
|
||||||
(elementCanBeReadonly && element.readOnly) ||
|
(elementCanBeReadonly && element.readOnly) ||
|
||||||
(elementCanBeFilled && element.disabled)
|
(elementCanBeFilled && element.disabled)
|
||||||
@@ -298,7 +310,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private triggerClickOnElement(element?: HTMLElement): void {
|
private triggerClickOnElement(element?: HTMLElement): void {
|
||||||
if (typeof element?.click !== TYPE_CHECK.FUNCTION) {
|
if (!element || typeof element.click !== TYPE_CHECK.FUNCTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +325,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void {
|
private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void {
|
||||||
if (typeof element?.focus !== TYPE_CHECK.FUNCTION) {
|
if (!element || typeof element.focus !== TYPE_CHECK.FUNCTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -144,7 +142,6 @@ export function createAutofillScriptMock(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
autosubmit: null,
|
autosubmit: null,
|
||||||
metadata: {},
|
|
||||||
properties: {
|
properties: {
|
||||||
delay_between_operations: 20,
|
delay_between_operations: 20,
|
||||||
},
|
},
|
||||||
@@ -299,7 +296,7 @@ export function createMutationRecordMock(customFields = {}): MutationRecord {
|
|||||||
oldValue: "default-oldValue",
|
oldValue: "default-oldValue",
|
||||||
previousSibling: null,
|
previousSibling: null,
|
||||||
removedNodes: mock<NodeList>(),
|
removedNodes: mock<NodeList>(),
|
||||||
target: null,
|
target: mock<Node>(),
|
||||||
type: "attributes",
|
type: "attributes",
|
||||||
...customFields,
|
...customFields,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user