diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm index 521e3324..4d30bd1b 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm @@ -32,7 +32,7 @@ @implementation Addressee (label) - (instancetype)initWithLabel:(const std::string &)label pub:(NSData*)pub concatKDFAlgorithmURI:(NSString *)concatKDFAlgorithmURI { std::map info = libcdoc::Recipient::parseLabel(label); - id cn = info.contains("cn") ? [NSString stringWithStdString:info["cn"]] : nil; + id cn = info.contains("cn") ? [NSString stringWithStdString:info["cn"]] : [NSString stringWithStdString:label]; id type = info.contains("type") ? [NSString stringWithStdString:info["type"]] : nil; id serial = info.contains("serial_number") ? [NSString stringWithStdString:info["serial_number"]] : nil; CertType certType = CertTypeUnknownType; diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift index c787812f..d9eb4a11 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift @@ -75,8 +75,74 @@ public actor CryptoContainer: CryptoContainerProtocol, Loggable { return containerFile } - public func addDataFiles(_ filesToAdd: [URL]) async { - dataFiles?.append(contentsOf: filesToAdd) + public func addDataFiles(_ filesToAdd: [URL]) async throws { + let cryptoContainersDirectory = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.ContainerFolder, Constants.Folder.Temp], + fileManager: fileManager + ) + + var movedFiles: [URL] = [] + var duplicateFileCount = 0 + var failedFileCount = 0 + let totalFileCount = filesToAdd.count + + let existingDataFiles = Set(dataFiles?.compactMap { $0?.lastPathComponent } ?? []) + + var duplicateFileName: String? + + for fileToAdd in filesToAdd { + let fileName = fileToAdd.lastPathComponent + let destinationURL = cryptoContainersDirectory.appendingPathComponent(fileName) + + if existingDataFiles.contains(fileName) || + movedFiles.contains(where: { $0.lastPathComponent == fileName }) { + + duplicateFileCount += 1 + + if duplicateFileCount == 1 { + duplicateFileName = fileName + } + + continue + } + + do { + if fileManager.fileExists(atPath: destinationURL.path) { + try fileManager.removeItem(at: destinationURL) + } + + try fileManager.copyItem(at: fileToAdd, to: destinationURL) + movedFiles.append(destinationURL) + + } catch { + failedFileCount += 1 + } + } + + if dataFiles == nil { + dataFiles = movedFiles + } else { + dataFiles?.append(contentsOf: movedFiles) + } + + if duplicateFileCount > 0 || failedFileCount > 0 { + var userInfo: [String: String] = [ + "totalFileCount": String(totalFileCount), + "failedFileCount": String(failedFileCount), + "duplicateFileCount": String(duplicateFileCount) + ] + + if duplicateFileCount == 1, let name = duplicateFileName { + userInfo["fileName"] = name + } + + throw CryptoError.addingFilesToContainerFailed( + CryptoErrorDetail( + message: "Unable to add files to container", + userInfo: userInfo + ) + ) + } } public func addRecipients(_ recipientsToAdd: [Addressee]) async { @@ -216,7 +282,6 @@ extension CryptoContainer { CryptoContainer.logger().info("Is single file: \(isSingleFile, privacy: .public)") CryptoContainer.logger().info("Is crypto container: \(isCryptoContainer, privacy: .public)") - guard isSingleFile && isCryptoContainer else { var defaultExtension = CommonsLib.Constants.Extension.DefaultCrypto if CDoc2Setting.isEncryptionEnabled { @@ -362,17 +427,25 @@ extension CryptoContainer { fileManager: fileManager ) - let isFileInTempCryptoContainersDirectory = containerFile.absoluteString.hasPrefix( - cryptoContainersDirectory.appending(path: Constants.Folder.Temp, directoryHint: .isDirectory).absoluteString + let savedContainersDirectory = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.SavedFiles], + fileManager: fileManager + ) + + // Do not rename when nested container opened + let isFileInSavedContainersDirectory = containerFile.absoluteString.hasPrefix( + savedContainersDirectory.absoluteString ) let isFileInRecentDocuments = containerFile.absoluteString.hasPrefix( cryptoContainersDirectory.absoluteString - ) && !isFileInTempCryptoContainersDirectory + ) + + let shouldRenameContainer = isFileInRecentDocuments && !isFileInSavedContainersDirectory var renamedContainerFile = containerFile - if !isFileInRecentDocuments { + if shouldRenameContainer { renamedContainerFile = Container.shared.containerUtil().getContainerFile( for: containerFile, in: isFileInRecentDocuments ? containerFile.deletingLastPathComponent() : diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift index a50c7b17..64a6cc8c 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift @@ -28,7 +28,7 @@ public protocol CryptoContainerProtocol: GeneralContainer, Sendable { func getContainerName() async -> String func getContainerMimetype() async -> String func getRawContainerFile() async -> URL? - func addDataFiles(_ filesToAdd: [URL]) async + func addDataFiles(_ filesToAdd: [URL]) async throws func addRecipients(_ recipientsToAdd: [Addressee]) async func getDataFiles() async -> [URL] diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index bb2775e3..e9056715 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -298,7 +298,6 @@ extension SignedContainer { @MainActor public static func openOrCreate( dataFiles: [URL], - containerUtil: ContainerUtilProtocol = Container.shared.containerUtil(), isSivaConfirmed: Bool ) async throws -> SignedContainerProtocol { logger().info("Opening or creating container. Found \(dataFiles.count) datafile(s)") diff --git a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift index fecfc52a..d6903d5e 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Mimetype/Parser/XMLParserHandler.swift @@ -38,9 +38,8 @@ final class XMLParserHandler: NSObject, XMLParserDelegate { ) { let formatAttribute = attributeDict["format"] let nameAttribute = attributeDict["Name"] - if elementName == "SignedDoc", ( - formatAttribute == "DIGIDOC-XML" || formatAttribute == "SK-XML" - ) { + if elementName == "SignedDoc", + formatAttribute == "DIGIDOC-XML" || formatAttribute == "SK-XML" { foundElement = .ddoc parser.abortParsing() } else if elementName == "denc:EncryptionProperty" && diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index 84f70b8f..27ad9219 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -202,7 +202,7 @@ struct EncryptView: View { isEditContainerButtonShown: viewModel.isEditButtonShown, isSaveButtonShown: viewModel.isContainerEncrypted || viewModel.isContainerDecrypted, isSignButtonShown: viewModel.isSignButtonShown, - isEncryptButtonShown: viewModel.isEncryptButtonShown, + isEncryptButtonShown: false, showLeftActionButton: false, showRightActionButton: viewModel.isEncryptButtonShown || viewModel.isDecryptButtonShown, @@ -324,7 +324,8 @@ struct EncryptView: View { viewModel: viewModel, showOpenFileButton: viewModel.isContainerUnlocked, showSaveFileButton: viewModel.isContainerUnlocked, - showRemoveFileButton: viewModel.isContainerWithoutRecipients, + showRemoveFileButton: !viewModel.isContainerEncrypted && + !viewModel.isContainerDecrypted, isNestedContainer: isNestedContainer, selectedDataFile: $selectedDataFile, showSivaMessage: $showSivaMessage, @@ -353,6 +354,7 @@ struct EncryptView: View { } } .padding(Dimensions.Padding.SPadding) + if viewModel.isShareButtonShown { if let containerFile = viewModel.containerURL { ShareButtonBottomBar( @@ -362,25 +364,25 @@ struct EncryptView: View { containerUrl: containerFile ) } - } else if !viewModel.isContainerEncrypted && !viewModel.isContainerDecrypted { - let rightButtonLabel = viewModel.isContainerWithoutRecipients ? nextLabel : encryptLabel - let rightButtonIconName = viewModel.isContainerWithoutRecipients - ? "ic_m3_arrow_forward_48pt_wght400" - : "ic_m3_encrypted_48pt_wght400" + } else if viewModel.isContainerUnencrypted { + let rightButtonLabel = viewModel.isContainerUnencrypted ? nextLabel : encryptLabel + let rightButtonIconName = viewModel.isContainerUnencrypted + ? "ic_m3_arrow_forward_48pt_wght400" + : "ic_m3_encrypted_48pt_wght400" UnsignedBottomBarView( - showLeftButton: viewModel.isContainerWithoutRecipients, + showLeftButton: viewModel.isContainerUnencrypted, leftButtonIconName: "ic_m3_add_48pt_wght400", leftButtonLabel: addMoreFilesLabel, leftButtonAccessibilityLabel: addMoreFilesLabel.lowercased(), leftButtonAction: { isImportingAddedFiles = true }, - rightButtonEnabled: viewModel.isContainerWithoutRecipients || encryptionButtonEnabled, + rightButtonEnabled: viewModel.isContainerUnencrypted || encryptionButtonEnabled, rightButtonIconName: rightButtonIconName, rightButtonLabel: rightButtonLabel, rightButtonAccessibilityLabel: rightButtonLabel.lowercased(), rightButtonAction: { - if viewModel.isContainerWithoutRecipients { + if viewModel.isContainerUnencrypted { pathManager.replaceLast(to: .encryptRecipientView) } else { if encryptionButtonEnabled { diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift index 565d462e..01805712 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift @@ -68,12 +68,11 @@ struct EncryptRecipientView: View { var body: some View { TopBarContainer( - isTopBarHidden: isSearchExpanded, title: nil, onLeftClick: { pathManager.replaceLast(to: .encryptView(isWithEncryption: false)) }, - showRightIcons: true, + showRightIcons: !isSearchExpanded, content: { ZStack { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift index 7366e7e6..24826a96 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift @@ -147,13 +147,15 @@ struct RecipientView: View { ) let certType = recipientUtil.getRecipientCertTypeText(certType: recipient.certType) - Text(verbatim: - "\(languageSettings.localized(certType)) " + - "\(languageSettings.localized("Valid to", [validToDate]))") - .font(typography.bodyMedium) - .foregroundStyle(theme.onSurfaceVariant) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.leading) + let validPart = validToDate.isEmpty + ? "" + : " " + languageSettings.localized("Valid to", [validToDate]) + + Text(verbatim: languageSettings.localized(certType) + validPart) + .font(typography.bodyMedium) + .foregroundStyle(theme.onSurfaceVariant) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) } .accessibilityElement(children: .combine) diff --git a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift index cf76abfc..8522362f 100644 --- a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift +++ b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift @@ -84,8 +84,8 @@ struct UnsignedBottomBarView: View { .disabled(!rightButtonEnabled) .foregroundStyle(theme.surfaceContainer) } - .padding(.vertical,Dimensions.Padding.SPadding) - .padding(.horizontal,Dimensions.Padding.MPadding) + .padding(.vertical, Dimensions.Padding.SPadding) + .padding(.horizontal, Dimensions.Padding.MPadding) .background(theme.surfaceContainer) } } diff --git a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift index a05b1976..0f05859d 100644 --- a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift +++ b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift @@ -28,7 +28,6 @@ struct TopBarContainer: View { @Environment(\.openURL) var openURL @Environment(LanguageSettings.self) private var languageSettings - var isTopBarHidden = false var title: String? var titleAccessibility: String? @@ -85,42 +84,40 @@ struct TopBarContainer: View { var body: some View { VStack(spacing: Dimensions.Padding.ZeroPadding) { - if !isTopBarHidden { - TopBar( - title: title, - titleAccessibility: titleAccessibility, - leftIcon: leftIcon, - leftIconAccessibility: leftIconAccessibility, - leftIconAccessibilityInput: leftIconAccessibilityInput, - onLeftClick: onLeftClick, - - rightPrimaryIcon: rightPrimaryIcon, - rightPrimaryIconAccessibility: rightPrimaryIconAccessibilityLabel, - rightPrimaryIconAccessibilityInput: rightPrimaryIconAccessibilityInput, - isRightPrimaryLink: isRightPrimaryLink, - onRightPrimaryClick: onRightPrimaryClick ?? { - if let url = URL(string: helpUrlString) { - openURL(url) - } - }, - - rightSecondaryIcon: rightSecondaryIcon, - rightSecondaryIconAccessibility: rightSecondaryIconAccessibility, - rightSecondaryIconAccessibilityInput: rightSecondaryIconAccessibilityInput, - onRightSecondaryClick: onRightSecondaryClick ?? { - showSettingsSheet = true - }, - - extraButtonIcon: extraButtonIcon, - extraButtonIconAccessibility: extraButtonIconAccessibility, - extraButtonIconAccessibilityInput: extraButtonIconAccessibilityInput, - showExtraButton: showExtraButton, - extraBadgeCount: extraBadgeCount, - onExtraButtonClick: onExtraButtonClick, - showRightIcons: showRightIcons - ) + TopBar( + title: title, + titleAccessibility: titleAccessibility, + leftIcon: leftIcon, + leftIconAccessibility: leftIconAccessibility, + leftIconAccessibilityInput: leftIconAccessibilityInput, + onLeftClick: onLeftClick, + + rightPrimaryIcon: rightPrimaryIcon, + rightPrimaryIconAccessibility: rightPrimaryIconAccessibilityLabel, + rightPrimaryIconAccessibilityInput: rightPrimaryIconAccessibilityInput, + isRightPrimaryLink: isRightPrimaryLink, + onRightPrimaryClick: onRightPrimaryClick ?? { + if let url = URL(string: helpUrlString) { + openURL(url) + } + }, + + rightSecondaryIcon: rightSecondaryIcon, + rightSecondaryIconAccessibility: rightSecondaryIconAccessibility, + rightSecondaryIconAccessibilityInput: rightSecondaryIconAccessibilityInput, + onRightSecondaryClick: onRightSecondaryClick ?? { + showSettingsSheet = true + }, + + extraButtonIcon: extraButtonIcon, + extraButtonIconAccessibility: extraButtonIconAccessibility, + extraButtonIconAccessibilityInput: extraButtonIconAccessibilityInput, + showExtraButton: showExtraButton, + extraBadgeCount: extraBadgeCount, + onExtraButtonClick: onExtraButtonClick, + showRightIcons: showRightIcons + ) - } content() } .bottomSheet(isPresented: $showSettingsSheet, actions: buildBottomSheetActions()) diff --git a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift index ccb1629d..641cd004 100644 --- a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift +++ b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift @@ -55,6 +55,7 @@ struct ToastOverlay: View { height: Dimensions.Icon.IconSizeXXS ) .foregroundStyle(style.foreground) + .accessibilityHidden(true) Text(verbatim: message) .lineLimit(nil) diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index 1f1c8e68..c39f5e02 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -66,6 +66,10 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { private(set) var isEditButtonShown = false private(set) var shouldShowDatafiles = false + var isContainerUnencrypted: Bool { + !isContainerEncrypted && !isContainerDecrypted + } + init( sharedContainerViewModel: SharedContainerViewModelProtocol, fileOpeningService: FileOpeningServiceProtocol, @@ -159,15 +163,77 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { return } - await cryptoContainer?.addDataFiles(files) - EncryptViewModel.logger().info("Added data files to container") - successMessage = ToastMessage( - key: files.count == 1 ? "File successfully added" : "Files successfully added", - args: [] - ) + do { + try await cryptoContainer?.addDataFiles(files) + + EncryptViewModel.logger().info("Added data files to crypto container") + successMessage = ToastMessage( + key: files.count == 1 ? "File successfully added" : "Files successfully added", + args: [] + ) + } catch { + EncryptViewModel.logger().error( + "Unable to add data files to container: \(String(reflecting: error))" + ) + + guard let containerUrl = containerURL else { + errorMessage = ToastMessage(key: "General error") + return + } + + await handleAddFilesError(error) + } + await loadContainerData(cryptoContainer: cryptoContainer) } + private func handleAddFilesError(_ error: Error) async { + SigningViewModel.logger().error("Unable to add data files to container: \(error.localizedDescription)") + + var totalFileCount = 0 + var failedFileCount = 0 + var duplicateFileCount = 0 + + guard let cryptoError = error as? CryptoError else { + errorMessage = ToastMessage(key: "General error", args: []) + return + } + + switch cryptoError { + case .addingFilesToContainerFailed(let errorDetail): + totalFileCount = Int(errorDetail.userInfo["totalFileCount"] ?? "0") ?? 0 + failedFileCount = Int(errorDetail.userInfo["failedFileCount"] ?? "0") ?? 0 + duplicateFileCount = Int(errorDetail.userInfo["duplicateFileCount"] ?? "0") ?? 0 + + if duplicateFileCount > 1 { + errorMessage = ToastMessage(key: "Multiple documents already exist", args: [String(duplicateFileCount)]) + } else if duplicateFileCount == 1 { + if let fileName = errorDetail.userInfo["fileName"] { + errorMessage = ToastMessage(key: "Document already exists", args: [fileName]) + } else { + errorMessage = ToastMessage(key: errorDetail.message, args: [String(failedFileCount)]) + } + } else { + errorMessage = ToastMessage(key: errorDetail.message, args: [String(failedFileCount)]) + } + + default: + errorMessage = ToastMessage(key: "General error", args: []) + } + + // Update container when at least one file has been added to container + guard totalFileCount > failedFileCount, totalFileCount > duplicateFileCount else { return } + + let successfulFilesCount = totalFileCount - failedFileCount - duplicateFileCount + + successMessage = successfulFilesCount == 1 ? + ToastMessage(key: "Single document added") : + ToastMessage( + key: "Multiple documents added", + args: [String(successfulFilesCount)] + ) + } + private func validateFiles(_ files: [URL]) throws { guard let firstFile = files.first else { throw FileOpeningError.noDataFiles @@ -388,7 +454,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { case .success(let fileURL): return await sivaRepository.isSivaConfirmationNeeded(files: [fileURL]) case .failure: - errorMessage = ToastMessage(key: "Failed to open container", args: [dataFile.lastPathComponent]) + errorMessage = ToastMessage(key: "Failed to open file", args: [dataFile.lastPathComponent]) return false } } diff --git a/RIADigiDoc/ViewModel/Protocols/EncryptViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/EncryptViewModelProtocol.swift index 49216ad5..29a972f0 100644 --- a/RIADigiDoc/ViewModel/Protocols/EncryptViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/EncryptViewModelProtocol.swift @@ -27,7 +27,7 @@ public protocol EncryptViewModelProtocol: Sendable { func loadContainerData(cryptoContainer: CryptoContainerProtocol?) async func createCopyOfContainerForSaving(containerURL: URL?) -> URL? func removeSavedFilesDirectory(savedFilesDirectory: URL?) - func addDataFiles(_ files: [URL]) async + func addDataFiles(_ files: [URL]) async throws func encryptContainer() async @discardableResult func renameContainer(to newName: String) async -> URL? func getDataFileURL(_ dataFile: URL) async -> Result