diff --git a/CHANGELOG.md b/CHANGELOG.md index d475f196..62d9178d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Attach .ovpn when reporting a connectivity issue, stripped of sensitive data. [#13](https://github.com/keeshux/passepartout-ios/pull/13) +- iTunes File Sharing (skythedesu). [#14](https://github.com/keeshux/passepartout-ios/pull/14) ## 1.0 beta 1107 (2018-10-26) diff --git a/Passepartout-iOS/Global/SwiftGen+Storyboards.swift b/Passepartout-iOS/Global/SwiftGen+Storyboards.swift index a101983c..9b6efc0c 100644 --- a/Passepartout-iOS/Global/SwiftGen+Storyboards.swift +++ b/Passepartout-iOS/Global/SwiftGen+Storyboards.swift @@ -90,6 +90,7 @@ internal enum StoryboardSegue { case addProviderSegueIdentifier = "AddProviderSegueIdentifier" case creditsSegueIdentifier = "CreditsSegueIdentifier" case disclaimerSegueIdentifier = "DisclaimerSegueIdentifier" + case importHostSegueIdentifier = "ImportHostSegueIdentifier" case selectProfileSegueIdentifier = "SelectProfileSegueIdentifier" case versionSegueIdentifier = "VersionSegueIdentifier" } diff --git a/Passepartout-iOS/Info.plist b/Passepartout-iOS/Info.plist index 68d21360..a54a6ad4 100644 --- a/Passepartout-iOS/Info.plist +++ b/Passepartout-iOS/Info.plist @@ -43,6 +43,8 @@ LSSupportsOpeningDocumentsInPlace + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift new file mode 100644 index 00000000..6a095b6d --- /dev/null +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -0,0 +1,113 @@ +// +// ImportedHostsViewController.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 10/27/18. +// Copyright (c) 2018 Davide De Rosa. All rights reserved. +// +// https://github.com/keeshux +// +// 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 UIKit +import TunnelKit +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +class ImportedHostsViewController: UITableViewController { + private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs().sorted { $0.normalizedFilename < $1.normalizedFilename } + + private var parsedFile: ParsedFile? + + weak var wizardDelegate: WizardDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + title = L10n.ImportedHosts.title + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard !pendingConfigurationURLs.isEmpty else { + let alert = Macros.alert( + L10n.ImportedHosts.title, + L10n.Organizer.Alerts.AddHost.message + ) + alert.addCancelAction(L10n.Global.ok) { + self.close() + } + present(alert, animated: true, completion: nil) + return + } + } + + // MARK: Actions + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + guard let cell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: cell) else { + return false + } + let url = pendingConfigurationURLs[indexPath.row] + guard let parsedFile = ParsedFile.from(url, withErrorAlertIn: self) else { + if let selectedIP = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: selectedIP, animated: true) + } + return false + } + self.parsedFile = parsedFile + return true + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let wizard = segue.destination as? WizardHostViewController else { + return + } + wizard.parsedFile = parsedFile + wizard.delegate = wizardDelegate + } + + @IBAction private func close() { + dismiss(animated: true, completion: nil) + } +} + +extension ImportedHostsViewController { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return pendingConfigurationURLs.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let url = pendingConfigurationURLs[indexPath.row] + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = url.normalizedFilename + return cell + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + let url = pendingConfigurationURLs[indexPath.row] + try? FileManager.default.removeItem(at: url) + pendingConfigurationURLs.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .top) + } +} diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift index 9b7bb949..f503499d 100644 --- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift @@ -139,6 +139,8 @@ class OrganizerViewController: UITableViewController, TableModelHost { providerVC.availableNames = availableProviderNames ?? [] } vc.delegate = self + } else if let vc = destination as? ImportedHostsViewController { + vc.wizardDelegate = self } } @@ -174,12 +176,7 @@ class OrganizerViewController: UITableViewController, TableModelHost { } private func addNewHost() { - let alert = Macros.alert( - L10n.Organizer.Sections.Hosts.header, - L10n.Organizer.Alerts.AddHost.message - ) - alert.addCancelAction(L10n.Global.ok) - present(alert, animated: true, completion: nil) + perform(segue: StoryboardSegue.Organizer.importHostSegueIdentifier) } private func removeProfile(at indexPath: IndexPath) { diff --git a/Passepartout-iOS/en.lproj/Organizer.storyboard b/Passepartout-iOS/en.lproj/Organizer.storyboard index d078b53a..f4006b7c 100644 --- a/Passepartout-iOS/en.lproj/Organizer.storyboard +++ b/Passepartout-iOS/en.lproj/Organizer.storyboard @@ -161,6 +161,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -175,7 +245,7 @@ - + @@ -223,6 +293,7 @@ + @@ -496,5 +567,6 @@ + diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index eafffd55..bd81e9bc 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */; }; 0EAAD71920E6669A0088754A /* GroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE8DED20C93E4C004C739C /* GroupConstants.swift */; }; 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */; }; + 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */; }; 0EBBE8F221822B4D00106008 /* ConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */; }; 0EBBE8F321822B4D00106008 /* ConnectionService.json in Resources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F121822B4D00106008 /* ConnectionService.json */; }; 0EBBE8F52182361800106008 /* ConnectionService+Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */; }; @@ -169,6 +170,7 @@ 0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = ""; }; 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsedFile+Alerts.swift"; sourceTree = ""; }; 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Search.swift"; sourceTree = ""; }; + 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedHostsViewController.swift; sourceTree = ""; }; 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionServiceTests.swift; sourceTree = ""; }; 0EBBE8F121822B4D00106008 /* ConnectionService.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ConnectionService.json; sourceTree = ""; }; 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionService+Migration.swift"; sourceTree = ""; }; @@ -344,6 +346,7 @@ isa = PBXGroup; children = ( 0EE3BBB1215ED3A900F30952 /* AboutViewController.swift */, + 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */, 0EFD943F215BED8E00529B64 /* LabelViewController.swift */, 0EBE3A78213C4E5400BFA2F5 /* OrganizerViewController.swift */, 0E3DA370215CB5BF00B40FC9 /* VersionViewController.swift */, @@ -852,6 +855,7 @@ 0ED38AEA214054A50004D387 /* OptionViewController.swift in Sources */, 0EFD943E215BE10800529B64 /* IssueReporter.swift in Sources */, 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */, + 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */, 0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */, 0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */, 0EDE8DE720C93945004C739C /* Credentials.swift in Sources */, diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index 67084496..e3a06b30 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -43,7 +43,7 @@ "organizer.cells.about.caption" = "About %@"; "organizer.cells.uninstall.caption" = "Delete VPN profile"; "organizer.alerts.exhausted_providers.message" = "You have created profiles for any available network."; -"organizer.alerts.add_host.message" = "Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile."; +"organizer.alerts.add_host.message" = "Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile.\n\nYou can also import an .ovpn with iTunes File Sharing."; "organizer.alerts.delete_vpn_profile.message" = "Do you really want to delete the VPN profile from the device?"; "account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix."; @@ -58,6 +58,8 @@ "parsed_file.alerts.parsing.message" = "Unable to parse the provided configuration file (%@)."; "parsed_file.alerts.buttons.report" = "Report an issue"; +"imported_hosts.title" = "Imported"; + "service.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile."; "service.sections.general.header" = "General"; "service.sections.vpn.header" = "VPN"; diff --git a/Passepartout/Sources/Model/ConnectionService+Configurations.swift b/Passepartout/Sources/Model/ConnectionService+Configurations.swift index 9a717fc9..d17ce951 100644 --- a/Passepartout/Sources/Model/ConnectionService+Configurations.swift +++ b/Passepartout/Sources/Model/ConnectionService+Configurations.swift @@ -24,6 +24,9 @@ // import Foundation +import SwiftyBeaver + +private let log = SwiftyBeaver.self extension ConnectionService { func save(configurationURL: URL, for profile: ConnectionProfile) throws -> URL { @@ -46,4 +49,14 @@ extension ConnectionService { let contextURL = ConnectionService.ProfileKey(profile).contextURL(in: self) return contextURL.appendingPathComponent(profile.id).appendingPathExtension("ovpn") } + + func pendingConfigurationURLs() -> [URL] { + do { + let list = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) + return list.filter { $0.pathExtension == "ovpn" } + } catch let e { + log.error("Could not list imported configurations: \(e)") + return [] + } + } } diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 29293221..7859a287 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -300,6 +300,11 @@ internal enum L10n { internal static let ok = L10n.tr("Localizable", "global.ok") } + internal enum ImportedHosts { + /// Imported + internal static let title = L10n.tr("Localizable", "imported_hosts.title") + } + internal enum IssueReporter { /// The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.\n\nThe .ovpn configuration file, if any, is attached stripped of any sensitive data.\n\nPlease double check the email attachments if unsure. internal static let message = L10n.tr("Localizable", "issue_reporter.message") @@ -338,7 +343,7 @@ internal enum L10n { internal enum Alerts { internal enum AddHost { - /// Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile. + /// Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile.\n\nYou can also import an .ovpn with iTunes File Sharing. internal static let message = L10n.tr("Localizable", "organizer.alerts.add_host.message") }