diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index b202f31..3f8de2a 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ A267002A1EEC466A00176B8A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A26700281EEC466A00176B8A /* MainInterface.storyboard */; }; A267002E1EEC466A00176B8A /* passExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A26700241EEC466A00176B8A /* passExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A26700371EEC475600176B8A /* passProcessor.js in Resources */ = {isa = PBXBuildFile; fileRef = A26700351EEC475600176B8A /* passProcessor.js */; }; + A2699ACD2402631400F36323 /* PasswordTableEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2699ACC2402631400F36323 /* PasswordTableEntry.swift */; }; + A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2699ACE24027D9500F36323 /* PasswordTableEntryTest.swift */; }; A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; }; A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2802BF81E70813A00879216 /* SliderTableViewCell.xib */; }; A2A61C131EEF90CB00CFE063 /* Base32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A262A58C1E68749C006B0890 /* Base32.framework */; }; @@ -327,6 +329,8 @@ A26700321EEC46C400176B8A /* pass.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = pass.entitlements; sourceTree = ""; }; A26700331EEC46C900176B8A /* passExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passExtension.entitlements; sourceTree = ""; }; A26700351EEC475600176B8A /* passProcessor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = passProcessor.js; sourceTree = ""; }; + A2699ACC2402631400F36323 /* PasswordTableEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableEntry.swift; sourceTree = ""; }; + A2699ACE24027D9500F36323 /* PasswordTableEntryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableEntryTest.swift; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = ""; }; @@ -545,6 +549,7 @@ 30C015A7214ED378005BB6DF /* Models */ = { isa = PBXGroup; children = ( + A2699ACE24027D9500F36323 /* PasswordTableEntryTest.swift */, 30B0485F209A5141001013CA /* PasswordTest.swift */, ); path = Models; @@ -661,6 +666,7 @@ 30697C4021F63CAB0064FCAC /* Password.swift */, 30697C3F21F63CAA0064FCAC /* PasswordEntity.swift */, 30697C4321F63CAB0064FCAC /* PasswordStore.swift */, + A2699ACC2402631400F36323 /* PasswordTableEntry.swift */, ); path = Models; sourceTree = ""; @@ -1323,6 +1329,7 @@ 30697C2A21F63C5A0064FCAC /* NotificationNames.swift in Sources */, 30CCA91623258C380048CA51 /* PgpInterface.swift in Sources */, 30697C4721F63CAB0064FCAC /* PasscodeLock.swift in Sources */, + A2699ACD2402631400F36323 /* PasswordTableEntry.swift in Sources */, 30697C3421F63C8B0064FCAC /* PasscodeLockViewController.swift in Sources */, 3087574F2343E42A00B971A2 /* Colors.swift in Sources */, 30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */, @@ -1345,6 +1352,7 @@ 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */, 301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */, 30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */, + A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */, A2AA934622DE3A8000D79A00 /* PGPAgentTest.swift in Sources */, 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */, diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index 18354cd..0448099 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -10,23 +10,11 @@ import UIKit import SVProgressHUD import passKit -fileprivate class PasswordsTableEntry : NSObject { - @objc var title: String - var isDir: Bool - var passwordEntity: PasswordEntity? - init(title: String, isDir: Bool, passwordEntity: PasswordEntity?) { - self.title = title - self.isDir = isDir - self.passwordEntity = passwordEntity - } -} - -fileprivate let hideSectionHeaderTreshold = 6 // hide section header if passwords count is less than the threshold +fileprivate let hideSectionHeaderThreshold = 6 // hide section header if passwords count is less than the threshold class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate, UISearchBarDelegate { - private var passwordsTableEntries: [PasswordsTableEntry] = [] - private var passwordsTableAllEntries: [PasswordsTableEntry] = [] - private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + private var passwordsTableEntries: [PasswordTableEntry] = [] + private var passwordsTableAllEntries: [PasswordTableEntry] = [] private var parentPasswordEntity: PasswordEntity? = nil private let passwordStore = PasswordStore.shared private let keychain = AppKeychain.shared @@ -34,9 +22,8 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV private var tapTabBarTime: TimeInterval = 0 private var tapNavigationBarGestureRecognizer: UITapGestureRecognizer! - private var sections = [(title: String, entries: [PasswordsTableEntry])]() + private var sections = [(title: String, entries: [PasswordTableEntry])]() - private var searchActive : Bool = false private enum PasswordLabel { case all case unsynced @@ -130,23 +117,18 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV @IBOutlet weak var tableView: UITableView! private func initPasswordsTableEntries(parent: PasswordEntity?) { - passwordsTableEntries.removeAll() - passwordsTableAllEntries.removeAll() - filteredPasswordsTableEntries.removeAll() - var passwordEntities = [PasswordEntity]() - var passwordAllEntities = [PasswordEntity]() - if Defaults.isShowFolderOn { - passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(parent: parent) - } else { - passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) + let passwordAllEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) + passwordsTableAllEntries = passwordAllEntities.compactMap { + PasswordTableEntry($0) } - passwordsTableEntries = passwordEntities.map { - PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0) - } - passwordAllEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) - passwordsTableAllEntries = passwordAllEntities.map { - PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0) + + let passwordEntities = Defaults.isShowFolderOn ? + self.passwordStore.fetchPasswordEntityCoreData(parent: parent) : + passwordAllEntities + passwordsTableEntries = passwordEntities.compactMap { + PasswordTableEntry($0) } + parentPasswordEntity = parent } @@ -186,7 +168,6 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.show(withStatus: "SyncingPasswordStore".localize()) - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in do { try self.passwordStore.pullRepository(credential: self.gitCredential, requestCredentialPassword: self.requestCredentialPassword, progressBlock: {(git_transfer_progress, stop) in @@ -286,10 +267,10 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV self.reloadTableView(parent: nil, label: .all) } let unsyncedAction = UIAlertAction(title: "Unsynced Passwords", style: .default) { _ in - self.filteredPasswordsTableEntries = self.passwordsTableEntries.filter { entry in - return !entry.passwordEntity!.synced + let filteredPasswordsTableEntries = self.passwordsTableEntries.filter { entry in + return !entry.synced } - self.reloadTableView(data: self.filteredPasswordsTableEntries, label: .unsynced) + self.reloadTableView(data: filteredPasswordsTableEntries, label: .unsynced) } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) @@ -343,7 +324,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return recognizer }() let entry = getPasswordEntry(by: indexPath) - let passwordEntity = entry.passwordEntity! + let passwordEntity = entry.passwordEntity let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) cell.textLabel?.text = passwordEntity.synced ? entry.title : "↻ \(entry.title)" @@ -367,7 +348,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV return cell } - private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry { + private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry { return sections[indexPath.section].entries[indexPath.row] } @@ -420,7 +401,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } private func hideSectionHeader() -> Bool { - if passwordsTableEntries.count < hideSectionHeaderTreshold || self.searchController.isActive { + if passwordsTableEntries.count < hideSectionHeaderThreshold || self.searchController.isActive { return true } return false @@ -475,7 +456,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) return } - let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity! + let passwordEntity = getPasswordEntry(by: indexPath).passwordEntity UIImpactFeedbackGenerator(style: .medium).impactOccurred() SVProgressHUD.dismiss() DispatchQueue.global(qos: .userInteractive).async { @@ -497,27 +478,27 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } } - private func generateSections(item: [PasswordsTableEntry]) { + private func generateSections(item: [PasswordTableEntry]) { let collation = UILocalizedIndexedCollation.current() let sectionTitles = collation.sectionIndexTitles - var newSections = [(title: String, entries: [PasswordsTableEntry])]() + var newSections = [(title: String, entries: [PasswordTableEntry])]() // initialize all sections for i in 0.. UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) let entry = getPasswordEntry(by: indexPath) - if entry.passwordEntity!.synced { + if entry.passwordEntity.synced { cell.textLabel?.text = entry.title } else { cell.textLabel?.text = "↻ \(entry.title)" } cell.accessoryType = .none cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) - cell.detailTextLabel?.text = entry.passwordEntity?.getCategoryText() + cell.detailTextLabel?.text = entry.categoryText return cell } @@ -151,7 +137,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa return } - let passwordEntity = entry.passwordEntity! + let passwordEntity = entry.passwordEntity UIImpactFeedbackGenerator(style: .medium).impactOccurred() DispatchQueue.global(qos: .userInteractive).async { var decryptedPassword: Password? @@ -212,16 +198,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { if let searchText = searchBar.text, searchText.isEmpty == false { - filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in - var matched = false - matched = matched || entry.title.range(of: searchText, options: .caseInsensitive) != nil - matched = matched || searchText.range(of: entry.title, options: .caseInsensitive) != nil - entry.categoryArray.forEach({ (category) in - matched = matched || category.range(of: searchText, options: .caseInsensitive) != nil - matched = matched || searchText.range(of: category, options: .caseInsensitive) != nil - }) - return matched - } + filteredPasswordsTableEntries = passwordsTableEntries.filter {$0.match(searchText)} searchActive = true } else { searchActive = false @@ -233,7 +210,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController, UITa searchBarSearchButtonClicked(searchBar) } - private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry { + private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry { if searchActive { return filteredPasswordsTableEntries[indexPath.row] } else { diff --git a/passExtension/Controllers/ExtensionViewController.swift b/passExtension/Controllers/ExtensionViewController.swift index 50bdfcd..0c17d62 100644 --- a/passExtension/Controllers/ExtensionViewController.swift +++ b/passExtension/Controllers/ExtensionViewController.swift @@ -10,19 +10,6 @@ import Foundation import MobileCoreServices import passKit -fileprivate class PasswordsTableEntry : NSObject { - var title: String - var categoryText: String - var categoryArray: [String] - var passwordEntity: PasswordEntity? - init(_ entity: PasswordEntity) { - self.title = entity.name! - self.categoryText = entity.getCategoryText() - self.categoryArray = entity.getCategoryArray() - self.passwordEntity = entity - } -} - class ExtensionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UINavigationBarDelegate { @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var tableView: UITableView! @@ -31,8 +18,8 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV private let keychain = AppKeychain.shared private var searchActive = false - private var passwordsTableEntries: [PasswordsTableEntry] = [] - private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + private var passwordsTableEntries: [PasswordTableEntry] = [] + private var filteredPasswordsTableEntries: [PasswordTableEntry] = [] enum Action { case findLogin, fillBrowser, unknown @@ -46,12 +33,11 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV }() private func initPasswordsTableEntries() { - passwordsTableEntries.removeAll() filteredPasswordsTableEntries.removeAll() - var passwordEntities = [PasswordEntity]() - passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) + + let passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) passwordsTableEntries = passwordEntities.map { - PasswordsTableEntry($0) + PasswordTableEntry($0) } } @@ -139,7 +125,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) let entry = getPasswordEntry(by: indexPath) - if entry.passwordEntity!.synced { + if entry.synced { cell.textLabel?.text = entry.title } else { cell.textLabel?.text = "↻ \(entry.title)" @@ -159,7 +145,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV return } - let passwordEntity = entry.passwordEntity! + let passwordEntity = entry.passwordEntity UIImpactFeedbackGenerator(style: .medium).impactOccurred() DispatchQueue.global(qos: .userInteractive).async { var decryptedPassword: Password? @@ -243,16 +229,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { if let searchText = searchBar.text, searchText.isEmpty == false { - filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in - var matched = false - matched = matched || entry.title.range(of: searchText, options: .caseInsensitive) != nil - matched = matched || searchText.range(of: entry.title, options: .caseInsensitive) != nil - entry.categoryArray.forEach({ (category) in - matched = matched || category.range(of: searchText, options: .caseInsensitive) != nil - matched = matched || searchText.range(of: category, options: .caseInsensitive) != nil - }) - return matched - } + filteredPasswordsTableEntries = passwordsTableEntries.filter {$0.match(searchText)} searchActive = true } else { searchActive = false @@ -264,7 +241,7 @@ class ExtensionViewController: UIViewController, UITableViewDataSource, UITableV searchBarSearchButtonClicked(searchBar) } - private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry { + private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry { if searchActive { return filteredPasswordsTableEntries[indexPath.row] } else { diff --git a/passKit/Models/PasswordTableEntry.swift b/passKit/Models/PasswordTableEntry.swift new file mode 100644 index 0000000..d934991 --- /dev/null +++ b/passKit/Models/PasswordTableEntry.swift @@ -0,0 +1,44 @@ +// +// PasswordTableEntry.swift +// passKit +// +// Created by Yishi Lin on 2020/2/23. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import Foundation + +public class PasswordTableEntry: NSObject { + public let passwordEntity: PasswordEntity + @objc public let title: String + public let isDir: Bool + public let synced: Bool + public let categoryText: String + + public init(_ entity: PasswordEntity) { + self.passwordEntity = entity + self.title = entity.name! + self.isDir = entity.isDir + self.synced = entity.synced + self.categoryText = entity.getCategoryText() + } + + public func match(_ searchText: String) -> Bool { + return PasswordTableEntry.match(nameWithCategory: passwordEntity.nameWithCategory, searchText: searchText) + } + + public static func match(nameWithCategory: String, searchText: String) -> Bool { + let titleSplit = nameWithCategory.split{ !($0.isLetter || $0.isNumber || $0 == ".") } + for str in titleSplit { + if (str.localizedCaseInsensitiveContains(searchText)) { + return true + } + if (searchText.localizedCaseInsensitiveContains(str)) { + return true + } + } + + return false + } +} + diff --git a/passKitTests/Models/PasswordTableEntryTest.swift b/passKitTests/Models/PasswordTableEntryTest.swift new file mode 100644 index 0000000..a5e0356 --- /dev/null +++ b/passKitTests/Models/PasswordTableEntryTest.swift @@ -0,0 +1,55 @@ +// +// PasswordTableEntryTest.swift +// passKitTests +// +// Created by Yishi Lin on 2020/2/23. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +class PasswordTableEntryTest: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + let nameWithCategoryList = [ + "github", + "github.com", + "www.github.com", + "personal/github", + "personal/github.com", + "personal/www.github.com", + "github/personal", + "github.com/personal", + "www.github.com/personal", + "github (personal)", + ] + let searchTextList1 = [ + "github.com", + "www.github.com" + ] + let searchTextList2 = [ + "xx.com", + "www.xx.com" + ] + + for nameWithCategory in nameWithCategoryList { + for searchText in searchTextList1 { + XCTAssertTrue(PasswordTableEntry.match(nameWithCategory: nameWithCategory, searchText: searchText)) + } + for searchText in searchTextList2 { + XCTAssertFalse(PasswordTableEntry.match(nameWithCategory: nameWithCategory, searchText: searchText)) + } + } + } + +}