// SPDX-License-Identifier: MIT // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. import UIKit import os.log class SettingsTableViewController: UITableViewController { enum SettingsFields { case iosAppVersion case goBackendVersion case exportZipArchive case exportLogFile var localizedUIString: String { switch self { case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS") case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend") case .exportZipArchive: return tr("settingsExportZipButtonTitle") case .exportLogFile: return tr("settingsExportLogFileButtonTitle") } } } let settingsFieldsBySection: [[SettingsFields]] = [ [.iosAppVersion, .goBackendVersion], [.exportZipArchive], [.exportLogFile] ] let tunnelsManager: TunnelsManager? var wireguardCaptionedImage: (view: UIView, size: CGSize)? init(tunnelsManager: TunnelsManager?) { self.tunnelsManager = tunnelsManager super.init(style: .grouped) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = tr("settingsViewTitle") navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) tableView.estimatedRowHeight = 44 tableView.rowHeight = UITableView.automaticDimension tableView.allowsSelection = false tableView.register(KeyValueCell.self) tableView.register(ButtonCell.self) tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf")) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard let logo = tableView.tableFooterView else { return } let bottomPadding = max(tableView.layoutMargins.bottom, 10) let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding) let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height var height = tableView.estimatedRowHeight * 1.5 var width = height * imageAspectRatio let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20) if width > maxWidth { width = maxWidth height = width / imageAspectRatio } let needsReload = height != logo.frame.height logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height) if needsReload { tableView.tableFooterView = logo } } @objc func doneTapped() { dismiss(animated: true, completion: nil) } func exportConfigurationsAsZipFile(sourceView: UIView) { guard let tunnelsManager = tunnelsManager else { return } guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip") _ = FileManager.deleteFile(at: destinationURL) let count = tunnelsManager.numberOfTunnels() let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration } ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in if let error = error { ErrorPresenter.showErrorAlert(error: error, from: self) return } let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService) self?.present(fileExportVC, animated: true, completion: nil) } } func exportLogForLastActivatedTunnel(sourceView: UIView) { guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename let timeStampString = dateFormatter.string(from: Date()) let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") DispatchQueue.global(qos: .userInitiated).async { if FileManager.default.fileExists(atPath: destinationURL.path) { let isDeleted = FileManager.deleteFile(at: destinationURL) if !isDeleted { ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self) return } } guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self) return } let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false DispatchQueue.main.async { guard isWritten else { ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self) return } let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) activityVC.popoverPresentationController?.sourceView = sourceView activityVC.popoverPresentationController?.sourceRect = sourceView.bounds activityVC.completionWithItemsHandler = { _, _, _, _ in // Remove the exported log file after the activity has completed _ = FileManager.deleteFile(at: destinationURL) } self.present(activityVC, animated: true) } } } } extension SettingsTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { return settingsFieldsBySection.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return settingsFieldsBySection[section].count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch section { case 0: return tr("settingsSectionTitleAbout") case 1: return tr("settingsSectionTitleExportConfigurations") case 2: return tr("settingsSectionTitleTunnelLog") default: return nil } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let field = settingsFieldsBySection[indexPath.section][indexPath.row] if field == .iosAppVersion || field == .goBackendVersion { let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.copyableGesture = false cell.key = field.localizedUIString if field == .iosAppVersion { var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { appVersion += " (\(appBuild))" } cell.value = appVersion } else if field == .goBackendVersion { cell.value = WIREGUARD_GO_VERSION } return cell } else if field == .exportZipArchive { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in self?.exportConfigurationsAsZipFile(sourceView: cell.button) } return cell } else { assert(field == .exportLogFile) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in self?.exportLogForLastActivatedTunnel(sourceView: cell.button) } return cell } } }