From 68077bf04cf2217282581d1940ce28ce12cfff12 Mon Sep 17 00:00:00 2001 From: Mingshen Sun Date: Sun, 17 Jan 2021 19:49:05 -0800 Subject: [PATCH] Rewrite PasswordViewController --- pass.xcodeproj/project.pbxproj | 42 +- pass/Base.lproj/Main.storyboard | 11 +- .../AddPasswordTableViewController.swift | 5 + .../PasswordNavigationViewController.swift | 437 +++++++++++ .../Controllers/PasswordsViewController.swift | 709 ------------------ .../Services/PasswordDecryptor.swift | 9 +- pass/Services/PasswordEncryptor.swift | 28 + pass/Services/PasswordManager.swift | 37 + .../PasswordNavigationDataSource.swift | 117 +++ passKit/Models/PasswordStore.swift | 10 + 10 files changed, 676 insertions(+), 729 deletions(-) create mode 100644 pass/Controllers/PasswordNavigationViewController.swift delete mode 100644 pass/Controllers/PasswordsViewController.swift rename {passAutoFillExtension => pass}/Services/PasswordDecryptor.swift (92%) create mode 100644 pass/Services/PasswordEncryptor.swift create mode 100644 pass/Services/PasswordManager.swift create mode 100644 pass/Services/PasswordNavigationDataSource.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 4f5dc38..23d436e 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -110,7 +110,6 @@ 9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; }; 9A58664825AAAB7E006719C2 /* SearchPassword.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A5865EF25AA944B006719C2 /* SearchPassword.storyboard */; }; 9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58665025AADB76006719C2 /* CredentialProvider.swift */; }; - 9A58665825AADC49006719C2 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */; }; 9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; }; 9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; }; 9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; }; @@ -118,9 +117,15 @@ 9A8A8387402FCCCECB1232A4 /* Pods_passKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */; }; 9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */; }; 9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; }; - 9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */; }; 9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */; }; 9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; }; + 9AFC87D325B39FF3008D6060 /* PasswordNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */; }; + 9AFC87E225B3B5C6008D6060 /* PasswordNavigationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */; }; + 9AFC87F025B514AD008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; }; + 9AFC87F825B51742008D6060 /* PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87F725B51742008D6060 /* PasswordManager.swift */; }; + 9AFC880025B51EC3008D6060 /* PasswordEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */; }; + 9AFC882725B53BF4008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; }; + 9AFC882E25B53BF5008D6060 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */; }; A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; }; A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */; }; A2367B9C1EEFE2E500C8FE8B /* SwiftyUserDefaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA049951E3357E000522E8F /* SwiftyUserDefaults.framework */; }; @@ -166,7 +171,6 @@ DC3E64E61E656F11009A83DE /* CommitLogsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */; }; DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; }; DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; }; - DC5734AE1E439AD400D09270 /* PasswordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */; }; DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */; }; DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */; }; DC917BD71E2E8231000FDF54 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC917BD61E2E8231000FDF54 /* AppDelegate.swift */; }; @@ -386,9 +390,13 @@ 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsTableDataSource.swift; sourceTree = ""; }; 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSelectionDelegate.swift; sourceTree = ""; }; 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = ""; }; - 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = ""; }; 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProvider.swift; sourceTree = ""; }; 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = ""; }; + 9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordNavigationViewController.swift; sourceTree = ""; }; + 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordNavigationDataSource.swift; sourceTree = ""; }; + 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = ""; }; + 9AFC87F725B51742008D6060 /* PasswordManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManager.swift; sourceTree = ""; }; + 9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEncryptor.swift; sourceTree = ""; }; A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = ""; }; A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A2367B9F1EF0387000C8FE8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -441,7 +449,6 @@ DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = ""; }; DC547D572040664E838F3DB3 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = ""; }; - DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordsViewController.swift; sourceTree = ""; }; DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = ""; }; DC8963BF1E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSHKeyUrlImportTableViewController.swift; sourceTree = ""; }; DC917BD31E2E8231000FDF54 /* Pass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pass.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -725,7 +732,6 @@ isa = PBXGroup; children = ( 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */, - 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */, 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */, ); path = Services; @@ -739,6 +745,17 @@ path = Protocols; sourceTree = ""; }; + 9AFC87E025B3B556008D6060 /* Services */ = { + isa = PBXGroup; + children = ( + 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */, + 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */, + 9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */, + 9AFC87F725B51742008D6060 /* PasswordManager.swift */, + ); + path = Services; + sourceTree = ""; + }; A2168A801EFD431A005EA873 /* Controllers */ = { isa = PBXGroup; children = ( @@ -897,7 +914,7 @@ DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */, DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */, DCFB77A81E502FF6008DE471 /* PasswordEditorTableViewController.swift */, - DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */, + 9AFC87D225B39FF2008D6060 /* PasswordNavigationViewController.swift */, DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */, 302269B223E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift */, 3066AD6723EE0D6500F65535 /* PGPKeyImporter.swift */, @@ -983,6 +1000,7 @@ DC917BD51E2E8231000FDF54 /* pass */ = { isa = PBXGroup; children = ( + 9AFC87E025B3B556008D6060 /* Services */, DC19400C1E4B39400077E0A3 /* Controllers */, DC19400E1E4B3A610077E0A3 /* Helpers */, 3005F35224B13BF3000519B5 /* Models */, @@ -1568,13 +1586,13 @@ buildActionMask = 2147483647; files = ( 9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */, + 9AFC882E25B53BF5008D6060 /* PasswordDecryptor.swift in Sources */, 9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */, 30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */, 9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */, 9A5D06F525A56F0E00FA59D4 /* PasswordTableViewCell.swift in Sources */, 9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */, 30697C5321F63E0B0064FCAC /* PasscodeExtensionDisplay.swift in Sources */, - 9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1657,12 +1675,12 @@ buildActionMask = 2147483647; files = ( 9A5D070225A5769A00FA59D4 /* PasswordTableViewCell.swift in Sources */, - 9A58665825AADC49006719C2 /* PasswordDecryptor.swift in Sources */, 9A58665125AADB76006719C2 /* CredentialProvider.swift in Sources */, 9A58662225AAAA3A006719C2 /* PasswordsViewController.swift in Sources */, 9A58662925AAAA79006719C2 /* PasswordSelectionDelegate.swift in Sources */, 30697C5021F63D7F0064FCAC /* ExtensionConstants.swift in Sources */, 9A58661425AAA4C1006719C2 /* PasscodeExtensionDisplay.swift in Sources */, + 9AFC882725B53BF4008D6060 /* PasswordDecryptor.swift in Sources */, 30697C4B21F63D460064FCAC /* ExtensionViewController.swift in Sources */, 9A58661B25AAA946006719C2 /* PasswordsTableDataSource.swift in Sources */, ); @@ -1687,10 +1705,13 @@ 306D970E24091CDD006C0E2E /* SwitchTableViewCell.swift in Sources */, A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */, DC8963C01E38EEB900828B09 /* SSHKeyUrlImportTableViewController.swift in Sources */, + 9AFC87F025B514AD008D6060 /* PasswordDecryptor.swift in Sources */, 3066AD6823EE0D6500F65535 /* PGPKeyImporter.swift in Sources */, + 9AFC87E225B3B5C6008D6060 /* PasswordNavigationDataSource.swift in Sources */, 30650E7123F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift in Sources */, DC193FFA1E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift in Sources */, DCFB77AB1E503729008DE471 /* ContentProvider.swift in Sources */, + 9AFC880025B51EC3008D6060 /* PasswordEncryptor.swift in Sources */, DCA0499C1E3362F400522E8F /* PGPKeyUrlImportTableViewController.swift in Sources */, DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */, DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */, @@ -1704,9 +1725,10 @@ DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */, 30C25DD821F4834D00BB27BB /* UICodeHighlightingLabel.swift in Sources */, 9A5D06EE25A56F0800FA59D4 /* PasswordTableViewCell.swift in Sources */, + 9AFC87F825B51742008D6060 /* PasswordManager.swift in Sources */, DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */, 30C25DD721F4834D00BB27BB /* UILocalizedLabel.swift in Sources */, - DC5734AE1E439AD400D09270 /* PasswordsViewController.swift in Sources */, + 9AFC87D325B39FF3008D6060 /* PasswordNavigationViewController.swift in Sources */, 300713C52219D54100F553AC /* AutoCellHeightUITableViewController.swift in Sources */, 302269B323E634B000F843A3 /* PGPKeyFIleImportTableViewController.swift in Sources */, DCD3C65E1EFB9BB400CBE842 /* SettingsSplitViewController.swift in Sources */, diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index bd3d892..b809b42 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -11,7 +11,7 @@ - + @@ -56,11 +56,10 @@ - - + @@ -737,8 +736,8 @@ - - + + @@ -1964,7 +1963,7 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f - + diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index 86948c8..163f18d 100644 --- a/pass/Controllers/AddPasswordTableViewController.swift +++ b/pass/Controllers/AddPasswordTableViewController.swift @@ -35,6 +35,11 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { return true } + @IBAction + private func cancel(_ sender: Any) { + navigationController?.popViewController(animated: true) + } + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) if segue.identifier == "saveAddPasswordSegue" { diff --git a/pass/Controllers/PasswordNavigationViewController.swift b/pass/Controllers/PasswordNavigationViewController.swift new file mode 100644 index 0000000..1d0119f --- /dev/null +++ b/pass/Controllers/PasswordNavigationViewController.swift @@ -0,0 +1,437 @@ +import UIKit +import passKit +import SVProgressHUD + +extension UIStoryboard { + static var passwordNavigationViewController: PasswordNavigationViewController { + UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "passwordNavigation") as! PasswordNavigationViewController + } +} + +class PasswordNavigationViewController: UIViewController { + @IBOutlet var tableView: UITableView! + + var dataSource: PasswordNavigationDataSource? + var parentPasswordEntity: PasswordEntity? + + var viewingUnsyncedPasswords = false + var tapTabBarTime: TimeInterval = 0 + + lazy var passwordManager = PasswordManager(viewController: self) + + lazy var searchController: UISearchController = { + let uiSearchController = UISearchController(searchResultsController: nil) + uiSearchController.searchBar.isTranslucent = true + uiSearchController.obscuresBackgroundDuringPresentation = false + uiSearchController.searchBar.sizeToFit() + if #available(iOS 13.0, *) { + uiSearchController.searchBar.searchTextField.clearButtonMode = .whileEditing + } + return uiSearchController + }() + + lazy var searchBar: UISearchBar = { + self.searchController.searchBar + }() + + lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) + return refreshControl + }() + + lazy var addPasswordUIBarButtonItem: UIBarButtonItem = { + var addPasswordUIBarButtonItem = UIBarButtonItem() + if #available(iOS 13.0, *) { + let addPasswordButton = UIButton(type: .system) + let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(weight: .regular)) + addPasswordButton.setImage(plusImage, for: .normal) + addPasswordButton.addTarget(self, action: #selector(self.addPasswordAction(_:)), for: .touchDown) + addPasswordUIBarButtonItem.customView = addPasswordButton + } else { + addPasswordUIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addPasswordAction(_:))) + } + return addPasswordUIBarButtonItem + }() + + lazy var gestureRecognizer: UILongPressGestureRecognizer = { + let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction)) + recognizer.minimumPressDuration = 0.6 + return recognizer + }() + + override func viewDidLoad() { + super.viewDidLoad() + searchBar.delegate = self + configureTableView(in: parentPasswordEntity) + configureNotification() + configureSearchBar() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + configureNavigationItem() + configureTabBarItem() + configureNavigationBar() + } + + private func configureSearchBar() { + if Defaults.isShowFolderOn, !isRootViewController() { + searchBar.scopeButtonTitles = SearchBarScope.allCases.map(\.localizedName) + } else { + searchBar.scopeButtonTitles = nil + } + } + + private func configureTableView(in dir: PasswordEntity?) { + let passwordTableEntries = fetchPasswordTableEntries(in: dir) + dataSource = PasswordNavigationDataSource(entries: passwordTableEntries) + tableView.addGestureRecognizer(gestureRecognizer) + tableView.dataSource = dataSource + tableView.delegate = self + let atribbutedTitle = "LastSynced".localize() + ": \(PasswordStore.shared.lastSyncedTimeString)" + refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle) + tableView.refreshControl = refreshControl + } + + private func configureTabBarItem() { + guard let tabBarItem = navigationController?.tabBarItem else { + return + } + + let numberOfLocalCommits = PasswordStore.shared.numberOfLocalCommits + if numberOfLocalCommits != 0 { + tabBarItem.badgeValue = "\(numberOfLocalCommits)" + } else { + tabBarItem.badgeValue = nil + } + } + + private func fetchPasswordTableEntries(in dir: PasswordEntity? = nil) -> [PasswordTableEntry] { + if Defaults.isShowFolderOn { + return PasswordStore.shared.fetchPasswordEntityCoreData(parent: dir).compactMap { PasswordTableEntry($0) } + } else { + return PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false).compactMap { PasswordTableEntry($0) } + } + } + + private func configureNavigationItem() { + if isRootViewController() { + navigationItem.largeTitleDisplayMode = .automatic + navigationItem.title = "PasswordStore".localize() + } else { + navigationItem.largeTitleDisplayMode = .never + navigationItem.title = parentPasswordEntity?.getName() + } + if viewingUnsyncedPasswords { + navigationItem.title = "Unsynced" + } + + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.rightBarButtonItem = addPasswordUIBarButtonItem + navigationItem.searchController = searchController + } + + private func configureNavigationBar() { + guard let navigationBar = navigationController?.navigationBar else { + return + } + + guard PasswordStore.shared.numberOfLocalCommits != 0 else { + return + } + + let tapNavigationBarGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar)) + tapNavigationBarGestureRecognizer.cancelsTouchesInView = false + navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer) + } + + private func isRootViewController () -> Bool { + navigationController?.viewControllers.count == 1 + } + + private func configureNotification() { + let notificationCenter = NotificationCenter.default + // Reset the data table if some password (maybe another one) has been updated. + notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordStoreUpdated, object: nil) + // Reset the data table if the disaply settings have been changed. + notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordDisplaySettingChanged, object: nil) + // Search entrypoint for home screen quick action. + notificationCenter.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil) + // A Siri shortcut can change the state of the app in the background. Hence, reload when opening the app. + notificationCenter.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: UIApplication.willEnterForegroundNotification, object: nil) + } + + @objc + func addPasswordAction(_: Any?) { + if shouldPerformSegue(withIdentifier: "addPasswordSegue", sender: self) { + performSegue(withIdentifier: "addPasswordSegue", sender: self) + } + } + + @objc + func didTapNavigationBar(_ sender: UITapGestureRecognizer) { + let location = sender.location(in: navigationController?.navigationBar) + let hitView = navigationController?.navigationBar.hitTest(location, with: nil) + guard !(hitView is UIControl) else { + return + } + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + let allAction = UIAlertAction(title: "All Passwords", style: .default) { _ in + self.configureTableView(in: self.parentPasswordEntity) + self.tableView.reloadData() + self.viewingUnsyncedPasswords = false + self.configureNavigationItem() + } + let unsyncedAction = UIAlertAction(title: "Unsynced Passwords", style: .default) { _ in + self.dataSource?.showUnsyncedTableEntries() + self.tableView.reloadData() + self.viewingUnsyncedPasswords = true + self.configureNavigationItem() + } + let cancelAction = UIAlertAction.cancel() + + alertController.addAction(allAction) + alertController.addAction(unsyncedAction) + alertController.addAction(cancelAction) + + present(alertController, animated: true) + } + + @objc + func longPressAction(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == UIGestureRecognizer.State.began { + let touchPoint = gesture.location(in: tableView) + if let indexPath = tableView.indexPathForRow(at: touchPoint) { + guard let dataSource = dataSource else { + return + } + let passwordTableEntry = dataSource.getPasswordTableEntry(at: indexPath) + passwordManager.providePasswordPasteboard(with: passwordTableEntry.passwordEntity.getPath()) + } + } + } + + @objc + func actOnSearchNotification() { + searchBar.becomeFirstResponder() + } + + @objc + func handleRefreshControl() { + syncPasswords() + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + } + } +} + +extension PasswordNavigationViewController: UITableViewDelegate { + func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let dataSource = dataSource else { + return + } + let entry = dataSource.getPasswordTableEntry(at: indexPath) + if entry.isDir { + showDir(in: entry) + } else { + showPasswordDetail(at: entry) + } + searchController.isActive = false + } + + func showDir(in entry: PasswordTableEntry) { + let passwordNavigationViewController = UIStoryboard.passwordNavigationViewController + passwordNavigationViewController.parentPasswordEntity = entry.passwordEntity + navigationController?.pushViewController(passwordNavigationViewController, animated: true) + } + + func showPasswordDetail(at entry: PasswordTableEntry) { + let segueIdentifier = "showPasswordDetail" + let sender = entry.passwordEntity + if shouldPerformSegue(withIdentifier: segueIdentifier, sender: sender) { + performSegue(withIdentifier: segueIdentifier, sender: sender) + } + } + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + UITableView.automaticDimension + } +} + +extension PasswordNavigationViewController { + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPasswordDetail" { + if let viewController = segue.destination as? PasswordDetailTableViewController { + viewController.passwordEntity = sender as? PasswordEntity + } + } else if segue.identifier == "addPasswordSegue" { + if let navController = segue.destination as? UINavigationController, + let viewController = navController.topViewController as? AddPasswordTableViewController, + let path = parentPasswordEntity?.getPath() { + viewController.defaultDirPrefix = "\(path)/" + } + } + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "showPasswordDetail" { + guard PGPAgent.shared.isPrepared else { + Utils.alert(title: "CannotShowPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self) + return false + } + } else if identifier == "addPasswordSegue" { + guard PGPAgent.shared.isPrepared && PasswordStore.shared.storeRepository != nil else { + Utils.alert(title: "CannotAddPassword".localize(), message: "MakeSurePgpAndGitProperlySet.".localize(), controller: self) + return false + } + } + return true + } + + @IBAction + private func cancelAddPassword(segue _: UIStoryboardSegue) {} + + @IBAction + private func saveAddPassword(segue: UIStoryboardSegue) { + if let controller = segue.source as? AddPasswordTableViewController { + passwordManager.addPassword(with: controller.password!) + } + } +} + +extension PasswordNavigationViewController { + @objc + func actOnReloadTableViewRelatedNotification() { + DispatchQueue.main.async { + self.navigationController?.popToRootViewController(animated: true) + self.resetViews() + } + } + + func resetViews() { + configureTableView(in: parentPasswordEntity) + tableView.reloadData() + configureNavigationItem() + configureTabBarItem() + configureNavigationBar() + } +} + +extension PasswordNavigationViewController: UISearchBarDelegate { + func search(matching text: String) { + dataSource?.showTableEntries(matching: text) + tableView.reloadData() + } + + func activateSearch(_ selectedScope: Int?) { + if selectedScope == SearchBarScope.all.rawValue { + configureTableView(in: nil) + } else { + configureTableView(in: parentPasswordEntity) + } + dataSource?.isSearchActive = true + tableView.reloadData() + } + + func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + activateSearch(selectedScope) + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + if Defaults.isShowFolderOn, Defaults.searchDefault == .all { + searchBar.selectedScopeButtonIndex = SearchBarScope.all.rawValue + } else { + searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue + } + activateSearch(searchBar.selectedScopeButtonIndex) + } + + func searchBar(_: UISearchBar, textDidChange searchText: String) { + search(matching: searchText) + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_: UISearchBar) { + cancelSearch() + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + cancelSearch() + } + + func cancelSearch() { + configureTableView(in: parentPasswordEntity) + dataSource?.isSearchActive = false + tableView.reloadData() + } +} + +extension PasswordNavigationViewController: PasswordAlertPresenter { + private func syncPasswords() { + guard PasswordStore.shared.repositoryExists() else { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { + Utils.alert(title: "Error".localize(), message: "NoPasswordStore.".localize(), controller: self, completion: nil) + } + return + } + SVProgressHUD.setDefaultMaskType(.black) + SVProgressHUD.setDefaultStyle(.light) + SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize()) + let keychain = AppKeychain.shared + var gitCredential: GitCredential { + GitCredential.from( + authenticationMethod: Defaults.gitAuthenticationMethod, + userName: Defaults.gitUsername, + keyStore: keychain + ) + } + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + do { + let pullOptions = gitCredential.getCredentialOptions(passwordProvider: self.present) + try PasswordStore.shared.pullRepository(options: pullOptions) { git_transfer_progress, _ in + DispatchQueue.main.async { + SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects) / Float(git_transfer_progress.pointee.total_objects), status: "PullingFromRemoteRepository".localize()) + } + } + if PasswordStore.shared.numberOfLocalCommits > 0 { + let pushOptions = gitCredential.getCredentialOptions(passwordProvider: self.present) + try PasswordStore.shared.pushRepository(options: pushOptions) { current, total, _, _ in + DispatchQueue.main.async { + SVProgressHUD.showProgress(Float(current) / Float(total), status: "PushingToRemoteRepository".localize()) + } + } + } + DispatchQueue.main.async { + SVProgressHUD.showSuccess(withStatus: "Done".localize()) + SVProgressHUD.dismiss(withDelay: 1) + } + } catch { + gitCredential.delete() + DispatchQueue.main.async { + SVProgressHUD.dismiss() + let error = error as NSError + var message = error.localizedDescription + if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError { + message = message | "UnderlyingError".localize(underlyingError.localizedDescription) + if underlyingError.localizedDescription.contains("WrongPassphrase".localize()) { + message = message | "RecoverySuggestion.".localize() + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { + Utils.alert(title: "Error".localize(), message: message, controller: self, completion: nil) + } + } + } + } + } +} diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift deleted file mode 100644 index 4484b9f..0000000 --- a/pass/Controllers/PasswordsViewController.swift +++ /dev/null @@ -1,709 +0,0 @@ -// -// PasswordsViewController.swift -// pass -// -// Created by Mingshen Sun on 3/2/2017. -// Copyright © 2017 Bob Sun. All rights reserved. -// - -import passKit -import SVProgressHUD -import UIKit - -class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate, UISearchBarDelegate, PasswordAlertPresenter { - // Arbitrary threshold to decide whether to show folders or not for only a few entries. - private static let hideSectionHeaderThreshold = 6 - - private var passwordsTableEntries: [PasswordTableEntry] = [] - private var passwordsTableAllEntries: [PasswordTableEntry] = [] - private var parentPasswordEntity: PasswordEntity? - private let passwordStore = PasswordStore.shared - private let keychain = AppKeychain.shared - private var gitCredential: GitCredential { - GitCredential.from( - authenticationMethod: Defaults.gitAuthenticationMethod, - userName: Defaults.gitUsername, - keyStore: keychain - ) - } - - private var tapTabBarTime: TimeInterval = 0 - private var tapNavigationBarGestureRecognizer: UITapGestureRecognizer! - - private var sections = [(title: String, entries: [PasswordTableEntry])]() - - private enum PasswordLabel { - case all - case unsynced - } - - private lazy var searchController: UISearchController = { - let uiSearchController = UISearchController(searchResultsController: nil) - uiSearchController.searchResultsUpdater = self - uiSearchController.dimsBackgroundDuringPresentation = false - uiSearchController.searchBar.isTranslucent = false - uiSearchController.searchBar.sizeToFit() - return uiSearchController - }() - - private lazy var syncControl: UIRefreshControl = { - let syncControl = UIRefreshControl() - syncControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControl.Event.valueChanged) - return syncControl - }() - - private lazy var searchBarView: UIView? = { - guard #available(iOS 11, *) else { - let uiView = UIView(frame: CGRect(x: 0, y: 64, width: self.view.bounds.width, height: 44)) - uiView.addSubview(self.searchController.searchBar) - return uiView - } - return nil - }() - - private lazy var backUIBarButtonItem: UIBarButtonItem = { - let backUIButton = UIButton(type: .system) - if #available(iOS 13.0, *) { - let leftImage = UIImage(systemName: "chevron.left", withConfiguration: UIImage.SymbolConfiguration(weight: .bold)) - backUIButton.setImage(leftImage, for: .normal) - backUIButton.setTitle("Back".localize(), for: .normal) - let padding = CGFloat(integerLiteral: 3) - backUIButton.contentEdgeInsets.right += padding - backUIButton.titleEdgeInsets.left = padding - backUIButton.titleEdgeInsets.right = -padding - } else { - backUIButton.setTitle("Back".localize(), for: .normal) - } - backUIButton.addTarget(self, action: #selector(self.backAction(_:)), for: .touchDown) - let backUIBarButtonItem = UIBarButtonItem(customView: backUIButton) - return backUIBarButtonItem - }() - - private lazy var addPasswordUIBarButtonItem: UIBarButtonItem = { - var addPasswordUIBarButtonItem = UIBarButtonItem() - if #available(iOS 13.0, *) { - let addPasswordButton = UIButton(type: .system) - let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(weight: .regular)) - addPasswordButton.setImage(plusImage, for: .normal) - addPasswordButton.addTarget(self, action: #selector(self.addPasswordAction(_:)), for: .touchDown) - addPasswordUIBarButtonItem.customView = addPasswordButton - } else { - addPasswordUIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addPasswordAction(_:))) - } - return addPasswordUIBarButtonItem - }() - - private lazy var transitionFromRight: CATransition = { - let transition = CATransition() - transition.type = CATransitionType.push - transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - transition.fillMode = CAMediaTimingFillMode.forwards - transition.duration = 0.25 - transition.subtype = CATransitionSubtype.fromRight - transition.delegate = self - return transition - }() - - private lazy var transitionFromLeft: CATransition = { - let transition = CATransition() - transition.type = CATransitionType.push - transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - transition.fillMode = CAMediaTimingFillMode.forwards - transition.duration = 0.25 - transition.subtype = CATransitionSubtype.fromLeft - transition.delegate = self - return transition - }() - - @IBOutlet var tableView: UITableView! - - private func initPasswordsTableEntries(parent: PasswordEntity?) { - let passwordAllEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false) - passwordsTableAllEntries = passwordAllEntities.compactMap { - PasswordTableEntry($0) - } - - let passwordEntities = Defaults.isShowFolderOn ? - passwordStore.fetchPasswordEntityCoreData(parent: parent) : - passwordAllEntities - passwordsTableEntries = passwordEntities.compactMap { - PasswordTableEntry($0) - } - - parentPasswordEntity = parent - } - - @IBAction - private func cancelAddPassword(segue _: UIStoryboardSegue) {} - - @IBAction - private func saveAddPassword(segue: UIStoryboardSegue) { - if let controller = segue.source as? AddPasswordTableViewController { - addPassword(password: controller.password!) - } - } - - private func addPassword(password: Password, keyID: String? = nil) { - SVProgressHUD.setDefaultMaskType(.black) - SVProgressHUD.setDefaultStyle(.light) - SVProgressHUD.show(withStatus: "Saving".localize()) - DispatchQueue.global(qos: .userInitiated).async { - do { - _ = try self.passwordStore.add(password: password, keyID: keyID) - DispatchQueue.main.async { - // will trigger reloadTableView() by a notification - SVProgressHUD.showSuccess(withStatus: "Done".localize()) - SVProgressHUD.dismiss(withDelay: 1) - } - } catch let AppError.pgpPublicKeyNotFound(keyID: key) { - DispatchQueue.main.async { - // alert: cancel or select keys - SVProgressHUD.dismiss() - let alert = UIAlertController(title: "Cannot Encrypt Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction.cancelAndPopView(controller: self)) - let selectKey = UIAlertAction.selectKey(controller: self) { action in - self.addPassword(password: password, keyID: action.title) - } - alert.addAction(selectKey) - - self.present(alert, animated: true, completion: nil) - } - return - } catch { - DispatchQueue.main.async { - Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil) - } - } - } - } - - private func syncPasswords() { - guard passwordStore.repositoryExists() else { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { - Utils.alert(title: "Error".localize(), message: "NoPasswordStore.".localize(), controller: self, completion: nil) - } - return - } - SVProgressHUD.setDefaultMaskType(.black) - SVProgressHUD.setDefaultStyle(.light) - SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize()) - - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - do { - let pullOptions = self.gitCredential.getCredentialOptions(passwordProvider: self.present) - try self.passwordStore.pullRepository(options: pullOptions) { git_transfer_progress, _ in - DispatchQueue.main.async { - SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects) / Float(git_transfer_progress.pointee.total_objects), status: "PullingFromRemoteRepository".localize()) - } - } - if self.passwordStore.numberOfLocalCommits > 0 { - let pushOptions = self.gitCredential.getCredentialOptions(passwordProvider: self.present) - try self.passwordStore.pushRepository(options: pushOptions) { current, total, _, _ in - DispatchQueue.main.async { - SVProgressHUD.showProgress(Float(current) / Float(total), status: "PushingToRemoteRepository".localize()) - } - } - } - DispatchQueue.main.async { - self.reloadTableView(parent: nil) - SVProgressHUD.showSuccess(withStatus: "Done".localize()) - SVProgressHUD.dismiss(withDelay: 1) - self.syncControl.endRefreshing() - } - } catch { - self.gitCredential.delete() - DispatchQueue.main.async { - SVProgressHUD.dismiss() - self.syncControl.endRefreshing() - let error = error as NSError - var message = error.localizedDescription - if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError { - message = message | "UnderlyingError".localize(underlyingError.localizedDescription) - if underlyingError.localizedDescription.contains("WrongPassphrase".localize()) { - message = message | "RecoverySuggestion.".localize() - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { - Utils.alert(title: "Error".localize(), message: message, controller: self, completion: nil) - } - } - } - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if Defaults.isShowFolderOn { - searchController.searchBar.scopeButtonTitles = SearchBarScope.allCases.map(\.localizedName) - } else { - searchController.searchBar.scopeButtonTitles = nil - } - } - - override func viewDidLoad() { - super.viewDidLoad() - searchController.searchBar.delegate = self - tableView.delegate = self - tableView.dataSource = self - definesPresentationContext = true - if #available(iOS 11.0, *) { - navigationItem.searchController = searchController - navigationController?.navigationBar.prefersLargeTitles = true - navigationItem.largeTitleDisplayMode = .automatic - navigationItem.hidesSearchBarWhenScrolling = false - } else { - // Fallback on earlier versions - tableView.contentInset = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0) - view.addSubview(searchBarView!) - } - navigationItem.title = "PasswordStore".localize() - tapNavigationBarGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar)) - - SVProgressHUD.setDefaultMaskType(.black) - tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell") - - // initialize the password table - reloadTableView(parent: nil) - - // reset the data table if some password (maybe another one) has been updated - NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordStoreUpdated, object: nil) - // reset the data table if the disaply settings have been changed - NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordDisplaySettingChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil) - // A Siri shortcut can change the state of the app in the background. Hence, reload when opening the app. - NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: UIApplication.willEnterForegroundNotification, object: nil) - - // listen to the swipe back guesture - let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture)) - swipeRight.direction = UISwipeGestureRecognizer.Direction.right - view.addGestureRecognizer(swipeRight) - } - - @objc - func didTapNavigationBar(_ sender: UITapGestureRecognizer) { - let location = sender.location(in: navigationController?.navigationBar) - let hitView = navigationController?.navigationBar.hitTest(location, with: nil) - guard !(hitView is UIControl) else { - return - } - guard passwordStore.numberOfLocalCommits != 0 else { - return - } - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let allAction = UIAlertAction(title: "All Passwords", style: .default) { _ in - self.reloadTableView(parent: nil, label: .all) - } - let unsyncedAction = UIAlertAction(title: "Unsynced Passwords", style: .default) { _ in - let filteredPasswordsTableEntries = self.passwordsTableEntries.filter { entry in - !entry.synced - } - self.reloadTableView(data: filteredPasswordsTableEntries, label: .unsynced) - } - let cancelAction = UIAlertAction.cancel() - - alertController.addAction(allAction) - alertController.addAction(unsyncedAction) - alertController.addAction(cancelAction) - - present(alertController, animated: true, completion: nil) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tabBarController!.delegate = self - if let path = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: path, animated: false) - } - - // Add gesture recognizer to the navigation bar when the view is about to appear - navigationController?.navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer) - - // This allows controlls in the navigation bar to continue receiving touches - tapNavigationBarGestureRecognizer.cancelsTouchesInView = false - - tableView.refreshControl = passwordStore.repositoryExists() ? syncControl : nil - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // Remove gesture recognizer from navigation bar when view is about to disappear - navigationController?.navigationBar.removeGestureRecognizer(tapNavigationBarGestureRecognizer) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - guard #available(iOS 11, *) else { - searchBarView?.frame = CGRect(x: 0, y: navigationController!.navigationBar.bounds.size.height + UIApplication.shared.statusBarFrame.height, width: UIScreen.main.bounds.width, height: 44) - searchController.searchBar.sizeToFit() - return - } - } - - func numberOfSections(in _: UITableView) -> Int { - sections.count - } - - func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - sections[section].entries.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let longPressGestureRecognizer: UILongPressGestureRecognizer = { - let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction)) - recognizer.minimumPressDuration = 0.6 - return recognizer - }() - let entry = getPasswordEntry(by: indexPath) - let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell - cell.configure(with: entry) - if !entry.isDir { - cell.addGestureRecognizer(longPressGestureRecognizer) - } - - return cell - } - - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - UITableView.automaticDimension - } - - private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry { - sections[indexPath.section].entries[indexPath.row] - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let entry = getPasswordEntry(by: indexPath) - if !entry.isDir { - let segueIdentifier = "showPasswordDetail" - let sender = tableView.cellForRow(at: indexPath) - if shouldPerformSegue(withIdentifier: segueIdentifier, sender: sender) { - performSegue(withIdentifier: segueIdentifier, sender: sender) - } - } else { - tableView.deselectRow(at: indexPath, animated: true) - searchController.isActive = false - reloadTableView(parent: entry.passwordEntity, anim: transitionFromRight) - } - } - - @objc - func respondToSwipeGesture(gesture: UIGestureRecognizer) { - if let swipeGesture = gesture as? UISwipeGestureRecognizer { - // swipe right -> swipe back - if swipeGesture.direction == .right, parentPasswordEntity != nil { - backAction(nil) - } - } - } - - @objc - func backAction(_: Any?) { - guard Defaults.isShowFolderOn else { - return - } - var anim: CATransition? = transitionFromLeft - if parentPasswordEntity == nil { - anim = nil - } - reloadTableView(parent: parentPasswordEntity?.parent, anim: anim) - } - - @objc - func addPasswordAction(_: Any?) { - if shouldPerformSegue(withIdentifier: "addPasswordSegue", sender: self) { - performSegue(withIdentifier: "addPasswordSegue", sender: self) - } - } - - @objc - func longPressAction(_ gesture: UILongPressGestureRecognizer) { - if gesture.state == UIGestureRecognizer.State.began { - let touchPoint = gesture.location(in: tableView) - if let indexPath = tableView.indexPathForRow(at: touchPoint) { - decryptThenCopyPassword(from: indexPath) - } - } - } - - private func hideSectionHeader() -> Bool { - passwordsTableEntries.count < Self.hideSectionHeaderThreshold || searchController.isActive - } - - func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { - if hideSectionHeader() { - return nil - } - return sections[section].title - } - - func sectionIndexTitles(for _: UITableView) -> [String]? { - if hideSectionHeader() { - return nil - } - return sections.map(\.title) - } - - func tableView(_: UITableView, sectionForSectionIndexTitle _: String, at index: Int) -> Int { - index - } - - func tableView(_: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { - decryptThenCopyPassword(from: indexPath) - } - - private func decryptThenCopyPassword(from indexPath: IndexPath) { - guard PGPAgent.shared.isPrepared else { - Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self) - return - } - let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - SVProgressHUD.dismiss() - decryptPassword(passwordEntity: passwordEntity) - } - - private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) { - DispatchQueue.global(qos: .userInteractive).async { - do { - let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self) - let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - - DispatchQueue.main.async { - SecurePasteboard.shared.copy(textToCopy: decryptedPassword.password) - SVProgressHUD.setDefaultMaskType(.black) - SVProgressHUD.setDefaultStyle(.dark) - SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize()) - SVProgressHUD.dismiss(withDelay: 0.6) - } - } catch let AppError.pgpPrivateKeyNotFound(keyID: key) { - DispatchQueue.main.async { - // alert: cancel or try again - let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction.cancelAndPopView(controller: self)) - let selectKey = UIAlertAction.selectKey(controller: self) { action in - self.decryptPassword(passwordEntity: passwordEntity, keyID: action.title) - } - alert.addAction(selectKey) - - self.present(alert, animated: true) - } - } catch { - DispatchQueue.main.async { - Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self) - } - } - } - } - - private func generateSections(item: [PasswordTableEntry]) { - let collation = UILocalizedIndexedCollation.current() - let sectionTitles = collation.sectionIndexTitles - var newSections = [(title: String, entries: [PasswordTableEntry])]() - - // initialize all sections - for titleNumber in 0 ..< sectionTitles.count { - newSections.append((title: sectionTitles[titleNumber], entries: [PasswordTableEntry]())) - } - - // put entries into sections - for entry in item { - let sectionNumber = collation.section(for: entry, collationStringSelector: #selector(getter: PasswordTableEntry.title)) - newSections[sectionNumber].entries.append(entry) - } - - // sort each list and set sectionTitles - for titleNumber in 0 ..< sectionTitles.count { - let entriesToSort = newSections[titleNumber].entries - let sortedEntries = collation.sortedArray(from: entriesToSort, collationStringSelector: #selector(getter: PasswordTableEntry.title)) - newSections[titleNumber].entries = sortedEntries as! [PasswordTableEntry] - } - - // only keep non-empty sections - sections = newSections.filter { !$0.entries.isEmpty } - } - - @objc - func actOnSearchNotification() { - searchController.searchBar.becomeFirstResponder() - } - - override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { - if identifier == "showPasswordDetail" { - guard PGPAgent.shared.isPrepared else { - Utils.alert(title: "CannotShowPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) - if let sender = sender as? UITableViewCell { - let selectedIndexPath = tableView.indexPath(for: sender)! - tableView.deselectRow(at: selectedIndexPath, animated: true) - } - return false - } - } else if identifier == "addPasswordSegue" { - guard PGPAgent.shared.isPrepared && passwordStore.storeRepository != nil else { - Utils.alert(title: "CannotAddPassword".localize(), message: "MakeSurePgpAndGitProperlySet.".localize(), controller: self, completion: nil) - return false - } - } - return true - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "showPasswordDetail" { - if let viewController = segue.destination as? PasswordDetailTableViewController { - let selectedIndexPath = tableView.indexPath(for: sender as! UITableViewCell)! - let passwordEntity = getPasswordEntry(by: selectedIndexPath).passwordEntity - viewController.passwordEntity = passwordEntity - } - } else if segue.identifier == "addPasswordSegue" { - if let navController = segue.destination as? UINavigationController { - if let viewController = navController.topViewController as? AddPasswordTableViewController { - if let path = parentPasswordEntity?.getPath() { - viewController.defaultDirPrefix = "\(path)/" - } - } - } - } - } - - func filterContentForSearchText(searchText: String, scope: SearchBarScope = .all) { - var entries: [PasswordTableEntry] = scope == .all ? passwordsTableAllEntries : passwordsTableEntries - if searchController.isActive, let searchBarText = searchController.searchBar.text, !searchBarText.isEmpty { - entries = entries.filter { $0.match(searchText) } - } - reloadTableView(data: entries) - } - - private func reloadTableView(data: [PasswordTableEntry], label: PasswordLabel = .all, anim: CAAnimation? = nil) { - // set navigation item - if passwordStore.numberOfLocalCommits != 0 { - navigationController?.tabBarItem.badgeValue = "\(passwordStore.numberOfLocalCommits)" - } else { - navigationController?.tabBarItem.badgeValue = nil - } - if parentPasswordEntity != nil { - navigationItem.leftBarButtonItem = backUIBarButtonItem - navigationItem.title = parentPasswordEntity?.getName() - if #available(iOS 11, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - navigationController?.navigationBar.removeGestureRecognizer(tapNavigationBarGestureRecognizer) - } else { - navigationItem.leftBarButtonItem = nil - switch label { - case .all: - navigationItem.title = "PasswordStore".localize() - case .unsynced: - navigationItem.title = "Unsynced" - } - if #available(iOS 11, *) { - navigationController?.navigationBar.prefersLargeTitles = true - } - navigationController?.navigationBar.addGestureRecognizer(tapNavigationBarGestureRecognizer) - } - navigationItem.rightBarButtonItem = addPasswordUIBarButtonItem - - // set the password table - generateSections(item: data) - if anim != nil { - tableView.layer.add(anim!, forKey: "UITableViewReloadDataAnimationKey") - } - tableView.reloadData() - tableView.layer.removeAnimation(forKey: "UITableViewReloadDataAnimationKey") - - // set the sync control title - let atribbutedTitle = "LastSynced".localize() + ": \(lastSyncedTimeString())" - syncControl.attributedTitle = NSAttributedString(string: atribbutedTitle) - } - - private func lastSyncedTimeString() -> String { - guard let date = passwordStore.lastSyncedTime else { - return "SyncAgain?".localize() - } - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - return formatter.string(from: date) - } - - private func reloadTableView(parent: PasswordEntity?, label: PasswordLabel = .all, anim: CAAnimation? = nil) { - initPasswordsTableEntries(parent: parent) - reloadTableView(data: passwordsTableEntries, label: label, anim: anim) - } - - @objc - func actOnReloadTableViewRelatedNotification() { - DispatchQueue.main.async { [weak weakSelf = self] in - guard let strongSelf = weakSelf else { - return - } - // Reset selectedScopeButtonIndex to make sure the correct reloadTableView - strongSelf.searchController.searchBar.selectedScopeButtonIndex = 0 - strongSelf.initPasswordsTableEntries(parent: nil) - strongSelf.reloadTableView(data: strongSelf.passwordsTableEntries) - } - } - - @objc - func handleRefresh(_: UIRefreshControl) { - syncPasswords() - } - - func tabBarController(_: UITabBarController, didSelect viewController: UIViewController) { - if viewController == navigationController { - let currentTime = Date().timeIntervalSince1970 - let duration = currentTime - tapTabBarTime - tapTabBarTime = currentTime - if duration < 0.35 { - let topIndexPath = IndexPath(row: 0, section: 0) - if tableView.numberOfSections > 0 { - tableView.scrollToRow(at: topIndexPath, at: .bottom, animated: true) - } - tapTabBarTime = 0 - return - } - backAction(self) - } - } - - func searchBar(_: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { - // update the default search scope - Defaults.searchDefault = SearchBarScope(rawValue: selectedScope) - updateSearchResults(for: searchController) - } - - func searchBarShouldBeginEditing(_: UISearchBar) -> Bool { - // set the default search scope to "all" - if Defaults.isShowFolderOn, Defaults.searchDefault == .all { - searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.all.rawValue - } else { - searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue - } - return true - } - - func searchBarShouldEndEditing(_: UISearchBar) -> Bool { - // set the default search scope to "current" - searchController.searchBar.selectedScopeButtonIndex = SearchBarScope.current.rawValue - updateSearchResults(for: searchController) - return true - } -} - -extension PasswordsViewController: UISearchResultsUpdating { - func updateSearchResults(for searchController: UISearchController) { - let scope = SearchBarScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .all - filterContentForSearchText(searchText: searchController.searchBar.text!, scope: scope) - } -} - -extension PasswordsViewController: CAAnimationDelegate { - func animationDidStart(_: CAAnimation) { - view.window?.backgroundColor = Colors.systemBackground - view.layer.backgroundColor = Colors.systemBackground.cgColor - } -} diff --git a/passAutoFillExtension/Services/PasswordDecryptor.swift b/pass/Services/PasswordDecryptor.swift similarity index 92% rename from passAutoFillExtension/Services/PasswordDecryptor.swift rename to pass/Services/PasswordDecryptor.swift index 30ccae1..4a9c6a1 100644 --- a/passAutoFillExtension/Services/PasswordDecryptor.swift +++ b/pass/Services/PasswordDecryptor.swift @@ -1,13 +1,14 @@ // // PasswordDecryptor.swift -// passAutoFillExtension +// pass // -// Created by Sun, Mingshen on 12/31/20. -// Copyright © 2020 Bob Sun. All rights reserved. +// Created by Sun, Mingshen on 1/17/21. +// Copyright © 2021 Bob Sun. All rights reserved. // -import UIKit import passKit +import SVProgressHUD +import UIKit func decryptPassword(in controller: UIViewController, with passwordPath: String, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) { DispatchQueue.global(qos: .userInteractive).async { diff --git a/pass/Services/PasswordEncryptor.swift b/pass/Services/PasswordEncryptor.swift new file mode 100644 index 0000000..efbc6bb --- /dev/null +++ b/pass/Services/PasswordEncryptor.swift @@ -0,0 +1,28 @@ +import passKit + +func encryptPassword(in controller: UIViewController, with password: Password, keyID: String? = nil, completion: @escaping (() -> Void)) { + DispatchQueue.global(qos: .userInitiated).async { + do { + _ = try PasswordStore.shared.add(password: password, keyID: keyID) + DispatchQueue.main.async { + completion() + } + } catch let AppError.pgpPublicKeyNotFound(keyID: key) { + DispatchQueue.main.async { + let alert = UIAlertController(title: "Cannot Encrypt Password", message: AppError.pgpPublicKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction.cancelAndPopView(controller: controller)) + let selectKey = UIAlertAction.selectKey(controller: controller) { action in + encryptPassword(in: controller, with: password, keyID: action.title, completion: completion) + } + alert.addAction(selectKey) + + controller.present(alert, animated: true) + } + return + } catch { + DispatchQueue.main.async { + Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: controller, completion: nil) + } + } + } +} diff --git a/pass/Services/PasswordManager.swift b/pass/Services/PasswordManager.swift new file mode 100644 index 0000000..35cafca --- /dev/null +++ b/pass/Services/PasswordManager.swift @@ -0,0 +1,37 @@ +import UIKit +import passKit +import SVProgressHUD + +class PasswordManager { + weak var viewController: UIViewController? + + init(viewController: UIViewController) { + self.viewController = viewController + } + + func providePasswordPasteboard(with passwordPath: String) { + guard let viewController = viewController else { + return + } + decryptPassword(in: viewController, with: passwordPath) { password in + SecurePasteboard.shared.copy(textToCopy: password.password) + SVProgressHUD.setDefaultMaskType(.black) + SVProgressHUD.setDefaultStyle(.dark) + SVProgressHUD.showSuccess(withStatus: "PasswordCopiedToPasteboard.".localize()) + SVProgressHUD.dismiss(withDelay: 1) + } + } + + func addPassword(with password: Password) { + guard let viewController = viewController else { + return + } + + encryptPassword(in: viewController, with: password) { + SVProgressHUD.setDefaultMaskType(.black) + SVProgressHUD.setDefaultStyle(.light) + SVProgressHUD.showSuccess(withStatus: "Done".localize()) + SVProgressHUD.dismiss(withDelay: 1) + } + } +} diff --git a/pass/Services/PasswordNavigationDataSource.swift b/pass/Services/PasswordNavigationDataSource.swift new file mode 100644 index 0000000..33e30e2 --- /dev/null +++ b/pass/Services/PasswordNavigationDataSource.swift @@ -0,0 +1,117 @@ +// +// PasswordNavigationDataSource.swift +// pass +// +// Created by Sun, Mingshen on 1/16/21. +// Copyright © 2021 Bob Sun. All rights reserved. +// + +import UIKit +import passKit + +struct Section { + var title: String + var entries: [PasswordTableEntry] +} + +class PasswordNavigationDataSource: NSObject, UITableViewDataSource { + var sections: [Section] + var filteredSections: [Section] + + var isSearchActive = false + var cellGestureRecognizer: UIGestureRecognizer? + + let hideSectionHeaderThreshold = 6 + + init(entries: [PasswordTableEntry] = []) { + sections = buildSections(from: entries) + filteredSections = sections + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + filteredSections[section].entries.count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + showSectionTitles() ? filteredSections[section].title : nil + } + + func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + index + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell + let entry = getPasswordTableEntry(at: indexPath) + cell.configure(with: entry) + + if let gestureRecognizer = cellGestureRecognizer, !entry.isDir { + cell.addGestureRecognizer(gestureRecognizer) + } + return cell + } + + func numberOfSections(in tableView: UITableView) -> Int { + filteredSections.count + } + + func sectionIndexTitles(for _: UITableView) -> [String]? { + showSectionTitles() ? filteredSections.map(\.title) : nil + } + + func showSectionTitles() -> Bool { + !isSearchActive && filteredSections.count > hideSectionHeaderThreshold + } + + func getPasswordTableEntry(at indexPath: IndexPath) -> PasswordTableEntry { + filteredSections[indexPath.section].entries[indexPath.row] + } + + func showTableEntries(matching text: String) { + guard !text.isEmpty else { + filteredSections = sections + return + } + + filteredSections = sections.map { section in + let entries = section.entries.filter { $0.match(text) } + return Section(title: section.title, entries: entries) + } + .filter { !$0.entries.isEmpty } + } + + func showUnsyncedTableEntries() { + filteredSections = sections.map { section in + let entries = section.entries.filter { !$0.synced } + return Section(title: section.title, entries: entries) + } + .filter { !$0.entries.isEmpty } + } +} + +private func buildSections(from entries: [PasswordTableEntry]) -> [Section] { + let collation = UILocalizedIndexedCollation.current() + let sectionTitles = collation.sectionIndexTitles + var sections = [Section]() + + // initialize all sections + for titleNumber in 0 ..< sectionTitles.count { + sections.append(Section(title: sectionTitles[titleNumber], entries: [PasswordTableEntry]())) + } + + // put entries into sections + for entry in entries { + let sectionNumber = collation.section(for: entry, collationStringSelector: #selector(getter: PasswordTableEntry.title)) + sections[sectionNumber].entries.append(entry) + } + + // sort each list and set sectionTitles + for titleNumber in 0 ..< sectionTitles.count { + let entriesToSort = sections[titleNumber].entries + let sortedEntries = collation.sortedArray(from: entriesToSort, collationStringSelector: #selector(getter: PasswordTableEntry.title)) + sections[titleNumber].entries = sortedEntries as! [PasswordTableEntry] + } + + // only keep non-empty sections + return sections.filter { !$0.entries.isEmpty } +} diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 5ae1934..b402706 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -97,6 +97,16 @@ public class PasswordStore { Defaults.lastSyncedTime } + public var lastSyncedTimeString: String { + guard let date = lastSyncedTime else { + return "SyncAgain?".localize() + } + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter.string(from: date) + } + public var numberOfCommits: UInt? { storeRepository?.numberOfCommits(inCurrentBranch: nil) }