diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index f40d48c43..597fd920e 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -308,19 +308,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - public static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { - let grantingType: OAuthAuthorizationGranting.Type + internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { switch type { case .feedly: - grantingType = FeedlyAccountDelegate.self + return FeedlyAccountDelegate.environment.oauthAuthorizationClient default: - fatalError("\(type) does not support OAuth authorization code granting.") + fatalError("\(type) is not a client for OAuth authorization code granting.") } - - return grantingType.oauthAuthorizationClient } - - public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, client: OAuthAuthorizationClient) -> URLRequest { + + public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest { let grantingType: OAuthAuthorizationGranting.Type switch type { case .feedly: @@ -329,7 +326,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fatalError("\(type) does not support OAuth authorization code granting.") } - return grantingType.oauthAuthorizationCodeGrantRequest(for: client) + return grantingType.oauthAuthorizationCodeGrantRequest() } public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, @@ -346,7 +343,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fatalError("\(accountType) does not support OAuth authorization code granting.") } - grantingType.requestOAuthAccessToken(with: response, client: client, transport: transport, completionHandler: completionHandler) + grantingType.requestOAuthAccessToken(with: response, transport: transport, completionHandler: completionHandler) } public func refreshAll(completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index e0f88b7d8..8044c4679 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -120,6 +120,8 @@ 9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; }; 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; }; 9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */; }; + 9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */; }; + 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */; }; 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; }; @@ -320,6 +322,8 @@ 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = ""; }; 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = ""; }; 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesService.swift; sourceTree = ""; }; + 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OAuthAuthorizationClient+Feedly.swift"; sourceTree = ""; }; + 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAccountAuthorizationOperation.swift; sourceTree = ""; }; 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = ""; }; 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = ""; }; 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = ""; }; @@ -616,7 +620,9 @@ 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */, 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */, 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */, + 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */, 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */, + 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */, 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */, 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */, 9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */, @@ -716,7 +722,9 @@ isa = PBXNativeTarget; buildConfigurationList = 8489350A1F62485000CEBD24 /* Build configuration list for PBXNativeTarget "Account" */; buildPhases = ( + 9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */, 848934F11F62484F00CEBD24 /* Sources */, + 9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */, 848934F21F62484F00CEBD24 /* Frameworks */, 848934F31F62484F00CEBD24 /* Headers */, 848934F41F62484F00CEBD24 /* Resources */, @@ -889,6 +897,42 @@ shellPath = /bin/sh; shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; }; + 9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script: Update OAuthAuthorizationClient+Feedly.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "FAILED=false\n\nif [ -z \"${FEEDLY_CLIENT_ID}\" ]; then\necho \"Missing Feedly Client ID\"\nFAILED=true\nfi\n\nif [ -z \"${FEEDLY_CLIENT_SECRET}\" ]; then\necho \"Missing Feedly Client Secret\"\nFAILED=true\nfi\n\nFEEDLY_CLIENT_SOURCE=\"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedly client ID or secret. ${FEEDLY_CLIENT_SOURCE} not changed.\"\nexit 0\nfi\n\n# echo \"Substituting variables in: ${FEEDLY_CLIENT_SOURCE}\"\n\nif [ -e \"${FEEDLY_CLIENT_SOURCE}\" ]\nthen\n sed -i .tmp \"s|{FEEDLY_CLIENT_ID}|${FEEDLY_CLIENT_ID}|g; s|{FEEDLY_CLIENT_SECRET}|${FEEDLY_CLIENT_SECRET}|g\" $FEEDLY_CLIENT_SOURCE\n # echo \"`git diff ${FEEDLY_CLIENT_SOURCE}`\"\n rm -f \"${FEEDLY_CLIENT_SOURCE}.tmp\"\nelse\n echo \"File does not exist at ${FEEDLY_CLIENT_SOURCE}. Has it been moved or renamed?\"\n exit -1\nfi\n\necho \"All env values found!\"\n"; + }; + 9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script: Reset OAuthAuthorizationClient+Feedly.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "git checkout \"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -932,6 +976,7 @@ 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */, 9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */, 844B297D2106C7EC004020B3 /* Feed.swift in Sources */, + 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */, 9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */, 9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */, 9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */, @@ -972,6 +1017,7 @@ 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */, 9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */, 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */, + 9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */, 9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift index 983c28e45..cd1a988d3 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift @@ -130,7 +130,7 @@ class FeedlySyncAllOperationTests: XCTestCase { OperationQueue.main.addOperation(syncAll) - waitForExpectations(timeout: 2) + waitForExpectations(timeout: 5) } func performInitialSync() { diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index dfe4e9c99..38fd68734 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -31,20 +31,10 @@ final class FeedlyAPICaller { var oauthAuthorizationClient: OAuthAuthorizationClient { switch self { - case .cloud: - /// Models private NetNewsWire client secrets. - /// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code - return OAuthAuthorizationClient(id: "{FEEDLY-ID}", - redirectUri: "{FEEDLY-REDIRECT-URI}", - state: nil, - secret: "{FEEDLY-SECRET}") case .sandbox: - /// Models public sandbox API values found at: - /// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw - return OAuthAuthorizationClient(id: "sandbox", - redirectUri: "http://localhost", - state: nil, - secret: "ReVGXA6WekanCxbf") + return .feedlySandboxClient + case .cloud: + return .feedlyCloudClient } } } diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift index abe0c465a..f0c3bae48 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift @@ -27,11 +27,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions" - static var oauthAuthorizationClient: OAuthAuthorizationClient { - return environment.oauthAuthorizationClient - } - - static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest { + static func oauthAuthorizationCodeGrantRequest() -> URLRequest { + let client = environment.oauthAuthorizationClient let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id, redirectUri: client.redirectUri, scope: oauthAuthorizationGrantScope, @@ -40,7 +37,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents) } - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result) -> ()) { + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result) -> ()) { + let client = environment.oauthAuthorizationClient let request = OAuthAccessTokenRequest(authorizationResponse: response, scope: oauthAuthorizationGrantScope, client: client) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 37e33ff16..2c35ffec3 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -17,6 +17,7 @@ final class FeedlyAccountDelegate: AccountDelegate { /// Feedly has a sandbox API and a production API. /// This property is referred to when clients need to know which environment it should be pointing to. + /// The value of this proptery must match any `OAuthAuthorizationClient` used. static var environment: FeedlyAPICaller.API { #if DEBUG // https://developer.feedly.com/v3/developer/ @@ -24,7 +25,6 @@ final class FeedlyAccountDelegate: AccountDelegate { return .cloud } return .sandbox - #else return .cloud #endif @@ -53,6 +53,8 @@ final class FeedlyAccountDelegate: AccountDelegate { } } + let oauthAuthorizationClient: OAuthAuthorizationClient + var accountMetadata: AccountMetadata? var refreshProgress = DownloadProgress(numberOfTasks: 0) @@ -97,6 +99,7 @@ final class FeedlyAccountDelegate: AccountDelegate { let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") self.database = SyncDatabase(databaseFilePath: databaseFilePath) + self.oauthAuthorizationClient = api.oauthAuthorizationClient } // MARK: Account API @@ -484,8 +487,7 @@ final class FeedlyAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { credentials = try? account.retrieveCredentials(type: .oauthAccessToken) - let client = FeedlyAccountDelegate.oauthAuthorizationClient - let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: client, log: log) + let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log) operationQueue.addOperation(refreshAccessToken) } diff --git a/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift new file mode 100644 index 000000000..72e867ef5 --- /dev/null +++ b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -0,0 +1,184 @@ +// +// OAuthAccountAuthorizationOperation.swift +// NetNewsWire +// +// Created by Kiel Gillard on 8/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import AuthenticationServices + +public protocol OAuthAccountAuthorizationOperationDelegate: class { + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) +} + +public final class OAuthAccountAuthorizationOperation: Operation, ASWebAuthenticationPresentationContextProviding { + + public weak var presentationAnchor: ASPresentationAnchor? + public weak var delegate: OAuthAccountAuthorizationOperationDelegate? + + private let accountType: AccountType + private let oauthClient: OAuthAuthorizationClient + private var session: ASWebAuthenticationSession? + + public init(accountType: AccountType) { + self.accountType = accountType + self.oauthClient = Account.oauthAuthorizationClient(for: accountType) + } + + override public func main() { + assert(Thread.isMainThread) + assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") + + guard !isCancelled else { + didFinish() + return + } + + let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType) + + guard let url = request.url else { + return DispatchQueue.main.async { + self.didEndAuthentication(url: nil, error: URLError(.badURL)) + } + } + + guard let redirectUri = URL(string: oauthClient.redirectUri), let scheme = redirectUri.scheme else { + assertionFailure("Could not get callback URL scheme from \(oauthClient.redirectUri)") + return DispatchQueue.main.async { + self.didEndAuthentication(url: nil, error: URLError(.badURL)) + } + } + + let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { url, error in + DispatchQueue.main.async { [weak self] in + self?.didEndAuthentication(url: url, error: error) + } + } + self.session = session + session.presentationContextProvider = self + + session.start() + } + + override public func cancel() { + session?.cancel() + super.cancel() + } + + private func didEndAuthentication(url: URL?, error: Error?) { + guard !isCancelled else { + didFinish() + return + } + + do { + guard let url = url else { + if let error = error { + throw error + } + throw URLError(.badURL) + } + + let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) + + Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completionHandler: didEndRequestingAccessToken(_:)) + + } catch is ASWebAuthenticationSessionError { + didFinish() // Primarily, cancellation. + + } catch { + didFinish(error) + } + } + + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + guard let anchor = presentationAnchor else { + fatalError("\(self) has outlived presentation anchor.") + } + return anchor + } + + private func didEndRequestingAccessToken(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let tokenResponse): + saveAccount(for: tokenResponse) + case .failure(let error): + didFinish(error) + } + } + + private func saveAccount(for grant: OAuthAuthorizationGrant) { + // TODO: Find an already existing account for this username? + let account = AccountManager.shared.createAccount(type: .feedly) + do { + + // Store the refresh token first because it sends this token to the account delegate. + if let token = grant.refreshToken { + try account.storeCredentials(token) + } + + // Now store the access token because we want the account delegate to use it. + try account.storeCredentials(grant.accessToken) + + didFinish() + } catch { + didFinish(error) + } + } + + // MARK: Managing Operation State + + private func didFinish() { + assert(Thread.isMainThread) + assert(!isFinished, "Finished operation is attempting to finish again.") + self.isExecutingOperation = false + self.isFinishedOperation = true + } + + private func didFinish(_ error: Error) { + assert(Thread.isMainThread) + assert(!isFinished, "Finished operation is attempting to finish again.") + delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error) + didFinish() + } + + override public func start() { + isExecutingOperation = true + DispatchQueue.main.async { + self.main() + } + } + + override public var isExecuting: Bool { + return isExecutingOperation + } + + private var isExecutingOperation = false { + willSet { + willChangeValue(for: \.isExecuting) + } + didSet { + didChangeValue(for: \.isExecuting) + } + } + + override public var isFinished: Bool { + return isFinishedOperation + } + + private var isFinishedOperation = false { + willSet { + willChangeValue(for: \.isFinished) + } + didSet { + didChangeValue(for: \.isFinished) + } + } +} diff --git a/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift b/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift new file mode 100644 index 000000000..c114bae87 --- /dev/null +++ b/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift @@ -0,0 +1,34 @@ +// +// OAuthAuthorizationClient+NetNewsWire.swift +// Account +// +// Created by Kiel Gillard on 8/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation + +extension OAuthAuthorizationClient { + + static var feedlyCloudClient: OAuthAuthorizationClient { + /// Models private NetNewsWire client secrets. + /// These placeholders are substitued at build time using a Run Script phase with build settings. + /// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code + return OAuthAuthorizationClient(id: "{FEEDLY_CLIENT_ID}", + redirectUri: "netnewswire://auth/feedly", + state: nil, + secret: "{FEEDLY_CLIENT_SECRET}") + } + + static var feedlySandboxClient: OAuthAuthorizationClient { + /// We use this funky redirect URI because ASWebAuthenticationSession will try to load http://localhost URLs. + /// See https://developer.feedly.com/v3/sandbox/ for more information. + /// The return value models public sandbox API values found at: + /// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw + /// They are due to expire on November 30 2019. + return OAuthAuthorizationClient(id: "sandbox", + redirectUri: "urn:ietf:wg:oauth:2.0:oob", + state: nil, + secret: "ReVGXA6WekanCxbf") + } +} diff --git a/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift b/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift index 433687c32..3f1aabbaf 100644 --- a/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift +++ b/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift @@ -61,7 +61,7 @@ public struct OAuthAuthorizationResponse { public extension OAuthAuthorizationResponse { init(url: URL, client: OAuthAuthorizationClient) throws { - guard let host = url.host, client.redirectUri.contains(host) else { + guard let scheme = url.scheme, client.redirectUri.hasPrefix(scheme) else { throw URLError(.unsupportedURL) } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { @@ -165,10 +165,8 @@ public protocol OAuthAuthorizationCodeGrantRequesting { } protocol OAuthAuthorizationGranting: AccountDelegate { + + static func oauthAuthorizationCodeGrantRequest() -> URLRequest - static var oauthAuthorizationClient: OAuthAuthorizationClient { get } - - static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest - - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result) -> ()) + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result) -> ()) } diff --git a/Frameworks/Account/xcconfig/Account_project.xcconfig b/Frameworks/Account/xcconfig/Account_project.xcconfig index f1144a6e9..b7f10e36e 100644 --- a/Frameworks/Account/xcconfig/Account_project.xcconfig +++ b/Frameworks/Account/xcconfig/Account_project.xcconfig @@ -9,7 +9,7 @@ PROVISIONING_PROFILE_SPECIFIER = #include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig" SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.14 +MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator @@ -18,7 +18,6 @@ SWIFT_VERSION = 5.1 COMBINE_HIDPI_IMAGES = YES COPY_PHASE_STRIP = NO -MACOSX_DEPLOYMENT_TARGET = 10.14 ALWAYS_SEARCH_USER_PATHS = NO CURRENT_PROJECT_VERSION = 1 VERSION_INFO_PREFIX = diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index 02b87bd38..5574248ea 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -101,9 +101,10 @@ extension AccountsAddViewController: NSTableViewDelegate { accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsReaderAPIWindowController case .feedly: - let accountsFeedlyWindowController = AccountsFeedlyWebWindowController() - accountsFeedlyWindowController.runSheetOnWindow(self.view.window!) - accountsAddWindowController = accountsFeedlyWindowController + let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) + addAccount.delegate = self + addAccount.presentationAnchor = self.view.window! + OperationQueue.main.addOperation(addAccount) default: break } @@ -113,3 +114,12 @@ extension AccountsAddViewController: NSTableViewDelegate { } } + +// MARK: OAuthAccountAuthorizationOperationDelegate + +extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate { + + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { + view.window?.presentError(error) + } +} diff --git a/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib b/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib deleted file mode 100644 index 73455ac32..000000000 --- a/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift deleted file mode 100644 index 7529a8a9a..000000000 --- a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// AccountsFeedlyWebWindowController.swift -// NetNewsWire -// -// Created by Kiel Gillard on 30/8/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Cocoa -import Account -import WebKit - -class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegate { - - @IBOutlet private weak var webView: WKWebView! - - private weak var hostWindow: NSWindow? - - convenience init() { - self.init(windowNibName: NSNib.Name("AccountsFeedlyWeb")) - } - - // MARK: API - - func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) { - self.hostWindow = hostWindow - hostWindow.beginSheet(window!, completionHandler: handler) - beginAuthorization() - } - - // MARK: Requesting an Access Token - let client = Account.oauthAuthorizationClient(for: .feedly) - - private func beginAuthorization() { - let request = Account.oauthAuthorizationCodeGrantRequest(for: .feedly, client: client) - webView.load(request) - } - - private func requestAccessToken(for response: OAuthAuthorizationResponse) { - Account.requestOAuthAccessToken(with: response, client: client, accountType: .feedly) { [weak self] result in - switch result { - case .success(let tokenResponse): - self?.saveAccount(for: tokenResponse) - case .failure(let error): - NSApplication.shared.presentError(error) - } - } - } - - // MARK: Actions - - @IBAction func cancel(_ sender: Any) { - hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - - do { - guard let url = navigationAction.request.url else { return } - - let response = try OAuthAuthorizationResponse(url: url, client: client) - - requestAccessToken(for: response) - - // No point the web view trying to load this. - return decisionHandler(.cancel) - - } catch let error as OAuthAuthorizationErrorResponse { - NSApplication.shared.presentError(error) - - } catch { - print(error) - } - - decisionHandler(.allow) - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - print(error) - } - - private func saveAccount(for grant: OAuthAuthorizationGrant) { - // TODO: Find an already existing account for this username? - let account = AccountManager.shared.createAccount(type: .feedly) - do { - - // Store the refresh token first because it sends this token to the account delegate. - if let token = grant.refreshToken { - try account.storeCredentials(token) - } - - // Now store the access token because we want the account delegate to use it. - try account.storeCredentials(grant.accessToken) - - self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) - } catch { - NSApplication.shared.presentError(error) - } - } -} diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 94820373d..5732cbf80 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -276,7 +276,6 @@ 65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; }; 65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; 65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97621ED9EB96007D329B /* SidebarViewController.swift */; }; - 65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; }; 65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */; }; 65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */; }; @@ -307,7 +306,6 @@ 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; }; - 65ED3FEB235DEF6C0081F399 /* (null) in Sources */ = {isa = PBXBuildFile; }; 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; }; 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; @@ -407,7 +405,6 @@ 65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; 65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; }; 65ED4052235DEF6C0081F399 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; - 65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; }; 65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 848363062262A3DD00DA1D35 /* Main.storyboard */; }; 65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51EF0F8D2279C9260050506E /* AccountsAdd.xib */; }; 65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */; }; @@ -615,8 +612,6 @@ 84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; }; 84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; }; 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; - 9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; }; - 9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; }; B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; }; D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D553737C20186C1F006D8857 /* Article+Scriptability.swift */; }; D57BE6E0204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57BE6DF204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift */; }; @@ -1538,8 +1533,6 @@ 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = ""; }; 84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = ""; }; - 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedlyWebWindowController.swift; sourceTree = ""; }; - 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedlyWeb.xib; sourceTree = ""; }; B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; }; B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; }; B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; @@ -2510,8 +2503,6 @@ 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */, 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */, 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */, - 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */, - 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */, 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */, 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */, 5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */, @@ -3359,7 +3350,6 @@ 65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */, 65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */, 65ED4052235DEF6C0081F399 /* template.html in Resources */, - 65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */, 65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */, 65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */, 65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */, @@ -3445,7 +3435,6 @@ 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */, 845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */, 848362FF2262A30E00DA1D35 /* template.html in Resources */, - 9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */, 848363082262A3DD00DA1D35 /* Main.storyboard in Resources */, 51EF0F8E2279C9260050506E /* AccountsAdd.xib in Resources */, 84C9FC8F22629E8F00D921D6 /* NetNewsWire.sdef in Resources */, @@ -3762,7 +3751,6 @@ 65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */, 65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */, 65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */, - 65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */, 65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */, 65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */, 65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */, @@ -3793,7 +3781,6 @@ 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */, 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */, 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */, - 65ED3FEB235DEF6C0081F399 /* (null) in Sources */, 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */, 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */, 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */, @@ -4053,7 +4040,6 @@ 849C78922362AB04009A71E4 /* ExportOPMLWindowController.swift in Sources */, 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */, 849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */, - 9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */, 849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */, 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */, 8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */,