internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
146
packages/services/src/Domain/Mutator/ImportDataUseCase.ts
Normal file
146
packages/services/src/Domain/Mutator/ImportDataUseCase.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ComponentContent,
|
||||
CopyPayloadWithContentOverride,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedItemInterface,
|
||||
DecryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||
|
||||
const Strings = {
|
||||
UnsupportedBackupFileVersion:
|
||||
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
|
||||
BackupFileMoreRecentThanAccount:
|
||||
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
|
||||
FileAccountPassword: 'File account password',
|
||||
}
|
||||
|
||||
export type ImportDataReturnType =
|
||||
| {
|
||||
affectedItems: DecryptedItemInterface[]
|
||||
errorCount: number
|
||||
}
|
||||
| {
|
||||
error: ClientDisplayableError
|
||||
}
|
||||
|
||||
export class ImportDataUseCase {
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private syncService: SyncServiceInterface,
|
||||
private protectionService: ProtectionsClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private challengeService: ChallengeServiceInterface,
|
||||
private historyService: HistoryServiceInterface,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* .affectedItems: Items that were either created or dirtied by this import
|
||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||
*/
|
||||
|
||||
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||
if (data.version) {
|
||||
/**
|
||||
* Prior to 003 backup files did not have a version field so we cannot
|
||||
* stop importing if there is no backup file version, only if there is
|
||||
* an unsupported version.
|
||||
*/
|
||||
const version = data.version as ProtocolVersion
|
||||
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return { error: new ClientDisplayableError(Strings.UnsupportedBackupFileVersion) }
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) }
|
||||
}
|
||||
}
|
||||
|
||||
let password: string | undefined
|
||||
|
||||
if (data.auth_params || data.keyParams) {
|
||||
/** Get import file password. */
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
||||
ChallengeReason.DecryptEncryptedFile,
|
||||
true,
|
||||
)
|
||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (passwordResponse == undefined) {
|
||||
/** Challenge was canceled */
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
password = passwordResponse?.values[0].value as string
|
||||
}
|
||||
|
||||
if (!(await this.protectionService.authorizeFileImport())) {
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
|
||||
data.items = data.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return CreateEncryptedBackupFileContextPayload(item)
|
||||
} else {
|
||||
return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload)
|
||||
}
|
||||
})
|
||||
|
||||
const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
}
|
||||
|
||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
||||
/* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess */
|
||||
if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) {
|
||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
||||
return CopyPayloadWithContentOverride(typedContent, {
|
||||
active: false,
|
||||
})
|
||||
} else {
|
||||
return payload
|
||||
}
|
||||
})
|
||||
|
||||
const affectedUuids = await this.payloadManager.importPayloads(
|
||||
validPayloads,
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
)
|
||||
|
||||
const promise = this.syncService.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
}
|
||||
|
||||
const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[]
|
||||
|
||||
return {
|
||||
affectedItems: affectedItems,
|
||||
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,92 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
ComponentMutator,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedItemInterface,
|
||||
FeatureRepoMutator,
|
||||
FileItem,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
ItemsKeyMutatorInterface,
|
||||
MutationType,
|
||||
PayloadEmitSource,
|
||||
PredicateInterface,
|
||||
SmartView,
|
||||
SNComponent,
|
||||
SNFeatureRepo,
|
||||
SNNote,
|
||||
SNTag,
|
||||
TransactionalMutation,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
|
||||
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
||||
import { SyncOptions } from '../Sync/SyncOptions'
|
||||
|
||||
export interface MutatorClientInterface {
|
||||
/**
|
||||
* Inserts the input item by its payload properties, and marks the item as dirty.
|
||||
* A sync is not performed after an item is inserted. This must be handled by the caller.
|
||||
*/
|
||||
insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
|
||||
insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty?: boolean): Promise<T>
|
||||
emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing item, marks it as dirty, and syncs it
|
||||
*/
|
||||
changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
|
||||
/**
|
||||
* Mutates pre-existing items, marks them as dirty, and syncs
|
||||
*/
|
||||
changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise<void>
|
||||
setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise<void>
|
||||
setItemsDirty(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
isUserModified?: boolean,
|
||||
): Promise<DecryptedItemInterface[]>
|
||||
createItem<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
|
||||
contentType: ContentType,
|
||||
content: C,
|
||||
needsSync?: boolean,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<T>
|
||||
|
||||
changeItem<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemToLookupUuidFor: I,
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<void>
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I>
|
||||
changeItems<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemsToLookupUuidsFor: I[],
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I[]>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing item and marks it as dirty. Does not sync changes.
|
||||
*/
|
||||
changeItem<M extends DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
changeItemsKey(
|
||||
itemToLookupUuidFor: ItemsKeyInterface,
|
||||
mutate: (mutator: ItemsKeyMutatorInterface) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<ItemsKeyInterface>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing items and marks them as dirty. Does not sync changes.
|
||||
*/
|
||||
changeItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
): Promise<(DecryptedItemInterface | undefined)[]>
|
||||
changeComponent(
|
||||
itemToLookupUuidFor: SNComponent,
|
||||
mutate: (mutator: ComponentMutator) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNComponent>
|
||||
|
||||
changeFeatureRepo(
|
||||
itemToLookupUuidFor: SNFeatureRepo,
|
||||
mutate: (mutator: FeatureRepoMutator) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNFeatureRepo>
|
||||
|
||||
/**
|
||||
* Run unique mutations per each item in the array, then only propagate all changes
|
||||
@@ -83,44 +105,11 @@ export interface MutatorClientInterface {
|
||||
payloadSourceKey?: string,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
|
||||
protectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
|
||||
items: I[],
|
||||
): Promise<I[]>
|
||||
|
||||
unprotectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
|
||||
items: I[],
|
||||
reason: ChallengeReason,
|
||||
): Promise<I[] | undefined>
|
||||
|
||||
protectNote(note: SNNote): Promise<SNNote>
|
||||
|
||||
unprotectNote(note: SNNote): Promise<SNNote | undefined>
|
||||
|
||||
protectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
|
||||
unprotectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
|
||||
protectFile(file: FileItem): Promise<FileItem>
|
||||
|
||||
unprotectFile(file: FileItem): Promise<FileItem | undefined>
|
||||
|
||||
/**
|
||||
* Takes the values of the input item and emits it onto global state.
|
||||
*/
|
||||
mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
|
||||
|
||||
/**
|
||||
* Creates an unmanaged item that can be added later.
|
||||
*/
|
||||
createTemplateItem<
|
||||
C extends ItemContent = ItemContent,
|
||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
contentType: ContentType,
|
||||
content?: C,
|
||||
override?: Partial<DecryptedPayload<C>>,
|
||||
): I
|
||||
|
||||
/**
|
||||
* @param isUserModified Whether to change the modified date the user
|
||||
* sees of the item.
|
||||
@@ -135,7 +124,13 @@ export interface MutatorClientInterface {
|
||||
|
||||
emptyTrash(): Promise<void>
|
||||
|
||||
duplicateItem<T extends DecryptedItemInterface>(item: T, additionalContent?: Partial<T['content']>): Promise<T>
|
||||
duplicateItem<T extends DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: T,
|
||||
isConflict?: boolean,
|
||||
additionalContent?: Partial<T['content']>,
|
||||
): Promise<T>
|
||||
|
||||
addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
|
||||
|
||||
/**
|
||||
* Migrates any tags containing a '.' character to sa chema-based heirarchy, removing
|
||||
@@ -146,41 +141,35 @@ export interface MutatorClientInterface {
|
||||
/**
|
||||
* Establishes a hierarchical relationship between two tags.
|
||||
*/
|
||||
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<void>
|
||||
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<SNTag>
|
||||
|
||||
/**
|
||||
* Remove the tag parent.
|
||||
*/
|
||||
unsetTagParent(childTag: SNTag): Promise<void>
|
||||
unsetTagParent(childTag: SNTag): Promise<SNTag>
|
||||
|
||||
findOrCreateTag(title: string): Promise<SNTag>
|
||||
findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise<SNTag>
|
||||
|
||||
/** Creates and returns the tag but does not run sync. Callers must perform sync. */
|
||||
createTagOrSmartView(title: string): Promise<SNTag | SmartView>
|
||||
createTagOrSmartView<T extends SNTag | SmartView>(title: string, vault?: VaultListingInterface): Promise<T>
|
||||
findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<SNTag>
|
||||
|
||||
/**
|
||||
* Activates or deactivates a component, depending on its
|
||||
* current state, and syncs.
|
||||
*/
|
||||
toggleComponent(component: SNComponent): Promise<void>
|
||||
associateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem | undefined>
|
||||
|
||||
toggleTheme(theme: SNComponent): Promise<void>
|
||||
disassociateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem>
|
||||
renameFile(file: FileItem, name: string): Promise<FileItem>
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* .affectedItems: Items that were either created or dirtied by this import
|
||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||
*/
|
||||
importData(
|
||||
data: BackupFile,
|
||||
awaitSync?: boolean,
|
||||
): Promise<
|
||||
| {
|
||||
affectedItems: DecryptedItemInterface[]
|
||||
errorCount: number
|
||||
}
|
||||
| {
|
||||
error: ClientDisplayableError
|
||||
}
|
||||
>
|
||||
unlinkItems(
|
||||
itemA: DecryptedItemInterface<ItemContent>,
|
||||
itemB: DecryptedItemInterface<ItemContent>,
|
||||
): Promise<DecryptedItemInterface<ItemContent>>
|
||||
createSmartView<T extends DecryptedItemInterface>(dto: {
|
||||
title: string
|
||||
predicate: PredicateInterface<T>
|
||||
iconString?: string
|
||||
vault?: VaultListingInterface
|
||||
}): Promise<SmartView>
|
||||
linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote>
|
||||
linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem>
|
||||
addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user