From 4c3d33efdc029d6e8b70e6181ed437fe4636a12e Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 27 Aug 2020 16:39:03 -0700 Subject: [PATCH] Shows source errors in SourcesViewController --- AltStore.xcodeproj/project.pbxproj | 16 ++++--- AltStore/AppDelegate.swift | 2 + AltStore/Base.lproj/Main.storyboard | 10 +++-- AltStore/Browse/BrowseViewController.swift | 3 +- .../Components/BannerCollectionViewCell.swift | 34 +++++++++++++- ...edFailure.swift => NSError+AltStore.swift} | 17 ++++++- AltStore/Managing Apps/AppManager.swift | 1 + .../AltStore 6.xcdatamodel/contents | 5 ++- AltStore/Model/SecureValueTransformer.swift | 26 +++++++++++ AltStore/Model/Source.swift | 2 + AltStore/News/NewsViewController.swift | 6 +++ AltStore/Sources/SourcesViewController.swift | 45 ++++++++++++++++++- AltStore/TabBarController.swift | 8 ++++ 13 files changed, 158 insertions(+), 17 deletions(-) rename AltStore/Extensions/{NSError+LocalizedFailure.swift => NSError+AltStore.swift} (68%) create mode 100644 AltStore/Model/SecureValueTransformer.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 8b9b1f1c..62c58e11 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; }; BF4E8456246F16D700ECCBD4 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; }; BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; }; + BF56333824EC5E9A00038F00 /* SecureValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56333724EC5E9A00038F00 /* SecureValueTransformer.swift */; }; BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; }; BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; }; BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; }; @@ -136,11 +137,11 @@ BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; }; BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; }; BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; }; - BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; + BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; }; BF6A5320246DC1B0004F59C8 /* FileManager+SharedDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */; }; - BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; }; + BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; }; BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; }; BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; }; @@ -479,6 +480,7 @@ BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = ""; }; BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = ""; }; + BF56333724EC5E9A00038F00 /* SecureValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueTransformer.swift; sourceTree = ""; }; BF56D2A823DF87570006506D /* AltStore 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 4.xcdatamodel"; sourceTree = ""; }; BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = ""; }; BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = ""; }; @@ -499,7 +501,7 @@ BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = ""; }; BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DirectorySize.swift"; sourceTree = ""; }; BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SharedDirectories.swift"; sourceTree = ""; }; - BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+LocalizedFailure.swift"; sourceTree = ""; }; + BF6C336124197D700034FD24 /* NSError+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+AltStore.swift"; sourceTree = ""; }; BF6C33632419ADEB0034FD24 /* AltStore 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 5.xcdatamodel"; sourceTree = ""; }; BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore4ToAltStore5.xcmappingmodel; sourceTree = ""; }; BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+Markdown.m"; path = "Dependencies/MarkdownAttributedString/NSAttributedString+Markdown.m"; sourceTree = SOURCE_ROOT; }; @@ -1266,6 +1268,7 @@ BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */, BFB11691229322E400BB457C /* DatabaseManager.swift */, BF3D64A122E8031100E9056B /* MergePolicy.swift */, + BF56333724EC5E9A00038F00 /* SecureValueTransformer.swift */, BFE6326722A858F300F30809 /* Account.swift */, BF56D2A923DF88310006506D /* AppID.swift */, BF3D648722E79A3700E9056B /* AppPermission.swift */, @@ -1291,7 +1294,7 @@ BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */, BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */, BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */, - BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */, + BF6C336124197D700034FD24 /* NSError+AltStore.swift */, BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */, BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */, BF8CAE4D248AEABA004D6CCE /* UIDevice+Jailbreak.swift */, @@ -2005,7 +2008,7 @@ BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */, BF580482246A28F7008AE704 /* ViewController.swift in Sources */, BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */, - BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */, + BF58049B246A432D008AE704 /* NSError+AltStore.swift in Sources */, BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2024,6 +2027,7 @@ buildActionMask = 2147483647; files = ( BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, + BF56333824EC5E9A00038F00 /* SecureValueTransformer.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, @@ -2061,7 +2065,7 @@ BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */, - BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */, + BF6C336224197D700034FD24 /* NSError+AltStore.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 47f200b8..f2b40e50 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -77,6 +77,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ServerManager.shared.startDiscovering() + SecureValueTransformer.register() + UserDefaults.standard.registerDefaults() if UserDefaults.standard.firstLaunch == nil diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 79a6a15b..18747512 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -38,6 +38,7 @@ + @@ -1004,7 +1005,7 @@ World - + @@ -1034,6 +1035,7 @@ World + @@ -1044,7 +1046,7 @@ World - + diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 356e0309..e1803372 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -59,7 +59,7 @@ class BrowseViewController: UICollectionViewController self.updateDataSource() } - @IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue) + @IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue) { self.fetchSource() } @@ -199,6 +199,7 @@ private extension BrowseViewController if self.dataSource.itemCount > 0 { let toastView = ToastView(error: error) + toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.show(in: self) } diff --git a/AltStore/Components/BannerCollectionViewCell.swift b/AltStore/Components/BannerCollectionViewCell.swift index 74ef7251..a478940b 100644 --- a/AltStore/Components/BannerCollectionViewCell.swift +++ b/AltStore/Components/BannerCollectionViewCell.swift @@ -10,7 +10,8 @@ import UIKit class BannerCollectionViewCell: UICollectionViewCell { - @IBOutlet var bannerView: AppBannerView! + private(set) var errorBadge: UIView? + @IBOutlet private(set) var bannerView: AppBannerView! override func awakeFromNib() { @@ -18,5 +19,36 @@ class BannerCollectionViewCell: UICollectionViewCell self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.preservesSuperviewLayoutMargins = true + + if #available(iOS 13.0, *) + { + let errorBadge = UIView() + errorBadge.translatesAutoresizingMaskIntoConstraints = false + errorBadge.isHidden = true + self.addSubview(errorBadge) + + // Solid background to make the X opaque white. + let backgroundView = UIView() + backgroundView.translatesAutoresizingMaskIntoConstraints = false + backgroundView.backgroundColor = .white + errorBadge.addSubview(backgroundView) + + let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill")) + badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large) + badgeView.tintColor = .systemRed + errorBadge.addSubview(badgeView, pinningEdgesWith: .zero) + + NSLayoutConstraint.activate([ + errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5), + errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5), + + backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor), + backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor), + backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5), + backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5) + ]) + + self.errorBadge = errorBadge + } } } diff --git a/AltStore/Extensions/NSError+LocalizedFailure.swift b/AltStore/Extensions/NSError+AltStore.swift similarity index 68% rename from AltStore/Extensions/NSError+LocalizedFailure.swift rename to AltStore/Extensions/NSError+AltStore.swift index adf9d23b..847d5a87 100644 --- a/AltStore/Extensions/NSError+LocalizedFailure.swift +++ b/AltStore/Extensions/NSError+AltStore.swift @@ -1,5 +1,5 @@ // -// NSError+LocalizedFailure.swift +// NSError+AltStore.swift // AltStore // // Created by Riley Testut on 3/11/20. @@ -27,6 +27,21 @@ extension NSError let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo) return error } + + func sanitizedForCoreData() -> NSError + { + var userInfo = self.userInfo + userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure + userInfo[NSLocalizedDescriptionKey] = self.localizedDescription + userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason + userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion + + // Remove non-ObjC-compliant userInfo values. + userInfo["NSCodingPath"] = nil + + let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo) + return error + } } protocol ALTLocalizedError: LocalizedError, CustomNSError diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index b922681d..203264c1 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -220,6 +220,7 @@ extension AppManager case .success(let source): fetchedSources.insert(source) case .failure(let error): let source = managedObjectContext.object(with: source.objectID) as! Source + source.error = (error as NSError).sanitizedForCoreData() errors[source] = error } diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore 6.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore 6.xcdatamodel/contents index 3a22e4d0..7e9c5bfb 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore 6.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore 6.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -104,6 +104,7 @@ + @@ -166,7 +167,7 @@ - + diff --git a/AltStore/Model/SecureValueTransformer.swift b/AltStore/Model/SecureValueTransformer.swift new file mode 100644 index 00000000..018df70d --- /dev/null +++ b/AltStore/Model/SecureValueTransformer.swift @@ -0,0 +1,26 @@ +// +// SecureValueTransformer.swift +// AltStore +// +// Created by Riley Testut on 8/18/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +@objc(ALTSecureValueTransformer) +final class SecureValueTransformer: NSSecureUnarchiveFromDataTransformer +{ + static let name = NSValueTransformerName(rawValue: "ALTSecureValueTransformer") + + override static var allowedTopLevelClasses: [AnyClass] { + let allowedClasses = super.allowedTopLevelClasses + [NSError.self] + return allowedClasses + } + + public static func register() + { + let transformer = SecureValueTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/AltStore/Model/Source.swift b/AltStore/Model/Source.swift index 8e4bf2b8..6403f075 100644 --- a/AltStore/Model/Source.swift +++ b/AltStore/Model/Source.swift @@ -43,6 +43,8 @@ class Source: NSManagedObject, Fetchable, Decodable @NSManaged var identifier: String @NSManaged var sourceURL: URL + @NSManaged var error: NSError? + /* Non-Core Data Properties */ var userInfo: [ALTSourceUserInfoKey: String]? diff --git a/AltStore/News/NewsViewController.swift b/AltStore/News/NewsViewController.swift index 5c7eb3e5..4129d1bc 100644 --- a/AltStore/News/NewsViewController.swift +++ b/AltStore/News/NewsViewController.swift @@ -99,6 +99,11 @@ class NewsViewController: UICollectionViewController self.collectionView.contentInset.bottom = 20 } } + + @IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue) + { + self.fetchSource() + } } private extension NewsViewController @@ -198,6 +203,7 @@ private extension NewsViewController if self.dataSource.itemCount > 0 { let toastView = ToastView(error: error) + toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside) toastView.show(in: self) } diff --git a/AltStore/Sources/SourcesViewController.swift b/AltStore/Sources/SourcesViewController.swift index 7ab840df..f38de96f 100644 --- a/AltStore/Sources/SourcesViewController.swift +++ b/AltStore/Sources/SourcesViewController.swift @@ -53,6 +53,8 @@ private extension SourcesViewController cell.bannerView.subtitleLabel.text = source.sourceURL.absoluteString cell.bannerView.subtitleLabel.numberOfLines = 2 + cell.errorBadge?.isHidden = (source.error == nil) + // Make sure refresh button is correct size. cell.layoutIfNeeded() } @@ -98,6 +100,29 @@ private extension SourcesViewController self.present(alertController, animated: true, completion: nil) } + + func present(_ error: Error) + { + let nsError = error as NSError + let message = nsError.userInfo[NSDebugDescriptionErrorKey] as? String ?? nsError.localizedRecoverySuggestion + + let alertController = UIAlertController(title: error.localizedDescription, message: message, preferredStyle: .alert) + alertController.addAction(.ok) + self.present(alertController, animated: true, completion: nil) + } +} + +extension SourcesViewController +{ + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) + { + self.collectionView.deselectItem(at: indexPath, animated: true) + + let source = self.dataSource.item(at: indexPath) + guard let error = source.error else { return } + + self.present(error) + } } extension SourcesViewController: UICollectionViewDelegateFlowLayout @@ -134,9 +159,13 @@ extension SourcesViewController override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let source = self.dataSource.item(at: indexPath) - guard source.identifier != Source.altStoreIdentifier else { return nil } return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in + let viewErrorAction = UIAction(title: NSLocalizedString("View Error", comment: ""), image: UIImage(systemName: "exclamationmark.circle")) { (action) in + guard let error = source.error else { return } + self.present(error) + } + let deleteAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in @@ -153,8 +182,20 @@ extension SourcesViewController } } } + + var actions: [UIAction] = [] + + if source.error != nil + { + actions.append(viewErrorAction) + } + + if source.identifier != Source.altStoreIdentifier + { + actions.append(deleteAction) + } - let menu = UIMenu(title: "", children: [deleteAction]) + let menu = UIMenu(title: "", children: actions) return menu } } diff --git a/AltStore/TabBarController.swift b/AltStore/TabBarController.swift index 904c28a3..cb200254 100644 --- a/AltStore/TabBarController.swift +++ b/AltStore/TabBarController.swift @@ -30,6 +30,14 @@ class TabBarController: UITabBarController } } +extension TabBarController +{ + @objc func presentSources(_ sender: Any) + { + self.performSegue(withIdentifier: "presentSources", sender: sender) + } +} + private extension TabBarController { @objc func openPatreonSettings(_ notification: Notification)