passforios/pass/Controllers/PasswordsViewController.swift

412 lines
17 KiB
Swift
Raw Normal View History

2017-01-19 21:15:47 +08:00
//
2017-02-03 13:01:41 +08:00
// PasswordsViewController.swift
2017-01-19 21:15:47 +08:00
// pass
//
2017-02-03 13:01:41 +08:00
// Created by Mingshen Sun on 3/2/2017.
2017-01-19 21:15:47 +08:00
// Copyright © 2017 Bob Sun. All rights reserved.
//
import UIKit
import SVProgressHUD
import SwiftyUserDefaults
2017-02-07 20:57:06 +08:00
import PasscodeLock
2017-01-19 21:15:47 +08:00
enum PasswordsTableEntryType {
case password, dir
}
struct PasswordsTableEntry {
var title: String
var isDir: Bool
var passwordEntity: PasswordEntity?
}
2017-02-03 13:01:41 +08:00
class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private var passwordsTableEntries: [PasswordsTableEntry] = []
private var filteredPasswordsTableEntries: [PasswordsTableEntry] = []
private var parentPasswordEntity: PasswordEntity? = nil
private func initPasswordsTableEntries() {
passwordsTableEntries.removeAll()
filteredPasswordsTableEntries.removeAll()
var passwordEntities = [PasswordEntity]()
if Defaults[.isShowFolderOn] {
passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData(parent: parentPasswordEntity)
} else {
passwordEntities = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false)
}
passwordsTableEntries = passwordEntities.map {
PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0)
}
}
2017-02-02 15:03:34 +08:00
var sections : [(index: Int, length :Int, title: String)] = Array()
2017-02-03 13:01:41 +08:00
var searchActive : Bool = false
let searchController = UISearchController(searchResultsController: nil)
2017-02-04 11:30:57 +08:00
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(PasswordsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
return refreshControl
}()
let searchBarView = UIView(frame: CGRect(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 44))
lazy var backUIBarButtonItem: UIBarButtonItem = {
let backUIBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(self.backAction(_:)))
return backUIBarButtonItem
}()
2017-02-03 13:01:41 +08:00
@IBOutlet weak var tableView: UITableView!
2017-01-23 13:43:06 +08:00
2017-02-10 22:15:01 +08:00
@IBAction func cancelAddPassword(segue: UIStoryboardSegue) {
}
@IBAction func saveAddPassword(segue: UIStoryboardSegue) {
if let controller = segue.source as? AddPasswordTableViewController {
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.light)
SVProgressHUD.show(withStatus: "Saving")
DispatchQueue.global(qos: .userInitiated).async {
PasswordStore.shared.add(password: controller.password!, progressBlock: { progress in
DispatchQueue.main.async {
SVProgressHUD.showProgress(progress, status: "Encrypting")
}
})
DispatchQueue.main.async {
SVProgressHUD.showSuccess(withStatus: "Done")
SVProgressHUD.dismiss(withDelay: 1)
NotificationCenter.default.post(Notification(name: Notification.Name("passwordUpdated")))
}
}
2017-02-10 22:15:01 +08:00
}
}
func syncPasswords() {
2017-02-08 01:29:00 +08:00
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.light)
2017-02-04 15:23:14 +08:00
SVProgressHUD.show(withStatus: "Sync Password Store")
let numberOfUnsyncedPasswords = PasswordStore.shared.getNumberOfUnsyncedPasswords()
2017-02-04 11:35:28 +08:00
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
2017-02-04 14:24:59 +08:00
do {
try PasswordStore.shared.pullRepository(transferProgressBlock: {(git_transfer_progress, stop) in
DispatchQueue.main.async {
SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Pull Remote Repository")
}
})
if numberOfUnsyncedPasswords > 0 {
try PasswordStore.shared.pushRepository(transferProgressBlock: {(current, total, bytes, stop) in
DispatchQueue.main.async {
SVProgressHUD.showProgress(Float(current)/Float(total), status: "Push Remote Repository")
}
})
}
2017-01-23 17:36:10 +08:00
DispatchQueue.main.async {
PasswordStore.shared.updatePasswordEntityCoreData()
self.parentPasswordEntity = nil
self.initPasswordsTableEntries()
self.reloadTableView(data: self.passwordsTableEntries)
PasswordStore.shared.setAllSynced()
self.setNavigationItemTitle()
2017-02-06 19:13:33 +08:00
Defaults[.lastUpdatedTime] = Date()
Defaults[.gitRepositoryPasswordAttempts] = 0
2017-01-23 17:36:10 +08:00
SVProgressHUD.showSuccess(withStatus: "Done")
SVProgressHUD.dismiss(withDelay: 1)
}
2017-02-04 14:24:59 +08:00
} catch {
DispatchQueue.main.async {
SVProgressHUD.showError(withStatus: error.localizedDescription)
2017-02-09 11:17:36 +08:00
SVProgressHUD.dismiss(withDelay: 1)
2017-02-04 14:24:59 +08:00
}
2017-01-23 16:29:36 +08:00
}
}
}
private func addNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordUpdatedNotification), name: NSNotification.Name(rawValue: "passwordUpdated"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordStoreErasedNotification), name: NSNotification.Name(rawValue: "passwordStoreErased"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnSearchNotification), name: NSNotification.Name(rawValue: "search"), object: nil)
}
2017-01-19 21:15:47 +08:00
override func viewDidLoad() {
super.viewDidLoad()
setNavigationItemTitle()
initPasswordsTableEntries()
addNotificationObservers()
2017-02-07 16:45:14 +08:00
generateSections(item: passwordsTableEntries)
2017-02-03 13:01:41 +08:00
tableView.delegate = self
tableView.dataSource = self
2017-01-23 12:48:20 +08:00
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
2017-02-07 17:24:23 +08:00
searchController.searchBar.isTranslucent = false
searchController.searchBar.backgroundColor = UIColor.gray
2017-02-03 13:01:41 +08:00
searchController.searchBar.sizeToFit()
2017-01-23 12:48:20 +08:00
definesPresentationContext = true
2017-02-03 13:01:41 +08:00
searchBarView.addSubview(searchController.searchBar)
view.addSubview(searchBarView)
2017-02-04 11:30:57 +08:00
tableView.insertSubview(refreshControl, at: 0)
SVProgressHUD.setDefaultMaskType(.black)
2017-02-06 19:13:33 +08:00
updateRefreshControlTitle()
2017-02-02 15:03:34 +08:00
}
2017-02-03 14:20:52 +08:00
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let path = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: path, animated: false)
}
}
2017-02-07 13:23:18 +08:00
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
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()
}
2017-02-03 13:01:41 +08:00
func numberOfSections(in tableView: UITableView) -> Int {
2017-02-02 15:03:34 +08:00
return sections.count
}
2017-02-03 13:01:41 +08:00
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].length
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath)
let entry = getPasswordEntry(by: indexPath)
if !entry.isDir {
if entry.passwordEntity!.synced {
cell.textLabel?.text = entry.title
} else {
cell.textLabel?.text = "\(entry.title)"
}
2017-03-02 15:57:39 +08:00
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.6
cell.addGestureRecognizer(longPressGestureRecognizer)
} else {
cell.textLabel?.text = "\(entry.title)/"
}
2017-02-03 13:01:41 +08:00
return cell
}
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry{
var entry: PasswordsTableEntry
let index = sections[indexPath.section].index + indexPath.row
if searchController.isActive && searchController.searchBar.text != "" {
entry = filteredPasswordsTableEntries[index]
} else {
entry = passwordsTableEntries[index]
}
return entry
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = getPasswordEntry(by: indexPath)
if !entry.isDir {
performSegue(withIdentifier: "showPasswordDetail", sender: tableView.cellForRow(at: indexPath))
} else {
tableView.deselectRow(at: indexPath, animated: true)
parentPasswordEntity = entry.passwordEntity
initPasswordsTableEntries()
reloadTableView(data: passwordsTableEntries)
}
}
func backAction(_ sender: Any?) {
parentPasswordEntity = parentPasswordEntity?.parent
initPasswordsTableEntries()
reloadTableView(data: passwordsTableEntries)
}
2017-02-08 01:29:00 +08:00
func longPressAction(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
let touchPoint = gesture.location(in: tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
copyToPasteboard(from: indexPath)
}
}
}
2017-02-03 13:01:41 +08:00
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return sections.map { $0.title }
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return index
}
2017-02-04 11:30:57 +08:00
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
2017-02-08 01:29:00 +08:00
copyToPasteboard(from: indexPath)
}
func copyToPasteboard(from indexPath: IndexPath) {
if Defaults[.pgpKeyID] == nil {
2017-02-16 00:54:42 +08:00
Utils.alert(title: "Cannot Copy Password", message: "PGP Key is not set. Please set your PGP Key first.", controller: self, completion: nil)
return
}
2017-03-02 16:14:24 +08:00
let password = getPasswordEntry(by: indexPath).passwordEntity!
2017-02-08 19:18:10 +08:00
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
var passphrase = ""
if Defaults[.isRememberPassphraseOn] && PasswordStore.shared.pgpKeyPassphrase != nil {
passphrase = PasswordStore.shared.pgpKeyPassphrase!
self.decryptThenCopyPassword(passwordEntity: password, passphrase: passphrase)
} else {
let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
passphrase = alert.textFields!.first!.text!
self.decryptThenCopyPassword(passwordEntity: password, passphrase: passphrase)
}))
alert.addTextField(configurationHandler: {(textField: UITextField!) in
textField.text = ""
textField.isSecureTextEntry = true
})
self.present(alert, animated: true, completion: nil)
}
}
func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) {
SVProgressHUD.setDefaultMaskType(.black)
2017-02-08 19:18:10 +08:00
SVProgressHUD.setDefaultStyle(.dark)
SVProgressHUD.show(withStatus: "Decrypting")
DispatchQueue.global(qos: .userInteractive).async {
var decryptedPassword: Password?
do {
decryptedPassword = try passwordEntity.decrypt(passphrase: passphrase)!
2017-02-15 11:37:19 +08:00
DispatchQueue.main.async {
2017-02-23 17:56:12 +08:00
Utils.copyToPasteboard(textToCopy: decryptedPassword?.password)
2017-03-02 16:14:24 +08:00
SVProgressHUD.showSuccess(withStatus: "Password copied, and will be cleared in 45 seconds.")
2017-02-15 11:37:19 +08:00
SVProgressHUD.dismiss(withDelay: 0.6)
}
2017-02-08 19:18:10 +08:00
} catch {
print(error)
2017-02-15 11:37:19 +08:00
DispatchQueue.main.async {
SVProgressHUD.showError(withStatus: error.localizedDescription)
SVProgressHUD.dismiss(withDelay: 1)
}
2017-02-08 19:18:10 +08:00
}
2017-02-06 14:28:57 +08:00
}
2017-02-04 11:30:57 +08:00
}
func generateSections(item: [PasswordsTableEntry]) {
2017-02-02 15:03:34 +08:00
sections.removeAll()
guard item.count != 0 else {
2017-02-02 15:03:34 +08:00
return
}
var index = 0
for i in 0 ..< item.count {
let title = item[index].title.uppercased()
let commonPrefix = item[i].title.commonPrefix(with: title, options: .caseInsensitive)
2017-02-02 15:03:34 +08:00
if commonPrefix.characters.count == 0 {
let firstCharacter = title[title.startIndex]
2017-02-02 15:03:34 +08:00
let newSection = (index: index, length: i - index, title: "\(firstCharacter)")
sections.append(newSection)
index = i
}
}
let title = item[index].title.uppercased()
let firstCharacter = title[title.startIndex]
2017-02-02 15:03:34 +08:00
let newSection = (index: index, length: item.count - index, title: "\(firstCharacter)")
sections.append(newSection)
2017-01-23 12:48:20 +08:00
}
2017-01-19 21:15:47 +08:00
func actOnPasswordUpdatedNotification() {
initPasswordsTableEntries()
reloadTableView(data: passwordsTableEntries)
setNavigationItemTitle()
}
private func setNavigationItemTitle() {
var title = ""
if parentPasswordEntity != nil {
title = parentPasswordEntity!.name!
} else {
title = "Password Store"
}
let numberOfUnsynced = PasswordStore.shared.getNumberOfUnsyncedPasswords()
if numberOfUnsynced == 0 {
navigationItem.title = "\(title)"
} else {
navigationItem.title = "\(title) (\(numberOfUnsynced))"
}
2017-01-19 21:15:47 +08:00
}
2017-02-07 16:45:14 +08:00
func actOnPasswordStoreErasedNotification() {
initPasswordsTableEntries()
reloadTableView(data: passwordsTableEntries)
setNavigationItemTitle()
2017-02-07 16:45:14 +08:00
}
2017-02-08 12:47:05 +08:00
func actOnSearchNotification() {
searchController.searchBar.becomeFirstResponder()
}
2017-02-07 16:45:14 +08:00
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "showPasswordDetail" {
if Defaults[.pgpKeyID] == nil {
2017-02-16 00:54:42 +08:00
Utils.alert(title: "Cannot Show Password", message: "PGP Key is not set. Please set your PGP Key first.", controller: self, completion: nil)
if let s = sender as? UITableViewCell {
let selectedIndexPath = tableView.indexPath(for: s)!
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
return false
}
}
return true
}
2017-02-03 13:01:41 +08:00
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showPasswordDetail" {
2017-02-02 21:04:31 +08:00
if let viewController = segue.destination as? PasswordDetailTableViewController {
2017-02-02 15:03:34 +08:00
let selectedIndexPath = self.tableView.indexPath(for: sender as! UITableViewCell)!
let passwordEntity = getPasswordEntry(by: selectedIndexPath).passwordEntity!
2017-02-06 14:28:57 +08:00
viewController.passwordEntity = passwordEntity
}
}
}
2017-02-04 11:30:57 +08:00
2017-02-03 13:01:41 +08:00
func filterContentForSearchText(searchText: String, scope: String = "All") {
filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in
return entry.title.lowercased().contains(searchText.lowercased())
2017-02-03 13:01:41 +08:00
}
if searchController.isActive && searchController.searchBar.text != "" {
reloadTableView(data: filteredPasswordsTableEntries)
2017-02-03 13:01:41 +08:00
} else {
reloadTableView(data: passwordsTableEntries)
2017-02-03 13:01:41 +08:00
}
2017-02-04 11:30:57 +08:00
}
2017-02-06 19:13:33 +08:00
func updateRefreshControlTitle() {
var atribbutedTitle = "Pull to Sync Password Store"
2017-02-10 15:32:01 +08:00
atribbutedTitle = "Last Synced: \(Utils.getLastUpdatedTimeString())"
2017-02-06 19:13:33 +08:00
refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
}
func reloadTableView(data: [PasswordsTableEntry]) {
setNavigationItemTitle()
if parentPasswordEntity != nil {
navigationItem.leftBarButtonItem = backUIBarButtonItem
} else {
navigationItem.leftBarButtonItem = nil
}
2017-02-04 11:30:57 +08:00
generateSections(item: data)
2017-02-03 13:01:41 +08:00
tableView.reloadData()
2017-02-06 19:13:33 +08:00
updateRefreshControlTitle()
2017-02-03 13:01:41 +08:00
}
2017-02-04 11:30:57 +08:00
func handleRefresh(_ refreshControl: UIRefreshControl) {
syncPasswords()
2017-02-04 11:30:57 +08:00
refreshControl.endRefreshing()
}
2017-01-19 21:15:47 +08:00
}
2017-01-23 16:29:36 +08:00
2017-02-03 13:01:41 +08:00
extension PasswordsViewController: UISearchResultsUpdating {
2017-01-23 16:29:36 +08:00
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchText: searchController.searchBar.text!)
}
}