Merge pull request #24 from passepartoutvpn/host-profile-renaming
Host profile renaming
This commit is contained in:
commit
e22cce510c
|
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Dot as a legal character in host profile title. [#22](https://github.com/keeshux/passepartout-ios/issues/22)
|
||||
- Host profiles can now be renamed. [#24](https://github.com/keeshux/passepartout-ios/issues/24)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Incorrect compression warnings when importing host configurations. [#20](https://github.com/keeshux/passepartout-ios/pull/20)
|
||||
|
|
|
@ -36,15 +36,16 @@ class Macros {
|
|||
}
|
||||
|
||||
extension UIAlertController {
|
||||
func addDefaultAction(_ title: String, handler: @escaping () -> Void) {
|
||||
@discardableResult func addDefaultAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
|
||||
let action = UIAlertAction(title: title, style: .default) { (action) in
|
||||
handler()
|
||||
}
|
||||
addAction(action)
|
||||
preferredAction = action
|
||||
return action
|
||||
}
|
||||
|
||||
func addCancelAction(_ title: String, handler: (() -> Void)? = nil) {
|
||||
@discardableResult func addCancelAction(_ title: String, handler: (() -> Void)? = nil) -> UIAlertAction {
|
||||
let action = UIAlertAction(title: title, style: .cancel) { (action) in
|
||||
handler?()
|
||||
}
|
||||
|
@ -52,20 +53,23 @@ extension UIAlertController {
|
|||
if actions.count == 1 {
|
||||
preferredAction = action
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func addAction(_ title: String, handler: @escaping () -> Void) {
|
||||
@discardableResult func addAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
|
||||
let action = UIAlertAction(title: title, style: .default) { (action) in
|
||||
handler()
|
||||
}
|
||||
addAction(action)
|
||||
return action
|
||||
}
|
||||
|
||||
func addDestructiveAction(_ title: String, handler: @escaping () -> Void) {
|
||||
@discardableResult func addDestructiveAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction {
|
||||
let action = UIAlertAction(title: title, style: .destructive) { (action) in
|
||||
handler()
|
||||
}
|
||||
addAction(action)
|
||||
preferredAction = action
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,6 +125,17 @@ extension UIButton {
|
|||
}
|
||||
}
|
||||
|
||||
extension UITextField {
|
||||
func applyProfileId(_ theme: Theme) {
|
||||
placeholder = L10n.Global.Host.TitleInput.placeholder
|
||||
clearButtonMode = .always
|
||||
keyboardType = .asciiCapable
|
||||
returnKeyType = .done
|
||||
autocapitalizationType = .none
|
||||
autocorrectionType = .no
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: status bar is broken
|
||||
extension MFMailComposeViewController {
|
||||
func apply(_ theme: Theme) {
|
||||
|
|
|
@ -70,10 +70,6 @@ class OrganizerViewController: UITableViewController, TableModelHost {
|
|||
|
||||
// MARK: UIViewController
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -94,8 +90,6 @@ class OrganizerViewController: UITableViewController, TableModelHost {
|
|||
}
|
||||
|
||||
service.delegate = self
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(wizardDidCreate(notification:)), name: .WizardDidCreate, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -428,26 +422,9 @@ extension OrganizerViewController {
|
|||
// MARK: -
|
||||
|
||||
extension OrganizerViewController: ConnectionServiceDelegate {
|
||||
func connectionService(didDeactivate profile: ConnectionProfile) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func connectionService(didActivate profile: ConnectionProfile) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension OrganizerViewController {
|
||||
@objc private func wizardDidCreate(notification: Notification) {
|
||||
guard let profile = notification.userInfo?[WizardCreationKey.profile] as? ConnectionProfile,
|
||||
let credentials = notification.userInfo?[WizardCreationKey.credentials] as? Credentials else {
|
||||
|
||||
fatalError("WizardDidCreate notification must post profile and credentials")
|
||||
}
|
||||
|
||||
service.addOrReplaceProfile(profile, credentials: credentials)
|
||||
func connectionService(didAdd profile: ConnectionProfile) {
|
||||
TransientStore.shared.serialize() // add
|
||||
|
||||
|
||||
reloadModel()
|
||||
tableView.reloadData()
|
||||
|
||||
|
@ -467,4 +444,24 @@ extension OrganizerViewController {
|
|||
}
|
||||
perform(segue: StoryboardSegue.Organizer.selectProfileSegueIdentifier, sender: profile)
|
||||
}
|
||||
|
||||
func connectionService(didRename oldProfile: ConnectionProfile, to newProfile: ConnectionProfile) {
|
||||
TransientStore.shared.serialize() // rename
|
||||
|
||||
reloadModel()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func connectionService(didRemoveProfileWithKey key: ConnectionService.ProfileKey) {
|
||||
reloadModel()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func connectionService(didDeactivate profile: ConnectionProfile) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func connectionService(didActivate profile: ConnectionProfile) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ class WizardHostViewController: UITableViewController, TableModelHost {
|
|||
lazy var model: TableModel<SectionType, RowType> = {
|
||||
let model: TableModel<SectionType, RowType> = TableModel()
|
||||
model.add(.meta)
|
||||
model.setFooter(L10n.Global.Host.TitleInput.message, for: .meta)
|
||||
if !existingHosts.isEmpty {
|
||||
model.add(.existing)
|
||||
model.setHeader(L10n.Wizards.Host.Sections.Existing.header, for: .existing)
|
||||
|
@ -130,9 +131,10 @@ class WizardHostViewController: UITableViewController, TableModelHost {
|
|||
guard let profile = createdProfile else {
|
||||
fatalError("No profile created?")
|
||||
}
|
||||
let service = TransientStore.shared.service
|
||||
if let url = parsedFile?.url {
|
||||
do {
|
||||
let savedURL = try TransientStore.shared.service.save(configurationURL: url, for: profile)
|
||||
let savedURL = try service.save(configurationURL: url, for: profile)
|
||||
log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedURL)")
|
||||
|
||||
// can now delete imported file
|
||||
|
@ -141,12 +143,8 @@ class WizardHostViewController: UITableViewController, TableModelHost {
|
|||
log.error("Could not associate .ovpn configuration file to profile: \(e)")
|
||||
}
|
||||
}
|
||||
|
||||
dismiss(animated: true) {
|
||||
NotificationCenter.default.post(name: .WizardDidCreate, object: nil, userInfo: [
|
||||
WizardCreationKey.profile: profile,
|
||||
WizardCreationKey.credentials: credentials
|
||||
])
|
||||
service.addOrReplaceProfile(profile, credentials: credentials)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,6 +186,10 @@ extension WizardHostViewController {
|
|||
return model.header(for: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
return model.footer(for: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return model.count(for: section)
|
||||
}
|
||||
|
@ -199,9 +201,7 @@ extension WizardHostViewController {
|
|||
cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption
|
||||
cell.captionWidth = 100.0
|
||||
cell.allowedCharset = .filename
|
||||
cell.field.placeholder = L10n.Wizards.Host.Cells.TitleInput.placeholder
|
||||
cell.field.clearButtonMode = .always
|
||||
cell.field.returnKeyType = .done
|
||||
cell.field.applyProfileId(Theme.current)
|
||||
cell.delegate = self
|
||||
return cell
|
||||
|
||||
|
|
|
@ -52,11 +52,9 @@ class WizardProviderViewController: UITableViewController {
|
|||
guard let profile = createdProfile else {
|
||||
fatalError("No profile created?")
|
||||
}
|
||||
let service = TransientStore.shared.service
|
||||
dismiss(animated: true) {
|
||||
NotificationCenter.default.post(name: .WizardDidCreate, object: nil, userInfo: [
|
||||
WizardCreationKey.profile: profile,
|
||||
WizardCreationKey.credentials: credentials
|
||||
])
|
||||
service.addOrReplaceProfile(profile, credentials: credentials)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,9 +35,12 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
|
||||
@IBOutlet private weak var labelWelcome: UILabel!
|
||||
|
||||
@IBOutlet private weak var itemEdit: UIBarButtonItem!
|
||||
|
||||
var profile: ConnectionProfile? {
|
||||
didSet {
|
||||
title = profile?.id
|
||||
navigationItem.rightBarButtonItem = (profile?.context == .host) ? itemEdit : nil
|
||||
reloadModel()
|
||||
updateViewsIfNeeded()
|
||||
}
|
||||
|
@ -47,6 +50,8 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
|
||||
private lazy var vpn = GracefulVPN(service: service)
|
||||
|
||||
private weak var pendingRenameAction: UIAlertAction?
|
||||
|
||||
private var lastInfrastructureUpdate: Date?
|
||||
|
||||
// MARK: Table
|
||||
|
@ -179,7 +184,7 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
viewWelcome?.isHidden = (profile != nil)
|
||||
}
|
||||
|
||||
@IBAction private func activate() {
|
||||
private func activateProfile() {
|
||||
service.activateProfile(uncheckedProfile)
|
||||
TransientStore.shared.serialize() // activate
|
||||
|
||||
|
@ -189,6 +194,28 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
vpn.disconnect(completionHandler: nil)
|
||||
}
|
||||
|
||||
@IBAction private func renameProfile() {
|
||||
let alert = Macros.alert(L10n.Service.Alerts.Rename.title, L10n.Global.Host.TitleInput.message)
|
||||
alert.addTextField { (field) in
|
||||
field.text = self.profile?.id
|
||||
field.applyProfileId(Theme.current)
|
||||
field.delegate = self
|
||||
}
|
||||
pendingRenameAction = alert.addDefaultAction(L10n.Global.ok) {
|
||||
guard let newId = alert.textFields?.first?.text else {
|
||||
return
|
||||
}
|
||||
self.doRenameCurrentProfile(to: newId)
|
||||
}
|
||||
alert.addCancelAction(L10n.Global.cancel)
|
||||
pendingRenameAction?.isEnabled = false
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func doRenameCurrentProfile(to newId: String) {
|
||||
profile = service.renameProfile(uncheckedHostProfile, to: newId)
|
||||
}
|
||||
|
||||
private func toggleVpnService(cell: ToggleTableViewCell) {
|
||||
if cell.isOn {
|
||||
guard !service.needsCredentials(for: uncheckedProfile) else {
|
||||
|
@ -713,7 +740,7 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog
|
|||
private func handle(row: RowType, cell: UITableViewCell) -> Bool {
|
||||
switch row {
|
||||
case .useProfile:
|
||||
activate()
|
||||
activateProfile()
|
||||
|
||||
case .reconnect:
|
||||
confirmVpnReconnection()
|
||||
|
@ -931,6 +958,25 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog
|
|||
|
||||
// MARK: -
|
||||
|
||||
extension ServiceViewController: UITextFieldDelegate {
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard string.rangeOfCharacter(from: CharacterSet.filename.inverted) == nil else {
|
||||
return false
|
||||
}
|
||||
if let text = textField.text {
|
||||
let replacement = (text as NSString).replacingCharacters(in: range, with: string)
|
||||
pendingRenameAction?.isEnabled = (replacement != uncheckedProfile.id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ServiceViewController: TrustedNetworksModelDelegate {
|
||||
func trustedNetworksCouldDisconnect(_: TrustedNetworksModel) -> Bool {
|
||||
return (service.preferences.trustPolicy == .disconnect) && (vpn.status != .disconnected)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="AAm-3V-G5F">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="AAm-3V-G5F">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -215,8 +215,15 @@
|
|||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="Vwa-AG-OnN"/>
|
||||
<navigationItem key="navigationItem" id="Vwa-AG-OnN">
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="edit" id="gbN-fY-AoW">
|
||||
<connections>
|
||||
<action selector="renameProfile" destination="BYZ-38-t0r" id="xxl-Nu-VS6"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="itemEdit" destination="gbN-fY-AoW" id="DGl-oS-ZSe"/>
|
||||
<outlet property="labelWelcome" destination="jEt-mV-gjN" id="kaN-fX-eRE"/>
|
||||
<outlet property="tableView" destination="14D-an-pBY" id="qzB-YR-Pss"/>
|
||||
<outlet property="viewWelcome" destination="p0F-IK-RD6" id="pGB-3J-Rwj"/>
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
0E89DFC5213DF7AE00741BA1 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */; };
|
||||
0E89DFC8213E8FC500741BA1 /* SessionProxy+Communication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */; };
|
||||
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; };
|
||||
0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCF213F223400741BA1 /* Wizard.swift */; };
|
||||
0E8D97E221388B52006FB4A0 /* InfrastructurePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */; };
|
||||
0E8D97E521389277006FB4A0 /* pia.json in Resources */ = {isa = PBXBuildFile; fileRef = 0E8D97E421389276006FB4A0 /* pia.json */; };
|
||||
0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */; };
|
||||
|
@ -166,7 +165,6 @@
|
|||
0E89DFC4213DF7AE00741BA1 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProxy+Communication.swift"; sourceTree = "<group>"; };
|
||||
0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardProviderViewController.swift; sourceTree = "<group>"; };
|
||||
0E89DFCF213F223400741BA1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = "<group>"; };
|
||||
0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructurePreset.swift; sourceTree = "<group>"; };
|
||||
0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = "<group>"; };
|
||||
0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsedFile+Alerts.swift"; sourceTree = "<group>"; };
|
||||
|
@ -399,7 +397,6 @@
|
|||
0E2B494120FD16540094784C /* TransientStore.swift */,
|
||||
0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */,
|
||||
0EBE3A8F213C6F4000BFA2F5 /* TrustPolicy.swift */,
|
||||
0E89DFCF213F223400741BA1 /* Wizard.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
|
@ -845,7 +842,6 @@
|
|||
0E05C5D520D1645F006EE732 /* SettingTableViewCell.swift in Sources */,
|
||||
0EBE3A84213C6ADE00BFA2F5 /* InfrastructureFactory.swift in Sources */,
|
||||
0E4FD7DE20D3E49A002221FF /* StandardVPNProvider.swift in Sources */,
|
||||
0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */,
|
||||
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */,
|
||||
0EBE3AA1213DC1A100BFA2F5 /* ConnectionService.swift in Sources */,
|
||||
0E1D72B2213BFFCF00BA1586 /* ProviderPresetViewController.swift in Sources */,
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
"global.ok" = "OK";
|
||||
"global.cancel" = "Cancel";
|
||||
"global.next" = "Next";
|
||||
"global.host.title_input.message" = "Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.).";
|
||||
"global.host.title_input.placeholder" = "My Profile";
|
||||
|
||||
"reddit.title" = "Reddit";
|
||||
"reddit.message" = "Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project.";
|
||||
|
@ -49,7 +51,6 @@
|
|||
"account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix.";
|
||||
|
||||
"wizards.host.cells.title_input.caption" = "Title";
|
||||
"wizards.host.cells.title_input.placeholder" = "My Profile";
|
||||
"wizards.host.sections.existing.header" = "Existing profiles";
|
||||
"wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?";
|
||||
|
||||
|
@ -105,6 +106,7 @@
|
|||
"service.cells.debug_log.caption" = "Debug log";
|
||||
"service.cells.report_issue.caption" = "Report connectivity issue";
|
||||
|
||||
"service.alerts.rename.title" = "Rename profile";
|
||||
"service.alerts.credentials_needed.message" = "You need to enter account credentials first.";
|
||||
"service.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?";
|
||||
"service.alerts.trusted.no_network.message" = "You are not connected to any Wi-Fi network.";
|
||||
|
|
|
@ -43,6 +43,8 @@ protocol ConnectionProfile: class, EndpointDataSource {
|
|||
var requiresCredentials: Bool { get }
|
||||
|
||||
func generate(from configuration: TunnelKitProvider.Configuration, preferences: Preferences) throws -> TunnelKitProvider.Configuration
|
||||
|
||||
func with(newId: String) -> ConnectionProfile
|
||||
}
|
||||
|
||||
extension ConnectionProfile {
|
||||
|
@ -70,4 +72,11 @@ extension ConnectionProfile {
|
|||
}
|
||||
try keychain.set(password: password, for: key, label: key)
|
||||
}
|
||||
|
||||
func removePassword(in keychain: Keychain) {
|
||||
guard let key = passwordKey else {
|
||||
return
|
||||
}
|
||||
keychain.removePassword(for: key)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ extension ConnectionService {
|
|||
return configurationURL(for: ProfileKey(profile))
|
||||
}
|
||||
|
||||
private func targetConfigurationURL(for key: ProfileKey) -> URL {
|
||||
func targetConfigurationURL(for key: ProfileKey) -> URL {
|
||||
return contextURL(key).appendingPathComponent(key.id).appendingPathExtension("ovpn")
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,12 @@ import SwiftyBeaver
|
|||
private let log = SwiftyBeaver.self
|
||||
|
||||
protocol ConnectionServiceDelegate: class {
|
||||
func connectionService(didAdd profile: ConnectionProfile)
|
||||
|
||||
func connectionService(didRename oldProfile: ConnectionProfile, to newProfile: ConnectionProfile)
|
||||
|
||||
func connectionService(didRemoveProfileWithKey key: ConnectionService.ProfileKey)
|
||||
|
||||
func connectionService(didActivate profile: ConnectionProfile)
|
||||
|
||||
func connectionService(didDeactivate profile: ConnectionProfile)
|
||||
|
@ -224,7 +230,7 @@ class ConnectionService: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
func saveProfiles() throws {
|
||||
func saveProfiles() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let fm = FileManager.default
|
||||
|
@ -337,18 +343,69 @@ class ConnectionService: Codable {
|
|||
}
|
||||
|
||||
// serialize immediately
|
||||
try? saveProfiles()
|
||||
saveProfiles()
|
||||
|
||||
delegate?.connectionService(didAdd: profile)
|
||||
}
|
||||
|
||||
func renameProfile(_ key: ProfileKey, to newId: String) -> ConnectionProfile? {
|
||||
precondition(newId != key.id)
|
||||
|
||||
// WARNING: can be a placeholder
|
||||
guard let oldProfile = cache[key] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let fm = FileManager.default
|
||||
let temporaryDelegate = delegate
|
||||
delegate = nil
|
||||
|
||||
// 1. add renamed profile
|
||||
let newProfile = oldProfile.with(newId: newId)
|
||||
let newKey = ProfileKey(newProfile)
|
||||
let sameCredentials = credentials(for: oldProfile)
|
||||
addOrReplaceProfile(newProfile, credentials: sameCredentials)
|
||||
|
||||
// 2. rename .ovpn (if present)
|
||||
if let cfgFrom = configurationURL(for: key) {
|
||||
let cfgTo = targetConfigurationURL(for: newKey)
|
||||
try? fm.removeItem(at: cfgTo)
|
||||
try? fm.moveItem(at: cfgFrom, to: cfgTo)
|
||||
}
|
||||
|
||||
// 3. remove old entry
|
||||
removeProfile(key)
|
||||
|
||||
// 4. replace active key (if active)
|
||||
if key == activeProfileKey {
|
||||
activeProfileKey = newKey
|
||||
}
|
||||
|
||||
// serialize immediately
|
||||
saveProfiles()
|
||||
|
||||
delegate = temporaryDelegate
|
||||
delegate?.connectionService(didRename: oldProfile, to: newProfile)
|
||||
|
||||
return newProfile
|
||||
}
|
||||
|
||||
func renameProfile(_ profile: ConnectionProfile, to id: String) -> ConnectionProfile? {
|
||||
return renameProfile(ProfileKey(profile), to: id)
|
||||
}
|
||||
|
||||
func removeProfile(_ key: ProfileKey) {
|
||||
guard let i = cache.index(forKey: key) else {
|
||||
guard let profile = cache[key] else {
|
||||
return
|
||||
}
|
||||
cache.remove(at: i)
|
||||
cache.removeValue(forKey: key)
|
||||
removeCredentials(for: profile)
|
||||
pendingRemoval.insert(key)
|
||||
if cache.isEmpty {
|
||||
activeProfileKey = nil
|
||||
}
|
||||
|
||||
delegate?.connectionService(didRemoveProfileWithKey: key)
|
||||
}
|
||||
|
||||
func containsProfile(_ key: ProfileKey) -> Bool {
|
||||
|
@ -402,6 +459,10 @@ class ConnectionService: Codable {
|
|||
try profile.setPassword(credentials?.password, in: keychain)
|
||||
}
|
||||
|
||||
func removeCredentials(for profile: ConnectionProfile) {
|
||||
profile.removePassword(in: keychain)
|
||||
}
|
||||
|
||||
// MARK: VPN
|
||||
|
||||
func vpnConfiguration() throws -> NetworkExtensionVPNConfiguration {
|
||||
|
@ -479,7 +540,7 @@ private class PlaceholderConnectionProfile: ConnectionProfile {
|
|||
|
||||
let id: String
|
||||
|
||||
var username: String?
|
||||
var username: String? = nil
|
||||
|
||||
var requiresCredentials: Bool = false
|
||||
|
||||
|
@ -487,6 +548,10 @@ private class PlaceholderConnectionProfile: ConnectionProfile {
|
|||
fatalError("Generating configuration from a PlaceholderConnectionProfile")
|
||||
}
|
||||
|
||||
func with(newId: String) -> ConnectionProfile {
|
||||
return PlaceholderConnectionProfile(ConnectionService.ProfileKey(context, newId))
|
||||
}
|
||||
|
||||
var mainAddress: String = ""
|
||||
|
||||
var addresses: [String] = []
|
||||
|
|
|
@ -65,6 +65,13 @@ class HostConnectionProfile: ConnectionProfile, Codable, Equatable {
|
|||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
func with(newId: String) -> ConnectionProfile {
|
||||
let profile = HostConnectionProfile(title: newId, hostname: hostname)
|
||||
profile.username = username
|
||||
profile.parameters = parameters
|
||||
return profile
|
||||
}
|
||||
}
|
||||
|
||||
extension HostConnectionProfile {
|
||||
|
|
|
@ -139,6 +139,10 @@ class ProviderConnectionProfile: ConnectionProfile, Codable, Equatable {
|
|||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
func with(newId: String) -> ConnectionProfile {
|
||||
fatalError("Cannot rename a ProviderConnectionProfile")
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderConnectionProfile {
|
||||
|
|
|
@ -79,6 +79,6 @@ class TransientStore {
|
|||
|
||||
func serialize() {
|
||||
try? JSONEncoder().encode(service).write(to: TransientStore.serviceURL)
|
||||
try? service.saveProfiles()
|
||||
service.saveProfiles()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// Wizard.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 9/4/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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
static let WizardDidCreate = Notification.Name("WizardDidCreate")
|
||||
}
|
||||
|
||||
enum WizardCreationKey: String {
|
||||
case profile
|
||||
|
||||
case credentials
|
||||
}
|
|
@ -243,6 +243,14 @@ internal enum L10n {
|
|||
internal static let next = L10n.tr("Localizable", "global.next")
|
||||
/// OK
|
||||
internal static let ok = L10n.tr("Localizable", "global.ok")
|
||||
internal enum Host {
|
||||
internal enum TitleInput {
|
||||
/// Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.).
|
||||
internal static let message = L10n.tr("Localizable", "global.host.title_input.message")
|
||||
/// My Profile
|
||||
internal static let placeholder = L10n.tr("Localizable", "global.host.title_input.placeholder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum ImportedHosts {
|
||||
|
@ -415,6 +423,10 @@ internal enum L10n {
|
|||
/// Do you want to reconnect to the VPN?
|
||||
internal static let message = L10n.tr("Localizable", "service.alerts.reconnect_vpn.message")
|
||||
}
|
||||
internal enum Rename {
|
||||
/// Rename profile
|
||||
internal static let title = L10n.tr("Localizable", "service.alerts.rename.title")
|
||||
}
|
||||
internal enum TestConnectivity {
|
||||
/// Connectivity
|
||||
internal static let title = L10n.tr("Localizable", "service.alerts.test_connectivity.title")
|
||||
|
@ -655,8 +667,6 @@ internal enum L10n {
|
|||
internal enum TitleInput {
|
||||
/// Title
|
||||
internal static let caption = L10n.tr("Localizable", "wizards.host.cells.title_input.caption")
|
||||
/// My Profile
|
||||
internal static let placeholder = L10n.tr("Localizable", "wizards.host.cells.title_input.placeholder")
|
||||
}
|
||||
}
|
||||
internal enum Sections {
|
||||
|
|
|
@ -206,7 +206,7 @@ extension CharacterSet {
|
|||
static let filename: CharacterSet = {
|
||||
var chars: CharacterSet = .decimalDigits
|
||||
let english = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
let symbols = "-_"
|
||||
let symbols = "-_."
|
||||
chars.formUnion(CharacterSet(charactersIn: english))
|
||||
chars.formUnion(CharacterSet(charactersIn: english.lowercased()))
|
||||
chars.formUnion(CharacterSet(charactersIn: symbols))
|
||||
|
|
Loading…
Reference in New Issue