2017-01-19 21:15:47 +08:00
//
2017-02-03 13:01:41 +08:00
// P a s s w o r d s V i e w C o n t r o l l e r . s w i f t
2017-01-19 21:15:47 +08:00
// p a s s
//
2017-02-03 13:01:41 +08:00
// C r e a t e d b y M i n g s h e n S u n o n 3 / 2 / 2 0 1 7 .
2017-01-19 21:15:47 +08:00
// C o p y r i g h t © 2 0 1 7 B o b S u n . A l l r i g h t s r e s e r v e d .
//
import UIKit
import SVProgressHUD
2017-02-06 10:55:24 +08:00
import SwiftyUserDefaults
2017-02-07 20:57:06 +08:00
import PasscodeLock
2017-01-19 21:15:47 +08:00
2017-03-02 14:51:40 +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 {
2017-03-02 14:51:40 +08:00
private var passwordsTableEntries : [ PasswordsTableEntry ] = [ ]
private var filteredPasswordsTableEntries : [ PasswordsTableEntry ] = [ ]
private var parentPasswordEntity : PasswordEntity ? = nil
private func initPasswordsTableEntries ( ) {
passwordsTableEntries . removeAll ( )
filteredPasswordsTableEntries . removeAll ( )
passwordsTableEntries = PasswordStore . shared . fetchPasswordEntityCoreData ( parent : parentPasswordEntity ) . 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 ) )
2017-03-02 14:51:40 +08:00
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 {
2017-02-11 19:48:47 +08:00
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
}
}
2017-03-02 14:51:40 +08:00
2017-02-04 11:47:57 +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 " )
2017-02-15 16:51:12 +08:00
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 " )
}
} )
2017-02-15 16:51:12 +08:00
if numberOfUnsyncedPasswords > 0 {
2017-02-12 13:31:29 +08:00
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 {
2017-02-15 16:51:12 +08:00
PasswordStore . shared . updatePasswordEntityCoreData ( )
2017-03-02 14:51:40 +08:00
self . parentPasswordEntity = nil
self . initPasswordsTableEntries ( )
self . reloadTableView ( data : self . passwordsTableEntries )
2017-02-12 01:59:40 +08:00
PasswordStore . shared . setAllSynced ( )
self . setNavigationItemTitle ( )
2017-02-06 19:13:33 +08:00
Defaults [ . lastUpdatedTime ] = Date ( )
2017-02-17 21:06:28 +08:00
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
}
}
}
2017-01-19 21:15:47 +08:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2017-02-12 01:59:40 +08:00
setNavigationItemTitle ( )
2017-03-02 14:51:40 +08:00
initPasswordsTableEntries ( )
2017-02-03 13:20:03 +08:00
NotificationCenter . default . addObserver ( self , selector : #selector ( PasswordsViewController . actOnPasswordUpdatedNotification ) , name : NSNotification . Name ( rawValue : " passwordUpdated " ) , object : nil )
2017-02-07 16:45:14 +08:00
NotificationCenter . default . addObserver ( self , selector : #selector ( PasswordsViewController . actOnPasswordStoreErasedNotification ) , name : NSNotification . Name ( rawValue : " passwordStoreErased " ) , object : nil )
2017-02-08 12:47:05 +08:00
NotificationCenter . default . addObserver ( self , selector : #selector ( PasswordsViewController . actOnSearchNotification ) , name : NSNotification . Name ( rawValue : " search " ) , object : nil )
2017-02-07 16:45:14 +08:00
2017-03-02 14:51:40 +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 )
2017-02-06 10:55:24 +08:00
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 )
2017-03-02 14:51:40 +08:00
let entry = getPasswordEntry ( by : indexPath )
if ! entry . isDir {
if entry . passwordEntity ! . synced {
cell . textLabel ? . text = entry . title
} else {
cell . textLabel ? . text = " ↻ \( entry . title ) "
}
2017-02-12 01:59:40 +08:00
} else {
2017-03-02 14:51:40 +08:00
cell . textLabel ? . text = " \( entry . title ) / "
2017-02-12 01:59:40 +08:00
}
2017-02-08 01:29:00 +08:00
let longPressGestureRecognizer = UILongPressGestureRecognizer ( target : self , action : #selector ( longPressAction ( _ : ) ) )
2017-02-08 10:40:48 +08:00
longPressGestureRecognizer . minimumPressDuration = 0.6
2017-02-08 01:29:00 +08:00
cell . addGestureRecognizer ( longPressGestureRecognizer )
2017-02-03 13:01:41 +08:00
return cell
}
2017-03-02 14:51:40 +08:00
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 ) {
2017-02-15 11:26:22 +08:00
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 )
2017-02-15 11:26:22 +08:00
return
}
2017-02-04 11:30:57 +08:00
let index = sections [ indexPath . section ] . index + indexPath . row
let password : PasswordEntity
if searchController . isActive && searchController . searchBar . text != " " {
2017-03-02 14:51:40 +08:00
password = passwordsTableEntries [ index ] . passwordEntity !
2017-02-04 11:30:57 +08:00
} else {
2017-03-02 14:51:40 +08:00
password = filteredPasswordsTableEntries [ index ] . passwordEntity !
2017-02-04 11:30:57 +08:00
}
2017-02-08 19:18:10 +08:00
UIImpactFeedbackGenerator ( style : . medium ) . impactOccurred ( )
2017-02-28 12:25:52 +08:00
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 ) {
2017-02-20 16:07:32 +08:00
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 {
2017-02-28 12:25:52 +08:00
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-02-15 11:37:19 +08:00
SVProgressHUD . showSuccess ( withStatus : " Password Copied " )
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
}
2017-03-02 14:51:40 +08:00
func generateSections ( item : [ PasswordsTableEntry ] ) {
2017-02-02 15:03:34 +08:00
sections . removeAll ( )
2017-03-02 14:51:40 +08:00
guard item . count != 0 else {
2017-02-02 15:03:34 +08:00
return
}
var index = 0
for i in 0 . . < item . count {
2017-03-02 14:51:40 +08:00
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 {
2017-03-02 14:51:40 +08:00
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
}
}
2017-03-02 14:51:40 +08:00
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 ( ) {
2017-03-02 14:51:40 +08:00
initPasswordsTableEntries ( )
reloadTableView ( data : passwordsTableEntries )
2017-02-12 01:59:40 +08:00
setNavigationItemTitle ( )
}
2017-03-02 14:51:40 +08:00
2017-02-12 01:59:40 +08:00
private func setNavigationItemTitle ( ) {
let numberOfUnsynced = PasswordStore . shared . getNumberOfUnsyncedPasswords ( )
if numberOfUnsynced = = 0 {
navigationItem . title = " Password Store "
} else {
navigationItem . title = " Password Store ( \( numberOfUnsynced ) ) "
}
2017-01-19 21:15:47 +08:00
}
2017-02-06 10:55:24 +08:00
2017-02-07 16:45:14 +08:00
func actOnPasswordStoreErasedNotification ( ) {
2017-03-02 14:51:40 +08:00
initPasswordsTableEntries ( )
reloadTableView ( data : passwordsTableEntries )
2017-02-20 16:58:15 +08:00
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
2017-02-06 10:55:24 +08:00
override func shouldPerformSegue ( withIdentifier identifier : String , sender : Any ? ) -> Bool {
if identifier = = " showPasswordDetail " {
2017-02-15 11:26:22 +08:00
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 )
2017-02-06 10:55:24 +08:00
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
2017-01-22 01:42:36 +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 ) !
2017-03-02 14:51:40 +08:00
let passwordEntity = getPasswordEntry ( by : selectedIndexPath ) . passwordEntity !
2017-02-06 14:28:57 +08:00
viewController . passwordEntity = passwordEntity
2017-01-22 01:42:36 +08:00
}
}
}
2017-02-04 11:30:57 +08:00
2017-02-03 13:01:41 +08:00
func filterContentForSearchText ( searchText : String , scope : String = " All " ) {
2017-03-02 14:51:40 +08:00
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 != " " {
2017-03-02 14:51:40 +08:00
reloadTableView ( data : filteredPasswordsTableEntries )
2017-02-03 13:01:41 +08:00
} else {
2017-03-02 14:51:40 +08:00
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 )
}
2017-03-02 14:51:40 +08:00
func reloadTableView ( data : [ PasswordsTableEntry ] ) {
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 ) {
2017-02-04 11:47:57 +08:00
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 ! )
}
}