mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-25521] Move importer metadata to native code (#16695)
* Add importer metadata to native code * Impl napi code in ts * Impl napi code in ts * Fix clippy * Fix clippy * remove ts util tests * Check for installed browsers * PR fixes * test fix * fix clippy * fix tests * Bug fix * clippy fix * Correct tests * fix clippy * fix clippy * Correct tests * Correct tests * [PM-25521] Wire up loading metadata on desktop (#16813) * Initial commit * Fix issues regarding now unused feature flag * Fixed ts-strict issues --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: adudek-bw <adudek@bitwarden.com> * Remove logic to skip Brave as that now happens via the native code * Define default capabilities which can be overwritten by specifc client/platform * Fix DI issues * Do not overwrite existing importers, just add new ones or update existing ones * feat: [PM-25521] return metadata directly (not as JSON) (#16882) * feat: return metadata directly (not as JSON) * Fix broken builds Move getMetaData into chromium_importer Remove chromium_importer_metadata and any related service Parse object from native instead of json * Run cargo fmt * Fix cargo dependency sort order * Use exposed type from NAPI instead of redefining it. * Run cargo fmt --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * Only enable chromium loader for installed and supported browsers --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
@@ -72,7 +72,11 @@ import {
|
||||
|
||||
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
||||
import { ImportOption, ImportResult, ImportType } from "../models";
|
||||
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
|
||||
import {
|
||||
ImportCollectionServiceAbstraction,
|
||||
ImportMetadataServiceAbstraction,
|
||||
ImportServiceAbstraction,
|
||||
} from "../services";
|
||||
|
||||
import { ImportChromeComponent } from "./chrome";
|
||||
import {
|
||||
@@ -236,6 +240,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected accountService: AccountService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private destroyRef: DestroyRef,
|
||||
protected importMetadataService: ImportMetadataServiceAbstraction,
|
||||
) {}
|
||||
|
||||
protected get importBlockedByPolicy(): boolean {
|
||||
@@ -254,9 +259,11 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.importMetadataService.init();
|
||||
|
||||
this.setImportOptions();
|
||||
|
||||
this.importService
|
||||
this.importMetadataService
|
||||
.metadata$(this.formGroup.controls.format.valueChanges)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
|
||||
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
|
||||
// to a general module.
|
||||
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
|
||||
export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>(
|
||||
"SystemServices",
|
||||
);
|
||||
|
||||
/** Import service factories */
|
||||
export const ImporterProviders: SafeProvider[] = [
|
||||
@@ -85,7 +87,6 @@ export const ImporterProviders: SafeProvider[] = [
|
||||
PinServiceAbstraction,
|
||||
AccountService,
|
||||
RestrictedItemTypesService,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./dialog";
|
||||
export * from "./importer-providers";
|
||||
|
||||
export { ImportComponent } from "./import.component";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./models";
|
||||
export * from "./metadata";
|
||||
export * from "./services";
|
||||
|
||||
export { Importer } from "./importers/importer";
|
||||
|
||||
@@ -2,26 +2,32 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { Loader, Instructions } from "./data";
|
||||
import { Instructions, Loader } from "./data";
|
||||
import { ImporterMetadata } from "./types";
|
||||
|
||||
// FIXME: load this data from rust code
|
||||
export type ImportersMetadata = Partial<Record<ImportType, ImporterMetadata>>;
|
||||
|
||||
/** List of all supported importers and their default capabilities
|
||||
* Note: the loaders listed here are the ones that are supported in all clients.
|
||||
* Specific clients may have additional loaders available based on platform capabilities.
|
||||
*/
|
||||
const importers = [
|
||||
{ id: "bitwardenjson", loaders: [Loader.file], instructions: Instructions.unique },
|
||||
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
|
||||
{ id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{ id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "operacsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{
|
||||
id: "vivaldicsv",
|
||||
loaders: [Loader.file, Loader.chromium],
|
||||
loaders: [Loader.file],
|
||||
instructions: Instructions.chromium,
|
||||
},
|
||||
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "bravecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{ id: "edgecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
|
||||
// FIXME: add other formats and remove `Partial` from export
|
||||
] as const;
|
||||
|
||||
/** Describes which loaders are available for each import type */
|
||||
export const Importers: Partial<Record<ImportType, ImporterMetadata>> = deepFreeze(
|
||||
export const Importers: ImportersMetadata = deepFreeze(
|
||||
Object.fromEntries(importers.map((i) => [i.id, i])),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
|
||||
import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata";
|
||||
import { ImportType } from "../models/import-options";
|
||||
import { availableLoaders } from "../util";
|
||||
|
||||
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
|
||||
export class DefaultImportMetadataService implements ImportMetadataServiceAbstraction {
|
||||
protected importers: ImportersMetadata = Importers;
|
||||
private logger: SemanticLogger;
|
||||
|
||||
constructor(protected system: SystemServiceProvider) {
|
||||
this.logger = system.log({ type: "ImportMetadataService" });
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
// no-op for default implementation
|
||||
}
|
||||
|
||||
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
|
||||
const client = this.system.environment.getClientType();
|
||||
const capabilities$ = type$.pipe(
|
||||
map((type) => {
|
||||
if (!this.importers) {
|
||||
return { type, loaders: [] };
|
||||
}
|
||||
|
||||
const loaders = availableLoaders(this.importers, type, client);
|
||||
|
||||
if (!loaders || loaders.length === 0) {
|
||||
return { type, loaders: [] };
|
||||
}
|
||||
|
||||
const capabilities: ImporterMetadata = { type, loaders };
|
||||
if (type in this.importers) {
|
||||
capabilities.instructions = this.importers[type]?.instructions;
|
||||
}
|
||||
|
||||
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
|
||||
|
||||
return capabilities;
|
||||
}),
|
||||
);
|
||||
|
||||
return capabilities$;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ImporterMetadata } from "../metadata";
|
||||
import { ImportType } from "../models/import-options";
|
||||
|
||||
export abstract class ImportMetadataServiceAbstraction {
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
/** describes the features supported by a format */
|
||||
abstract metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||
}
|
||||
110
libs/importer/src/services/import-metadata.service.spec.ts
Normal file
110
libs/importer/src/services/import-metadata.service.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
|
||||
import { ImporterMetadata, Instructions } from "../metadata";
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { DefaultImportMetadataService } from "./default-import-metadata.service";
|
||||
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
|
||||
describe("ImportMetadataService", () => {
|
||||
let sut: ImportMetadataServiceAbstraction;
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
|
||||
});
|
||||
|
||||
sut = new DefaultImportMetadataService(systemServiceProvider);
|
||||
});
|
||||
|
||||
describe("metadata$", () => {
|
||||
let typeSubject: Subject<ImportType>;
|
||||
let mockLogger: { debug: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
typeSubject = new Subject<ImportType>();
|
||||
mockLogger = { debug: jest.fn() };
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue(mockLogger),
|
||||
});
|
||||
|
||||
// Recreate the service with the updated mocks for logging tests
|
||||
sut = new DefaultImportMetadataService(systemServiceProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
typeSubject.complete();
|
||||
});
|
||||
|
||||
it("should emit metadata when type$ emits", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(sut.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: testType,
|
||||
loaders: expect.any(Array),
|
||||
instructions: Instructions.chromium,
|
||||
});
|
||||
expect(result.type).toBe(testType);
|
||||
});
|
||||
|
||||
it("should update when type$ changes", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
const subscription = sut.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
typeSubject.next("chromecsv");
|
||||
typeSubject.next("bravecsv");
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions).toHaveLength(2);
|
||||
expect(emissions[0].type).toBe("chromecsv");
|
||||
expect(emissions[1].type).toBe("bravecsv");
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should log debug information with correct data", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(sut.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
await metadataPromise;
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
{ importType: testType, capabilities: expect.any(Object) },
|
||||
"capabilities updated",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -8,7 +7,6 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata } from "../metadata";
|
||||
import { ImportOption, ImportType } from "../models/import-options";
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
@@ -17,9 +15,6 @@ export abstract class ImportServiceAbstraction {
|
||||
regularImportOptions: readonly ImportOption[];
|
||||
getImportOptions: () => ImportOption[];
|
||||
|
||||
/** describes the features supported by a format */
|
||||
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||
|
||||
import: (
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -25,8 +18,6 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata, Instructions, Loader } from "../metadata";
|
||||
import { ImportType } from "../models";
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
||||
@@ -44,7 +35,6 @@ describe("ImportService", () => {
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherService = mock<CipherService>();
|
||||
@@ -57,18 +47,6 @@ describe("ImportService", () => {
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
restrictedItemTypesService = mock<RestrictedItemTypesService>();
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false));
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
|
||||
});
|
||||
|
||||
importService = new ImportService(
|
||||
cipherService,
|
||||
folderService,
|
||||
@@ -80,7 +58,6 @@ describe("ImportService", () => {
|
||||
pinService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
systemServiceProvider,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -268,136 +245,6 @@ describe("ImportService", () => {
|
||||
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("metadata$", () => {
|
||||
let featureFlagSubject: BehaviorSubject<boolean>;
|
||||
let typeSubject: Subject<ImportType>;
|
||||
let mockLogger: { debug: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
featureFlagSubject = new BehaviorSubject(false);
|
||||
typeSubject = new Subject<ImportType>();
|
||||
mockLogger = { debug: jest.fn() };
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockReturnValue(featureFlagSubject);
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue(mockLogger),
|
||||
});
|
||||
|
||||
// Recreate the service with the updated mocks for logging tests
|
||||
importService = new ImportService(
|
||||
cipherService,
|
||||
folderService,
|
||||
importApiService,
|
||||
i18nService,
|
||||
collectionService,
|
||||
keyService,
|
||||
encryptService,
|
||||
pinService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
systemServiceProvider,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
featureFlagSubject.complete();
|
||||
typeSubject.complete();
|
||||
});
|
||||
|
||||
it("should emit metadata when type$ emits", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: testType,
|
||||
loaders: expect.any(Array),
|
||||
instructions: Instructions.chromium,
|
||||
});
|
||||
expect(result.type).toBe(testType);
|
||||
});
|
||||
|
||||
it("should include all loaders when chromium feature flag is enabled", async () => {
|
||||
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
|
||||
featureFlagSubject.next(true);
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result.loaders).toContain(Loader.chromium);
|
||||
expect(result.loaders).toContain(Loader.file);
|
||||
});
|
||||
|
||||
it("should update when type$ changes", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
typeSubject.next("chromecsv");
|
||||
typeSubject.next("bravecsv");
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions).toHaveLength(2);
|
||||
expect(emissions[0].type).toBe("chromecsv");
|
||||
expect(emissions[1].type).toBe("bravecsv");
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should update when both type$ and feature flag change", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
|
||||
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
// Initial emission
|
||||
typeSubject.next("chromecsv");
|
||||
|
||||
// Change both at the same time
|
||||
featureFlagSubject.next(true);
|
||||
typeSubject.next("bravecsv");
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions.length).toBeGreaterThanOrEqual(2);
|
||||
const lastEmission = emissions[emissions.length - 1];
|
||||
expect(lastEmission.type).toBe("bravecsv");
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should log debug information with correct data", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
await metadataPromise;
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
{ importType: testType, capabilities: expect.any(Object) },
|
||||
"capabilities updated",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
||||
@@ -20,8 +19,6 @@ import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -98,7 +95,6 @@ import {
|
||||
PasswordDepot17XmlImporter,
|
||||
} from "../importers";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata, Importers, Loader } from "../metadata";
|
||||
import {
|
||||
featuredImportOptions,
|
||||
ImportOption,
|
||||
@@ -108,15 +104,12 @@ import {
|
||||
import { ImportResult } from "../models/import-result";
|
||||
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
||||
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
||||
import { availableLoaders as availableLoaders } from "../util";
|
||||
|
||||
export class ImportService implements ImportServiceAbstraction {
|
||||
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
||||
|
||||
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
||||
|
||||
private logger: SemanticLogger;
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
@@ -128,55 +121,12 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
private pinService: PinServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private system: SystemServiceProvider,
|
||||
) {
|
||||
this.logger = system.log({ type: "ImportService" });
|
||||
}
|
||||
) {}
|
||||
|
||||
getImportOptions(): ImportOption[] {
|
||||
return this.featuredImportOptions.concat(this.regularImportOptions);
|
||||
}
|
||||
|
||||
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
|
||||
const client = this.system.environment.getClientType();
|
||||
const capabilities$ = combineLatest([type$]).pipe(
|
||||
map(([type]) => {
|
||||
let loaders = availableLoaders(type, client);
|
||||
|
||||
// Mac App Store is currently disabled due to sandboxing.
|
||||
let isUnsupported = this.system.environment.isMacAppStore();
|
||||
|
||||
// disable the chromium loader for Brave on Windows only
|
||||
if (type === "bravecsv") {
|
||||
try {
|
||||
const device = this.system.environment.getDevice();
|
||||
const isWindowsDesktop = device === DeviceType.WindowsDesktop;
|
||||
if (isWindowsDesktop) {
|
||||
isUnsupported = true;
|
||||
}
|
||||
} catch {
|
||||
isUnsupported = true;
|
||||
}
|
||||
}
|
||||
// If the browser is unsupported, remove the chromium loader
|
||||
if (isUnsupported) {
|
||||
loaders = loaders?.filter((loader) => loader !== Loader.chromium);
|
||||
}
|
||||
|
||||
const capabilities: ImporterMetadata = { type, loaders };
|
||||
if (type in Importers) {
|
||||
capabilities.instructions = Importers[type].instructions;
|
||||
}
|
||||
|
||||
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
|
||||
|
||||
return capabilities;
|
||||
}),
|
||||
);
|
||||
|
||||
return capabilities$;
|
||||
}
|
||||
|
||||
async import(
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
|
||||
@@ -4,4 +4,7 @@ export { ImportApiService } from "./import-api.service";
|
||||
export { ImportServiceAbstraction } from "./import.service.abstraction";
|
||||
export { ImportService } from "./import.service";
|
||||
|
||||
export { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
export { DefaultImportMetadataService } from "./default-import-metadata.service";
|
||||
|
||||
export { ImportCollectionServiceAbstraction } from "./import-collection.service.abstraction";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
import { Loader } from "./metadata";
|
||||
import { availableLoaders } from "./util";
|
||||
|
||||
describe("availableLoaders", () => {
|
||||
describe("given valid import types", () => {
|
||||
it("returns available loaders when client supports all loaders", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||
});
|
||||
|
||||
it("returns filtered loaders when client supports some loaders", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns single loader for import types with one loader", () => {
|
||||
const result = availableLoaders("chromecsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns all supported loaders for multi-loader import types", () => {
|
||||
const result = availableLoaders("bravecsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given unknown import types", () => {
|
||||
it("returns undefined when import type is not found in metadata", () => {
|
||||
const result = availableLoaders("nonexistent" as any, ClientType.Desktop);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("given different client types", () => {
|
||||
it("returns appropriate loaders for Browser client", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns appropriate loaders for Web client", () => {
|
||||
const result = availableLoaders("chromecsv", ClientType.Web);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns appropriate loaders for CLI client", () => {
|
||||
const result = availableLoaders("vivaldicsv", ClientType.Cli);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
import { LoaderAvailability, Importers } from "./metadata";
|
||||
import { LoaderAvailability, ImportersMetadata } from "./metadata";
|
||||
import { ImportType } from "./models";
|
||||
|
||||
/** Lookup the loaders supported by a specific client.
|
||||
@@ -8,12 +8,20 @@ import { ImportType } from "./models";
|
||||
* @returns `undefined` when metadata is not defined for the type, or
|
||||
* an array identifying the supported clients.
|
||||
*/
|
||||
export function availableLoaders(type: ImportType, client: ClientType) {
|
||||
if (!(type in Importers)) {
|
||||
export function availableLoaders(
|
||||
importersMetadata: ImportersMetadata,
|
||||
type: ImportType,
|
||||
client: ClientType,
|
||||
) {
|
||||
if (!importersMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const capabilities = Importers[type]?.loaders ?? [];
|
||||
if (!(type in importersMetadata)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const capabilities = importersMetadata[type]?.loaders ?? [];
|
||||
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
|
||||
return available;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user