mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-04 18:37:45 +01:00
[PM-3455] Allow adding and autofilling Cards and Identities via Context Menu (#6050)
* PoC autofill card and identity from context menu * PoC trigger identity and card autofills via messages * update card and identity cipher titles in the context menu * remove unused url argument from loadOptions * do not show no logins message for card and identity sub-menu * allow context menu actions to create identity or card ciphers * open new single-action windows for cipher creation when requested from the context menu * add context menu items for adding a login cipher when none are available to the page * adjust titles for Card and Identity context menu items * fix translations and add no ciphers available messages to submenus * cleanup and update tests * remove unrelated changes * pass uri of context menu page to cipher creation view * Apply suggestions from code review Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> * cleanup * handle cipher edit background messages with browserPopoutWindowService as well * consolidate doAutoFillNonLoginActiveTab into doAutoFillActiveTab * cleanup --------- Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
parent
be8f522aac
commit
a42cea8570
@ -91,6 +91,15 @@
|
|||||||
"autoFill": {
|
"autoFill": {
|
||||||
"message": "Auto-fill"
|
"message": "Auto-fill"
|
||||||
},
|
},
|
||||||
|
"autoFillLogin": {
|
||||||
|
"message": "Auto-fill login"
|
||||||
|
},
|
||||||
|
"autoFillCard": {
|
||||||
|
"message": "Auto-fill card"
|
||||||
|
},
|
||||||
|
"autoFillIdentity": {
|
||||||
|
"message": "Auto-fill identity"
|
||||||
|
},
|
||||||
"generatePasswordCopied": {
|
"generatePasswordCopied": {
|
||||||
"message": "Generate password (copied)"
|
"message": "Generate password (copied)"
|
||||||
},
|
},
|
||||||
@ -100,6 +109,21 @@
|
|||||||
"noMatchingLogins": {
|
"noMatchingLogins": {
|
||||||
"message": "No matching logins"
|
"message": "No matching logins"
|
||||||
},
|
},
|
||||||
|
"noCards": {
|
||||||
|
"message": "No cards"
|
||||||
|
},
|
||||||
|
"noIdentities": {
|
||||||
|
"message": "No identities"
|
||||||
|
},
|
||||||
|
"addLoginMenu": {
|
||||||
|
"message": "Add login"
|
||||||
|
},
|
||||||
|
"addCardMenu": {
|
||||||
|
"message": "Add card"
|
||||||
|
},
|
||||||
|
"addIdentityMenu": {
|
||||||
|
"message": "Add identity"
|
||||||
|
},
|
||||||
"unlockVaultMenu": {
|
"unlockVaultMenu": {
|
||||||
"message": "Unlock your vault"
|
"message": "Unlock your vault"
|
||||||
},
|
},
|
||||||
|
@ -69,19 +69,20 @@ describe("CipherContextMenuHandler", () => {
|
|||||||
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("only adds login ciphers including ciphers that require reprompt", async () => {
|
it("only adds autofill ciphers including ciphers that require reprompt", async () => {
|
||||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
mainContextMenuHandler.init.mockResolvedValue(true);
|
mainContextMenuHandler.init.mockResolvedValue(true);
|
||||||
|
|
||||||
const realCipher = {
|
const loginCipher = {
|
||||||
id: "5",
|
id: "5",
|
||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
name: "Test Cipher",
|
name: "Test Cipher",
|
||||||
login: { username: "Test Username" },
|
login: { username: "Test Username" },
|
||||||
};
|
};
|
||||||
const repromptCipher = {
|
|
||||||
|
const repromptLoginCipher = {
|
||||||
id: "6",
|
id: "6",
|
||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
reprompt: CipherRepromptType.Password,
|
reprompt: CipherRepromptType.Password,
|
||||||
@ -89,34 +90,49 @@ describe("CipherContextMenuHandler", () => {
|
|||||||
login: { username: "Test Username" },
|
login: { username: "Test Username" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cardCipher = {
|
||||||
|
id: "7",
|
||||||
|
type: CipherType.Card,
|
||||||
|
name: "Test Card Cipher",
|
||||||
|
card: { username: "Test Username" },
|
||||||
|
};
|
||||||
|
|
||||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([
|
cipherService.getAllDecryptedForUrl.mockResolvedValue([
|
||||||
null, // invalid cipher
|
null, // invalid cipher
|
||||||
undefined, // invalid cipher
|
undefined, // invalid cipher
|
||||||
{ type: CipherType.Card }, // invalid cipher
|
{ type: CipherType.SecureNote }, // invalid cipher
|
||||||
realCipher, // valid cipher
|
loginCipher, // valid cipher
|
||||||
repromptCipher,
|
repromptLoginCipher,
|
||||||
|
cardCipher, // valid cipher
|
||||||
] as any[]);
|
] as any[]);
|
||||||
|
|
||||||
await sut.update("https://test.com");
|
await sut.update("https://test.com");
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", [
|
||||||
|
CipherType.Card,
|
||||||
|
CipherType.Identity,
|
||||||
|
]);
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||||
"Test Cipher (Test Username)",
|
"Test Cipher (Test Username)",
|
||||||
"5",
|
"5",
|
||||||
"https://test.com",
|
loginCipher
|
||||||
realCipher
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||||
"Test Reprompt Cipher (Test Username)",
|
"Test Reprompt Cipher (Test Username)",
|
||||||
"6",
|
"6",
|
||||||
"https://test.com",
|
repromptLoginCipher
|
||||||
repromptCipher
|
);
|
||||||
|
|
||||||
|
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
|
||||||
|
"Test Card Cipher",
|
||||||
|
"7",
|
||||||
|
cardCipher
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
cipherServiceFactory,
|
cipherServiceFactory,
|
||||||
CipherServiceInitOptions,
|
CipherServiceInitOptions,
|
||||||
} from "../../vault/background/service_factories/cipher-service.factory";
|
} from "../../vault/background/service_factories/cipher-service.factory";
|
||||||
|
import { AutofillCipherTypeId } from "../types";
|
||||||
|
|
||||||
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
||||||
|
|
||||||
@ -159,29 +160,67 @@ export class CipherContextMenuHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(url);
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(url, [
|
||||||
|
CipherType.Card,
|
||||||
|
CipherType.Identity,
|
||||||
|
]);
|
||||||
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||||
|
|
||||||
if (ciphers.length === 0) {
|
const groupedCiphers: Record<AutofillCipherTypeId, CipherView[]> = ciphers.reduce(
|
||||||
await this.mainContextMenuHandler.noLogins(url);
|
(ciphersByType, cipher) => {
|
||||||
return;
|
if (!cipher?.type) {
|
||||||
|
return ciphersByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCiphersOfType = ciphersByType[cipher.type as AutofillCipherTypeId] || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ciphersByType,
|
||||||
|
[cipher.type]: [...existingCiphersOfType, cipher],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[CipherType.Login]: [],
|
||||||
|
[CipherType.Card]: [],
|
||||||
|
[CipherType.Identity]: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupedCiphers[CipherType.Login].length === 0) {
|
||||||
|
await this.mainContextMenuHandler.noLogins();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedCiphers[CipherType.Identity].length === 0) {
|
||||||
|
await this.mainContextMenuHandler.noIdentities();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedCiphers[CipherType.Card].length === 0) {
|
||||||
|
await this.mainContextMenuHandler.noCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cipher of ciphers) {
|
for (const cipher of ciphers) {
|
||||||
await this.updateForCipher(url, cipher);
|
await this.updateForCipher(cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateForCipher(url: string, cipher: CipherView) {
|
private async updateForCipher(cipher: CipherView) {
|
||||||
if (cipher == null || cipher.type !== CipherType.Login) {
|
if (
|
||||||
|
cipher == null ||
|
||||||
|
!new Set([CipherType.Login, CipherType.Card, CipherType.Identity]).has(cipher.type)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = cipher.name;
|
let title = cipher.name;
|
||||||
if (!Utils.isNullOrEmpty(title)) {
|
|
||||||
|
if (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(title) && cipher.login?.username) {
|
||||||
title += ` (${cipher.login.username})`;
|
title += ` (${cipher.login.username})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mainContextMenuHandler.loadOptions(title, cipher.id, url, cipher);
|
if (cipher.type === CipherType.Card && cipher.card?.subTitle) {
|
||||||
|
title += ` ${cipher.card.subTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.mainContextMenuHandler.loadOptions(title, cipher.id, cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,15 @@ import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
|||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AUTOFILL_ID,
|
||||||
|
COPY_PASSWORD_ID,
|
||||||
|
COPY_USERNAME_ID,
|
||||||
|
COPY_VERIFICATIONCODE_ID,
|
||||||
|
GENERATE_PASSWORD_ID,
|
||||||
|
NOOP_COMMAND_SUFFIX,
|
||||||
|
} from "../constants";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CopyToClipboardAction,
|
CopyToClipboardAction,
|
||||||
ContextMenuClickedHandler,
|
ContextMenuClickedHandler,
|
||||||
@ -17,13 +26,6 @@ import {
|
|||||||
GeneratePasswordToClipboardAction,
|
GeneratePasswordToClipboardAction,
|
||||||
AutofillAction,
|
AutofillAction,
|
||||||
} from "./context-menu-clicked-handler";
|
} from "./context-menu-clicked-handler";
|
||||||
import {
|
|
||||||
AUTOFILL_ID,
|
|
||||||
COPY_PASSWORD_ID,
|
|
||||||
COPY_USERNAME_ID,
|
|
||||||
COPY_VERIFICATIONCODE_ID,
|
|
||||||
GENERATE_PASSWORD_ID,
|
|
||||||
} from "./main-context-menu-handler";
|
|
||||||
|
|
||||||
describe("ContextMenuClickedHandler", () => {
|
describe("ContextMenuClickedHandler", () => {
|
||||||
const createData = (
|
const createData = (
|
||||||
@ -51,6 +53,7 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
type: CipherType.Login,
|
type: CipherType.Login,
|
||||||
} as any)
|
} as any)
|
||||||
);
|
);
|
||||||
|
|
||||||
cipherView.login.username = username ?? "USERNAME";
|
cipherView.login.username = username ?? "USERNAME";
|
||||||
cipherView.login.password = password ?? "PASSWORD";
|
cipherView.login.password = password ?? "PASSWORD";
|
||||||
cipherView.login.totp = totp ?? "TOTP";
|
cipherView.login.totp = totp ?? "TOTP";
|
||||||
@ -106,7 +109,7 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
const cipher = createCipher();
|
const cipher = createCipher();
|
||||||
cipherService.getAllDecrypted.mockResolvedValue([cipher]);
|
cipherService.getAllDecrypted.mockResolvedValue([cipher]);
|
||||||
|
|
||||||
await sut.run(createData("T_1", AUTOFILL_ID), { id: 5 } as any);
|
await sut.run(createData(`${AUTOFILL_ID}_1`, AUTOFILL_ID), { id: 5 } as any);
|
||||||
|
|
||||||
expect(autofill).toBeCalledTimes(1);
|
expect(autofill).toBeCalledTimes(1);
|
||||||
|
|
||||||
@ -118,11 +121,16 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
createCipher({ username: "TEST_USERNAME" }),
|
createCipher({ username: "TEST_USERNAME" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sut.run(createData("T_1", COPY_USERNAME_ID));
|
await sut.run(createData(`${COPY_USERNAME_ID}_1`, COPY_USERNAME_ID), {
|
||||||
|
url: "https://test.com",
|
||||||
|
} as any);
|
||||||
|
|
||||||
expect(copyToClipboard).toBeCalledTimes(1);
|
expect(copyToClipboard).toBeCalledTimes(1);
|
||||||
|
|
||||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_USERNAME", options: undefined });
|
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||||
|
text: "TEST_USERNAME",
|
||||||
|
tab: { url: "https://test.com" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies password to clipboard", async () => {
|
it("copies password to clipboard", async () => {
|
||||||
@ -130,11 +138,16 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
createCipher({ password: "TEST_PASSWORD" }),
|
createCipher({ password: "TEST_PASSWORD" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sut.run(createData("T_1", COPY_PASSWORD_ID));
|
await sut.run(createData(`${COPY_PASSWORD_ID}_1`, COPY_PASSWORD_ID), {
|
||||||
|
url: "https://test.com",
|
||||||
|
} as any);
|
||||||
|
|
||||||
expect(copyToClipboard).toBeCalledTimes(1);
|
expect(copyToClipboard).toBeCalledTimes(1);
|
||||||
|
|
||||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "TEST_PASSWORD", options: undefined });
|
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||||
|
text: "TEST_PASSWORD",
|
||||||
|
tab: { url: "https://test.com" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies totp code to clipboard", async () => {
|
it("copies totp code to clipboard", async () => {
|
||||||
@ -148,11 +161,16 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
return Promise.resolve("654321");
|
return Promise.resolve("654321");
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.run(createData("T_1", COPY_VERIFICATIONCODE_ID));
|
await sut.run(createData(`${COPY_VERIFICATIONCODE_ID}_1`, COPY_VERIFICATIONCODE_ID), {
|
||||||
|
url: "https://test.com",
|
||||||
|
} as any);
|
||||||
|
|
||||||
expect(totpService.getCode).toHaveBeenCalledTimes(1);
|
expect(totpService.getCode).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(copyToClipboard).toHaveBeenCalledWith({ text: "123456" });
|
expect(copyToClipboard).toHaveBeenCalledWith({
|
||||||
|
text: "123456",
|
||||||
|
tab: { url: "https://test.com" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("attempts to find a cipher when noop but unlocked", async () => {
|
it("attempts to find a cipher when noop but unlocked", async () => {
|
||||||
@ -163,11 +181,13 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
} as any,
|
} as any,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
|
await sut.run(createData(`${COPY_USERNAME_ID}_${NOOP_COMMAND_SUFFIX}`, COPY_USERNAME_ID), {
|
||||||
|
url: "https://test.com",
|
||||||
|
} as any);
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
|
||||||
|
|
||||||
expect(copyToClipboard).toHaveBeenCalledTimes(1);
|
expect(copyToClipboard).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
@ -185,11 +205,13 @@ describe("ContextMenuClickedHandler", () => {
|
|||||||
} as any,
|
} as any,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sut.run(createData("T_noop", COPY_USERNAME_ID), { url: "https://test.com" } as any);
|
await sut.run(createData(`${COPY_USERNAME_ID}_${NOOP_COMMAND_SUFFIX}`, COPY_USERNAME_ID), {
|
||||||
|
url: "https://test.com",
|
||||||
|
} as any);
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
|
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -30,16 +31,21 @@ import {
|
|||||||
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
||||||
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
||||||
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AUTOFILL_CARD_ID,
|
||||||
AUTOFILL_ID,
|
AUTOFILL_ID,
|
||||||
|
AUTOFILL_IDENTITY_ID,
|
||||||
COPY_IDENTIFIER_ID,
|
COPY_IDENTIFIER_ID,
|
||||||
COPY_PASSWORD_ID,
|
COPY_PASSWORD_ID,
|
||||||
COPY_USERNAME_ID,
|
COPY_USERNAME_ID,
|
||||||
COPY_VERIFICATIONCODE_ID,
|
COPY_VERIFICATIONCODE_ID,
|
||||||
|
CREATE_CARD_ID,
|
||||||
|
CREATE_IDENTITY_ID,
|
||||||
|
CREATE_LOGIN_ID,
|
||||||
GENERATE_PASSWORD_ID,
|
GENERATE_PASSWORD_ID,
|
||||||
NOOP_COMMAND_SUFFIX,
|
NOOP_COMMAND_SUFFIX,
|
||||||
} from "./main-context-menu-handler";
|
} from "../constants";
|
||||||
|
import { AutofillCipherTypeId } from "../types";
|
||||||
|
|
||||||
export type CopyToClipboardOptions = { text: string; tab: chrome.tabs.Tab };
|
export type CopyToClipboardOptions = { text: string; tab: chrome.tabs.Tab };
|
||||||
export type CopyToClipboardAction = (options: CopyToClipboardOptions) => void;
|
export type CopyToClipboardAction = (options: CopyToClipboardOptions) => void;
|
||||||
@ -142,18 +148,16 @@ export class ContextMenuClickedHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
|
async run(info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) {
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (info.menuItemId) {
|
switch (info.menuItemId) {
|
||||||
case GENERATE_PASSWORD_ID:
|
case GENERATE_PASSWORD_ID:
|
||||||
if (!tab) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.generatePasswordToClipboard(tab);
|
await this.generatePasswordToClipboard(tab);
|
||||||
break;
|
break;
|
||||||
case COPY_IDENTIFIER_ID:
|
case COPY_IDENTIFIER_ID:
|
||||||
if (!tab) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
|
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -161,7 +165,11 @@ export class ContextMenuClickedHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cipherAction(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
|
async cipherAction(info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) {
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||||
const retryMessage: LockedVaultPendingNotificationsItem = {
|
const retryMessage: LockedVaultPendingNotificationsItem = {
|
||||||
commandToRetry: {
|
commandToRetry: {
|
||||||
@ -182,32 +190,57 @@ export class ContextMenuClickedHandler {
|
|||||||
|
|
||||||
// NOTE: We don't actually use the first part of this ID, we further switch based on the parentMenuItemId
|
// NOTE: We don't actually use the first part of this ID, we further switch based on the parentMenuItemId
|
||||||
// I would really love to not add it but that is a departure from how it currently works.
|
// I would really love to not add it but that is a departure from how it currently works.
|
||||||
const id = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
|
const menuItemId = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
|
||||||
let cipher: CipherView | undefined;
|
let cipher: CipherView | undefined;
|
||||||
if (id === NOOP_COMMAND_SUFFIX) {
|
const isCreateCipherAction = [CREATE_LOGIN_ID, CREATE_IDENTITY_ID, CREATE_CARD_ID].includes(
|
||||||
|
menuItemId as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCreateCipherAction) {
|
||||||
|
// pass; defer to logic below
|
||||||
|
} else if (menuItemId === NOOP_COMMAND_SUFFIX) {
|
||||||
|
const additionalCiphersToGet =
|
||||||
|
info.parentMenuItemId === AUTOFILL_IDENTITY_ID
|
||||||
|
? [CipherType.Identity]
|
||||||
|
: info.parentMenuItemId === AUTOFILL_CARD_ID
|
||||||
|
? [CipherType.Card]
|
||||||
|
: [];
|
||||||
|
|
||||||
// This NOOP item has come through which is generally only for no access state but since we got here
|
// This NOOP item has come through which is generally only for no access state but since we got here
|
||||||
// we are actually unlocked we will do our best to find a good match of an item to autofill this is useful
|
// we are actually unlocked we will do our best to find a good match of an item to autofill this is useful
|
||||||
// in scenarios like unlock on autofill
|
// in scenarios like unlock on autofill
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||||
|
tab.url,
|
||||||
|
additionalCiphersToGet
|
||||||
|
);
|
||||||
|
|
||||||
cipher = ciphers[0];
|
cipher = ciphers[0];
|
||||||
} else {
|
} else {
|
||||||
const ciphers = await this.cipherService.getAllDecrypted();
|
const ciphers = await this.cipherService.getAllDecrypted();
|
||||||
cipher = ciphers.find((c) => c.id === id);
|
cipher = ciphers.find(({ id }) => id === menuItemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher == null) {
|
if (!cipher && !isCreateCipherAction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (info.parentMenuItemId) {
|
switch (info.parentMenuItemId) {
|
||||||
case AUTOFILL_ID:
|
case AUTOFILL_ID:
|
||||||
if (tab == null) {
|
case AUTOFILL_IDENTITY_ID:
|
||||||
return;
|
case AUTOFILL_CARD_ID: {
|
||||||
|
const cipherType = this.getCipherCreationType(menuItemId);
|
||||||
|
|
||||||
|
if (cipherType) {
|
||||||
|
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||||
|
cipherType,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
|
// The action here is passed on to the single-use reprompt window and doesn't change based on cipher type
|
||||||
action: AUTOFILL_ID,
|
action: AUTOFILL_ID,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -215,14 +248,29 @@ export class ContextMenuClickedHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case COPY_USERNAME_ID:
|
case COPY_USERNAME_ID:
|
||||||
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
|
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||||
|
cipherType: CipherType.Login,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
this.copyToClipboard({ text: cipher.login.username, tab: tab });
|
||||||
break;
|
break;
|
||||||
case COPY_PASSWORD_ID:
|
case COPY_PASSWORD_ID:
|
||||||
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
|
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||||
|
cipherType: CipherType.Login,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: COPY_PASSWORD_ID,
|
action: info.parentMenuItemId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||||
@ -231,10 +279,17 @@ export class ContextMenuClickedHandler {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case COPY_VERIFICATIONCODE_ID:
|
case COPY_VERIFICATIONCODE_ID:
|
||||||
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
|
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||||
|
cipherType: CipherType.Login,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: COPY_VERIFICATIONCODE_ID,
|
action: info.parentMenuItemId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyToClipboard({
|
this.copyToClipboard({
|
||||||
@ -254,6 +309,16 @@ export class ContextMenuClickedHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCipherCreationType(menuItemId?: string): AutofillCipherTypeId | null {
|
||||||
|
return menuItemId === CREATE_IDENTITY_ID
|
||||||
|
? CipherType.Identity
|
||||||
|
: menuItemId === CREATE_CARD_ID
|
||||||
|
? CipherType.Card
|
||||||
|
: menuItemId === CREATE_LOGIN_ID
|
||||||
|
? CipherType.Login
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
BrowserApi.sendTabsMessage(
|
BrowserApi.sendTabsMessage(
|
||||||
|
@ -60,7 +60,7 @@ describe("context-menu", () => {
|
|||||||
|
|
||||||
const createdMenu = await sut.init();
|
const createdMenu = await sut.init();
|
||||||
expect(createdMenu).toBeTruthy();
|
expect(createdMenu).toBeTruthy();
|
||||||
expect(createSpy).toHaveBeenCalledTimes(7);
|
expect(createSpy).toHaveBeenCalledTimes(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has menu enabled and has premium", async () => {
|
it("has menu enabled and has premium", async () => {
|
||||||
@ -70,7 +70,7 @@ describe("context-menu", () => {
|
|||||||
|
|
||||||
const createdMenu = await sut.init();
|
const createdMenu = await sut.init();
|
||||||
expect(createdMenu).toBeTruthy();
|
expect(createdMenu).toBeTruthy();
|
||||||
expect(createSpy).toHaveBeenCalledTimes(8);
|
expect(createSpy).toHaveBeenCalledTimes(11);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ describe("context-menu", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("is not a login cipher", async () => {
|
it("is not a login cipher", async () => {
|
||||||
await sut.loadOptions("TEST_TITLE", "1", "", {
|
await sut.loadOptions("TEST_TITLE", "1", {
|
||||||
...createCipher(),
|
...createCipher(),
|
||||||
type: CipherType.SecureNote,
|
type: CipherType.SecureNote,
|
||||||
} as any);
|
} as any);
|
||||||
@ -109,7 +109,6 @@ describe("context-menu", () => {
|
|||||||
await sut.loadOptions(
|
await sut.loadOptions(
|
||||||
"TEST_TITLE",
|
"TEST_TITLE",
|
||||||
"1",
|
"1",
|
||||||
"",
|
|
||||||
createCipher({
|
createCipher({
|
||||||
username: "",
|
username: "",
|
||||||
totp: "",
|
totp: "",
|
||||||
@ -123,18 +122,18 @@ describe("context-menu", () => {
|
|||||||
it("create entry for each cipher piece", async () => {
|
it("create entry for each cipher piece", async () => {
|
||||||
stateService.getCanAccessPremium.mockResolvedValue(true);
|
stateService.getCanAccessPremium.mockResolvedValue(true);
|
||||||
|
|
||||||
await sut.loadOptions("TEST_TITLE", "1", "", createCipher());
|
await sut.loadOptions("TEST_TITLE", "1", createCipher());
|
||||||
|
|
||||||
// One for autofill, copy username, copy password, and copy totp code
|
// One for autofill, copy username, copy password, and copy totp code
|
||||||
expect(createSpy).toHaveBeenCalledTimes(4);
|
expect(createSpy).toHaveBeenCalledTimes(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates noop item for no cipher", async () => {
|
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
|
||||||
stateService.getCanAccessPremium.mockResolvedValue(true);
|
stateService.getCanAccessPremium.mockResolvedValue(true);
|
||||||
|
|
||||||
await sut.loadOptions("TEST_TITLE", "NOOP", "");
|
await sut.loadOptions("TEST_TITLE", "NOOP");
|
||||||
|
|
||||||
expect(createSpy).toHaveBeenCalledTimes(4);
|
expect(createSpy).toHaveBeenCalledTimes(6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -21,22 +21,24 @@ import {
|
|||||||
StateServiceInitOptions,
|
StateServiceInitOptions,
|
||||||
} from "../../platform/background/service-factories/state-service.factory";
|
} from "../../platform/background/service-factories/state-service.factory";
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||||
|
import {
|
||||||
export const ROOT_ID = "root";
|
AUTOFILL_CARD_ID,
|
||||||
|
AUTOFILL_ID,
|
||||||
export const AUTOFILL_ID = "autofill";
|
AUTOFILL_IDENTITY_ID,
|
||||||
export const COPY_USERNAME_ID = "copy-username";
|
COPY_IDENTIFIER_ID,
|
||||||
export const COPY_PASSWORD_ID = "copy-password";
|
COPY_PASSWORD_ID,
|
||||||
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
|
COPY_USERNAME_ID,
|
||||||
export const COPY_IDENTIFIER_ID = "copy-identifier";
|
COPY_VERIFICATIONCODE_ID,
|
||||||
|
CREATE_CARD_ID,
|
||||||
const SEPARATOR_ID = "separator";
|
CREATE_IDENTITY_ID,
|
||||||
export const GENERATE_PASSWORD_ID = "generate-password";
|
CREATE_LOGIN_ID,
|
||||||
|
GENERATE_PASSWORD_ID,
|
||||||
export const NOOP_COMMAND_SUFFIX = "noop";
|
NOOP_COMMAND_SUFFIX,
|
||||||
|
ROOT_ID,
|
||||||
|
SEPARATOR_ID,
|
||||||
|
} from "../constants";
|
||||||
|
|
||||||
export class MainContextMenuHandler {
|
export class MainContextMenuHandler {
|
||||||
//
|
|
||||||
private initRunning = false;
|
private initRunning = false;
|
||||||
|
|
||||||
create: (options: chrome.contextMenus.CreateProperties) => Promise<void>;
|
create: (options: chrome.contextMenus.CreateProperties) => Promise<void>;
|
||||||
@ -120,7 +122,7 @@ export class MainContextMenuHandler {
|
|||||||
await create({
|
await create({
|
||||||
id: AUTOFILL_ID,
|
id: AUTOFILL_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("autoFill"),
|
title: this.i18nService.t("autoFillLogin"),
|
||||||
});
|
});
|
||||||
|
|
||||||
await create({
|
await create({
|
||||||
@ -144,7 +146,25 @@ export class MainContextMenuHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await create({
|
await create({
|
||||||
id: SEPARATOR_ID,
|
id: SEPARATOR_ID + 1,
|
||||||
|
type: "separator",
|
||||||
|
parentId: ROOT_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
await create({
|
||||||
|
id: AUTOFILL_IDENTITY_ID,
|
||||||
|
parentId: ROOT_ID,
|
||||||
|
title: this.i18nService.t("autoFillIdentity"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await create({
|
||||||
|
id: AUTOFILL_CARD_ID,
|
||||||
|
parentId: ROOT_ID,
|
||||||
|
title: this.i18nService.t("autoFillCard"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await create({
|
||||||
|
id: SEPARATOR_ID + 2,
|
||||||
type: "separator",
|
type: "separator",
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
});
|
});
|
||||||
@ -194,40 +214,52 @@ export class MainContextMenuHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadOptions(title: string, id: string, url: string, cipher?: CipherView | undefined) {
|
async loadOptions(title: string, optionId: string, cipher?: CipherView) {
|
||||||
if (cipher != null && cipher.type !== CipherType.Login) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sanitizedTitle = MainContextMenuHandler.sanitizeContextMenuTitle(title);
|
const sanitizedTitle = MainContextMenuHandler.sanitizeContextMenuTitle(title);
|
||||||
|
|
||||||
const createChildItem = async (parent: string) => {
|
const createChildItem = async (parentId: string) => {
|
||||||
const menuItemId = `${parent}_${id}`;
|
const menuItemId = `${parentId}_${optionId}`;
|
||||||
|
|
||||||
return await this.create({
|
return await this.create({
|
||||||
type: "normal",
|
type: "normal",
|
||||||
id: menuItemId,
|
id: menuItemId,
|
||||||
parentId: parent,
|
parentId,
|
||||||
title: sanitizedTitle,
|
title: sanitizedTitle,
|
||||||
contexts: ["all"],
|
contexts: ["all"],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.password)) {
|
if (
|
||||||
|
!cipher ||
|
||||||
|
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
|
||||||
|
) {
|
||||||
await createChildItem(AUTOFILL_ID);
|
await createChildItem(AUTOFILL_ID);
|
||||||
|
|
||||||
if (cipher?.viewPassword ?? true) {
|
if (cipher?.viewPassword ?? true) {
|
||||||
await createChildItem(COPY_PASSWORD_ID);
|
await createChildItem(COPY_PASSWORD_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher == null || !Utils.isNullOrEmpty(cipher.login.username)) {
|
if (
|
||||||
|
!cipher ||
|
||||||
|
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.username))
|
||||||
|
) {
|
||||||
await createChildItem(COPY_USERNAME_ID);
|
await createChildItem(COPY_USERNAME_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
if (canAccessPremium && (cipher == null || !Utils.isNullOrEmpty(cipher.login.totp))) {
|
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
|
||||||
await createChildItem(COPY_VERIFICATIONCODE_ID);
|
await createChildItem(COPY_VERIFICATIONCODE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((!cipher || cipher.type === CipherType.Card) && optionId !== CREATE_LOGIN_ID) {
|
||||||
|
await createChildItem(AUTOFILL_CARD_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!cipher || cipher.type === CipherType.Identity) && optionId !== CREATE_LOGIN_ID) {
|
||||||
|
await createChildItem(AUTOFILL_IDENTITY_ID);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
this.logService.warning(error.message);
|
||||||
}
|
}
|
||||||
@ -242,13 +274,72 @@ export class MainContextMenuHandler {
|
|||||||
const authed = await this.stateService.getIsAuthenticated();
|
const authed = await this.stateService.getIsAuthenticated();
|
||||||
await this.loadOptions(
|
await this.loadOptions(
|
||||||
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
||||||
NOOP_COMMAND_SUFFIX,
|
NOOP_COMMAND_SUFFIX
|
||||||
"<all_urls>"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async noLogins(url: string) {
|
async noCards() {
|
||||||
await this.loadOptions(this.i18nService.t("noMatchingLogins"), NOOP_COMMAND_SUFFIX, url);
|
await this.create({
|
||||||
|
id: `${AUTOFILL_CARD_ID}_NOTICE`,
|
||||||
|
enabled: false,
|
||||||
|
parentId: AUTOFILL_CARD_ID,
|
||||||
|
title: this.i18nService.t("noCards"),
|
||||||
|
type: "normal",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_CARD_ID}_${SEPARATOR_ID}`,
|
||||||
|
parentId: AUTOFILL_CARD_ID,
|
||||||
|
type: "separator",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_CARD_ID}_${CREATE_CARD_ID}`,
|
||||||
|
parentId: AUTOFILL_CARD_ID,
|
||||||
|
title: this.i18nService.t("addCardMenu"),
|
||||||
|
type: "normal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async noIdentities() {
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_IDENTITY_ID}_NOTICE`,
|
||||||
|
enabled: false,
|
||||||
|
parentId: AUTOFILL_IDENTITY_ID,
|
||||||
|
title: this.i18nService.t("noIdentities"),
|
||||||
|
type: "normal",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_IDENTITY_ID}_${SEPARATOR_ID}`,
|
||||||
|
parentId: AUTOFILL_IDENTITY_ID,
|
||||||
|
type: "separator",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_IDENTITY_ID}_${CREATE_IDENTITY_ID}`,
|
||||||
|
parentId: AUTOFILL_IDENTITY_ID,
|
||||||
|
title: this.i18nService.t("addIdentityMenu"),
|
||||||
|
type: "normal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async noLogins() {
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_ID}_NOTICE`,
|
||||||
|
enabled: false,
|
||||||
|
parentId: AUTOFILL_ID,
|
||||||
|
title: this.i18nService.t("noMatchingLogins"),
|
||||||
|
type: "normal",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create({
|
||||||
|
id: `${AUTOFILL_ID}_${SEPARATOR_ID}` + 1,
|
||||||
|
parentId: AUTOFILL_ID,
|
||||||
|
type: "separator",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,19 @@ export const EVENTS = {
|
|||||||
KEYPRESS: "keypress",
|
KEYPRESS: "keypress",
|
||||||
KEYUP: "keyup",
|
KEYUP: "keyup",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/* Context Menu item Ids */
|
||||||
|
export const AUTOFILL_CARD_ID = "autofill-card";
|
||||||
|
export const AUTOFILL_ID = "autofill";
|
||||||
|
export const AUTOFILL_IDENTITY_ID = "autofill-identity";
|
||||||
|
export const COPY_IDENTIFIER_ID = "copy-identifier";
|
||||||
|
export const COPY_PASSWORD_ID = "copy-password";
|
||||||
|
export const COPY_USERNAME_ID = "copy-username";
|
||||||
|
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
|
||||||
|
export const CREATE_CARD_ID = "create-card";
|
||||||
|
export const CREATE_IDENTITY_ID = "create-identity";
|
||||||
|
export const CREATE_LOGIN_ID = "create-login";
|
||||||
|
export const GENERATE_PASSWORD_ID = "generate-password";
|
||||||
|
export const NOOP_COMMAND_SUFFIX = "noop";
|
||||||
|
export const ROOT_ID = "root";
|
||||||
|
export const SEPARATOR_ID = "separator";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { UriMatchType } from "@bitwarden/common/enums";
|
import { UriMatchType } from "@bitwarden/common/enums";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import AutofillField from "../../models/autofill-field";
|
import AutofillField from "../../models/autofill-field";
|
||||||
@ -55,5 +56,9 @@ export abstract class AutofillService {
|
|||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
fromCommand: boolean
|
fromCommand: boolean
|
||||||
) => Promise<string | null>;
|
) => Promise<string | null>;
|
||||||
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string | null>;
|
doAutoFillActiveTab: (
|
||||||
|
pageDetails: PageDetail[],
|
||||||
|
fromCommand: boolean,
|
||||||
|
cipherType?: CipherType
|
||||||
|
) => Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
@ -304,21 +304,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autofill the active tab with the next login item from the cache
|
* Autofill the active tab with the next cipher from the cache
|
||||||
* @param {PageDetail[]} pageDetails The data scraped from the page
|
* @param {PageDetail[]} pageDetails The data scraped from the page
|
||||||
* @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
* @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`)
|
||||||
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
* @returns {Promise<string | null>} The TOTP code of the successfully autofilled login, if any
|
||||||
*/
|
*/
|
||||||
async doAutoFillActiveTab(
|
async doAutoFillActiveTab(
|
||||||
pageDetails: PageDetail[],
|
pageDetails: PageDetail[],
|
||||||
fromCommand: boolean
|
fromCommand: boolean,
|
||||||
|
cipherType?: CipherType
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const tab = await this.getActiveTab();
|
const tab = await this.getActiveTab();
|
||||||
|
|
||||||
if (!tab || !tab.url) {
|
if (!tab || !tab.url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
|
if (!cipherType || cipherType === CipherType.Login) {
|
||||||
|
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cipher is a non-login type
|
||||||
|
const cipher: CipherView = (
|
||||||
|
(await this.cipherService.getAllDecryptedForUrl(tab.url, [cipherType])) || []
|
||||||
|
).find(({ type }) => type === cipherType);
|
||||||
|
|
||||||
|
if (!cipher || cipher.reprompt !== CipherRepromptType.None) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.doAutoFill({
|
||||||
|
tab: tab,
|
||||||
|
cipher: cipher,
|
||||||
|
pageDetails: pageDetails,
|
||||||
|
skipLastUsed: !fromCommand,
|
||||||
|
skipUsernameOnlyFill: !fromCommand,
|
||||||
|
onlyEmptyFields: !fromCommand,
|
||||||
|
onlyVisibleFields: !fromCommand,
|
||||||
|
fillNewPassword: false,
|
||||||
|
allowUntrustedIframe: fromCommand,
|
||||||
|
allowTotpAutofill: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
|
||||||
export type UserSettings = {
|
export type UserSettings = {
|
||||||
avatarColor: string | null;
|
avatarColor: string | null;
|
||||||
@ -58,3 +59,5 @@ export type FillableFormFieldElement = HTMLInputElement | HTMLSelectElement | HT
|
|||||||
export type FormFieldElement = FillableFormFieldElement | HTMLSpanElement;
|
export type FormFieldElement = FillableFormFieldElement | HTMLSpanElement;
|
||||||
|
|
||||||
export type FormElementWithAttribute = FormFieldElement & Record<string, string | null | undefined>;
|
export type FormElementWithAttribute = FormFieldElement & Record<string, string | null | undefined>;
|
||||||
|
|
||||||
|
export type AutofillCipherTypeId = CipherType.Login | CipherType.Card | CipherType.Identity;
|
||||||
|
@ -6,6 +6,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
|
||||||
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
@ -123,12 +124,28 @@ export default class RuntimeBackground {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "openAddEditCipher": {
|
case "openAddEditCipher": {
|
||||||
const addEditCipherUrl =
|
const isNewCipher = !cipherId;
|
||||||
cipherId == null
|
const cipherType = msg.data?.cipherType;
|
||||||
? "popup/index.html#/edit-cipher"
|
const senderTab = sender.tab;
|
||||||
: "popup/index.html#/edit-cipher?cipherId=" + cipherId;
|
|
||||||
|
if (!senderTab) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNewCipher) {
|
||||||
|
await this.browserPopoutWindowService.openCipherCreation(senderTab.windowId, {
|
||||||
|
cipherType,
|
||||||
|
senderTabId: senderTab.id,
|
||||||
|
senderTabURI: senderTab.url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.browserPopoutWindowService.openCipherEdit(senderTab.windowId, {
|
||||||
|
cipherId,
|
||||||
|
senderTabId: senderTab.id,
|
||||||
|
senderTabURI: senderTab.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "closeTab":
|
case "closeTab":
|
||||||
@ -174,6 +191,34 @@ export default class RuntimeBackground {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "autofill_card": {
|
||||||
|
await this.autofillService.doAutoFillActiveTab(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
frameId: sender.frameId,
|
||||||
|
tab: msg.tab,
|
||||||
|
details: msg.details,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
CipherType.Card
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "autofill_identity": {
|
||||||
|
await this.autofillService.doAutoFillActiveTab(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
frameId: sender.frameId,
|
||||||
|
tab: msg.tab,
|
||||||
|
details: msg.details,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
CipherType.Identity
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "contextMenu":
|
case "contextMenu":
|
||||||
clearTimeout(this.autofillTimeout);
|
clearTimeout(this.autofillTimeout);
|
||||||
this.pageDetailsToAutoFill.push({
|
this.pageDetailsToAutoFill.push({
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
|
||||||
interface BrowserPopoutWindowService {
|
interface BrowserPopoutWindowService {
|
||||||
openUnlockPrompt(senderWindowId: number): Promise<void>;
|
openUnlockPrompt(senderWindowId: number): Promise<void>;
|
||||||
closeUnlockPrompt(): Promise<void>;
|
closeUnlockPrompt(): Promise<void>;
|
||||||
@ -9,6 +11,22 @@ interface BrowserPopoutWindowService {
|
|||||||
senderTabId: number;
|
senderTabId: number;
|
||||||
}
|
}
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
openCipherCreation(
|
||||||
|
senderWindowId: number,
|
||||||
|
promptData: {
|
||||||
|
cipherType?: CipherType;
|
||||||
|
senderTabId: number;
|
||||||
|
senderTabURI: string;
|
||||||
|
}
|
||||||
|
): Promise<void>;
|
||||||
|
openCipherEdit(
|
||||||
|
senderWindowId: number,
|
||||||
|
promptData: {
|
||||||
|
cipherId: string;
|
||||||
|
senderTabId: number;
|
||||||
|
senderTabURI: string;
|
||||||
|
}
|
||||||
|
): Promise<void>;
|
||||||
closePasswordRepromptPrompt(): Promise<void>;
|
closePasswordRepromptPrompt(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
|
||||||
import { BrowserPopoutWindowService as BrowserPopupWindowServiceInterface } from "./abstractions/browser-popout-window.service";
|
import { BrowserPopoutWindowService as BrowserPopupWindowServiceInterface } from "./abstractions/browser-popout-window.service";
|
||||||
@ -45,6 +47,50 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
|
|||||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt");
|
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openCipherCreation(
|
||||||
|
senderWindowId: number,
|
||||||
|
{
|
||||||
|
cipherType = CipherType.Login,
|
||||||
|
senderTabId,
|
||||||
|
senderTabURI,
|
||||||
|
}: {
|
||||||
|
cipherType?: CipherType;
|
||||||
|
senderTabId: number;
|
||||||
|
senderTabURI: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const promptWindowPath =
|
||||||
|
"popup/index.html#/edit-cipher" +
|
||||||
|
"?uilocation=popout" +
|
||||||
|
`&type=${cipherType}` +
|
||||||
|
`&senderTabId=${senderTabId}` +
|
||||||
|
`&uri=${senderTabURI}`;
|
||||||
|
|
||||||
|
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherCreation");
|
||||||
|
}
|
||||||
|
|
||||||
|
async openCipherEdit(
|
||||||
|
senderWindowId: number,
|
||||||
|
{
|
||||||
|
cipherId,
|
||||||
|
senderTabId,
|
||||||
|
senderTabURI,
|
||||||
|
}: {
|
||||||
|
cipherId: string;
|
||||||
|
senderTabId: number;
|
||||||
|
senderTabURI: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const promptWindowPath =
|
||||||
|
"popup/index.html#/edit-cipher" +
|
||||||
|
"?uilocation=popout" +
|
||||||
|
`&cipherId=${cipherId}` +
|
||||||
|
`&senderTabId=${senderTabId}` +
|
||||||
|
`&uri=${senderTabURI}`;
|
||||||
|
|
||||||
|
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherEdit");
|
||||||
|
}
|
||||||
|
|
||||||
async closePasswordRepromptPrompt() {
|
async closePasswordRepromptPrompt() {
|
||||||
await this.closeSingleActionPopout("passwordReprompt");
|
await this.closeSingleActionPopout("passwordReprompt");
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
showAttachments = true;
|
showAttachments = true;
|
||||||
openAttachmentsInPopup: boolean;
|
openAttachmentsInPopup: boolean;
|
||||||
showAutoFillOnPageLoadOptions: boolean;
|
showAutoFillOnPageLoadOptions: boolean;
|
||||||
|
senderTabId?: number;
|
||||||
|
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
||||||
|
inPopout = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
@ -81,6 +84,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||||
|
this.senderTabId = parseInt(params?.senderTabId, 10) || undefined;
|
||||||
|
this.uilocation = params?.uilocation;
|
||||||
|
|
||||||
if (params.cipherId) {
|
if (params.cipherId) {
|
||||||
this.cipherId = params.cipherId;
|
this.cipherId = params.cipherId;
|
||||||
}
|
}
|
||||||
@ -128,6 +134,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window);
|
this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
|
||||||
|
|
||||||
if (!this.editMode) {
|
if (!this.editMode) {
|
||||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||||
this.currentUris =
|
this.currentUris =
|
||||||
@ -162,6 +170,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.senderTabId && this.inPopout) {
|
||||||
|
setTimeout(() => this.close(), 1000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.cloneMode) {
|
if (this.cloneMode) {
|
||||||
this.router.navigate(["/tabs/vault"]);
|
this.router.navigate(["/tabs/vault"]);
|
||||||
} else {
|
} else {
|
||||||
@ -194,6 +207,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
cancel() {
|
cancel() {
|
||||||
super.cancel();
|
super.cancel();
|
||||||
|
|
||||||
|
if (this.senderTabId && this.inPopout) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.popupUtilsService.inTab(window)) {
|
if (this.popupUtilsService.inTab(window)) {
|
||||||
this.messagingService.send("closeTab");
|
this.messagingService.send("closeTab");
|
||||||
return;
|
return;
|
||||||
@ -202,6 +220,14 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for closing single-action views
|
||||||
|
close() {
|
||||||
|
BrowserApi.focusTab(this.senderTabId);
|
||||||
|
window.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
async generateUsername(): Promise<boolean> {
|
async generateUsername(): Promise<boolean> {
|
||||||
const confirmed = await super.generateUsername();
|
const confirmed = await super.generateUsername();
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
Loading…
Reference in New Issue
Block a user