mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
PM-15998 - Update browser context menu options when the page domain is a blocked domain (#13378)
* update main context menu handler to skip creating menu entries which do not pass blocked uri checks * refactor to remove menu entries which do not pass blocked uri checks * allow context menu autofill items without a password if they have other autofillable attributes * include ciphers without passwords in autofill context menu options and track context menu state
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
type InitContextMenuItems = Omit<chrome.contextMenus.CreateProperties, "contexts"> & {
|
type InitContextMenuItems = Omit<chrome.contextMenus.CreateProperties, "contexts"> & {
|
||||||
checkPremiumAccess?: boolean;
|
requiresPremiumAccess?: boolean;
|
||||||
|
requiresUnblockedUri?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { InitContextMenuItems };
|
export { InitContextMenuItems };
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class CipherContextMenuHandler {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async update(url: string) {
|
async update(url: string, currentUriIsBlocked: boolean = false) {
|
||||||
if (this.mainContextMenuHandler.initRunning) {
|
if (this.mainContextMenuHandler.initRunning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,10 @@ export class CipherContextMenuHandler {
|
|||||||
for (const cipher of ciphers) {
|
for (const cipher of ciphers) {
|
||||||
await this.updateForCipher(cipher);
|
await this.updateForCipher(cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentUriIsBlocked) {
|
||||||
|
await this.mainContextMenuHandler.removeBlockedUriMenuItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateForCipher(cipher: CipherView) {
|
private async updateForCipher(cipher: CipherView) {
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
|
import {
|
||||||
|
AUTOFILL_CARD_ID,
|
||||||
|
AUTOFILL_ID,
|
||||||
|
AUTOFILL_IDENTITY_ID,
|
||||||
|
COPY_IDENTIFIER_ID,
|
||||||
|
COPY_PASSWORD_ID,
|
||||||
|
COPY_USERNAME_ID,
|
||||||
|
COPY_VERIFICATION_CODE_ID,
|
||||||
|
NOOP_COMMAND_SUFFIX,
|
||||||
|
SEPARATOR_ID,
|
||||||
|
} from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -15,6 +25,43 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
|
|
||||||
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in place of Set method `symmetricDifference`, which is only available to node version 22.0.0 or greater:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
|
||||||
|
*/
|
||||||
|
function symmetricDifference(setA: Set<string>, setB: Set<string>) {
|
||||||
|
const _difference = new Set(setA);
|
||||||
|
for (const elem of setB) {
|
||||||
|
if (_difference.has(elem)) {
|
||||||
|
_difference.delete(elem);
|
||||||
|
} else {
|
||||||
|
_difference.add(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _difference;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCipher = (data?: {
|
||||||
|
id?: CipherView["id"];
|
||||||
|
username?: CipherView["login"]["username"];
|
||||||
|
password?: CipherView["login"]["password"];
|
||||||
|
totp?: CipherView["login"]["totp"];
|
||||||
|
viewPassword?: CipherView["viewPassword"];
|
||||||
|
}): CipherView => {
|
||||||
|
const { id, username, password, totp, viewPassword } = data || {};
|
||||||
|
const cipherView = new CipherView(
|
||||||
|
new Cipher({
|
||||||
|
id: id ?? "1",
|
||||||
|
type: CipherType.Login,
|
||||||
|
viewPassword: viewPassword ?? true,
|
||||||
|
} as any),
|
||||||
|
);
|
||||||
|
cipherView.login.username = username ?? "USERNAME";
|
||||||
|
cipherView.login.password = password ?? "PASSWORD";
|
||||||
|
cipherView.login.totp = totp ?? "TOTP";
|
||||||
|
return cipherView;
|
||||||
|
};
|
||||||
|
|
||||||
describe("context-menu", () => {
|
describe("context-menu", () => {
|
||||||
let stateService: MockProxy<StateService>;
|
let stateService: MockProxy<StateService>;
|
||||||
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
||||||
@@ -59,6 +106,9 @@ describe("context-menu", () => {
|
|||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.spyOn(MainContextMenuHandler, "remove");
|
||||||
|
|
||||||
autofillSettingsService.enableContextMenu$ = of(true);
|
autofillSettingsService.enableContextMenu$ = of(true);
|
||||||
accountService.activeAccount$ = of({
|
accountService.activeAccount$ = of({
|
||||||
id: "userId" as UserId,
|
id: "userId" as UserId,
|
||||||
@@ -68,7 +118,10 @@ describe("context-menu", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => jest.resetAllMocks());
|
afterEach(async () => {
|
||||||
|
await MainContextMenuHandler.removeAll();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("init", () => {
|
describe("init", () => {
|
||||||
it("has menu disabled", async () => {
|
it("has menu disabled", async () => {
|
||||||
@@ -97,27 +150,6 @@ describe("context-menu", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("loadOptions", () => {
|
describe("loadOptions", () => {
|
||||||
const createCipher = (data?: {
|
|
||||||
id?: CipherView["id"];
|
|
||||||
username?: CipherView["login"]["username"];
|
|
||||||
password?: CipherView["login"]["password"];
|
|
||||||
totp?: CipherView["login"]["totp"];
|
|
||||||
viewPassword?: CipherView["viewPassword"];
|
|
||||||
}): CipherView => {
|
|
||||||
const { id, username, password, totp, viewPassword } = data || {};
|
|
||||||
const cipherView = new CipherView(
|
|
||||||
new Cipher({
|
|
||||||
id: id ?? "1",
|
|
||||||
type: CipherType.Login,
|
|
||||||
viewPassword: viewPassword ?? true,
|
|
||||||
} as any),
|
|
||||||
);
|
|
||||||
cipherView.login.username = username ?? "USERNAME";
|
|
||||||
cipherView.login.password = password ?? "PASSWORD";
|
|
||||||
cipherView.login.totp = totp ?? "TOTP";
|
|
||||||
return cipherView;
|
|
||||||
};
|
|
||||||
|
|
||||||
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(),
|
||||||
@@ -128,33 +160,124 @@ describe("context-menu", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates item for autofill", async () => {
|
it("creates item for autofill", async () => {
|
||||||
await sut.loadOptions(
|
const cipher = createCipher({
|
||||||
"TEST_TITLE",
|
username: "",
|
||||||
"1",
|
totp: "",
|
||||||
createCipher({
|
viewPassword: true,
|
||||||
username: "",
|
});
|
||||||
totp: "",
|
const optionId = "1";
|
||||||
viewPassword: false,
|
await sut.loadOptions("TEST_TITLE", optionId, cipher);
|
||||||
}),
|
|
||||||
|
expect(createSpy).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(2);
|
||||||
|
|
||||||
|
const expectedMenuItems = new Set([
|
||||||
|
AUTOFILL_ID + `_${optionId}`,
|
||||||
|
COPY_PASSWORD_ID + `_${optionId}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
|
||||||
|
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
|
||||||
|
const expectedReceivedDiff = symmetricDifference(
|
||||||
|
expectedMenuItems,
|
||||||
|
MainContextMenuHandler["existingMenuItems"],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
expect(expectedReceivedDiff.size).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("create entry for each cipher piece", async () => {
|
it("create entry for each cipher piece", async () => {
|
||||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
const optionId = "arbitraryString";
|
||||||
await sut.loadOptions("TEST_TITLE", "1", createCipher());
|
await sut.loadOptions("TEST_TITLE", optionId, createCipher());
|
||||||
|
|
||||||
expect(createSpy).toHaveBeenCalledTimes(4);
|
expect(createSpy).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
|
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
|
||||||
|
|
||||||
|
const expectedMenuItems = new Set([
|
||||||
|
AUTOFILL_ID + `_${optionId}`,
|
||||||
|
COPY_PASSWORD_ID + `_${optionId}`,
|
||||||
|
COPY_USERNAME_ID + `_${optionId}`,
|
||||||
|
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
|
||||||
|
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
|
||||||
|
const expectedReceivedDiff = symmetricDifference(
|
||||||
|
expectedMenuItems,
|
||||||
|
MainContextMenuHandler["existingMenuItems"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(expectedReceivedDiff.size).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
|
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
|
||||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
|
||||||
await sut.loadOptions("TEST_TITLE", "NOOP");
|
const optionId = "NOOP";
|
||||||
|
await sut.loadOptions("TEST_TITLE", optionId);
|
||||||
|
|
||||||
expect(createSpy).toHaveBeenCalledTimes(6);
|
expect(createSpy).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
|
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(6);
|
||||||
|
|
||||||
|
const expectedMenuItems = new Set([
|
||||||
|
AUTOFILL_ID + `_${optionId}`,
|
||||||
|
COPY_PASSWORD_ID + `_${optionId}`,
|
||||||
|
COPY_USERNAME_ID + `_${optionId}`,
|
||||||
|
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
|
||||||
|
AUTOFILL_CARD_ID + `_${optionId}`,
|
||||||
|
AUTOFILL_IDENTITY_ID + `_${optionId}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
|
||||||
|
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
|
||||||
|
const expectedReceivedDiff = symmetricDifference(
|
||||||
|
expectedMenuItems,
|
||||||
|
MainContextMenuHandler["existingMenuItems"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(expectedReceivedDiff.size).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("removeBlockedUriMenuItems", () => {
|
||||||
|
it("removes menu items that require code injection", async () => {
|
||||||
|
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
autofillSettingsService.enableContextMenu$ = of(true);
|
||||||
|
stateService.getIsAuthenticated.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const optionId = "1";
|
||||||
|
await sut.loadOptions("TEST_TITLE", optionId, createCipher());
|
||||||
|
|
||||||
|
await sut.removeBlockedUriMenuItems();
|
||||||
|
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledTimes(5);
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_ID);
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_IDENTITY_ID);
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_CARD_ID);
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(SEPARATOR_ID + 2);
|
||||||
|
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(COPY_IDENTIFIER_ID);
|
||||||
|
|
||||||
|
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
|
||||||
|
|
||||||
|
const expectedMenuItems = new Set([
|
||||||
|
AUTOFILL_ID + `_${optionId}`,
|
||||||
|
COPY_PASSWORD_ID + `_${optionId}`,
|
||||||
|
COPY_USERNAME_ID + `_${optionId}`,
|
||||||
|
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
|
||||||
|
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
|
||||||
|
const expectedReceivedDiff = symmetricDifference(
|
||||||
|
expectedMenuItems,
|
||||||
|
MainContextMenuHandler["existingMenuItems"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(expectedReceivedDiff.size).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
|
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
|
||||||
|
|
||||||
export class MainContextMenuHandler {
|
export class MainContextMenuHandler {
|
||||||
|
static existingMenuItems: Set<string> = new Set();
|
||||||
initRunning = false;
|
initRunning = false;
|
||||||
private initContextMenuItems: InitContextMenuItems[] = [
|
private initContextMenuItems: InitContextMenuItems[] = [
|
||||||
{
|
{
|
||||||
@@ -41,6 +42,7 @@ export class MainContextMenuHandler {
|
|||||||
id: AUTOFILL_ID,
|
id: AUTOFILL_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("autoFillLogin"),
|
title: this.i18nService.t("autoFillLogin"),
|
||||||
|
requiresUnblockedUri: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: COPY_USERNAME_ID,
|
id: COPY_USERNAME_ID,
|
||||||
@@ -56,7 +58,7 @@ export class MainContextMenuHandler {
|
|||||||
id: COPY_VERIFICATION_CODE_ID,
|
id: COPY_VERIFICATION_CODE_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("copyVerificationCode"),
|
title: this.i18nService.t("copyVerificationCode"),
|
||||||
checkPremiumAccess: true,
|
requiresPremiumAccess: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SEPARATOR_ID + 1,
|
id: SEPARATOR_ID + 1,
|
||||||
@@ -67,16 +69,19 @@ export class MainContextMenuHandler {
|
|||||||
id: AUTOFILL_IDENTITY_ID,
|
id: AUTOFILL_IDENTITY_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("autoFillIdentity"),
|
title: this.i18nService.t("autoFillIdentity"),
|
||||||
|
requiresUnblockedUri: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: AUTOFILL_CARD_ID,
|
id: AUTOFILL_CARD_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("autoFillCard"),
|
title: this.i18nService.t("autoFillCard"),
|
||||||
|
requiresUnblockedUri: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: SEPARATOR_ID + 2,
|
id: SEPARATOR_ID + 2,
|
||||||
type: "separator",
|
type: "separator",
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
|
requiresUnblockedUri: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: GENERATE_PASSWORD_ID,
|
id: GENERATE_PASSWORD_ID,
|
||||||
@@ -87,6 +92,7 @@ export class MainContextMenuHandler {
|
|||||||
id: COPY_IDENTIFIER_ID,
|
id: COPY_IDENTIFIER_ID,
|
||||||
parentId: ROOT_ID,
|
parentId: ROOT_ID,
|
||||||
title: this.i18nService.t("copyElementIdentifier"),
|
title: this.i18nService.t("copyElementIdentifier"),
|
||||||
|
requiresUnblockedUri: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
private noCardsContextMenuItems: chrome.contextMenus.CreateProperties[] = [
|
private noCardsContextMenuItems: chrome.contextMenus.CreateProperties[] = [
|
||||||
@@ -175,13 +181,19 @@ export class MainContextMenuHandler {
|
|||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const options of this.initContextMenuItems) {
|
for (const menuItem of this.initContextMenuItems) {
|
||||||
if (options.checkPremiumAccess && !hasPremium) {
|
const {
|
||||||
|
requiresPremiumAccess,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
requiresUnblockedUri, // destructuring this out of being passed to `create`
|
||||||
|
...otherOptions
|
||||||
|
} = menuItem;
|
||||||
|
|
||||||
|
if (requiresPremiumAccess && !hasPremium) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete options.checkPremiumAccess;
|
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
|
||||||
await MainContextMenuHandler.create({ ...options, contexts: ["all"] });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
this.logService.warning(error.message);
|
||||||
@@ -202,12 +214,16 @@ export class MainContextMenuHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
chrome.contextMenus.create(options, () => {
|
const itemId = chrome.contextMenus.create(options, () => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
return reject(chrome.runtime.lastError);
|
return reject(chrome.runtime.lastError);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.existingMenuItems.add(`${itemId}`);
|
||||||
|
|
||||||
|
return itemId;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,12 +237,16 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.existingMenuItems = new Set();
|
||||||
|
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static remove(menuItemId: string) {
|
static remove(menuItemId: string) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
chrome.contextMenus.remove(menuItemId, () => {
|
const itemId = chrome.contextMenus.remove(menuItemId, () => {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
reject(chrome.runtime.lastError);
|
reject(chrome.runtime.lastError);
|
||||||
return;
|
return;
|
||||||
@@ -234,6 +254,10 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.existingMenuItems.delete(`${itemId}`);
|
||||||
|
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +268,11 @@ export class MainContextMenuHandler {
|
|||||||
const createChildItem = async (parentId: string) => {
|
const createChildItem = async (parentId: string) => {
|
||||||
const menuItemId = `${parentId}_${optionId}`;
|
const menuItemId = `${parentId}_${optionId}`;
|
||||||
|
|
||||||
|
const itemAlreadyExists = MainContextMenuHandler.existingMenuItems.has(menuItemId);
|
||||||
|
if (itemAlreadyExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return await MainContextMenuHandler.create({
|
return await MainContextMenuHandler.create({
|
||||||
type: "normal",
|
type: "normal",
|
||||||
id: menuItemId,
|
id: menuItemId,
|
||||||
@@ -255,10 +284,18 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!cipher ||
|
!cipher ||
|
||||||
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
|
(cipher.type === CipherType.Login &&
|
||||||
|
(!Utils.isNullOrEmpty(cipher.login?.username) ||
|
||||||
|
!Utils.isNullOrEmpty(cipher.login?.password) ||
|
||||||
|
!Utils.isNullOrEmpty(cipher.login?.totp)))
|
||||||
) {
|
) {
|
||||||
await createChildItem(AUTOFILL_ID);
|
await createChildItem(AUTOFILL_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!cipher ||
|
||||||
|
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
|
||||||
|
) {
|
||||||
if (cipher?.viewPassword ?? true) {
|
if (cipher?.viewPassword ?? true) {
|
||||||
await createChildItem(COPY_PASSWORD_ID);
|
await createChildItem(COPY_PASSWORD_ID);
|
||||||
}
|
}
|
||||||
@@ -305,10 +342,22 @@ export class MainContextMenuHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeBlockedUriMenuItems() {
|
||||||
|
try {
|
||||||
|
for (const menuItem of this.initContextMenuItems) {
|
||||||
|
if (menuItem.requiresUnblockedUri && menuItem.id) {
|
||||||
|
await MainContextMenuHandler.remove(menuItem.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logService.warning(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async noCards() {
|
async noCards() {
|
||||||
try {
|
try {
|
||||||
for (const option of this.noCardsContextMenuItems) {
|
for (const menuItem of this.noCardsContextMenuItems) {
|
||||||
await MainContextMenuHandler.create(option);
|
await MainContextMenuHandler.create(menuItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
this.logService.warning(error.message);
|
||||||
@@ -317,8 +366,8 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
async noIdentities() {
|
async noIdentities() {
|
||||||
try {
|
try {
|
||||||
for (const option of this.noIdentitiesContextMenuItems) {
|
for (const menuItem of this.noIdentitiesContextMenuItems) {
|
||||||
await MainContextMenuHandler.create(option);
|
await MainContextMenuHandler.create(menuItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logService.warning(error.message);
|
this.logService.warning(error.message);
|
||||||
@@ -327,8 +376,8 @@ export class MainContextMenuHandler {
|
|||||||
|
|
||||||
async noLogins() {
|
async noLogins() {
|
||||||
try {
|
try {
|
||||||
for (const option of this.noLoginsContextMenuItems) {
|
for (const menuItem of this.noLoginsContextMenuItems) {
|
||||||
await MainContextMenuHandler.create(option);
|
await MainContextMenuHandler.create(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);
|
||||||
|
|||||||
@@ -1374,17 +1374,41 @@ export default class MainBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mainContextMenuHandler?.init();
|
const contextMenuIsEnabled = await this.mainContextMenuHandler?.init();
|
||||||
|
if (!contextMenuIsEnabled) {
|
||||||
|
this.onUpdatedRan = this.onReplacedRan = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||||
|
|
||||||
if (tab) {
|
if (tab) {
|
||||||
await this.cipherContextMenuHandler?.update(tab.url);
|
const currentUriIsBlocked = await firstValueFrom(
|
||||||
|
this.domainSettingsService.blockedInteractionsUris$.pipe(
|
||||||
|
map((blockedInteractionsUris) => {
|
||||||
|
if (blockedInteractionsUris && tab?.url?.length) {
|
||||||
|
const tabURL = new URL(tab.url);
|
||||||
|
const tabIsBlocked = Object.keys(blockedInteractionsUris).some((blockedHostname) =>
|
||||||
|
tabURL.hostname.endsWith(blockedHostname),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tabIsBlocked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.cipherContextMenuHandler?.update(tab.url, currentUriIsBlocked);
|
||||||
this.onUpdatedRan = this.onReplacedRan = false;
|
this.onUpdatedRan = this.onReplacedRan = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOverlayCiphers() {
|
async updateOverlayCiphers() {
|
||||||
// overlayBackground null in popup only contexts
|
// `overlayBackground` is null in popup only contexts
|
||||||
if (this.overlayBackground) {
|
if (this.overlayBackground) {
|
||||||
await this.overlayBackground.updateOverlayCiphers();
|
await this.overlayBackground.updateOverlayCiphers();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user