diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 24f6e3c85..4b8563f6c 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -113,7 +113,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, let dataFolder: String let database: ArticlesDatabase - let delegate: AccountDelegate + var delegate: AccountDelegate static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0) private var unreadCounts = [String: Int]() // [feedID: Int] @@ -228,6 +228,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil) + delegate.credentials = try? retrieveBasicCredentials() + pullObjectsFromDisk() DispatchQueue.main.async { @@ -242,99 +244,32 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, // MARK: - API public func storeCredentials(_ credentials: Credentials) throws { - - guard let username = credentials.username, let password = credentials.password, let server = delegate.server else { + guard let server = delegate.server else { throw CredentialsError.incompleteCredentials } - self.username = username - - let passwordData = password.data(using: String.Encoding.utf8)! - - let updateQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrAccount as String: username, - kSecAttrServer as String: server] - let attributes: [String: Any] = [kSecValueData as String: passwordData] - let status = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary) - - switch status { - case errSecSuccess: - return - case errSecItemNotFound: - break - default: - throw CredentialsError.unhandledError(status: status) - } - - guard status == errSecItemNotFound else { - return + switch credentials { + case .basic(let username, _): + self.username = username } - let addQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrAccount as String: username, - kSecAttrServer as String: server, - kSecValueData as String: passwordData] - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - if addStatus != errSecSuccess { - throw CredentialsError.unhandledError(status: status) - } + try CredentialsManager.storeCredentials(credentials, server: server) } - public func retrieveCredentials() throws -> Credentials? { - + public func retrieveBasicCredentials() throws -> Credentials? { guard let username = self.username, let server = delegate.server else { return nil } - - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrAccount as String: username, - kSecAttrServer as String: server, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true] - - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - - guard status != errSecItemNotFound else { - return nil - } - - guard status == errSecSuccess else { - throw CredentialsError.unhandledError(status: status) - } - - guard let existingItem = item as? [String : Any], - let passwordData = existingItem[kSecValueData as String] as? Data, - let password = String(data: passwordData, encoding: String.Encoding.utf8) else { - return nil - } - - return BasicCredentials(username: username, password: password) - + return try CredentialsManager.retrieveBasicCredentials(server: server, username: username) } - public func removeCredentials() throws { - + public func removeBasicCredentials() throws { guard let username = self.username, let server = delegate.server else { return } - - let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrAccount as String: username, - kSecAttrServer as String: server, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnAttributes as String: true, - kSecReturnData as String: true] - - let status = SecItemDelete(query as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - throw CredentialsError.unhandledError(status: status) - } - + try CredentialsManager.removeBasicCredentials(server: server, username: username) self.username = nil - } public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index d99477fea..f0abdc5a3 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -214,26 +214,26 @@ 848934EC1F62484F00CEBD24 = { isa = PBXGroup; children = ( - 846E77531F6F00E300A165E2 /* AccountManager.swift */, 848935101F62486800CEBD24 /* Account.swift */, - 84AF4EA3222CFDD100F6A800 /* AccountSettings.swift */, 841974241F6DDCE4006346C4 /* AccountDelegate.swift */, - 841974001F6DD1EC006346C4 /* Folder.swift */, + 846E77531F6F00E300A165E2 /* AccountManager.swift */, + 84AF4EA3222CFDD100F6A800 /* AccountSettings.swift */, + 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, + 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, + 8419740D1F6DD25F006346C4 /* Container.swift */, + 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */, + 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, 844B297C2106C7EC004020B3 /* Feed.swift */, 84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */, + 841974001F6DD1EC006346C4 /* Folder.swift */, 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */, - 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */, - 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, - 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, - 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, - 8419740D1F6DD25F006346C4 /* Container.swift */, - 8419742B1F6DDE84006346C4 /* LocalAccount */, + 848935031F62484F00CEBD24 /* AccountTests */, 84245C7D1FDDD2580074AFBB /* Feedbin */, 8469F80F1F6DC3C10084783E /* Frameworks */, - 848934FA1F62484F00CEBD24 /* Info.plist */, - 848935031F62484F00CEBD24 /* AccountTests */, + 8419742B1F6DDE84006346C4 /* LocalAccount */, 848934F71F62484F00CEBD24 /* Products */, D511EEB4202422BB00712EC3 /* xcconfig */, + 848934FA1F62484F00CEBD24 /* Info.plist */, ); sourceTree = ""; usesTabs = 1; diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 022dc1762..6766ef848 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -14,8 +14,7 @@ public protocol AccountDelegate { // Local account does not; some synced accounts might. var supportsSubFolders: Bool { get } var server: String? { get } - - static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) + var credentials: Credentials? { get set } var refreshProgress: DownloadProgress { get } @@ -34,4 +33,7 @@ public protocol AccountDelegate { // Saved to disk with other Account data. Could be called at any time. // And called many times. func userInfo(for: Account) -> NSDictionary? + + static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) + } diff --git a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift index 23e4d71c2..feda37dcd 100644 --- a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift +++ b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift @@ -26,12 +26,12 @@ class AccountCredentialsTest: XCTestCase { // Make sure any left over from failed tests are gone do { - try account.removeCredentials() + try account.removeBasicCredentials() } catch { XCTFail(error.localizedDescription) } - var credentials: Credentials? = BasicCredentials(username: "maurice", password: "hardpasswd") + var credentials: Credentials? = Credentials.basic(username: "maurice", password: "hardpasswd") // Store the credentials do { @@ -43,15 +43,19 @@ class AccountCredentialsTest: XCTestCase { // Retrieve them credentials = nil do { - credentials = try account.retrieveCredentials() + credentials = try account.retrieveBasicCredentials() } catch { XCTFail(error.localizedDescription) } - XCTAssertEqual("maurice", credentials!.username) - XCTAssertEqual("hardpasswd", credentials!.password) + + switch credentials! { + case .basic(let username, let password): + XCTAssertEqual("maurice", username) + XCTAssertEqual("hardpasswd", password) + } // Update them - credentials?.password = "easypasswd" + credentials = Credentials.basic(username: "maurice", password: "easypasswd") do { try account.storeCredentials(credentials!) } catch { @@ -61,23 +65,27 @@ class AccountCredentialsTest: XCTestCase { // Retrieve them again credentials = nil do { - credentials = try account.retrieveCredentials() + credentials = try account.retrieveBasicCredentials() } catch { XCTFail(error.localizedDescription) } - XCTAssertEqual("maurice", credentials!.username) - XCTAssertEqual("easypasswd", credentials!.password) - + + switch credentials! { + case .basic(let username, let password): + XCTAssertEqual("maurice", username) + XCTAssertEqual("easypasswd", password) + } + // Delete them do { - try account.removeCredentials() + try account.removeBasicCredentials() } catch { XCTFail(error.localizedDescription) } // Make sure they are gone do { - try credentials = account.retrieveCredentials() + try credentials = account.retrieveBasicCredentials() } catch { XCTFail(error.localizedDescription) } diff --git a/Frameworks/Account/AccountTests/NilTransport.swift b/Frameworks/Account/AccountTests/NilTransport.swift index 693edc2de..d8c2566f5 100644 --- a/Frameworks/Account/AccountTests/NilTransport.swift +++ b/Frameworks/Account/AccountTests/NilTransport.swift @@ -11,8 +11,11 @@ import RSWeb struct NilTransport: Transport { - func send(request: URLRequest, completion: @escaping (Result) -> Void) { - completion(.success(Data())) + func send(request: URLRequest, resultType: T.Type, completion: @escaping (Result<(HTTPHeaders, T), Error>) -> Void) where T : Decodable, T : Encodable { + } + + + func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) { } } diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index 53785f633..e8ed64ecc 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -13,13 +13,14 @@ final class FeedbinAPICaller: NSObject { private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")! private var transport: Transport! + var credentials: Credentials? init(transport: Transport) { super.init() self.transport = transport } - func validateCredentials(credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) { + func validateCredentials(completionHandler completion: @escaping (Result) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("authentication.json") let request = URLRequest(url: callURL, credentials: credentials) @@ -27,20 +28,38 @@ final class FeedbinAPICaller: NSObject { transport.send(request: request) { result in switch result { case .success: - handler(.success(true)) + completion(.success(true)) case .failure(let error): switch error { case TransportError.httpError(let status): if status == 401 { - handler(.success(false)) + completion(.success(false)) } else { - handler(.failure(error)) + completion(.failure(error)) } default: - handler(.failure(error)) + completion(.failure(error)) } } } } + + func retrieveSubscriptions(completionHandler completion: @escaping (Result<[FeedbinFeed], Error>) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, resultType: [FeedbinFeed].self) { result in + switch result { + case .success(let (headers, feeds)): + break // TODO: put pageing implementation here + case .failure(let error): + completion(.failure(error)) + } + + } + + } + } diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 6c55f6502..76d7fa7b2 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -15,17 +15,23 @@ final class FeedbinAccountDelegate: AccountDelegate { let server: String? = "api.feedbin.com" private let caller: FeedbinAPICaller + var credentials: Credentials? { + didSet { + caller.credentials = credentials + } + } init(transport: Transport) { - caller = FeedbinAPICaller(transport: transport) + caller = FeedbinAPICaller(transport: transport) } var refreshProgress = DownloadProgress(numberOfTasks: 0) static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) { - let caller = FeedbinAPICaller(transport: transport) - caller.validateCredentials(credentials: credentials) { result in + let caller = FeedbinAPICaller(transport: transport) + caller.credentials = credentials + caller.validateCredentials() { result in handler(result) } diff --git a/Frameworks/Account/Feedbin/FeedbinFeed.swift b/Frameworks/Account/Feedbin/FeedbinFeed.swift index d3a81b71e..835d296a3 100644 --- a/Frameworks/Account/Feedbin/FeedbinFeed.swift +++ b/Frameworks/Account/Feedbin/FeedbinFeed.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import RSParser -struct FeedbinFeed { +struct FeedbinFeed: Codable { // https://github.com/feedbin/feedbin-api/blob/master/content/feeds.md // @@ -28,45 +28,13 @@ struct FeedbinFeed { let url: String let homePageURL: String? - struct Key { - static let subscriptionID = "id" - static let feedID = "feed_id" - static let creationDate = "created_at" - static let name = "title" - static let url = "feed_url" - static let homePageURL = "site_url" + enum CodingKeys: String, CodingKey { + case subscriptionID = "id" + case feedID = "feed_id" + case creationDate = "created_at" + case name = "title" + case url = "feed_url" + case homePageURL = "site_url" } - init?(dictionary: JSONDictionary) { - - guard let subscriptionID = dictionary[Key.subscriptionID] as? Int else { - return nil - } - guard let feedID = dictionary[Key.feedID] as? Int else { - return nil - } - guard let url = dictionary[Key.url] as? String else { - return nil - } - - self.subscriptionID = subscriptionID - self.feedID = feedID - self.url = url - - if let creationDateString = dictionary[Key.creationDate] as? String { - self.creationDate = RSDateWithString(creationDateString) - } - else { - self.creationDate = nil - } - - self.name = dictionary[Key.name] as? String - self.homePageURL = dictionary[Key.homePageURL] as? String - } - - static func feeds(with array: JSONArray) -> [FeedbinFeed]? { - - let subs = array.compactMap { FeedbinFeed(dictionary: $0) } - return subs.isEmpty ? nil : subs - } } diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index d3be8426d..2f5e525c7 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -13,6 +13,7 @@ final class LocalAccountDelegate: AccountDelegate { let supportsSubFolders = false let server: String? = nil + var credentials: Credentials? private let refresher = LocalAccountRefresher() diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index ff8b6a3b9..f58891d1f 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -27,9 +27,11 @@ class AccountsFeedbinWindowController: NSWindowController { } override func windowDidLoad() { - if let account = account, let credentials = try? account.retrieveCredentials() { - usernameTextField.stringValue = credentials.username ?? "" - passwordTextField.stringValue = credentials.password ?? "" + if let account = account, let credentials = try? account.retrieveBasicCredentials() { + if case .basic(let username, let password) = credentials { + usernameTextField.stringValue = username + passwordTextField.stringValue = password + } actionButton.title = NSLocalizedString("Update", comment: "Update") } else { actionButton.title = NSLocalizedString("Create", comment: "Create") @@ -62,7 +64,7 @@ class AccountsFeedbinWindowController: NSWindowController { progressIndicator.isHidden = false progressIndicator.startAnimation(self) - let credentials = BasicCredentials(username: usernameTextField.stringValue, password: passwordTextField.stringValue) + let credentials = Credentials.basic(username: usernameTextField.stringValue, password: passwordTextField.stringValue) Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in guard let self = self else { return } @@ -81,7 +83,7 @@ class AccountsFeedbinWindowController: NSWindowController { } do { - try self.account?.removeCredentials() + try self.account?.removeBasicCredentials() try self.account?.storeCredentials(credentials) self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) } catch { diff --git a/submodules/RSWeb b/submodules/RSWeb index 731711fff..64732f6b0 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 731711fff487f923d5be8ea1f4a9c19a58f059c3 +Subproject commit 64732f6b09b56374059bc3feb1ba1afb859dd0fa