1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-23 03:22:50 +02:00

[PM-10079] Add keyboard shortcut to autofill identity and credit cards (#10254)

* [BEEEP] Autofill Identity and Card Ciphers From Keyboard Shortcut

* [PM-10079] Add keyboard shortcut to autofill identity and credit card ciphers

* [PM-10079] Fixing jest tests

* [PM-10079] Added an enum for the autofill commands, and adjusted how we filter out cipher types before sorting them by last used when calling for ID and card ciphers

* [PM-10079] Updating copywriting for the autofill settings revolving around keyboard shortcuts

* [PM-10079] Setting a method within CipherService as private
This commit is contained in:
Cesar Gonzalez 2024-07-31 12:52:04 -05:00 committed by GitHub
parent 85c8ff04a1
commit 86acca3bec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 187 additions and 61 deletions

View File

@ -1343,8 +1343,14 @@
"commandOpenSidebar": { "commandOpenSidebar": {
"message": "Open vault in sidebar" "message": "Open vault in sidebar"
}, },
"commandAutofillDesc": { "commandAutofillLoginDesc": {
"message": "Auto-fill the last used login for the current website" "message": "Autofill the last used login for the current website"
},
"commandAutofillCardDesc": {
"message": "Autofill the last used card for the current website"
},
"commandAutofillIdentityDesc": {
"message": "Autofill the last used identity for the current website"
}, },
"commandGeneratePasswordDesc": { "commandGeneratePasswordDesc": {
"message": "Generate and copy a new random password to the clipboard" "message": "Generate and copy a new random password to the clipboard"
@ -2774,14 +2780,17 @@
"autofillKeyboardShortcutUpdateLabel": { "autofillKeyboardShortcutUpdateLabel": {
"message": "Change shortcut" "message": "Change shortcut"
}, },
"autofillKeyboardManagerShortcutsLabel": {
"message": "Manage shortcuts"
},
"autofillShortcut": { "autofillShortcut": {
"message": "Autofill keyboard shortcut" "message": "Autofill keyboard shortcut"
}, },
"autofillShortcutNotSet": { "autofillLoginShortcutNotSet": {
"message": "The autofill shortcut is not set. Change this in the browser's settings." "message": "The autofill login shortcut is not set. Change this in the browser's settings."
}, },
"autofillShortcutText": { "autofillLoginShortcutText": {
"message": "The autofill shortcut is: $COMMAND$. Change this in the browser's settings.", "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.",
"placeholders": { "placeholders": {
"command": { "command": {
"content": "$1", "content": "$1",

View File

@ -4,6 +4,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -151,7 +152,7 @@ describe("NotificationBackground", () => {
const message: NotificationBackgroundExtensionMessage = { const message: NotificationBackgroundExtensionMessage = {
command: "unlockCompleted", command: "unlockCompleted",
data: { data: {
commandToRetry: { message: { command: "autofill_login" } }, commandToRetry: { message: { command: ExtensionCommand.AutofillLogin } },
} as LockedVaultPendingNotificationsData, } as LockedVaultPendingNotificationsData,
}; };
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();

View File

@ -4,7 +4,11 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; import {
ExtensionCommand,
ExtensionCommandType,
NOTIFICATION_BAR_LIFESPAN_MS,
} from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
@ -45,6 +49,11 @@ export default class NotificationBackground {
private openUnlockPopout = openUnlockPopout; private openUnlockPopout = openUnlockPopout;
private openAddEditVaultItemPopout = openAddEditVaultItemPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private notificationQueue: NotificationQueueMessageItem[] = []; private notificationQueue: NotificationQueueMessageItem[] = [];
private allowedRetryCommands: Set<ExtensionCommandType> = new Set([
ExtensionCommand.AutofillLogin,
ExtensionCommand.AutofillCard,
ExtensionCommand.AutofillIdentity,
]);
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
bgGetFolderData: () => this.getFolderData(), bgGetFolderData: () => this.getFolderData(),
@ -689,8 +698,8 @@ export default class NotificationBackground {
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
): Promise<void> { ): Promise<void> {
const messageData = message.data as LockedVaultPendingNotificationsData; const messageData = message.data as LockedVaultPendingNotificationsData;
const retryCommand = messageData.commandToRetry.message.command; const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType;
if (retryCommand === "autofill_login") { if (this.allowedRetryCommands.has(retryCommand)) {
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
} }

View File

@ -16,6 +16,7 @@ import {
CREATE_CARD_ID, CREATE_CARD_ID,
CREATE_IDENTITY_ID, CREATE_IDENTITY_ID,
CREATE_LOGIN_ID, CREATE_LOGIN_ID,
ExtensionCommand,
GENERATE_PASSWORD_ID, GENERATE_PASSWORD_ID,
NOOP_COMMAND_SUFFIX, NOOP_COMMAND_SUFFIX,
} from "@bitwarden/common/autofill/constants"; } from "@bitwarden/common/autofill/constants";
@ -79,7 +80,7 @@ export class ContextMenuClickedHandler {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsData = { const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: { commandToRetry: {
message: { command: NOOP_COMMAND_SUFFIX, contextMenuOnClickData: info }, message: { command: ExtensionCommand.NoopCommand, contextMenuOnClickData: info },
sender: { tab: tab }, sender: { tab: tab },
}, },
target: "contextmenus.background", target: "contextmenus.background",

View File

@ -159,9 +159,9 @@ export class AutofillV1Component implements OnInit {
private async setAutofillKeyboardHelperText(command: string) { private async setAutofillKeyboardHelperText(command: string) {
if (command) { if (command) {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command); this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
} else { } else {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet"); this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
} }
} }

View File

@ -107,7 +107,7 @@
</bit-section-header> </bit-section-header>
<bit-item> <bit-item>
<button bit-item-content type="button" (click)="openURI($event, browserShortcutsURI)"> <button bit-item-content type="button" (click)="openURI($event, browserShortcutsURI)">
<h3 bitTypography="h5">{{ "autofillKeyboardShortcutUpdateLabel" | i18n }}</h3> <h3 bitTypography="h5">{{ "autofillKeyboardManagerShortcutsLabel" | i18n }}</h3>
<bit-hint slot="secondary" class="tw-text-sm tw-whitespace-normal"> <bit-hint slot="secondary" class="tw-text-sm tw-whitespace-normal">
{{ autofillKeyboardHelperText }} {{ autofillKeyboardHelperText }}
</bit-hint> </bit-hint>

View File

@ -215,9 +215,9 @@ export class AutofillComponent implements OnInit {
private async setAutofillKeyboardHelperText(command: string) { private async setAutofillKeyboardHelperText(command: string) {
if (command) { if (command) {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command); this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
} else { } else {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet"); this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
} }
} }

View File

@ -1232,22 +1232,21 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab); jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation(); jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl") .spyOn(autofillService["cipherService"], "getNextCardCipher")
.mockResolvedValueOnce([cardCipher]); .mockResolvedValueOnce(cardCipher);
await autofillService.doAutoFillActiveTab(cardFormPageDetails, false, CipherType.Card); await autofillService.doAutoFillActiveTab(cardFormPageDetails, true, CipherType.Card);
expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({ expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab, tab: tab,
cipher: cardCipher, cipher: cardCipher,
pageDetails: cardFormPageDetails, pageDetails: cardFormPageDetails,
skipLastUsed: true, skipLastUsed: false,
skipUsernameOnlyFill: true, skipUsernameOnlyFill: false,
onlyEmptyFields: true, onlyEmptyFields: false,
onlyVisibleFields: true, onlyVisibleFields: false,
fillNewPassword: false, fillNewPassword: false,
allowUntrustedIframe: false, allowUntrustedIframe: true,
allowTotpAutofill: false, allowTotpAutofill: false,
}); });
}); });
@ -1280,26 +1279,21 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab); jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation(); jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl") .spyOn(autofillService["cipherService"], "getNextIdentityCipher")
.mockResolvedValueOnce([identityCipher]); .mockResolvedValueOnce(identityCipher);
await autofillService.doAutoFillActiveTab( await autofillService.doAutoFillActiveTab(identityFormPageDetails, true, CipherType.Identity);
identityFormPageDetails,
false,
CipherType.Identity,
);
expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({ expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab, tab: tab,
cipher: identityCipher, cipher: identityCipher,
pageDetails: identityFormPageDetails, pageDetails: identityFormPageDetails,
skipLastUsed: true, skipLastUsed: false,
skipUsernameOnlyFill: true, skipUsernameOnlyFill: false,
onlyEmptyFields: true, onlyEmptyFields: false,
onlyVisibleFields: true, onlyVisibleFields: false,
fillNewPassword: false, fillNewPassword: false,
allowUntrustedIframe: false, allowUntrustedIframe: true,
allowTotpAutofill: false, allowTotpAutofill: false,
}); });
}); });

