mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-02 03:41:09 +01:00
[PM-7489] Introduce MessageSender
& MessageListener
(#8709)
* Introduce MessageSender * Update `messageSenderFactory` * Remove Comment * Use BrowserApi * Update Comment * Rename to CommandDefinition * Add More Documentation to MessageSender * Add `EMPTY` helpers and remove NoopMessageSender * Calm Down Logging * Limit Logging On Known Errors * Use `messageStream` Parameter Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add eslint rules * Update Error Handling Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> * Delete Lazy Classes In Favor of Observable Factories * Remove Fido Messages --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
parent
9a4279c8bb
commit
395ed3f5d4
@ -246,6 +246,22 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"excludedFiles": ["**/platform/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"**/platform/**/internal", // General internal pattern
|
||||
// All features that have been converted to barrel files
|
||||
"**/platform/messaging/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Subject, firstValueFrom, merge } from "rxjs";
|
||||
|
||||
import {
|
||||
PinCryptoServiceAbstraction,
|
||||
@ -82,7 +82,6 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
@ -95,6 +94,9 @@ import {
|
||||
DefaultBiometricStateService,
|
||||
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
@ -208,13 +210,14 @@ import { Account } from "../models/account";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||
/* eslint-enable no-restricted-imports */
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
||||
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
||||
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
||||
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
|
||||
import I18nService from "../platform/services/i18n.service";
|
||||
@ -223,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
|
||||
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
||||
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
||||
import FilelessImporterBackground from "../tools/background/fileless-importer.background";
|
||||
import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background";
|
||||
@ -236,7 +240,7 @@ import { NativeMessagingBackground } from "./nativeMessaging.background";
|
||||
import RuntimeBackground from "./runtime.background";
|
||||
|
||||
export default class MainBackground {
|
||||
messagingService: MessagingServiceAbstraction;
|
||||
messagingService: MessageSender;
|
||||
storageService: BrowserLocalStorageService;
|
||||
secureStorageService: AbstractStorageService;
|
||||
memoryStorageService: AbstractMemoryStorageService;
|
||||
@ -326,6 +330,8 @@ export default class MainBackground {
|
||||
stateEventRunnerService: StateEventRunnerService;
|
||||
ssoLoginService: SsoLoginServiceAbstraction;
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
|
||||
intraprocessMessagingSubject: Subject<Message<object>>;
|
||||
userKeyInitService: UserKeyInitService;
|
||||
scriptInjectorService: BrowserScriptInjectorService;
|
||||
|
||||
@ -369,15 +375,25 @@ export default class MainBackground {
|
||||
const logoutCallback = async (expired: boolean, userId?: UserId) =>
|
||||
await this.logout(expired, userId);
|
||||
|
||||
this.messagingService =
|
||||
this.isPrivateMode && BrowserApi.isManifestVersion(2)
|
||||
? new BrowserMessagingPrivateModeBackgroundService()
|
||||
: new BrowserMessagingService();
|
||||
this.logService = new ConsoleLogService(false);
|
||||
this.cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
this.storageService = new BrowserLocalStorageService();
|
||||
|
||||
this.intraprocessMessagingSubject = new Subject<Message<object>>();
|
||||
|
||||
this.messagingService = MessageSender.combine(
|
||||
new SubjectMessageSender(this.intraprocessMessagingSubject),
|
||||
new ChromeMessageSender(this.logService),
|
||||
);
|
||||
|
||||
const messageListener = new MessageListener(
|
||||
merge(
|
||||
this.intraprocessMessagingSubject.asObservable(), // For messages from the same context
|
||||
fromChromeRuntimeMessaging(), // For messages from other contexts
|
||||
),
|
||||
);
|
||||
|
||||
const mv3MemoryStorageCreator = (partitionName: string) => {
|
||||
// TODO: Consider using multithreaded encrypt service in popup only context
|
||||
return new LocalBackedSessionStorageService(
|
||||
@ -560,21 +576,6 @@ export default class MainBackground {
|
||||
|
||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const that = this;
|
||||
const backgroundMessagingService = new (class extends MessagingServiceAbstraction {
|
||||
// AuthService should send the messages to the background not popup.
|
||||
send = (subscriber: string, arg: any = {}) => {
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
that.messagingService.send(subscriber, arg);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
void that.runtimeBackground.processMessage(message, that as any);
|
||||
};
|
||||
})();
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
@ -605,7 +606,7 @@ export default class MainBackground {
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
backgroundMessagingService,
|
||||
this.messagingService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateService,
|
||||
@ -626,7 +627,7 @@ export default class MainBackground {
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
backgroundMessagingService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.keyConnectorService,
|
||||
this.environmentService,
|
||||
@ -914,6 +915,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.fido2Background,
|
||||
messageListener,
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.accountService,
|
||||
|
@ -399,7 +399,7 @@ export class NativeMessagingBackground {
|
||||
|
||||
// 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
|
||||
this.runtimeBackground.processMessage({ command: "unlocked" }, null);
|
||||
this.runtimeBackground.processMessage({ command: "unlocked" });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, mergeMap } from "rxjs";
|
||||
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
@ -10,6 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { MessageListener } from "../../../../libs/common/src/platform/messaging";
|
||||
import {
|
||||
closeUnlockPopout,
|
||||
openSsoAuthResultPopout,
|
||||
@ -44,6 +45,7 @@ export default class RuntimeBackground {
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
private fido2Background: Fido2Background,
|
||||
private messageListener: MessageListener,
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
@ -60,92 +62,47 @@ export default class RuntimeBackground {
|
||||
const backgroundMessageListener = (
|
||||
msg: any,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: any,
|
||||
sendResponse: (response: any) => void,
|
||||
) => {
|
||||
const messagesWithResponse = ["biometricUnlock"];
|
||||
|
||||
if (messagesWithResponse.includes(msg.command)) {
|
||||
this.processMessage(msg, sender).then(
|
||||
this.processMessageWithSender(msg, sender).then(
|
||||
(value) => sendResponse({ result: value }),
|
||||
(error) => sendResponse({ error: { ...error, message: error.message } }),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.processMessage(msg, sender).catch((e) => this.logService.error(e));
|
||||
void this.processMessageWithSender(msg, sender).catch((err) =>
|
||||
this.logService.error(
|
||||
`Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
this.messageListener.allMessages$
|
||||
.pipe(
|
||||
mergeMap(async (message: any) => {
|
||||
await this.processMessage(message);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// For messages that require the full on message interface
|
||||
BrowserApi.messageListener("runtime.background", backgroundMessageListener);
|
||||
if (this.main.popupOnlyContext) {
|
||||
(self as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
|
||||
}
|
||||
}
|
||||
|
||||
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
|
||||
// Messages that need the chrome sender and send back a response need to be registered in this method.
|
||||
async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) {
|
||||
switch (msg.command) {
|
||||
case "loggedIn":
|
||||
case "unlocked": {
|
||||
let item: LockedVaultPendingNotificationsData;
|
||||
|
||||
if (msg.command === "loggedIn") {
|
||||
await this.sendBwInstalledMessageToVault();
|
||||
}
|
||||
|
||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||
item = this.lockedVaultPendingNotifications.pop();
|
||||
await closeUnlockPopout();
|
||||
}
|
||||
|
||||
await this.notificationsService.updateConnection(msg.command === "loggedIn");
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu(false);
|
||||
this.systemService.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
|
||||
await BrowserApi.focusTab(item.commandToRetry.sender.tab.id);
|
||||
await BrowserApi.tabSendMessageData(
|
||||
item.commandToRetry.sender.tab,
|
||||
"unlockCompleted",
|
||||
item,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "addToLockedVaultPendingNotifications":
|
||||
this.lockedVaultPendingNotifications.push(msg.data);
|
||||
break;
|
||||
case "logout":
|
||||
await this.main.logout(msg.expired, msg.userId);
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (msg.successfully) {
|
||||
setTimeout(async () => {
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
}, 2000);
|
||||
await this.configService.ensureConfigFetched();
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
await this.main.openPopup();
|
||||
break;
|
||||
case "triggerAutofillScriptInjection":
|
||||
await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||
break;
|
||||
case "bgCollectPageDetails":
|
||||
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
|
||||
break;
|
||||
case "bgUpdateContextMenu":
|
||||
case "editedCipher":
|
||||
case "addedCipher":
|
||||
case "deletedCipher":
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
break;
|
||||
case "bgReseedStorage":
|
||||
await this.main.reseedStorage();
|
||||
break;
|
||||
case "collectPageDetailsResponse":
|
||||
switch (msg.sender) {
|
||||
case "autofiller":
|
||||
@ -209,6 +166,72 @@ export default class RuntimeBackground {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "biometricUnlock": {
|
||||
const result = await this.main.biometricUnlock();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async processMessage(msg: any) {
|
||||
switch (msg.command) {
|
||||
case "loggedIn":
|
||||
case "unlocked": {
|
||||
let item: LockedVaultPendingNotificationsData;
|
||||
|
||||
if (msg.command === "loggedIn") {
|
||||
await this.sendBwInstalledMessageToVault();
|
||||
}
|
||||
|
||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||
item = this.lockedVaultPendingNotifications.pop();
|
||||
await closeUnlockPopout();
|
||||
}
|
||||
|
||||
await this.notificationsService.updateConnection(msg.command === "loggedIn");
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu(false);
|
||||
this.systemService.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
|
||||
await BrowserApi.focusTab(item.commandToRetry.sender.tab.id);
|
||||
await BrowserApi.tabSendMessageData(
|
||||
item.commandToRetry.sender.tab,
|
||||
"unlockCompleted",
|
||||
item,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "addToLockedVaultPendingNotifications":
|
||||
this.lockedVaultPendingNotifications.push(msg.data);
|
||||
break;
|
||||
case "logout":
|
||||
await this.main.logout(msg.expired, msg.userId);
|
||||
break;
|
||||
case "syncCompleted":
|
||||
if (msg.successfully) {
|
||||
setTimeout(async () => {
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
}, 2000);
|
||||
await this.configService.ensureConfigFetched();
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
await this.main.openPopup();
|
||||
break;
|
||||
case "bgUpdateContextMenu":
|
||||
case "editedCipher":
|
||||
case "addedCipher":
|
||||
case "deletedCipher":
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
break;
|
||||
case "bgReseedStorage":
|
||||
await this.main.reseedStorage();
|
||||
break;
|
||||
case "authResult": {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
@ -265,9 +288,6 @@ export default class RuntimeBackground {
|
||||
await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs);
|
||||
break;
|
||||
}
|
||||
case "biometricUnlock": {
|
||||
return await this.main.biometricUnlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
|
||||
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||
|
||||
type MessagingServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type MessageSenderInitOptions = MessagingServiceFactoryOptions;
|
||||
|
||||
export function messageSenderFactory(
|
||||
cache: { messagingService?: MessageSender } & CachedServices,
|
||||
opts: MessageSenderInitOptions,
|
||||
): Promise<MessageSender> {
|
||||
// NOTE: Name needs to match that of MainBackground property until we delete these.
|
||||
return factory(cache, "messagingService", opts, () => {
|
||||
throw new Error("Not implemented, not expected to be used.");
|
||||
});
|
||||
}
|
@ -1,19 +1,5 @@
|
||||
import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
FactoryOptions,
|
||||
} from "../../background/service-factories/factory-options";
|
||||
import BrowserMessagingService from "../../services/browser-messaging.service";
|
||||
|
||||
type MessagingServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type MessagingServiceInitOptions = MessagingServiceFactoryOptions;
|
||||
|
||||
export function messagingServiceFactory(
|
||||
cache: { messagingService?: AbstractMessagingService } & CachedServices,
|
||||
opts: MessagingServiceInitOptions,
|
||||
): Promise<AbstractMessagingService> {
|
||||
return factory(cache, "messagingService", opts, () => new BrowserMessagingService());
|
||||
}
|
||||
// Export old messaging service stuff to minimize changes
|
||||
export {
|
||||
messageSenderFactory as messagingServiceFactory,
|
||||
MessageSenderInitOptions as MessagingServiceInitOptions,
|
||||
} from "./message-sender.factory";
|
||||
|
37
apps/browser/src/platform/messaging/chrome-message.sender.ts
Normal file
37
apps/browser/src/platform/messaging/chrome-message.sender.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
|
||||
|
||||
type ErrorHandler = (logger: LogService, command: string) => void;
|
||||
|
||||
const HANDLED_ERRORS: Record<string, ErrorHandler> = {
|
||||
"Could not establish connection. Receiving end does not exist.": (logger, command) =>
|
||||
logger.debug(`Receiving end didn't exist for command '${command}'`),
|
||||
|
||||
"The message port closed before a response was received.": (logger, command) =>
|
||||
logger.debug(`Port was closed for command '${command}'`),
|
||||
};
|
||||
|
||||
export class ChromeMessageSender implements MessageSender {
|
||||
constructor(private readonly logService: LogService) {}
|
||||
|
||||
send<T extends object>(
|
||||
commandDefinition: string | CommandDefinition<T>,
|
||||
payload: object | T = {},
|
||||
): void {
|
||||
const command = getCommand(commandDefinition);
|
||||
chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message];
|
||||
if (errorHandler != null) {
|
||||
errorHandler(this.logService, command);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.warning(
|
||||
`Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService {
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
(self as any).bitwardenPopupMainMessageListener(message);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export default class BrowserMessagingPrivateModePopupService implements MessagingService {
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
(self as any).bitwardenBackgroundMessageListener(message);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
export default class BrowserMessagingService implements MessagingService {
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
return BrowserApi.sendMessage(subscriber, arg);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { map, share } from "rxjs";
|
||||
|
||||
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
|
||||
|
||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
|
||||
/**
|
||||
* Creates an observable that listens to messages through `chrome.runtime.onMessage`.
|
||||
* @returns An observable stream of messages.
|
||||
*/
|
||||
export const fromChromeRuntimeMessaging = () => {
|
||||
return fromChromeEvent(chrome.runtime.onMessage).pipe(
|
||||
map(([message, sender]) => {
|
||||
message ??= {};
|
||||
|
||||
// Force the sender onto the message as long as we won't overwrite anything
|
||||
if (!("webExtSender" in message)) {
|
||||
message.webExtSender = sender;
|
||||
}
|
||||
|
||||
return message;
|
||||
}),
|
||||
tagAsExternal,
|
||||
share(),
|
||||
);
|
||||
};
|
@ -1,17 +1,16 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs";
|
||||
import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
|
||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
||||
@ -34,7 +33,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
@ -46,7 +44,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private dialogService: DialogService,
|
||||
private browserMessagingApi: ZonedMessageListenerService,
|
||||
private messageListener: MessageListener,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
@ -78,77 +76,76 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
window.onkeypress = () => this.recordActivity();
|
||||
});
|
||||
|
||||
const bitwardenPopupMainMessageListener = (msg: any, sender: any) => {
|
||||
if (msg.command === "doneLoggingOut") {
|
||||
this.authService.logOut(async () => {
|
||||
if (msg.expired) {
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: this.i18nService.t("loggedOut"),
|
||||
message: this.i18nService.t("loginExpired"),
|
||||
this.messageListener.allMessages$
|
||||
.pipe(
|
||||
tap((msg: any) => {
|
||||
if (msg.command === "doneLoggingOut") {
|
||||
this.authService.logOut(async () => {
|
||||
if (msg.expired) {
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: this.i18nService.t("loggedOut"),
|
||||
message: this.i18nService.t("loginExpired"),
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
this.router.navigate(["home"]);
|
||||
});
|
||||
this.changeDetectorRef.detectChanges();
|
||||
} else if (msg.command === "authBlocked") {
|
||||
// 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
|
||||
this.router.navigate(["home"]);
|
||||
} else if (
|
||||
msg.command === "locked" &&
|
||||
(msg.userId == null || msg.userId == this.activeUserId)
|
||||
) {
|
||||
// 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
|
||||
this.router.navigate(["lock"]);
|
||||
} else if (msg.command === "showDialog") {
|
||||
// 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
|
||||
this.showDialog(msg);
|
||||
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
|
||||
// TODO: Should be refactored to live in another service.
|
||||
// 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
|
||||
this.showNativeMessagingFingerprintDialog(msg);
|
||||
} else if (msg.command === "showToast") {
|
||||
this.toastService._showToast(msg);
|
||||
} else if (msg.command === "reloadProcess") {
|
||||
const forceWindowReload =
|
||||
this.platformUtilsService.isSafari() ||
|
||||
this.platformUtilsService.isFirefox() ||
|
||||
this.platformUtilsService.isOpera();
|
||||
// Wait to make sure background has reloaded first.
|
||||
window.setTimeout(
|
||||
() => BrowserApi.reloadExtension(forceWindowReload ? window : null),
|
||||
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
|
||||
this.router.navigate(["/"]);
|
||||
} else if (msg.command === "convertAccountToKeyConnector") {
|
||||
// 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
|
||||
this.router.navigate(["/remove-password"]);
|
||||
} else if (msg.command === "switchAccountFinish") {
|
||||
// TODO: unset loading?
|
||||
// this.loading = false;
|
||||
} else if (msg.command == "update-temp-password") {
|
||||
// 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
|
||||
this.router.navigate(["/update-temp-password"]);
|
||||
}
|
||||
|
||||
// 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
|
||||
this.router.navigate(["home"]);
|
||||
});
|
||||
this.changeDetectorRef.detectChanges();
|
||||
} else if (msg.command === "authBlocked") {
|
||||
// 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
|
||||
this.router.navigate(["home"]);
|
||||
} else if (
|
||||
msg.command === "locked" &&
|
||||
(msg.userId == null || msg.userId == this.activeUserId)
|
||||
) {
|
||||
// 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
|
||||
this.router.navigate(["lock"]);
|
||||
} else if (msg.command === "showDialog") {
|
||||
// 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
|
||||
this.showDialog(msg);
|
||||
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
|
||||
// TODO: Should be refactored to live in another service.
|
||||
// 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
|
||||
this.showNativeMessagingFingerprintDialog(msg);
|
||||
} else if (msg.command === "showToast") {
|
||||
this.toastService._showToast(msg);
|
||||
} else if (msg.command === "reloadProcess") {
|
||||
const forceWindowReload =
|
||||
this.platformUtilsService.isSafari() ||
|
||||
this.platformUtilsService.isFirefox() ||
|
||||
this.platformUtilsService.isOpera();
|
||||
// Wait to make sure background has reloaded first.
|
||||
window.setTimeout(
|
||||
() => BrowserApi.reloadExtension(forceWindowReload ? window : null),
|
||||
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
|
||||
this.router.navigate(["/"]);
|
||||
} else if (msg.command === "convertAccountToKeyConnector") {
|
||||
// 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
|
||||
this.router.navigate(["/remove-password"]);
|
||||
} else if (msg.command === "switchAccountFinish") {
|
||||
// TODO: unset loading?
|
||||
// this.loading = false;
|
||||
} else if (msg.command == "update-temp-password") {
|
||||
// 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
|
||||
this.router.navigate(["/update-temp-password"]);
|
||||
} else {
|
||||
msg.webExtSender = sender;
|
||||
this.broadcasterService.send(msg);
|
||||
}
|
||||
};
|
||||
|
||||
(self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener;
|
||||
this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, merge } from "rxjs";
|
||||
|
||||
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
|
||||
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
|
||||
@ -11,6 +12,7 @@ import {
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
SafeInjectionToken,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import {
|
||||
@ -54,7 +56,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
@ -63,6 +64,9 @@ import {
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
@ -89,20 +93,23 @@ import AutofillService from "../../autofill/services/autofill.service";
|
||||
import MainBackground from "../../background/main.background";
|
||||
import { Account } from "../../models/account";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
|
||||
/* eslint-enable no-restricted-imports */
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
||||
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
||||
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
|
||||
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service";
|
||||
import I18nService from "../../platform/services/i18n.service";
|
||||
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
||||
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
|
||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
||||
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
|
||||
@ -155,15 +162,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: UnauthGuardService,
|
||||
deps: [AuthServiceAbstraction, Router],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessagingService,
|
||||
useFactory: () => {
|
||||
return needsBackgroundInit && BrowserApi.isManifestVersion(2)
|
||||
? new BrowserMessagingPrivateModePopupService()
|
||||
: new BrowserMessagingService();
|
||||
},
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorService,
|
||||
useFactory: getBgService<TwoFactorService>("twoFactorService"),
|
||||
@ -484,6 +482,65 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: BrowserSendStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageListener,
|
||||
useFactory: (subject: Subject<Message<object>>, ngZone: NgZone) =>
|
||||
new MessageListener(
|
||||
merge(
|
||||
subject.asObservable(), // For messages in the same context
|
||||
fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context
|
||||
),
|
||||
),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageSender,
|
||||
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
|
||||
MessageSender.combine(
|
||||
new SubjectMessageSender(subject), // For sending messages in the same context
|
||||
new ChromeMessageSender(logService), // For sending messages to different contexts
|
||||
),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
useFactory: () => {
|
||||
if (BrowserPopupUtils.backgroundInitializationRequired()) {
|
||||
// There is no persistent main background which means we have one in memory,
|
||||
// we need the same instance that our in memory background is utilizing.
|
||||
return getBgService("intraprocessMessagingSubject")();
|
||||
} else {
|
||||
return new Subject<Message<object>>();
|
||||
}
|
||||
},
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageSender,
|
||||
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
|
||||
MessageSender.combine(
|
||||
new SubjectMessageSender(subject), // For sending messages in the same context
|
||||
new ChromeMessageSender(logService), // For sending messages to different contexts
|
||||
),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
useFactory: () => {
|
||||
if (needsBackgroundInit) {
|
||||
// We will have created a popup within this context, in that case
|
||||
// we want to make sure we have the same subject as that context so we
|
||||
// can message with it.
|
||||
return getBgService("intraprocessMessagingSubject")();
|
||||
} else {
|
||||
// There isn't a locally created background so we will communicate with
|
||||
// the true background through chrome apis, in that case, we can just create
|
||||
// one for ourself.
|
||||
return new Subject<Message<object>>();
|
||||
}
|
||||
},
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -60,10 +60,10 @@ import {
|
||||
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
@ -75,7 +75,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import {
|
||||
@ -155,7 +154,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser;
|
||||
const packageJson = require("../package.json");
|
||||
|
||||
export class Main {
|
||||
messagingService: NoopMessagingService;
|
||||
messagingService: MessageSender;
|
||||
storageService: LowdbStorageService;
|
||||
secureStorageService: NodeEnvSecureStorageService;
|
||||
memoryStorageService: MemoryStorageService;
|
||||
@ -212,7 +211,6 @@ export class Main {
|
||||
organizationService: OrganizationService;
|
||||
providerService: ProviderService;
|
||||
twoFactorService: TwoFactorService;
|
||||
broadcasterService: BroadcasterService;
|
||||
folderApiService: FolderApiService;
|
||||
userVerificationApiService: UserVerificationApiService;
|
||||
organizationApiService: OrganizationApiServiceAbstraction;
|
||||
@ -298,7 +296,7 @@ export class Main {
|
||||
stateEventRegistrarService,
|
||||
);
|
||||
|
||||
this.messagingService = new NoopMessagingService();
|
||||
this.messagingService = MessageSender.EMPTY;
|
||||
|
||||
this.accountService = new AccountServiceImplementation(
|
||||
this.messagingService,
|
||||
@ -422,8 +420,6 @@ export class Main {
|
||||
|
||||
this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
|
||||
|
||||
this.broadcasterService = new BroadcasterService();
|
||||
|
||||
this.collectionService = new CollectionService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { APP_INITIALIZER, NgModule } from "@angular/core";
|
||||
import { Subject, merge } from "rxjs";
|
||||
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
@ -14,6 +15,7 @@ import {
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
SafeInjectionToken,
|
||||
STATE_FACTORY,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
@ -23,7 +25,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
@ -42,6 +43,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
@ -63,11 +67,12 @@ import {
|
||||
ELECTRON_SUPPORTS_SECURE_STORAGE,
|
||||
ElectronPlatformUtilsService,
|
||||
} from "../../platform/services/electron-platform-utils.service";
|
||||
import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service";
|
||||
import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender";
|
||||
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
|
||||
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
||||
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
||||
@ -138,9 +143,24 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessagingServiceAbstraction,
|
||||
useClass: ElectronRendererMessagingService,
|
||||
deps: [BroadcasterServiceAbstraction],
|
||||
provide: MessageSender,
|
||||
useFactory: (subject: Subject<Message<object>>) =>
|
||||
MessageSender.combine(
|
||||
new ElectronRendererMessageSender(), // Communication with main process
|
||||
new SubjectMessageSender(subject), // Communication with ourself
|
||||
),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageListener,
|
||||
useFactory: (subject: Subject<Message<object>>) =>
|
||||
new MessageListener(
|
||||
merge(
|
||||
subject.asObservable(), // For messages from the same context
|
||||
fromIpcMessaging(), // For messages from the main process
|
||||
),
|
||||
),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AbstractStorageService,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as path from "path";
|
||||
|
||||
import { app } from "electron";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
@ -11,6 +11,9 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
@ -18,7 +21,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||
@ -59,7 +61,7 @@ export class Main {
|
||||
storageService: ElectronStorageService;
|
||||
memoryStorageService: MemoryStorageService;
|
||||
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
messagingService: MessageSender;
|
||||
stateService: StateService;
|
||||
environmentService: DefaultEnvironmentService;
|
||||
mainCryptoFunctionService: MainCryptoFunctionService;
|
||||
@ -131,7 +133,7 @@ export class Main {
|
||||
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
|
||||
|
||||
const accountService = new AccountServiceImplementation(
|
||||
new NoopMessagingService(),
|
||||
MessageSender.EMPTY,
|
||||
this.logService,
|
||||
globalStateProvider,
|
||||
);
|
||||
@ -223,7 +225,13 @@ export class Main {
|
||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
|
||||
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
|
||||
|
||||
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
|
||||
const messageSubject = new Subject<Message<object>>();
|
||||
this.messagingService = MessageSender.combine(
|
||||
new SubjectMessageSender(messageSubject), // For local messages
|
||||
new ElectronMainMessagingService(this.windowMain),
|
||||
);
|
||||
|
||||
messageSubject.asObservable().subscribe((message) => {
|
||||
this.messagingMain.onMessage(message);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { powerMonitor } from "electron";
|
||||
|
||||
import { ElectronMainMessagingService } from "../services/electron-main-messaging.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
|
||||
import { isSnapStore } from "../utils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds
|
||||
export class PowerMonitorMain {
|
||||
private idle = false;
|
||||
|
||||
constructor(private messagingService: ElectronMainMessagingService) {}
|
||||
constructor(private messagingService: MessageSender) {}
|
||||
|
||||
init() {
|
||||
// ref: https://github.com/electron/electron/issues/13767
|
||||
|
@ -124,12 +124,21 @@ export default {
|
||||
|
||||
sendMessage: (message: { command: string } & any) =>
|
||||
ipcRenderer.send("messagingService", message),
|
||||
onMessage: (callback: (message: { command: string } & any) => void) => {
|
||||
ipcRenderer.on("messagingService", (_event, message: any) => {
|
||||
if (message.command) {
|
||||
callback(message);
|
||||
}
|
||||
});
|
||||
onMessage: {
|
||||
addListener: (callback: (message: { command: string } & any) => void) => {
|
||||
ipcRenderer.addListener("messagingService", (_event, message: any) => {
|
||||
if (message.command) {
|
||||
callback(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
removeListener: (callback: (message: { command: string } & any) => void) => {
|
||||
ipcRenderer.removeListener("messagingService", (_event, message: any) => {
|
||||
if (message.command) {
|
||||
callback(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri),
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging";
|
||||
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
|
||||
|
||||
export class ElectronRendererMessageSender implements MessageSender {
|
||||
send<T extends object>(
|
||||
commandDefinition: CommandDefinition<T> | string,
|
||||
payload: object | T = {},
|
||||
): void {
|
||||
const command = getCommand(commandDefinition);
|
||||
ipc.platform.sendMessage(Object.assign({}, { command: command }, payload));
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export class ElectronRendererMessagingService implements MessagingService {
|
||||
constructor(private broadcasterService: BroadcasterService) {
|
||||
ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false));
|
||||
}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
this.sendMessage(subscriber, arg, true);
|
||||
}
|
||||
|
||||
private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.broadcasterService.send(message);
|
||||
if (toMain) {
|
||||
ipc.platform.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
15
apps/desktop/src/platform/utils/from-ipc-messaging.ts
Normal file
15
apps/desktop/src/platform/utils/from-ipc-messaging.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { fromEventPattern, share } from "rxjs";
|
||||
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
|
||||
|
||||
/**
|
||||
* Creates an observable that when subscribed to will listen to messaging events through IPC.
|
||||
* @returns An observable stream of messages.
|
||||
*/
|
||||
export const fromIpcMessaging = () => {
|
||||
return fromEventPattern<Message<object>>(
|
||||
(handler) => ipc.platform.onMessage.addListener(handler),
|
||||
(handler) => ipc.platform.onMessage.removeListener(handler),
|
||||
).pipe(tagAsExternal, share());
|
||||
};
|
@ -2,18 +2,17 @@ import * as path from "path";
|
||||
|
||||
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron";
|
||||
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation
|
||||
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls";
|
||||
|
||||
import { WindowMain } from "../main/window.main";
|
||||
import { RendererMenuItem } from "../utils";
|
||||
|
||||
export class ElectronMainMessagingService implements MessagingService {
|
||||
constructor(
|
||||
private windowMain: WindowMain,
|
||||
private onMessage: (message: any) => void,
|
||||
) {
|
||||
export class ElectronMainMessagingService implements MessageSender {
|
||||
constructor(private windowMain: WindowMain) {
|
||||
ipcMain.handle("appVersion", () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService {
|
||||
});
|
||||
}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.onMessage(message);
|
||||
send<T extends object>(commandDefinition: CommandDefinition<T> | string, arg: T | object = {}) {
|
||||
const command = getCommand(commandDefinition);
|
||||
const message = Object.assign({}, { command: command }, arg);
|
||||
if (this.windowMain.win != null) {
|
||||
this.windowMain.win.webContents.send("messagingService", message);
|
||||
}
|
||||
|
@ -103,7 +103,8 @@ export class TrialBillingStepComponent implements OnInit {
|
||||
planDescription,
|
||||
});
|
||||
|
||||
this.messagingService.send("organizationCreated", organizationId);
|
||||
// TODO: No one actually listening to this?
|
||||
this.messagingService.send("organizationCreated", { organizationId });
|
||||
}
|
||||
|
||||
protected changedCountry() {
|
||||
|
@ -587,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
this.formPromise = doSubmit();
|
||||
const organizationId = await this.formPromise;
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
this.messagingService.send("organizationCreated", organizationId);
|
||||
// TODO: No one actually listening to this message?
|
||||
this.messagingService.send("organizationCreated", { organizationId });
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
@Injectable()
|
||||
export class BroadcasterMessagingService implements MessagingService {
|
||||
constructor(private broadcasterService: BroadcasterService) {}
|
||||
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
const message = Object.assign({}, { command: subscriber }, arg);
|
||||
this.broadcasterService.send(message);
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
@ -51,7 +50,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi
|
||||
import { WindowStorageService } from "../platform/window-storage.service";
|
||||
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
||||
|
||||
import { BroadcasterMessagingService } from "./broadcaster-messaging.service";
|
||||
import { EventService } from "./event.service";
|
||||
import { InitService } from "./init.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
@ -117,11 +115,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebPlatformUtilsService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessagingServiceAbstraction,
|
||||
useClass: BroadcasterMessagingService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ModalServiceAbstraction,
|
||||
useClass: ModalService,
|
||||
|
@ -4,15 +4,15 @@ import { of } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { BadgeModule, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumBadgeComponent } from "./premium-badge.component";
|
||||
|
||||
class MockMessagingService implements MessagingService {
|
||||
send(subscriber: string, arg?: any) {
|
||||
class MockMessagingService implements MessageSender {
|
||||
send = () => {
|
||||
alert("Clicked on badge");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -31,7 +31,7 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MessagingService,
|
||||
provide: MessageSender,
|
||||
useFactory: () => {
|
||||
return new MockMessagingService();
|
||||
},
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service";
|
||||
|
||||
@Injectable()
|
||||
export class BroadcasterService extends BaseBroadcasterService {}
|
@ -1,5 +1,5 @@
|
||||
import { InjectionToken } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
|
||||
declare const tag: unique symbol;
|
||||
/**
|
||||
@ -49,3 +50,6 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURE
|
||||
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
|
||||
"SYSTEM_THEME_OBSERVABLE",
|
||||
);
|
||||
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>(
|
||||
"INTRAPROCESS_MESSAGING_SUBJECT",
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
@ -116,7 +117,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@ -137,6 +138,9 @@ import {
|
||||
DefaultBiometricStateService,
|
||||
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
@ -147,6 +151,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
|
||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
@ -247,7 +252,6 @@ import {
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { UnauthGuard } from "../auth/guards/unauth.guard";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
|
||||
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
|
||||
@ -270,6 +274,7 @@ import {
|
||||
SYSTEM_LANGUAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
WINDOW,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
} from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
@ -625,7 +630,11 @@ const safeProviders: SafeProvider[] = [
|
||||
BillingAccountProfileStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }),
|
||||
safeProvider({
|
||||
provide: BroadcasterService,
|
||||
useClass: DefaultBroadcasterService,
|
||||
deps: [MessageSender, MessageListener],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultTimeoutSettingsServiceAbstraction,
|
||||
useClass: VaultTimeoutSettingsService,
|
||||
@ -1127,6 +1136,21 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: LoggingErrorHandler,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
useFactory: () => new Subject<Message<object>>(),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageListener,
|
||||
useFactory: (subject: Subject<Message<object>>) => new MessageListener(subject.asObservable()),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MessageSender,
|
||||
useFactory: (subject: Subject<Message<object>>) => new SubjectMessageSender(subject),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ProviderApiServiceAbstraction,
|
||||
useClass: ProviderApiService,
|
||||
|
@ -1,3 +1,3 @@
|
||||
export abstract class MessagingService {
|
||||
abstract send(subscriber: string, arg?: any): void;
|
||||
}
|
||||
// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR,
|
||||
// team specific PR's will come after.
|
||||
export { MessageSender as MessagingService } from "../messaging/message.sender";
|
||||
|
46
libs/common/src/platform/messaging/helpers.spec.ts
Normal file
46
libs/common/src/platform/messaging/helpers.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { getCommand, isExternalMessage, tagAsExternal } from "./helpers";
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
describe("helpers", () => {
|
||||
describe("getCommand", () => {
|
||||
it("can get the command from just a string", () => {
|
||||
const command = getCommand("myCommand");
|
||||
|
||||
expect(command).toEqual("myCommand");
|
||||
});
|
||||
|
||||
it("can get the command from a message definition", () => {
|
||||
const commandDefinition = new CommandDefinition<object>("myCommand");
|
||||
|
||||
const command = getCommand(commandDefinition);
|
||||
|
||||
expect(command).toEqual("myCommand");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tag integration", () => {
|
||||
it("can tag and identify as tagged", async () => {
|
||||
const messagesSubject = new Subject<Message<object>>();
|
||||
|
||||
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal);
|
||||
|
||||
const firstValuePromise = firstValueFrom(taggedMessages);
|
||||
|
||||
messagesSubject.next({ command: "test" });
|
||||
|
||||
const result = await firstValuePromise;
|
||||
|
||||
expect(isExternalMessage(result)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isExternalMessage", () => {
|
||||
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
|
||||
Record<string, unknown>
|
||||
>[])("returns false when value is %s", (value: Message<object>) => {
|
||||
expect(isExternalMessage(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
23
libs/common/src/platform/messaging/helpers.ts
Normal file
23
libs/common/src/platform/messaging/helpers.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { MonoTypeOperatorFunction, map } from "rxjs";
|
||||
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
export const getCommand = (commandDefinition: CommandDefinition<object> | string) => {
|
||||
if (typeof commandDefinition === "string") {
|
||||
return commandDefinition;
|
||||
} else {
|
||||
return commandDefinition.command;
|
||||
}
|
||||
};
|
||||
|
||||
export const EXTERNAL_SOURCE_TAG = Symbol("externalSource");
|
||||
|
||||
export const isExternalMessage = (message: Message<object>) => {
|
||||
return (message as Record<PropertyKey, unknown>)?.[EXTERNAL_SOURCE_TAG] === true;
|
||||
};
|
||||
|
||||
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map(
|
||||
(message: Message<object>) => {
|
||||
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
|
||||
},
|
||||
);
|
4
libs/common/src/platform/messaging/index.ts
Normal file
4
libs/common/src/platform/messaging/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { MessageListener } from "./message.listener";
|
||||
export { MessageSender } from "./message.sender";
|
||||
export { Message, CommandDefinition } from "./types";
|
||||
export { isExternalMessage } from "./helpers";
|
5
libs/common/src/platform/messaging/internal.ts
Normal file
5
libs/common/src/platform/messaging/internal.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// Built in implementations
|
||||
export { SubjectMessageSender } from "./subject-message.sender";
|
||||
|
||||
// Helpers meant to be used only by other implementations
|
||||
export { tagAsExternal, getCommand } from "./helpers";
|
47
libs/common/src/platform/messaging/message.listener.spec.ts
Normal file
47
libs/common/src/platform/messaging/message.listener.spec.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { subscribeTo } from "../../../spec/observable-tracker";
|
||||
|
||||
import { MessageListener } from "./message.listener";
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
describe("MessageListener", () => {
|
||||
const subject = new Subject<Message<{ test: number }>>();
|
||||
const sut = new MessageListener(subject.asObservable());
|
||||
|
||||
const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand");
|
||||
|
||||
describe("allMessages$", () => {
|
||||
it("runs on all nexts", async () => {
|
||||
const tracker = subscribeTo(sut.allMessages$);
|
||||
|
||||
const pausePromise = tracker.pauseUntilReceived(2);
|
||||
|
||||
subject.next({ command: "command1", test: 1 });
|
||||
subject.next({ command: "command2", test: 2 });
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 });
|
||||
expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("messages$", () => {
|
||||
it("runs on only my commands", async () => {
|
||||
const tracker = subscribeTo(sut.messages$(testCommandDefinition));
|
||||
|
||||
const pausePromise = tracker.pauseUntilReceived(2);
|
||||
|
||||
subject.next({ command: "notMyCommand", test: 1 });
|
||||
subject.next({ command: "myCommand", test: 2 });
|
||||
subject.next({ command: "myCommand", test: 3 });
|
||||
subject.next({ command: "notMyCommand", test: 4 });
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 });
|
||||
expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 });
|
||||
});
|
||||
});
|
||||
});
|
41
libs/common/src/platform/messaging/message.listener.ts
Normal file
41
libs/common/src/platform/messaging/message.listener.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { EMPTY, Observable, filter } from "rxjs";
|
||||
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
/**
|
||||
* A class that allows for listening to messages coming through the application,
|
||||
* allows for listening of all messages or just the messages you care about.
|
||||
*
|
||||
* @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of
|
||||
* data that is persisted. This can serve messages that might have been used to notify of settings changes
|
||||
* or vault data changes and those observables should be preferred over messaging.
|
||||
*/
|
||||
export class MessageListener {
|
||||
constructor(private readonly messageStream: Observable<Message<object>>) {}
|
||||
|
||||
/**
|
||||
* A stream of all messages sent through the application. It does not contain type information for the
|
||||
* other properties on the messages. You are encouraged to instead subscribe to an individual message
|
||||
* through {@link messages$}.
|
||||
*/
|
||||
allMessages$ = this.messageStream;
|
||||
|
||||
/**
|
||||
* Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed
|
||||
* to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being
|
||||
* sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message
|
||||
* payload being the expected type.
|
||||
*
|
||||
* @param commandDefinition The CommandDefinition containing the information about the message type you care about.
|
||||
*/
|
||||
messages$<T extends object>(commandDefinition: CommandDefinition<T>): Observable<T> {
|
||||
return this.allMessages$.pipe(
|
||||
filter((msg) => msg?.command === commandDefinition.command),
|
||||
) as Observable<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper property for returning a MessageListener that will never emit any messages and will immediately complete.
|
||||
*/
|
||||
static readonly EMPTY = new MessageListener(EMPTY);
|
||||
}
|
62
libs/common/src/platform/messaging/message.sender.ts
Normal file
62
libs/common/src/platform/messaging/message.sender.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { CommandDefinition } from "./types";
|
||||
|
||||
class MultiMessageSender implements MessageSender {
|
||||
constructor(private readonly innerMessageSenders: MessageSender[]) {}
|
||||
|
||||
send<T extends object>(
|
||||
commandDefinition: string | CommandDefinition<T>,
|
||||
payload: object | T = {},
|
||||
): void {
|
||||
for (const messageSender of this.innerMessageSenders) {
|
||||
messageSender.send(commandDefinition, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MessageSender {
|
||||
/**
|
||||
* A method for sending messages in a type safe manner. The passed in command definition
|
||||
* will require you to provide a compatible type in the payload parameter.
|
||||
*
|
||||
* @example
|
||||
* const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand");
|
||||
*
|
||||
* this.messageSender.send(MY_COMMAND, { test: 14 });
|
||||
*
|
||||
* @param commandDefinition
|
||||
* @param payload
|
||||
*/
|
||||
abstract send<T extends object>(commandDefinition: CommandDefinition<T>, payload: T): void;
|
||||
|
||||
/**
|
||||
* A legacy method for sending messages in a non-type safe way.
|
||||
*
|
||||
* @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to
|
||||
* get compilation errors when defining an incompatible payload.
|
||||
*
|
||||
* @param command The string based command of your message.
|
||||
* @param payload Extra contextual information regarding the message. Be aware that this payload may
|
||||
* be serialized and lose all prototype information.
|
||||
*/
|
||||
abstract send(command: string, payload?: object): void;
|
||||
|
||||
/** Implementation of the other two overloads, read their docs instead. */
|
||||
abstract send<T extends object>(
|
||||
commandDefinition: CommandDefinition<T> | string,
|
||||
payload: T | object,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* A helper method for combine multiple {@link MessageSender}'s.
|
||||
* @param messageSenders The message senders that should be combined.
|
||||
* @returns A message sender that will relay all messages to the given message senders.
|
||||
*/
|
||||
static combine(...messageSenders: MessageSender[]) {
|
||||
return new MultiMessageSender(messageSenders);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper property for creating a {@link MessageSender} that sends to nowhere.
|
||||
*/
|
||||
static readonly EMPTY: MessageSender = new MultiMessageSender([]);
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { subscribeTo } from "../../../spec/observable-tracker";
|
||||
|
||||
import { SubjectMessageSender } from "./internal";
|
||||
import { MessageSender } from "./message.sender";
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
describe("SubjectMessageSender", () => {
|
||||
const subject = new Subject<Message<{ test: number }>>();
|
||||
const subjectObservable = subject.asObservable();
|
||||
|
||||
const sut: MessageSender = new SubjectMessageSender(subject);
|
||||
|
||||
describe("send", () => {
|
||||
it("will send message with command from message definition", async () => {
|
||||
const commandDefinition = new CommandDefinition<{ test: number }>("myCommand");
|
||||
|
||||
const tracker = subscribeTo(subjectObservable);
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
sut.send(commandDefinition, { test: 1 });
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
|
||||
});
|
||||
|
||||
it("will send message with command from normal string", async () => {
|
||||
const tracker = subscribeTo(subjectObservable);
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
sut.send("myCommand", { test: 1 });
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
|
||||
});
|
||||
|
||||
it("will send message with object even if payload not given", async () => {
|
||||
const tracker = subscribeTo(subjectObservable);
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
sut.send("myCommand");
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
|
||||
});
|
||||
|
||||
it.each([null, undefined])(
|
||||
"will send message with object even if payload is null-ish (%s)",
|
||||
async (payloadValue) => {
|
||||
const tracker = subscribeTo(subjectObservable);
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
sut.send("myCommand", payloadValue);
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
17
libs/common/src/platform/messaging/subject-message.sender.ts
Normal file
17
libs/common/src/platform/messaging/subject-message.sender.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { getCommand } from "./internal";
|
||||
import { MessageSender } from "./message.sender";
|
||||
import { Message, CommandDefinition } from "./types";
|
||||
|
||||
export class SubjectMessageSender implements MessageSender {
|
||||
constructor(private readonly messagesSubject: Subject<Message<object>>) {}
|
||||
|
||||
send<T extends object>(
|
||||
commandDefinition: string | CommandDefinition<T>,
|
||||
payload: object | T = {},
|
||||
): void {
|
||||
const command = getCommand(commandDefinition);
|
||||
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));
|
||||
}
|
||||
}
|
13
libs/common/src/platform/messaging/types.ts
Normal file
13
libs/common/src/platform/messaging/types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
declare const tag: unique symbol;
|
||||
|
||||
/**
|
||||
* A class for defining information about a message, this is helpful
|
||||
* alonside `MessageSender` and `MessageListener` for providing a type
|
||||
* safe(-ish) way of sending and receiving messages.
|
||||
*/
|
||||
export class CommandDefinition<T extends object> {
|
||||
[tag]: T;
|
||||
constructor(readonly command: string) {}
|
||||
}
|
||||
|
||||
export type Message<T extends object> = { command: string } & T;
|
@ -1,34 +0,0 @@
|
||||
import {
|
||||
BroadcasterService as BroadcasterServiceAbstraction,
|
||||
MessageBase,
|
||||
} from "../abstractions/broadcaster.service";
|
||||
|
||||
export class BroadcasterService implements BroadcasterServiceAbstraction {
|
||||
subscribers: Map<string, (message: MessageBase) => void> = new Map<
|
||||
string,
|
||||
(message: MessageBase) => void
|
||||
>();
|
||||
|
||||
send(message: MessageBase, id?: string) {
|
||||
if (id != null) {
|
||||
if (this.subscribers.has(id)) {
|
||||
this.subscribers.get(id)(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribers.forEach((value) => {
|
||||
value(message);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
|
||||
this.subscribers.set(id, messageCallback);
|
||||
}
|
||||
|
||||
unsubscribe(id: string) {
|
||||
if (this.subscribers.has(id)) {
|
||||
this.subscribers.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service";
|
||||
import { MessageListener, MessageSender } from "../messaging";
|
||||
|
||||
/**
|
||||
* Temporary implementation that just delegates to the message sender and message listener
|
||||
* and manages their subscriptions.
|
||||
*/
|
||||
export class DefaultBroadcasterService implements BroadcasterService {
|
||||
subscriptions = new Map<string, Subscription>();
|
||||
|
||||
constructor(
|
||||
private readonly messageSender: MessageSender,
|
||||
private readonly messageListener: MessageListener,
|
||||
) {}
|
||||
|
||||
send(message: MessageBase, id?: string) {
|
||||
this.messageSender.send(message?.command, message);
|
||||
}
|
||||
|
||||
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
|
||||
this.subscriptions.set(
|
||||
id,
|
||||
this.messageListener.allMessages$.subscribe((message) => {
|
||||
messageCallback(message);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe(id: string) {
|
||||
const subscription = this.subscriptions.get(id);
|
||||
subscription?.unsubscribe();
|
||||
this.subscriptions.delete(id);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
|
||||
export class NoopMessagingService implements MessagingService {
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
// Do nothing...
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user