// // OpenVPNView+Import.swift // Passepartout // // Created by Davide De Rosa on 12/8/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn // // This file is part of Passepartout. // // Passepartout is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Passepartout is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // import CommonLibrary import CommonUtils import PassepartoutKit import SwiftUI extension OpenVPNView { struct ImportModifier: ViewModifier { @Binding var draft: OpenVPNModule.Builder let impl: OpenVPNModule.Implementation? @Binding var isImporting: Bool @ObservedObject var errorHandler: ErrorHandler @State private var importURL: URL? @State private var importPassphrase: String? @State private var requiresPassphrase = false func body(content: Content) -> some View { content .fileImporter( isPresented: $isImporting, allowedContentTypes: [.item], onCompletion: importConfiguration ) .alert( draft.moduleType.localizedDescription, isPresented: $requiresPassphrase, presenting: importURL, actions: { url in SecureField( Strings.Placeholders.secret, text: $importPassphrase ?? "" ) Button(Strings.Alerts.Import.Passphrase.ok) { importConfiguration(from: .success(url)) } Button(Strings.Global.Actions.cancel, role: .cancel) { isImporting = false } }, message: { Text(Strings.Alerts.Import.Passphrase.message($0.lastPathComponent)) } ) } } } private extension OpenVPNView.ImportModifier { func importConfiguration(from result: Result) { do { let url = try result.get() guard url.startAccessingSecurityScopedResource() else { throw AppError.permissionDenied } defer { url.stopAccessingSecurityScopedResource() } importURL = url guard let impl else { fatalError("Requires OpenVPNModule implementation") } guard let parser = impl.importer as? StandardOpenVPNParser else { fatalError("OpenVPNModule importer should be StandardOpenVPNParser") } let parsed = try parser.parsed(fromURL: url, passphrase: importPassphrase) draft.configurationBuilder = parsed.configuration.builder() } catch StandardOpenVPNParserError.encryptionPassphrase, StandardOpenVPNParserError.unableToDecrypt { Task { // XXX: re-present same alert after artificial delay try? await Task.sleep(for: .milliseconds(500)) importPassphrase = nil requiresPassphrase = true } } catch { pp_log(.app, .error, "Unable to import OpenVPN configuration: \(error)") errorHandler.handle( (error as? StandardOpenVPNParserError)?.asPassepartoutError ?? error, title: draft.moduleType.localizedDescription ) } } }