View File

@ -520,16 +520,30 @@ export default class AutofillService implements AutofillServiceInterface {
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand); return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
} }
// Cipher is a non-login type let cipher: CipherView;
const cipher: CipherView = ( let cacheKey = "";
(await this.cipherService.getAllDecryptedForUrl(tab.url, [cipherType])) || []
).find(({ type }) => type === cipherType);
if (!cipher || cipher.reprompt !== CipherRepromptType.None) { if (cipherType === CipherType.Card) {
cacheKey = "cardCiphers";
cipher = await this.cipherService.getNextCardCipher();
} else {
cacheKey = "identityCiphers";
cipher = await this.cipherService.getNextIdentityCipher();
}
if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) {
return null; return null;
} }
return await this.doAutoFill({ if (await this.isPasswordRepromptRequired(cipher, tab)) {
if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
}
return null;
}
const totpCode = await this.doAutoFill({
tab: tab, tab: tab,
cipher: cipher, cipher: cipher,
pageDetails: pageDetails, pageDetails: pageDetails,
@ -541,6 +555,12 @@ export default class AutofillService implements AutofillServiceInterface {
allowUntrustedIframe: fromCommand, allowUntrustedIframe: fromCommand,
allowTotpAutofill: false, allowTotpAutofill: false,
}); });
if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
}
return totpCode;
} }
/** /**

View File

@ -1,6 +1,7 @@
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ExtensionCommand, ExtensionCommandType } from "@bitwarden/common/autofill/constants";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@ -47,8 +48,23 @@ export default class CommandsBackground {
case "generate_password": case "generate_password":
await this.generatePasswordToClipboard(); await this.generatePasswordToClipboard();
break; break;
case "autofill_login": case ExtensionCommand.AutofillLogin:
await this.autoFillLogin(sender ? sender.tab : null); await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillCommand,
);
break;
case ExtensionCommand.AutofillCard:
await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillCard,
);
break;
case ExtensionCommand.AutofillIdentity:
await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillIdentity,
);
break; break;
case "open_popup": case "open_popup":
await this.openPopup(); await this.openPopup();
@ -68,19 +84,27 @@ export default class CommandsBackground {
await this.passwordGenerationService.addHistory(password); await this.passwordGenerationService.addHistory(password);
} }
private async autoFillLogin(tab?: chrome.tabs.Tab) { private async triggerAutofillCommand(
tab?: chrome.tabs.Tab,
commandSender?: ExtensionCommandType,
) {
if (!tab) { if (!tab) {
tab = await BrowserApi.getTabFromCurrentWindowId(); tab = await BrowserApi.getTabFromCurrentWindowId();
} }
if (tab == null) { if (tab == null || !commandSender) {
return; return;
} }
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsData = { const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: { commandToRetry: {
message: { command: "autofill_login" }, message: {
command:
commandSender === ExtensionCommand.AutofillCommand
? ExtensionCommand.AutofillLogin
: commandSender,
},
sender: { tab: tab }, sender: { tab: tab },
}, },
target: "commands.background", target: "commands.background",
@ -95,7 +119,7 @@ export default class CommandsBackground {
return; return;
} }
await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd"); await this.main.collectPageDetailsForContentScript(tab, commandSender);
} }
private async openPopup() { private async openPopup() {

View File

@ -2,7 +2,7 @@ import { firstValueFrom, map, mergeMap } from "rxjs";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility, ExtensionCommand } 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -117,7 +117,7 @@ export default class RuntimeBackground {
case "collectPageDetailsResponse": case "collectPageDetailsResponse":
switch (msg.sender) { switch (msg.sender) {
case "autofiller": case "autofiller":
case "autofill_cmd": { case ExtensionCommand.AutofillCommand: {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)), this.accountService.activeAccount$.pipe(map((a) => a?.id)),
); );
@ -130,14 +130,14 @@ export default class RuntimeBackground {
details: msg.details, details: msg.details,
}, },
], ],
msg.sender === "autofill_cmd", msg.sender === ExtensionCommand.AutofillCommand,
); );
if (totpCode != null) { if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode); this.platformUtilsService.copyToClipboard(totpCode);
} }
break; break;
} }
case "autofill_card": { case ExtensionCommand.AutofillCard: {
await this.autofillService.doAutoFillActiveTab( await this.autofillService.doAutoFillActiveTab(
[ [
{ {
@ -146,12 +146,12 @@ export default class RuntimeBackground {
details: msg.details, details: msg.details,
}, },
], ],
false, msg.sender === ExtensionCommand.AutofillCard,
CipherType.Card, CipherType.Card,
); );
break; break;
} }
case "autofill_identity": { case ExtensionCommand.AutofillIdentity: {
await this.autofillService.doAutoFillActiveTab( await this.autofillService.doAutoFillActiveTab(
[ [
{ {
@ -160,7 +160,7 @@ export default class RuntimeBackground {
details: msg.details, details: msg.details,
}, },
], ],
false, msg.sender === ExtensionCommand.AutofillIdentity,
CipherType.Identity, CipherType.Identity,
); );
break; break;

View File

@ -94,7 +94,13 @@
"suggested_key": { "suggested_key": {
"default": "Ctrl+Shift+L" "default": "Ctrl+Shift+L"
}, },
"description": "__MSG_commandAutofillDesc__" "description": "__MSG_commandAutofillLoginDesc__"
},
"autofill_card": {
"description": "__MSG_commandAutofillCardDesc__"
},
"autofill_identity": {
"description": "__MSG_commandAutofillIdentityDesc__"
}, },
"generate_password": { "generate_password": {
"suggested_key": { "suggested_key": {

View File

@ -99,7 +99,13 @@
"suggested_key": { "suggested_key": {
"default": "Ctrl+Shift+L" "default": "Ctrl+Shift+L"
}, },
"description": "__MSG_commandAutofillDesc__" "description": "__MSG_commandAutofillLoginDesc__"
},
"autofill_card": {
"description": "__MSG_commandAutofillCardDesc__"
},
"autofill_identity": {
"description": "__MSG_commandAutofillIdentityDesc__"
}, },
"generate_password": { "generate_password": {
"suggested_key": { "suggested_key": {

View File

@ -1,3 +1,4 @@
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { import {
ClipboardOptions, ClipboardOptions,
@ -298,7 +299,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
autofillCommand = "Cmd+Shift+L"; autofillCommand = "Cmd+Shift+L";
} else if (this.isFirefox()) { } else if (this.isFirefox()) {
autofillCommand = (await browser.commands.getAll()).find( autofillCommand = (await browser.commands.getAll()).find(
(c) => c.name === "autofill_login", (c) => c.name === ExtensionCommand.AutofillLogin,
).shortcut; ).shortcut;
// Firefox is returning Ctrl instead of Cmd for the modifier key on macOS if // Firefox is returning Ctrl instead of Cmd for the modifier key on macOS if
// the command is the default one set on installation. // the command is the default one set on installation.
@ -311,7 +312,9 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
} else { } else {
await new Promise((resolve) => await new Promise((resolve) =>
chrome.commands.getAll((c) => chrome.commands.getAll((c) =>
resolve((autofillCommand = c.find((c) => c.name === "autofill_login").shortcut)), resolve(
(autofillCommand = c.find((c) => c.name === ExtensionCommand.AutofillLogin).shortcut),
),
), ),
); );
} }

View File

@ -85,3 +85,17 @@ export const DisablePasswordManagerUris = {
Vivaldi: "vivaldi://settings/autofill", Vivaldi: "vivaldi://settings/autofill",
Unknown: "https://bitwarden.com/help/disable-browser-autofill/", Unknown: "https://bitwarden.com/help/disable-browser-autofill/",
} as const; } as const;
export const ExtensionCommand = {
AutofillCommand: "autofill_cmd",
AutofillCard: "autofill_card",
AutofillIdentity: "autofill_identity",
AutofillLogin: "autofill_login",
OpenAutofillOverlay: "open_autofill_overlay",
GeneratePassword: "generate_password",
OpenPopup: "open_popup",
LockVault: "lock_vault",
NoopCommand: "noop",
} as const;
export type ExtensionCommandType = (typeof ExtensionCommand)[keyof typeof ExtensionCommand];

View File

@ -162,4 +162,6 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
newUserKey: UserKey, newUserKey: UserKey,
userId: UserId, userId: UserId,
) => Promise<CipherWithIdRequest[]>; ) => Promise<CipherWithIdRequest[]>;
getNextCardCipher: () => Promise<CipherView>;
getNextIdentityCipher: () => Promise<CipherView>;
} }

View File

@ -500,6 +500,13 @@ export class CipherService implements CipherServiceAbstraction {
}); });
} }
private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise<CipherView[]> {
const ciphers = await this.getAllDecrypted();
return ciphers
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
}
async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> { async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
const response = await this.apiService.getCiphersOrganization(organizationId); const response = await this.apiService.getCiphersOrganization(organizationId);
return await this.decryptOrganizationCiphersResponse(response, organizationId); return await this.decryptOrganizationCiphersResponse(response, organizationId);
@ -549,6 +556,36 @@ export class CipherService implements CipherServiceAbstraction {
return this.getCipherForUrl(url, false, false, false); return this.getCipherForUrl(url, false, false, false);
} }
async getNextCardCipher(): Promise<CipherView> {
const cacheKey = "cardCiphers";
if (!this.sortedCiphersCache.isCached(cacheKey)) {
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]);
if (!ciphers?.length) {
return null;
}
this.sortedCiphersCache.addCiphers(cacheKey, ciphers);
}
return this.sortedCiphersCache.getNext(cacheKey);
}
async getNextIdentityCipher() {
const cacheKey = "identityCiphers";
if (!this.sortedCiphersCache.isCached(cacheKey)) {
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]);
if (!ciphers?.length) {
return null;
}
this.sortedCiphersCache.addCiphers(cacheKey, ciphers);
}
return this.sortedCiphersCache.getNext(cacheKey);
}
updateLastUsedIndexForUrl(url: string) { updateLastUsedIndexForUrl(url: string) {
this.sortedCiphersCache.updateLastUsedIndex(url); this.sortedCiphersCache.updateLastUsedIndex(url);
} }