2024-09-23 13:02:26 +00:00
|
|
|
//
|
|
|
|
// ProfileEditor.swift
|
|
|
|
// Passepartout
|
|
|
|
//
|
|
|
|
// Created by Davide De Rosa on 2/17/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 <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
|
2024-09-26 21:13:55 +00:00
|
|
|
import AppLibrary
|
2024-09-23 13:02:26 +00:00
|
|
|
import Combine
|
|
|
|
import CommonLibrary
|
|
|
|
import Foundation
|
|
|
|
import PassepartoutKit
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
final class ProfileEditor: ObservableObject {
|
|
|
|
private(set) var id: Profile.ID
|
|
|
|
|
|
|
|
@Published
|
|
|
|
var name: String
|
|
|
|
|
|
|
|
@Published
|
|
|
|
private(set) var modules: [any EditableModule]
|
|
|
|
|
|
|
|
@Published
|
|
|
|
private(set) var activeModulesIds: Set<UUID>
|
|
|
|
|
|
|
|
@Published
|
|
|
|
private var moduleNames: [UUID: String]
|
|
|
|
|
|
|
|
private(set) var removedModules: [UUID: any EditableModule]
|
|
|
|
|
|
|
|
convenience init() {
|
|
|
|
self.init(modules: [])
|
|
|
|
}
|
|
|
|
|
|
|
|
init(modules: [any EditableModule]) {
|
|
|
|
id = UUID()
|
|
|
|
name = ""
|
|
|
|
self.modules = modules
|
|
|
|
activeModulesIds = Set(modules.map(\.id))
|
|
|
|
moduleNames = [:]
|
|
|
|
removedModules = [:]
|
|
|
|
}
|
|
|
|
|
|
|
|
init(profile: Profile) {
|
|
|
|
id = profile.id
|
|
|
|
name = profile.name
|
|
|
|
modules = profile.modulesBuilders
|
|
|
|
activeModulesIds = profile.activeModulesIds
|
|
|
|
moduleNames = profile.moduleNames
|
|
|
|
removedModules = [:]
|
|
|
|
}
|
|
|
|
|
|
|
|
func editProfile(_ profile: Profile) {
|
|
|
|
id = profile.id
|
|
|
|
name = profile.name
|
|
|
|
modules = profile.modulesBuilders
|
|
|
|
activeModulesIds = profile.activeModulesIds
|
|
|
|
moduleNames = profile.moduleNames
|
|
|
|
removedModules = [:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - CRUD
|
|
|
|
|
|
|
|
extension ProfileEditor {
|
|
|
|
var moduleTypes: [ModuleType] {
|
|
|
|
modules
|
|
|
|
.compactMap {
|
|
|
|
$0 as? ModuleTypeProviding
|
|
|
|
}
|
|
|
|
.map(\.moduleType)
|
|
|
|
}
|
|
|
|
|
|
|
|
var availableModuleTypes: [ModuleType] {
|
|
|
|
ModuleType
|
|
|
|
.allCases
|
|
|
|
.filter {
|
|
|
|
// TODO: hide manual OpenVPN/WireGuard until editable
|
|
|
|
$0 != .openVPN && $0 != .wireGuard
|
|
|
|
}
|
|
|
|
.filter {
|
|
|
|
!moduleTypes.contains($0)
|
|
|
|
}
|
|
|
|
.sorted {
|
|
|
|
$0.localizedDescription < $1.localizedDescription
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func displayName(forModuleWithId moduleId: UUID) -> String? {
|
|
|
|
guard let name = moduleNames[moduleId] else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
return !trimmedName.isEmpty ? trimmedName : nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func name(forModuleWithId moduleId: UUID) -> String? {
|
|
|
|
moduleNames[moduleId]
|
|
|
|
}
|
|
|
|
|
|
|
|
func setName(_ name: String, forModuleWithId moduleId: UUID) {
|
|
|
|
moduleNames[moduleId] = name
|
|
|
|
}
|
|
|
|
|
|
|
|
func module(withId moduleId: UUID) -> (any EditableModule)? {
|
|
|
|
modules.first {
|
|
|
|
$0.id == moduleId
|
|
|
|
} ?? removedModules[moduleId]
|
|
|
|
}
|
|
|
|
|
|
|
|
func moveModules(from offsets: IndexSet, to newOffset: Int) {
|
|
|
|
modules.move(fromOffsets: offsets, toOffset: newOffset)
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeModules(at offsets: IndexSet) {
|
|
|
|
offsets.forEach {
|
|
|
|
let module = modules[$0]
|
|
|
|
removedModules[module.id] = module
|
|
|
|
modules.remove(at: $0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func removeModule(withId moduleId: UUID) {
|
|
|
|
guard let index = modules.firstIndex(where: { $0.id == moduleId }) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let module = modules[index]
|
|
|
|
removedModules[module.id] = module
|
|
|
|
modules.remove(at: index)
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveModule(_ module: any EditableModule, activating: Bool) {
|
|
|
|
if let index = modules.firstIndex(where: { $0.id == module.id }) {
|
|
|
|
modules[index] = module
|
|
|
|
} else {
|
|
|
|
modules.append(module)
|
|
|
|
}
|
|
|
|
if activating {
|
|
|
|
activateModule(module)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Active modules
|
|
|
|
|
|
|
|
extension ProfileEditor {
|
|
|
|
func isActiveModule(withId moduleId: UUID) -> Bool {
|
|
|
|
activeModulesIds.contains(moduleId) && !removedModules.keys.contains(moduleId)
|
|
|
|
}
|
|
|
|
|
|
|
|
var activeConnectionModule: (any EditableModule)? {
|
|
|
|
modules.first {
|
|
|
|
isActiveModule(withId: $0.id) && $0.buildsConnectionModule
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var activeModules: [any EditableModule] {
|
|
|
|
modules.filter {
|
|
|
|
activeModulesIds.contains($0.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func activateModule(_ module: any EditableModule) {
|
|
|
|
activeModulesIds.insert(module.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toggleModule(withId moduleId: UUID) {
|
|
|
|
guard let existingModule = module(withId: moduleId) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if isActiveModule(withId: moduleId) {
|
|
|
|
activeModulesIds.remove(moduleId)
|
|
|
|
} else {
|
|
|
|
activateModule(existingModule)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension ProfileEditor {
|
|
|
|
func checkConstraints() throws {
|
|
|
|
if activeConnectionModule == nil,
|
|
|
|
let ipModule = modules.first(where: { activeModulesIds.contains($0.id) && $0 is IPModule.Builder }) {
|
|
|
|
throw AppError.ipModuleRequiresConnection(ipModule)
|
|
|
|
}
|
|
|
|
|
|
|
|
let connectionModules = modules.filter {
|
|
|
|
activeModulesIds.contains($0.id) && $0.buildsConnectionModule
|
|
|
|
}
|
|
|
|
guard connectionModules.count <= 1 else {
|
|
|
|
throw AppError.multipleConnectionModules(connectionModules)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Building
|
|
|
|
|
|
|
|
extension ProfileEditor {
|
|
|
|
func build() throws -> Profile {
|
|
|
|
try checkConstraints()
|
|
|
|
|
|
|
|
var builder = Profile.Builder(id: id)
|
|
|
|
let trimmedName = name.trimmingCharacters(in: .whitespaces)
|
|
|
|
guard !trimmedName.isEmpty else {
|
|
|
|
throw AppError.emptyProfileName
|
|
|
|
}
|
|
|
|
builder.name = trimmedName
|
|
|
|
builder.modules = try modules.compactMap {
|
|
|
|
do {
|
|
|
|
return try $0.tryBuild()
|
|
|
|
} catch {
|
|
|
|
throw AppError.malformedModule($0, error: error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
builder.activeModulesIds = activeModulesIds
|
|
|
|
builder.moduleNames = moduleNames.reduce(into: [:]) {
|
|
|
|
let trimmedName = $1.value.trimmingCharacters(in: .whitespaces)
|
|
|
|
guard !trimmedName.isEmpty else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
$0[$1.key] = trimmedName
|
|
|
|
}
|
|
|
|
let profile = try builder.tryBuild()
|
|
|
|
|
|
|
|
// update local view
|
|
|
|
modules = profile.modulesBuilders
|
|
|
|
removedModules.removeAll()
|
|
|
|
|
|
|
|
return profile
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension Profile {
|
|
|
|
var modulesBuilders: [any EditableModule] {
|
|
|
|
modules.compactMap {
|
|
|
|
guard let buildableModule = $0 as? any BuildableType else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
let builder = buildableModule.builder() as any BuilderType
|
|
|
|
return builder as? any EditableModule
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Saving
|
|
|
|
|
|
|
|
extension ProfileEditor {
|
|
|
|
func save(to profileManager: ProfileManager) async throws {
|
|
|
|
do {
|
|
|
|
let newProfile = try build()
|
|
|
|
try await profileManager.save(newProfile)
|
|
|
|
} catch {
|
|
|
|
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|