diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 764b8b5611c..a57b65f6982 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -128,6 +128,21 @@ "copyLicenseNumber": { "message": "Copy license number" }, + "copyCustomField": { + "message": "Copy $FIELD$", + "placeholders": { + "field": { + "content": "$1", + "example": "Custom field label" + } + } + }, + "copyWebsite": { + "message": "Copy website" + }, + "copyNotes": { + "message": "Copy notes" + }, "autoFill": { "message": "Autofill" }, @@ -2253,6 +2268,10 @@ "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "searchSends": { "message": "Search Sends", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2264,6 +2283,9 @@ "sendTypeText": { "message": "Text" }, + "sendTypeTextToShare": { + "message": "Text to share" + }, "sendTypeFile": { "message": "File" }, @@ -2271,6 +2293,9 @@ "message": "All Sends", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "hideTextByDefault": { + "message": "Hide text by default" + }, "maxAccessCountReached": { "message": "Max access count reached", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." @@ -2344,6 +2369,10 @@ "message": "The Send will be permanently deleted on the specified date and time.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "expirationDate": { "message": "Expiration date" }, diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index e5c09db6428..fb636ecaf6d 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -1,9 +1,10 @@ import { CommonModule, Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, map, of, startWith, switchMap, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LockService } from "@bitwarden/auth/common"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -70,6 +71,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private authService: AuthService, private configService: ConfigService, + private lockService: LockService, ) {} get accountLimit() { @@ -131,26 +133,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { async lockAll() { this.loading = true; - this.availableAccounts$ - .pipe( - map((accounts) => - accounts - .filter((account) => account.id !== this.specialAddAccountId) - .sort((a, b) => (a.isActive ? -1 : b.isActive ? 1 : 0)) // Log out of the active account first - .map((account) => account.id), - ), - switchMap(async (accountIds) => { - if (accountIds.length === 0) { - return; - } - - // Must lock active (first) account first, then order doesn't matter - await this.vaultTimeoutService.lock(accountIds.shift()); - await Promise.all(accountIds.map((id) => this.vaultTimeoutService.lock(id))); - }), - takeUntil(this.destroy$), - ) - .subscribe(() => this.router.navigate(["lock"])); + await this.lockService.lockAll(); + await this.router.navigate(["lock"]); } async logOut(userId: UserId) { diff --git a/apps/browser/src/auth/popup/accounts/foreground-lock.service.ts b/apps/browser/src/auth/popup/accounts/foreground-lock.service.ts new file mode 100644 index 00000000000..20a52a90d8b --- /dev/null +++ b/apps/browser/src/auth/popup/accounts/foreground-lock.service.ts @@ -0,0 +1,32 @@ +import { filter, firstValueFrom } from "rxjs"; + +import { LockService } from "@bitwarden/auth/common"; +import { + CommandDefinition, + MessageListener, + MessageSender, +} from "@bitwarden/common/platform/messaging"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +const LOCK_ALL_FINISHED = new CommandDefinition<{ requestId: string }>("lockAllFinished"); +const LOCK_ALL = new CommandDefinition<{ requestId: string }>("lockAll"); + +export class ForegroundLockService implements LockService { + constructor( + private readonly messageSender: MessageSender, + private readonly messageListener: MessageListener, + ) {} + + async lockAll(): Promise { + const requestId = Utils.newGuid(); + const finishMessage = firstValueFrom( + this.messageListener + .messages$(LOCK_ALL_FINISHED) + .pipe(filter((m) => m.requestId === requestId)), + ); + + this.messageSender.send(LOCK_ALL, { requestId }); + + await finishMessage; + } +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1cb615fe067..f54f1de1dd5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -9,6 +9,7 @@ import { AuthRequestService, LoginEmailServiceAbstraction, LogoutReason, + DefaultLockService, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -1037,6 +1038,14 @@ export default class MainBackground { const systemUtilsServiceReloadCallback = async () => { await this.taskSchedulerService.clearAllScheduledTasks(); + if (this.platformUtilsService.isSafari()) { + // If we do `chrome.runtime.reload` on safari they will send an onInstalled reason of install + // and that prompts us to show a new tab, this apparently doesn't happen on sideloaded + // extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298 + self.location.reload(); + return; + } + BrowserApi.reloadExtension(); }; @@ -1065,6 +1074,9 @@ export default class MainBackground { this.scriptInjectorService, this.configService, ); + + const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService); + this.runtimeBackground = new RuntimeBackground( this, this.autofillService, @@ -1079,6 +1091,7 @@ export default class MainBackground { this.fido2Background, messageListener, this.accountService, + lockService, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.cryptoService, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 424449f0b65..1ec7edcc30c 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,5 +1,6 @@ import { firstValueFrom, map, mergeMap, of, switchMap } from "rxjs"; +import { LockService } from "@bitwarden/auth/common"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants"; @@ -48,6 +49,7 @@ export default class RuntimeBackground { private fido2Background: Fido2Background, private messageListener: MessageListener, private accountService: AccountService, + private readonly lockService: LockService, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -245,6 +247,12 @@ export default class RuntimeBackground { case "lockVault": await this.main.vaultTimeoutService.lock(msg.userId); break; + case "lockAll": + { + await this.lockService.lockAll(); + this.messagingService.send("lockAllFinished", { requestId: msg.requestId }); + } + break; case "logout": await this.main.logout(msg.expired, msg.userId); break; diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index a5ca7a65ff4..33f18ce5723 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -1,6 +1,7 @@ import { Observable } from "rxjs"; import { DeviceType } from "@bitwarden/common/enums"; +import { isBrowserSafariApi } from "@bitwarden/platform"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; @@ -9,10 +10,7 @@ import { registerContentScriptsPolyfill } from "./browser-api.register-content-s export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; - static isSafariApi: boolean = - navigator.userAgent.indexOf(" Safari/") !== -1 && - navigator.userAgent.indexOf(" Chrome/") === -1 && - navigator.userAgent.indexOf(" Chromium/") === -1; + static isSafariApi: boolean = isBrowserSafariApi(); static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined"; static isFirefoxOnAndroid: boolean = navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1; diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index c2713f70a16..35c55633c0c 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -1,4 +1,4 @@ -import { switchMap, merge, delay, filter, concatMap, map } from "rxjs"; +import { switchMap, merge, delay, filter, concatMap, map, first, of } from "rxjs"; import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; import { @@ -61,7 +61,18 @@ export class PopupViewCacheBackgroundService { merge( // on tab changed, excluding extension tabs fromChromeEvent(chrome.tabs.onActivated).pipe( - switchMap(([tabInfo]) => BrowserApi.getTab(tabInfo.tabId)), + switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)), + switchMap((tab) => { + // FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event + if (tab.url !== "about:blank") { + return of(tab); + } + + return fromChromeEvent(chrome.tabs.onUpdated).pipe( + first(), + switchMap(([tabId]) => BrowserApi.getTab(tabId)), + ); + }), map((tab) => tab.url || tab.pendingUrl), filter((url) => !url.startsWith(chrome.runtime.getURL(""))), ), diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 477152fff85..12d5b109c20 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -127,6 +127,12 @@ export class AppComponent implements OnInit, OnDestroy { this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { this.toastService._showToast(msg); + } else if (msg.command === "reloadProcess") { + if (this.platformUtilsService.isSafari()) { + window.setTimeout(() => { + window.location.reload(); + }, 2000); + } } else if (msg.command === "reloadPopup") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 098c6eb91ce..efbe9ce6bf5 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -17,7 +17,7 @@ import { } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; -import { PinServiceAbstraction } from "@bitwarden/auth/common"; +import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; @@ -91,6 +91,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; @@ -560,6 +561,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionAnonLayoutWrapperDataService, deps: [], }), + safeProvider({ + provide: LockService, + useClass: ForegroundLockService, + deps: [MessageSender, MessageListener], + }), ]; @NgModule({ diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index e8aab69dbe9..b2ef6701b42 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -20,17 +20,15 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { AsyncActionsModule, - SearchModule, ButtonModule, - IconButtonModule, DialogService, + IconButtonModule, + SearchModule, ToastService, } from "@bitwarden/components"; -import { TotpCaptureService } from "@bitwarden/vault"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; -import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; @@ -41,7 +39,6 @@ import { VaultPopupAutofillService } from "./../../../services/vault-popup-autof selector: "app-view-v2", templateUrl: "view-v2.component.html", standalone: true, - providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }], imports: [ CommonModule, SearchModule, diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts index e790735dc52..2c9afacffd7 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts @@ -13,15 +13,10 @@ describe("BrowserTotpCaptureService", () => { let testBed: TestBed; let service: BrowserTotpCaptureService; let mockCaptureVisibleTab: jest.SpyInstance; - let createNewTabSpy: jest.SpyInstance; const validTotpUrl = "otpauth://totp/label?secret=123"; beforeEach(() => { - const tabReturn = new Promise((resolve) => - resolve({ url: "google.com", active: true } as chrome.tabs.Tab), - ); - createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn); mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab"); mockCaptureVisibleTab.mockResolvedValue("screenshot"); @@ -71,10 +66,4 @@ describe("BrowserTotpCaptureService", () => { expect(result).toBeNull(); }); - - it("should call BrowserApi.createNewTab with a given loginURI", async () => { - await service.openAutofillNewTab("www.google.com"); - - expect(createNewTabSpy).toHaveBeenCalledWith("www.google.com"); - }); }); diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts index 8f93db45c0e..3f8ba61ed36 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -20,8 +20,4 @@ export class BrowserTotpCaptureService implements TotpCaptureService { } return null; } - - async openAutofillNewTab(loginUri: string) { - await BrowserApi.createNewTab(loginUri); - } } diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 359d856525e..891314aa568 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -419,6 +419,23 @@ "enableHardwareAccelerationDesc" | i18n }} +
+
+ +
+ {{ + "allowScreenshotsDesc" | i18n + }} +