mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -1343,8 +1343,14 @@
|
||||
"commandOpenSidebar": {
|
||||
"message": "Open vault in sidebar"
|
||||
},
|
||||
"commandAutofillDesc": {
|
||||
"message": "Auto-fill the last used login for the current website"
|
||||
"commandAutofillLoginDesc": {
|
||||
"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": {
|
||||
"message": "Generate and copy a new random password to the clipboard"
|
||||
@@ -1650,6 +1656,10 @@
|
||||
"message": "Base domain",
|
||||
"description": "Domain name. Ex. website.com"
|
||||
},
|
||||
"baseDomainOptionRecommended": {
|
||||
"message": "Base domain (recommended)",
|
||||
"description": "Domain name. Ex. website.com"
|
||||
},
|
||||
"domainName": {
|
||||
"message": "Domain name",
|
||||
"description": "Domain name. Ex. website.com"
|
||||
@@ -2774,14 +2784,17 @@
|
||||
"autofillKeyboardShortcutUpdateLabel": {
|
||||
"message": "Change shortcut"
|
||||
},
|
||||
"autofillKeyboardManagerShortcutsLabel": {
|
||||
"message": "Manage shortcuts"
|
||||
},
|
||||
"autofillShortcut": {
|
||||
"message": "Autofill keyboard shortcut"
|
||||
},
|
||||
"autofillShortcutNotSet": {
|
||||
"message": "The autofill shortcut is not set. Change this in the browser's settings."
|
||||
"autofillLoginShortcutNotSet": {
|
||||
"message": "The autofill login shortcut is not set. Change this in the browser's settings."
|
||||
},
|
||||
"autofillShortcutText": {
|
||||
"message": "The autofill shortcut is: $COMMAND$. Change this in the browser's settings.",
|
||||
"autofillLoginShortcutText": {
|
||||
"message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.",
|
||||
"placeholders": {
|
||||
"command": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -190,7 +190,6 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
||||
}: BackgroundOnMessageHandlerParams) => void;
|
||||
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
unlockCompleted: ({ message }: BackgroundMessageParam) => void;
|
||||
doFullSync: () => void;
|
||||
addedCipher: () => void;
|
||||
addEditCipherSubmitted: () => void;
|
||||
editedCipher: () => void;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
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 { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -151,7 +152,7 @@ describe("NotificationBackground", () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "unlockCompleted",
|
||||
data: {
|
||||
commandToRetry: { message: { command: "autofill_login" } },
|
||||
commandToRetry: { message: { command: ExtensionCommand.AutofillLogin } },
|
||||
} as LockedVaultPendingNotificationsData,
|
||||
};
|
||||
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
|
||||
|
||||
@@ -4,7 +4,11 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
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 { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
@@ -45,6 +49,11 @@ export default class NotificationBackground {
|
||||
private openUnlockPopout = openUnlockPopout;
|
||||
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
|
||||
private notificationQueue: NotificationQueueMessageItem[] = [];
|
||||
private allowedRetryCommands: Set<ExtensionCommandType> = new Set([
|
||||
ExtensionCommand.AutofillLogin,
|
||||
ExtensionCommand.AutofillCard,
|
||||
ExtensionCommand.AutofillIdentity,
|
||||
]);
|
||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
|
||||
bgGetFolderData: () => this.getFolderData(),
|
||||
@@ -689,8 +698,8 @@ export default class NotificationBackground {
|
||||
sender: chrome.runtime.MessageSender,
|
||||
): Promise<void> {
|
||||
const messageData = message.data as LockedVaultPendingNotificationsData;
|
||||
const retryCommand = messageData.commandToRetry.message.command;
|
||||
if (retryCommand === "autofill_login") {
|
||||
const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType;
|
||||
if (this.allowedRetryCommands.has(retryCommand)) {
|
||||
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
|
||||
}
|
||||
|
||||
|
||||
@@ -2014,7 +2014,6 @@ describe("OverlayBackground", () => {
|
||||
|
||||
describe("extension messages that trigger an update of the inline menu ciphers", () => {
|
||||
const extensionMessages = [
|
||||
"doFullSync",
|
||||
"addedCipher",
|
||||
"addEditCipherSubmitted",
|
||||
"editedCipher",
|
||||
|
||||
@@ -120,7 +120,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
|
||||
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
|
||||
unlockCompleted: ({ message }) => this.unlockCompleted(message),
|
||||
doFullSync: () => this.updateOverlayCiphers(),
|
||||
addedCipher: () => this.updateOverlayCiphers(),
|
||||
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
||||
editedCipher: () => this.updateOverlayCiphers(),
|
||||
@@ -273,7 +272,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
|
||||
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||
|
||||
return cipherViews.concat(...this.cardAndIdentityCiphers);
|
||||
return this.cardAndIdentityCiphers
|
||||
? cipherViews.concat(...this.cardAndIdentityCiphers)
|
||||
: cipherViews;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CREATE_CARD_ID,
|
||||
CREATE_IDENTITY_ID,
|
||||
CREATE_LOGIN_ID,
|
||||
ExtensionCommand,
|
||||
GENERATE_PASSWORD_ID,
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
@@ -79,7 +80,7 @@ export class ContextMenuClickedHandler {
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsData = {
|
||||
commandToRetry: {
|
||||
message: { command: NOOP_COMMAND_SUFFIX, contextMenuOnClickData: info },
|
||||
message: { command: ExtensionCommand.NoopCommand, contextMenuOnClickData: info },
|
||||
sender: { tab: tab },
|
||||
},
|
||||
target: "contextmenus.background",
|
||||
|
||||
@@ -399,6 +399,11 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
});
|
||||
|
||||
it("sets the z-index of to a lower value", async () => {
|
||||
autofillInlineMenuContentService["handlePersistentLastChildOverrideTimeout"] = setTimeout(
|
||||
jest.fn(),
|
||||
1000,
|
||||
);
|
||||
|
||||
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
|
||||
await waitForIdleCallback();
|
||||
|
||||
@@ -411,8 +416,9 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
});
|
||||
globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild);
|
||||
|
||||
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
|
||||
await waitForIdleCallback();
|
||||
await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"](
|
||||
persistentLastChild,
|
||||
);
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
|
||||
overlayElement: AutofillOverlayElement.Button,
|
||||
@@ -425,8 +431,9 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
});
|
||||
globalThis.document.elementFromPoint = jest.fn(() => persistentLastChild);
|
||||
|
||||
await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"]();
|
||||
await waitForIdleCallback();
|
||||
await autofillInlineMenuContentService["verifyInlineMenuIsNotObscured"](
|
||||
persistentLastChild,
|
||||
);
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", {
|
||||
overlayElement: AutofillOverlayElement.List,
|
||||
|
||||
@@ -33,6 +33,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
private bodyElementMutationObserver: MutationObserver;
|
||||
private mutationObserverIterations = 0;
|
||||
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
|
||||
private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout;
|
||||
private lastElementOverrides: WeakMap<Element, number> = new WeakMap();
|
||||
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
|
||||
all: "initial",
|
||||
@@ -405,7 +406,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
}
|
||||
|
||||
if (this.lastElementOverrides.get(lastChild) >= 3) {
|
||||
await this.handlePersistentLastChildOverride(lastChild);
|
||||
this.handlePersistentLastChildOverride(lastChild);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -430,6 +431,26 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
globalThis.document.body.insertBefore(lastChild, this.buttonElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the behavior of a persistent child element that is forcing itself to
|
||||
* the bottom of the body element. This method will ensure that the inline menu
|
||||
* elements are not obscured by the persistent child element.
|
||||
*
|
||||
* @param lastChild - The last child of the body element.
|
||||
*/
|
||||
private handlePersistentLastChildOverride(lastChild: Element) {
|
||||
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
|
||||
if (lastChildZIndex >= 2147483647) {
|
||||
(lastChild as HTMLElement).style.zIndex = "2147483646";
|
||||
}
|
||||
|
||||
this.clearPersistentLastChildOverrideTimeout();
|
||||
this.handlePersistentLastChildOverrideTimeout = globalThis.setTimeout(
|
||||
() => this.verifyInlineMenuIsNotObscured(lastChild),
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the last child of the body element is overlaying the inline menu elements.
|
||||
* This is triggered when the last child of the body is being forced by some script to
|
||||
@@ -437,12 +458,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
*
|
||||
* @param lastChild - The last child of the body element.
|
||||
*/
|
||||
private async handlePersistentLastChildOverride(lastChild: Element) {
|
||||
const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
|
||||
if (lastChildZIndex >= 2147483647) {
|
||||
(lastChild as HTMLElement).style.zIndex = "2147483646";
|
||||
}
|
||||
|
||||
private verifyInlineMenuIsNotObscured = async (lastChild: Element) => {
|
||||
const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage(
|
||||
"getAutofillInlineMenuPosition",
|
||||
);
|
||||
@@ -456,7 +472,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
if (!!list && this.elementAtCenterOfInlineMenuPosition(list) === lastChild) {
|
||||
this.closeInlineMenu();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the element present at the center of the inline menu position.
|
||||
@@ -470,6 +486,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timeout that is used to verify that the last child of the body element
|
||||
* is not overlaying the inline menu elements.
|
||||
*/
|
||||
private clearPersistentLastChildOverrideTimeout() {
|
||||
if (this.handlePersistentLastChildOverrideTimeout) {
|
||||
globalThis.clearTimeout(this.handlePersistentLastChildOverrideTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the mutation observer is triggering excessive iterations.
|
||||
* Will trigger a blur of the most recently focused field and remove the
|
||||
@@ -503,5 +529,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
*/
|
||||
destroy() {
|
||||
this.closeInlineMenu();
|
||||
this.clearPersistentLastChildOverrideTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +377,21 @@ describe("AutofillInlineMenuIframeService", () => {
|
||||
autofillInlineMenuIframeService["ariaAlertElement"],
|
||||
);
|
||||
});
|
||||
|
||||
it("resets the fade in timeout if it is set", () => {
|
||||
autofillInlineMenuIframeService["fadeInTimeout"] = setTimeout(jest.fn, 100);
|
||||
const styles = { top: "100px", left: "100px" };
|
||||
jest.spyOn(autofillInlineMenuIframeService as any, "handleFadeInInlineMenuIframe");
|
||||
|
||||
sendPortMessage(portSpy, {
|
||||
command: "updateAutofillInlineMenuPosition",
|
||||
styles,
|
||||
});
|
||||
|
||||
expect(
|
||||
autofillInlineMenuIframeService["handleFadeInInlineMenuIframe"],
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the visibility of the iframe", () => {
|
||||
|
||||
@@ -260,10 +260,13 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = this.fadeInTimeout ? Object.assign(position, { opacity: "0" }) : position;
|
||||
this.updateElementStyles(this.iframe, styles);
|
||||
|
||||
if (this.fadeInTimeout) {
|
||||
this.handleFadeInInlineMenuIframe();
|
||||
}
|
||||
this.updateElementStyles(this.iframe, position);
|
||||
|
||||
this.announceAriaAlert();
|
||||
}
|
||||
|
||||
@@ -320,10 +323,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
||||
*/
|
||||
private handleFadeInInlineMenuIframe() {
|
||||
this.clearFadeInTimeout();
|
||||
this.fadeInTimeout = globalThis.setTimeout(
|
||||
() => this.updateElementStyles(this.iframe, { display: "block", opacity: "1" }),
|
||||
10,
|
||||
);
|
||||
this.fadeInTimeout = globalThis.setTimeout(() => {
|
||||
this.updateElementStyles(this.iframe, { display: "block", opacity: "1" });
|
||||
this.clearFadeInTimeout();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,7 @@ export class AutofillV1Component implements OnInit {
|
||||
{ name: i18nService.t("fiveMinutes"), value: 300 },
|
||||
];
|
||||
this.uriMatchOptions = [
|
||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||
@@ -159,9 +159,9 @@ export class AutofillV1Component implements OnInit {
|
||||
|
||||
private async setAutofillKeyboardHelperText(command: string) {
|
||||
if (command) {
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command);
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
|
||||
} else {
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet");
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "autofillSuggestionsSectionTitle" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<bit-form-control>
|
||||
@@ -87,7 +87,7 @@
|
||||
/>
|
||||
<bit-label for="showCardsSuggestions">{{ "showCardsInVaultView" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<bit-form-control disableMargin>
|
||||
<input
|
||||
bitCheckbox
|
||||
id="showIdentitiesSuggestions"
|
||||
@@ -103,11 +103,11 @@
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "autofillKeyboardShortcutSectionTitle" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "autofillKeyboardShortcutSectionTitle" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-item>
|
||||
<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">
|
||||
{{ autofillKeyboardHelperText }}
|
||||
</bit-hint>
|
||||
@@ -122,7 +122,7 @@
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "enableAutoFillOnPageLoadSectionTitle" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "enableAutoFillOnPageLoadSectionTitle" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<bit-hint class="tw-mb-6 tw-text-sm">
|
||||
@@ -148,7 +148,7 @@
|
||||
/>
|
||||
<bit-label for="autofillOnPageLoad">{{ "enableAutoFillOnPageLoad" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-field>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label for="defaultAutofill">{{ "defaultAutoFillOnPageLoad" | i18n }}</bit-label>
|
||||
<select
|
||||
bitInput
|
||||
@@ -159,7 +159,7 @@
|
||||
>
|
||||
<option
|
||||
*ngFor="let o of autofillOnPageLoadOptions"
|
||||
[value]="o.value"
|
||||
[ngValue]="o.value"
|
||||
[label]="o.name"
|
||||
></option>
|
||||
</select>
|
||||
@@ -171,7 +171,7 @@
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "additionalOptions" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "additionalOptions" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<bit-form-control>
|
||||
@@ -206,14 +206,14 @@
|
||||
<option
|
||||
*ngFor="let o of clearClipboardOptions"
|
||||
[label]="o.name"
|
||||
[value]="o.value"
|
||||
[ngValue]="o.value"
|
||||
></option>
|
||||
</select>
|
||||
<bit-hint class="tw-text-sm" id="clearClipboardHelp">
|
||||
{{ "clearClipboardDesc" | i18n }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label for="defaultUriMatch">{{ "defaultUriMatchDetection" | i18n }}</bit-label>
|
||||
<select
|
||||
aria-describedby="defaultUriMatchHelp"
|
||||
@@ -222,7 +222,7 @@
|
||||
(change)="saveDefaultUriMatch()"
|
||||
[(ngModel)]="defaultUriMatch"
|
||||
>
|
||||
<option *ngFor="let o of uriMatchOptions" [label]="o.name" [value]="o.value"></option>
|
||||
<option *ngFor="let o of uriMatchOptions" [label]="o.name" [ngValue]="o.value"></option>
|
||||
</select>
|
||||
<bit-hint class="tw-text-sm" id="defaultUriMatchHelp">
|
||||
{{ "defaultUriMatchDetectionDesc" | i18n }}
|
||||
|
||||
@@ -126,7 +126,7 @@ export class AutofillComponent implements OnInit {
|
||||
{ name: i18nService.t("fiveMinutes"), value: ClearClipboardDelay.FiveMinutes },
|
||||
];
|
||||
this.uriMatchOptions = [
|
||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||
@@ -215,9 +215,9 @@ export class AutofillComponent implements OnInit {
|
||||
|
||||
private async setAutofillKeyboardHelperText(command: string) {
|
||||
if (command) {
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command);
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
|
||||
} else {
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet");
|
||||
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "domainsTitle" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2>
|
||||
<span bitTypography="body2" slot="end">{{ excludedDomainsState?.length || 0 }}</span>
|
||||
</bit-section-header>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "vaultSaveOptionsTitle" | i18n }}</h2>
|
||||
<h2 bitTypography="h6">{{ "vaultSaveOptionsTitle" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<div>
|
||||
|
||||
@@ -393,6 +393,7 @@ export class IdentityAutoFillConstants {
|
||||
"address-line-2",
|
||||
"addr-2",
|
||||
"street-2",
|
||||
"address-ext",
|
||||
];
|
||||
|
||||
static readonly Address3FieldNames: string[] = [
|
||||
@@ -409,6 +410,7 @@ export class IdentityAutoFillConstants {
|
||||
"zip-code",
|
||||
"postal-code",
|
||||
"post-code",
|
||||
"postcode",
|
||||
"address-zip",
|
||||
"address-postal",
|
||||
"address-code",
|
||||
|
||||
@@ -1232,22 +1232,21 @@ describe("AutofillService", () => {
|
||||
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
|
||||
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
|
||||
jest
|
||||
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
|
||||
.mockResolvedValueOnce([cardCipher]);
|
||||
.spyOn(autofillService["cipherService"], "getNextCardCipher")
|
||||
.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({
|
||||
tab: tab,
|
||||
cipher: cardCipher,
|
||||
pageDetails: cardFormPageDetails,
|
||||
skipLastUsed: true,
|
||||
skipUsernameOnlyFill: true,
|
||||
onlyEmptyFields: true,
|
||||
onlyVisibleFields: true,
|
||||
skipLastUsed: false,
|
||||
skipUsernameOnlyFill: false,
|
||||
onlyEmptyFields: false,
|
||||
onlyVisibleFields: false,
|
||||
fillNewPassword: false,
|
||||
allowUntrustedIframe: false,
|
||||
allowUntrustedIframe: true,
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
});
|
||||
@@ -1280,26 +1279,21 @@ describe("AutofillService", () => {
|
||||
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
|
||||
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
|
||||
jest
|
||||
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
|
||||
.mockResolvedValueOnce([identityCipher]);
|
||||
.spyOn(autofillService["cipherService"], "getNextIdentityCipher")
|
||||
.mockResolvedValueOnce(identityCipher);
|
||||
|
||||
await autofillService.doAutoFillActiveTab(
|
||||
identityFormPageDetails,
|
||||
false,
|
||||
CipherType.Identity,
|
||||
);
|
||||
await autofillService.doAutoFillActiveTab(identityFormPageDetails, true, CipherType.Identity);
|
||||
|
||||
expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
|
||||
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
|
||||
tab: tab,
|
||||
cipher: identityCipher,
|
||||
pageDetails: identityFormPageDetails,
|
||||
skipLastUsed: true,
|
||||
skipUsernameOnlyFill: true,
|
||||
onlyEmptyFields: true,
|
||||
onlyVisibleFields: true,
|
||||
skipLastUsed: false,
|
||||
skipUsernameOnlyFill: false,
|
||||
onlyEmptyFields: false,
|
||||
onlyVisibleFields: false,
|
||||
fillNewPassword: false,
|
||||
allowUntrustedIframe: false,
|
||||
allowUntrustedIframe: true,
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -520,16 +520,30 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
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);
|
||||
let cipher: CipherView;
|
||||
let cacheKey = "";
|
||||
|
||||
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 await this.doAutoFill({
|
||||
if (await this.isPasswordRepromptRequired(cipher, tab)) {
|
||||
if (fromCommand) {
|
||||
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const totpCode = await this.doAutoFill({
|
||||
tab: tab,
|
||||
cipher: cipher,
|
||||
pageDetails: pageDetails,
|
||||
@@ -541,6 +555,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
allowUntrustedIframe: fromCommand,
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
|
||||
if (fromCommand) {
|
||||
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
|
||||
}
|
||||
|
||||
return totpCode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ export class InlineMenuFieldQualificationService
|
||||
implements InlineMenuFieldQualificationServiceInterface
|
||||
{
|
||||
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
|
||||
private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
|
||||
private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
|
||||
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
|
||||
private usernameAutocompleteValue = "username";
|
||||
private emailAutocompleteValue = "email";
|
||||
@@ -244,6 +244,10 @@ export class InlineMenuFieldQualificationService
|
||||
* @param pageDetails - The details of the page that the field is on.
|
||||
*/
|
||||
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isUsernameField(field) && !this.isPasswordField(field)) {
|
||||
return false;
|
||||
}
|
||||
@@ -286,6 +290,10 @@ export class InlineMenuFieldQualificationService
|
||||
* @param _pageDetails - Currently unused, will likely be required in the future
|
||||
*/
|
||||
isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean {
|
||||
if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) {
|
||||
return true;
|
||||
}
|
||||
@@ -658,7 +666,14 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address1FieldNames, false)
|
||||
this.keywordsFoundInFieldData(
|
||||
field,
|
||||
[
|
||||
...IdentityAutoFillConstants.AddressFieldNames,
|
||||
...IdentityAutoFillConstants.Address1FieldNames,
|
||||
],
|
||||
false,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -722,7 +737,7 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.AddressFieldNames, false)
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -833,7 +848,7 @@ export class InlineMenuFieldQualificationService
|
||||
isUsernameField = (field: AutofillField): boolean => {
|
||||
if (
|
||||
!this.usernameFieldTypes.has(field.type) ||
|
||||
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)
|
||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -852,7 +867,7 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
return (
|
||||
!this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) &&
|
||||
!this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) &&
|
||||
this.keywordsFoundInFieldData(field, AutoFillConstants.EmailFieldNames)
|
||||
);
|
||||
};
|
||||
@@ -898,7 +913,7 @@ export class InlineMenuFieldQualificationService
|
||||
const isInputPasswordType = field.type === "password";
|
||||
if (
|
||||
(!isInputPasswordType &&
|
||||
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)) ||
|
||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) ||
|
||||
this.fieldHasDisqualifyingAttributeValue(field)
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@@ -47,8 +48,23 @@ export default class CommandsBackground {
|
||||
case "generate_password":
|
||||
await this.generatePasswordToClipboard();
|
||||
break;
|
||||
case "autofill_login":
|
||||
await this.autoFillLogin(sender ? sender.tab : null);
|
||||
case ExtensionCommand.AutofillLogin:
|
||||
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;
|
||||
case "open_popup":
|
||||
await this.openPopup();
|
||||
@@ -68,19 +84,27 @@ export default class CommandsBackground {
|
||||
await this.passwordGenerationService.addHistory(password);
|
||||
}
|
||||
|
||||
private async autoFillLogin(tab?: chrome.tabs.Tab) {
|
||||
private async triggerAutofillCommand(
|
||||
tab?: chrome.tabs.Tab,
|
||||
commandSender?: ExtensionCommandType,
|
||||
) {
|
||||
if (!tab) {
|
||||
tab = await BrowserApi.getTabFromCurrentWindowId();
|
||||
}
|
||||
|
||||
if (tab == null) {
|
||||
if (tab == null || !commandSender) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
const retryMessage: LockedVaultPendingNotificationsData = {
|
||||
commandToRetry: {
|
||||
message: { command: "autofill_login" },
|
||||
message: {
|
||||
command:
|
||||
commandSender === ExtensionCommand.AutofillCommand
|
||||
? ExtensionCommand.AutofillLogin
|
||||
: commandSender,
|
||||
},
|
||||
sender: { tab: tab },
|
||||
},
|
||||
target: "commands.background",
|
||||
@@ -95,7 +119,7 @@ export default class CommandsBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd");
|
||||
await this.main.collectPageDetailsForContentScript(tab, commandSender);
|
||||
}
|
||||
|
||||
private async openPopup() {
|
||||
|
||||
@@ -1245,6 +1245,13 @@ export default class MainBackground {
|
||||
}
|
||||
}
|
||||
|
||||
async updateOverlayCiphers() {
|
||||
// overlayBackground null in popup only contexts
|
||||
if (this.overlayBackground) {
|
||||
await this.overlayBackground.updateOverlayCiphers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch accounts to indicated userId -- null is no active user
|
||||
*/
|
||||
@@ -1273,7 +1280,7 @@ export default class MainBackground {
|
||||
if (userId == null) {
|
||||
await this.refreshBadge();
|
||||
await this.refreshMenu();
|
||||
await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts
|
||||
await this.updateOverlayCiphers();
|
||||
this.messagingService.send("goHome");
|
||||
return;
|
||||
}
|
||||
@@ -1296,7 +1303,7 @@ export default class MainBackground {
|
||||
this.messagingService.send("unlocked", { userId: userId });
|
||||
await this.refreshBadge();
|
||||
await this.refreshMenu();
|
||||
await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts
|
||||
await this.updateOverlayCiphers();
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
} finally {
|
||||
@@ -1480,7 +1487,17 @@ export default class MainBackground {
|
||||
* Temporary solution to handle initialization of the overlay background behind a feature flag.
|
||||
* Will be reverted to instantiation within the constructor once the feature flag is removed.
|
||||
*/
|
||||
private async initOverlayAndTabsBackground() {
|
||||
async initOverlayAndTabsBackground() {
|
||||
if (
|
||||
this.popupOnlyContext ||
|
||||
this.overlayBackground ||
|
||||
this.tabsBackground ||
|
||||
(await firstValueFrom(this.authService.activeAccountStatus$)) ===
|
||||
AuthenticationStatus.LoggedOut
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.InlineMenuPositioningImprovements,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { firstValueFrom, map, mergeMap } from "rxjs";
|
||||
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -117,7 +117,7 @@ export default class RuntimeBackground {
|
||||
case "collectPageDetailsResponse":
|
||||
switch (msg.sender) {
|
||||
case "autofiller":
|
||||
case "autofill_cmd": {
|
||||
case ExtensionCommand.AutofillCommand: {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
@@ -130,14 +130,14 @@ export default class RuntimeBackground {
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
msg.sender === "autofill_cmd",
|
||||
msg.sender === ExtensionCommand.AutofillCommand,
|
||||
);
|
||||
if (totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(totpCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "autofill_card": {
|
||||
case ExtensionCommand.AutofillCard: {
|
||||
await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
@@ -146,12 +146,12 @@ export default class RuntimeBackground {
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
false,
|
||||
msg.sender === ExtensionCommand.AutofillCard,
|
||||
CipherType.Card,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "autofill_identity": {
|
||||
case ExtensionCommand.AutofillIdentity: {
|
||||
await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
@@ -160,7 +160,7 @@ export default class RuntimeBackground {
|
||||
details: msg.details,
|
||||
},
|
||||
],
|
||||
false,
|
||||
msg.sender === ExtensionCommand.AutofillIdentity,
|
||||
CipherType.Identity,
|
||||
);
|
||||
break;
|
||||
@@ -200,6 +200,7 @@ export default class RuntimeBackground {
|
||||
let item: LockedVaultPendingNotificationsData;
|
||||
|
||||
if (msg.command === "loggedIn") {
|
||||
await this.main.initOverlayAndTabsBackground();
|
||||
await this.sendBwInstalledMessageToVault();
|
||||
await this.autofillService.reloadAutofillScripts();
|
||||
}
|
||||
@@ -246,6 +247,7 @@ export default class RuntimeBackground {
|
||||
await this.main.refreshMenu();
|
||||
}, 2000);
|
||||
await this.configService.ensureConfigFetched();
|
||||
await this.main.updateOverlayCiphers();
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
|
||||
@@ -94,7 +94,13 @@
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+L"
|
||||
},
|
||||
"description": "__MSG_commandAutofillDesc__"
|
||||
"description": "__MSG_commandAutofillLoginDesc__"
|
||||
},
|
||||
"autofill_card": {
|
||||
"description": "__MSG_commandAutofillCardDesc__"
|
||||
},
|
||||
"autofill_identity": {
|
||||
"description": "__MSG_commandAutofillIdentityDesc__"
|
||||
},
|
||||
"generate_password": {
|
||||
"suggested_key": {
|
||||
|
||||
@@ -99,7 +99,13 @@
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+L"
|
||||
},
|
||||
"description": "__MSG_commandAutofillDesc__"
|
||||
"description": "__MSG_commandAutofillLoginDesc__"
|
||||
},
|
||||
"autofill_card": {
|
||||
"description": "__MSG_commandAutofillCardDesc__"
|
||||
},
|
||||
"autofill_identity": {
|
||||
"description": "__MSG_commandAutofillIdentityDesc__"
|
||||
},
|
||||
"generate_password": {
|
||||
"suggested_key": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
ClipboardOptions,
|
||||
@@ -298,7 +299,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
autofillCommand = "Cmd+Shift+L";
|
||||
} else if (this.isFirefox()) {
|
||||
autofillCommand = (await browser.commands.getAll()).find(
|
||||
(c) => c.name === "autofill_login",
|
||||
(c) => c.name === ExtensionCommand.AutofillLogin,
|
||||
).shortcut;
|
||||
// Firefox is returning Ctrl instead of Cmd for the modifier key on macOS if
|
||||
// the command is the default one set on installation.
|
||||
@@ -311,7 +312,9 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
} else {
|
||||
await new Promise((resolve) =>
|
||||
chrome.commands.getAll((c) =>
|
||||
resolve((autofillCommand = c.find((c) => c.name === "autofill_login").shortcut)),
|
||||
resolve(
|
||||
(autofillCommand = c.find((c) => c.name === ExtensionCommand.AutofillLogin).shortcut),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
protected biometricReady = false;
|
||||
private biometricAsked = false;
|
||||
private autoPromptBiometric = false;
|
||||
private timerId: any;
|
||||
|
||||
constructor(
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
@@ -135,11 +136,18 @@ export class LockComponent extends BaseLockComponent {
|
||||
});
|
||||
});
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
|
||||
// start background listener until destroyed on interval
|
||||
this.timerId = setInterval(async () => {
|
||||
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
||||
this.biometricReady = await this.canUseBiometric();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
|
||||
@@ -39,6 +39,7 @@ import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.
|
||||
import { DesktopSettingsService } from "./platform/services/desktop-settings.service";
|
||||
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
|
||||
import { ElectronStorageService } from "./platform/services/electron-storage.service";
|
||||
import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service";
|
||||
import { I18nMainService } from "./platform/services/i18n.main.service";
|
||||
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
|
||||
import { isMacAppStore } from "./utils";
|
||||
@@ -224,6 +225,8 @@ export class Main {
|
||||
|
||||
this.clipboardMain = new ClipboardMain();
|
||||
this.clipboardMain.init();
|
||||
|
||||
new EphemeralValueStorageService();
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
||||
@@ -99,6 +99,14 @@ const crypto = {
|
||||
ipcRenderer.invoke("crypto.argon2", { password, salt, iterations, memory, parallelism }),
|
||||
};
|
||||
|
||||
const ephemeralStore = {
|
||||
setEphemeralValue: (key: string, value: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setEphemeralValue", { key, value }),
|
||||
getEphemeralValue: (key: string): Promise<string> => ipcRenderer.invoke("getEphemeralValue", key),
|
||||
removeEphemeralValue: (key: string): Promise<void> =>
|
||||
ipcRenderer.invoke("deleteEphemeralValue", key),
|
||||
};
|
||||
|
||||
export default {
|
||||
versions: {
|
||||
app: (): Promise<string> => ipcRenderer.invoke("appVersion"),
|
||||
@@ -156,6 +164,7 @@ export default {
|
||||
powermonitor,
|
||||
nativeMessaging,
|
||||
crypto,
|
||||
ephemeralStore,
|
||||
};
|
||||
|
||||
function deviceType(): DeviceType {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
/**
|
||||
* The ephemeral value store holds values that should be accessible to the renderer past a process reload.
|
||||
* In the current state, this store must not contain any keys that can decrypt a vault by themselves.
|
||||
*/
|
||||
export class EphemeralValueStorageService {
|
||||
private ephemeralValues = new Map<string, string>();
|
||||
|
||||
constructor() {
|
||||
ipcMain.handle("setEphemeralValue", async (event, { key, value }) => {
|
||||
this.ephemeralValues.set(key, value);
|
||||
});
|
||||
ipcMain.handle("getEphemeralValue", async (event, key: string) => {
|
||||
return this.ephemeralValues.get(key);
|
||||
});
|
||||
ipcMain.handle("deleteEphemeralValue", async (event, key: string) => {
|
||||
this.ephemeralValues.delete(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -30,8 +29,6 @@ const HashAlgorithmForAsymmetricEncryption = "sha1";
|
||||
|
||||
@Injectable()
|
||||
export class NativeMessagingService {
|
||||
private sharedSecrets = new Map<string, SymmetricCryptoKey>();
|
||||
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private cryptoService: CryptoService,
|
||||
@@ -104,7 +101,7 @@ export class NativeMessagingService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sharedSecrets.get(appId) == null) {
|
||||
if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) {
|
||||
ipc.platform.nativeMessaging.sendMessage({
|
||||
command: "invalidateEncryption",
|
||||
appId: appId,
|
||||
@@ -115,7 +112,7 @@ export class NativeMessagingService {
|
||||
const message: LegacyMessage = JSON.parse(
|
||||
await this.cryptoService.decryptToUtf8(
|
||||
rawMessage as EncString,
|
||||
this.sharedSecrets.get(appId),
|
||||
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -147,11 +144,6 @@ export class NativeMessagingService {
|
||||
return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId);
|
||||
}
|
||||
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId);
|
||||
}
|
||||
|
||||
const biometricUnlockPromise =
|
||||
message.userId == null
|
||||
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||
@@ -185,6 +177,7 @@ export class NativeMessagingService {
|
||||
},
|
||||
appId,
|
||||
);
|
||||
await ipc.platform.reloadProcess();
|
||||
} else {
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
@@ -205,7 +198,7 @@ export class NativeMessagingService {
|
||||
|
||||
const encrypted = await this.cryptoService.encrypt(
|
||||
JSON.stringify(message),
|
||||
this.sharedSecrets.get(appId),
|
||||
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
||||
);
|
||||
|
||||
ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted });
|
||||
@@ -213,7 +206,10 @@ export class NativeMessagingService {
|
||||
|
||||
private async secureCommunication(remotePublicKey: Uint8Array, appId: string) {
|
||||
const secret = await this.cryptoFunctionService.randomBytes(64);
|
||||
this.sharedSecrets.set(appId, new SymmetricCryptoKey(secret));
|
||||
await ipc.platform.ephemeralStore.setEphemeralValue(
|
||||
appId,
|
||||
new SymmetricCryptoKey(secret).keyB64,
|
||||
);
|
||||
|
||||
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
|
||||
secret,
|
||||
|
||||
@@ -92,7 +92,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
protected canUseSecretsManager$: Observable<boolean>;
|
||||
|
||||
protected EnableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
|
||||
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableUpgradePasswordManagerSub,
|
||||
);
|
||||
|
||||
@@ -476,10 +476,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
this.organization.productTierType === ProductTierType.TeamsStarter ||
|
||||
this.organization.productTierType === ProductTierType.Families)
|
||||
) {
|
||||
const EnableUpgradePasswordManagerSub = await firstValueFrom(
|
||||
this.EnableUpgradePasswordManagerSub$,
|
||||
const enableUpgradePasswordManagerSub = await firstValueFrom(
|
||||
this.enableUpgradePasswordManagerSub$,
|
||||
);
|
||||
if (EnableUpgradePasswordManagerSub) {
|
||||
if (enableUpgradePasswordManagerSub) {
|
||||
const reference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
|
||||
@@ -86,7 +86,10 @@ import {
|
||||
BulkShareDialogResult,
|
||||
openBulkShareDialog,
|
||||
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
|
||||
import { openIndividualVaultCollectionsDialog } from "./collections.component";
|
||||
import {
|
||||
CollectionsDialogResult,
|
||||
openIndividualVaultCollectionsDialog,
|
||||
} from "./collections.component";
|
||||
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
@@ -573,7 +576,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
openIndividualVaultCollectionsDialog(this.dialogService, { data: { cipherId: cipher.id } });
|
||||
const dialog = openIndividualVaultCollectionsDialog(this.dialogService, {
|
||||
data: { cipherId: cipher.id },
|
||||
});
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
if (result === CollectionsDialogResult.Saved) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
|
||||
90
libs/angular/src/directives/copy-click.directive.spec.ts
Normal file
90
libs/angular/src/directives/copy-click.directive.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Component, ElementRef, ViewChild } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CopyClickDirective } from "./copy-click.directive";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<button appCopyClick="no toast shown" #noToast></button>
|
||||
<button appCopyClick="info toast shown" showToast="info" #infoToast></button>
|
||||
<button appCopyClick="success toast shown" showToast #successToast></button>
|
||||
`,
|
||||
})
|
||||
class TestCopyClickComponent {
|
||||
@ViewChild("noToast") noToastButton: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("infoToast") infoToastButton: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("successToast") successToastButton: ElementRef<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
describe("CopyClickDirective", () => {
|
||||
let fixture: ComponentFixture<TestCopyClickComponent>;
|
||||
const copyToClipboard = jest.fn();
|
||||
const showToast = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
copyToClipboard.mockClear();
|
||||
showToast.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CopyClickDirective, TestCopyClickComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
|
||||
{ provide: ToastService, useValue: { showToast } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestCopyClickComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("copies the the value for all variants of toasts ", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("no toast shown");
|
||||
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("info toast shown");
|
||||
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("success toast shown");
|
||||
});
|
||||
|
||||
it("does not show a toast when showToast is not present", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a success toast when showToast is present", () => {
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "success",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the toast variant when set with showToast", () => {
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "info",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { ToastVariant } from "@bitwarden/components/src/toast/toast.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyClick]",
|
||||
})
|
||||
export class CopyClickDirective {
|
||||
private _showToast = false;
|
||||
private toastVariant: ToastVariant = "success";
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
@@ -16,14 +19,36 @@ export class CopyClickDirective {
|
||||
) {}
|
||||
|
||||
@Input("appCopyClick") valueToCopy = "";
|
||||
@Input({ transform: coerceBooleanProperty }) showToast?: boolean;
|
||||
|
||||
/**
|
||||
* When set without a value, a success toast will be shown when the value is copied
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast/></app-component>
|
||||
* ```
|
||||
* When set with a value, a toast with the specified variant will be shown when the value is copied
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
||||
* ```
|
||||
*/
|
||||
@Input() set showToast(value: ToastVariant | "") {
|
||||
// When the `showToast` is set without a value, an empty string will be passed
|
||||
if (value === "") {
|
||||
this._showToast = true;
|
||||
} else {
|
||||
this._showToast = true;
|
||||
this.toastVariant = value;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||
|
||||
if (this.showToast) {
|
||||
if (this._showToast) {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
variant: this.toastVariant,
|
||||
title: null,
|
||||
message: this.i18nService.t("copySuccessful"),
|
||||
});
|
||||
|
||||
@@ -85,3 +85,17 @@ export const DisablePasswordManagerUris = {
|
||||
Vivaldi: "vivaldi://settings/autofill",
|
||||
Unknown: "https://bitwarden.com/help/disable-browser-autofill/",
|
||||
} 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];
|
||||
|
||||
@@ -162,4 +162,6 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
) => Promise<CipherWithIdRequest[]>;
|
||||
getNextCardCipher: () => Promise<CipherView>;
|
||||
getNextIdentityCipher: () => Promise<CipherView>;
|
||||
}
|
||||
|
||||
@@ -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[]> {
|
||||
const response = await this.apiService.getCiphersOrganization(organizationId);
|
||||
return await this.decryptOrganizationCiphersResponse(response, organizationId);
|
||||
@@ -549,6 +556,36 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
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) {
|
||||
this.sortedCiphersCache.updateLastUsedIndex(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user