Get rid of SecretsManager. It wasn’t thread-safe, and it existed only for tests (and it wasn’t thread-safe for tests either). Pass SecretsProvider parameter where it’s needed.

This commit is contained in:
Brent Simmons 2024-03-10 22:22:41 -07:00
parent 13403df8f1
commit 78047fcaf7
31 changed files with 119 additions and 118 deletions

View File

@ -258,7 +258,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return delegate.refreshProgress
}
init(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) {
init(dataFolder: String, type: AccountType, accountID: String, secretsProvider: SecretsProvider, transport: Transport? = nil) {
switch type {
case .onMyMac:
self.delegate = LocalAccountDelegate()
@ -267,17 +267,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
case .feedbin:
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
case .feedly:
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment, secretsProvider: secretsProvider)
case .newsBlur:
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
case .freshRSS:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS)
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS, secretsProvider: secretsProvider)
case .inoreader:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader)
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader, secretsProvider: secretsProvider)
case .bazQux:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .bazQux)
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .bazQux, secretsProvider: secretsProvider)
case .theOldReader:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .theOldReader)
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .theOldReader, secretsProvider: secretsProvider)
}
self.delegate.accountMetadata = metadata
@ -367,29 +367,29 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
try CredentialsManager.removeCredentials(type: type, server: server, username: username)
}
public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) {
public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
switch type {
case .feedbin:
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, secretsProvider: secretsProvider, completion: completion)
case .newsBlur:
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, secretsProvider: secretsProvider, completion: completion)
case .freshRSS, .inoreader, .bazQux, .theOldReader:
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion)
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, secretsProvider: secretsProvider, completion: completion)
default:
break
}
}
internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient {
internal static func oauthAuthorizationClient(for type: AccountType, secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
switch type {
case .feedly:
return FeedlyAccountDelegate.environment.oauthAuthorizationClient
return FeedlyAccountDelegate.environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
default:
fatalError("\(type) is not a client for OAuth authorization code granting.")
}
}
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest {
public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, secretsProvider: SecretsProvider) -> URLRequest {
let grantingType: OAuthAuthorizationGranting.Type
switch type {
case .feedly:
@ -398,13 +398,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(type) does not support OAuth authorization code granting.")
}
return grantingType.oauthAuthorizationCodeGrantRequest()
return grantingType.oauthAuthorizationCodeGrantRequest(secretsProvider: secretsProvider)
}
public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse,
client: OAuthAuthorizationClient,
accountType: AccountType,
transport: Transport = URLSession.webserviceTransport(),
secretsProvider: SecretsProvider,
completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let grantingType: OAuthAuthorizationGranting.Type
@ -415,7 +416,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fatalError("\(accountType) does not support OAuth authorization code granting.")
}
grantingType.requestOAuthAccessToken(with: response, transport: transport, completion: completion)
grantingType.requestOAuthAccessToken(with: response, transport: transport, secretsProvider: secretsProvider, completion: completion)
}
public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {

View File

@ -52,7 +52,7 @@ protocol AccountDelegate {
func accountWillBeDeleted(_ account: Account)
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void)
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void)
/// Suspend all network activity
func suspendNetwork()

View File

@ -12,6 +12,7 @@ import RSWeb
import Articles
import ArticlesDatabase
import Database
import Secrets
// Main thread only.
@ -29,6 +30,8 @@ public final class AccountManager: UnreadCountProvider {
private let defaultAccountFolderName = "OnMyMac"
private let defaultAccountIdentifier = "OnMyMac"
private let secretsProvider: SecretsProvider
public var isSuspended = false
public var isUnreadCountsInitialized: Bool {
for account in activeAccounts {
@ -94,9 +97,11 @@ public final class AccountManager: UnreadCountProvider {
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
}
public init(accountsFolder: String) {
self.accountsFolder = accountsFolder
public init(accountsFolder: String, secretsProvider: SecretsProvider) {
self.accountsFolder = accountsFolder
self.secretsProvider = secretsProvider
// The local "On My Mac" account must always exist, even if it's empty.
let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac")
do {
@ -107,7 +112,7 @@ public final class AccountManager: UnreadCountProvider {
abort()
}
defaultAccount = Account(dataFolder: localAccountFolder, type: .onMyMac, accountID: defaultAccountIdentifier)
defaultAccount = Account(dataFolder: localAccountFolder, type: .onMyMac, accountID: defaultAccountIdentifier, secretsProvider: secretsProvider)
accountsDictionary[defaultAccount.accountID] = defaultAccount
readAccountsFromDisk()
@ -134,7 +139,7 @@ public final class AccountManager: UnreadCountProvider {
abort()
}
let account = Account(dataFolder: accountFolder, type: type, accountID: accountID)
let account = Account(dataFolder: accountFolder, type: type, accountID: accountID, secretsProvider: secretsProvider)
accountsDictionary[accountID] = account
var userInfo = [String: Any]()
@ -430,7 +435,7 @@ private extension AccountManager {
}
func loadAccount(_ accountSpecifier: AccountSpecifier) -> Account? {
return Account(dataFolder: accountSpecifier.folderPath, type: accountSpecifier.type, accountID: accountSpecifier.identifier)
return Account(dataFolder: accountSpecifier.folderPath, type: accountSpecifier.type, accountID: accountSpecifier.identifier, secretsProvider: secretsProvider)
}
func loadAccount(_ filename: String) -> Account? {

View File

@ -451,7 +451,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
articlesZone.resetChangeToken()
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil))
}

View File

@ -583,7 +583,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) {
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
let caller = FeedbinAPICaller(transport: transport)
caller.credentials = credentials

View File

@ -37,12 +37,12 @@ final class FeedlyAPICaller {
return components
}
var oauthAuthorizationClient: OAuthAuthorizationClient {
func oauthAuthorizationClient(secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
switch self {
case .sandbox:
return .feedlySandboxClient
case .cloud:
return .feedlyCloudClient
return OAuthAuthorizationClient.feedlyCloudClient(secretsProvider: secretsProvider)
}
}
}
@ -50,11 +50,13 @@ final class FeedlyAPICaller {
private let transport: Transport
private let baseUrlComponents: URLComponents
private let uriComponentAllowed: CharacterSet
private let secretsProvider: SecretsProvider
init(transport: Transport, api: API) {
init(transport: Transport, api: API, secretsProvider: SecretsProvider) {
self.transport = transport
self.baseUrlComponents = api.baseUrlComponents
self.secretsProvider = secretsProvider
var urlHostAllowed = CharacterSet.urlHostAllowed
urlHostAllowed.remove("+")
uriComponentAllowed = urlHostAllowed

View File

@ -25,11 +25,11 @@ public struct FeedlyOAuthAccessTokenResponse: Decodable, OAuthAccessTokenRespons
}
extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions"
static func oauthAuthorizationCodeGrantRequest() -> URLRequest {
let client = environment.oauthAuthorizationClient
static func oauthAuthorizationCodeGrantRequest(secretsProvider: SecretsProvider) -> URLRequest {
let client = environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id,
redirectUri: client.redirectUri,
scope: oauthAuthorizationGrantScope,
@ -38,12 +38,12 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting {
return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents)
}
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let client = environment.oauthAuthorizationClient
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, secretsProvider: SecretsProvider, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ()) {
let client = environment.oauthAuthorizationClient(secretsProvider: secretsProvider)
let request = OAuthAccessTokenRequest(authorizationResponse: response,
scope: oauthAuthorizationGrantScope,
client: client)
let caller = FeedlyAPICaller(transport: transport, api: environment)
let caller = FeedlyAPICaller(transport: transport, api: environment, secretsProvider: secretsProvider)
caller.requestAccessToken(request) { result in
switch result {
case .success(let response):

View File

@ -67,15 +67,15 @@ final class FeedlyAccountDelegate: AccountDelegate {
private weak var currentSyncAllOperation: MainThreadOperation?
private let operationQueue = MainThreadOperationQueue()
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API) {
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API, secretsProvider: SecretsProvider) {
// Many operations have their own operation queues, such as the sync all operation.
// Making this a serial queue at this higher level of abstraction means we can ensure,
// for example, a `FeedlyRefreshAccessTokenOperation` occurs before a `FeedlySyncAllOperation`,
// improving our ability to debug, reason about and predict the behaviour of the code.
if let transport = transport {
self.caller = FeedlyAPICaller(transport: transport, api: api)
self.caller = FeedlyAPICaller(transport: transport, api: api, secretsProvider: secretsProvider)
} else {
let sessionConfiguration = URLSessionConfiguration.default
@ -92,12 +92,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
let session = URLSession(configuration: sessionConfiguration)
self.caller = FeedlyAPICaller(transport: session, api: api)
self.caller = FeedlyAPICaller(transport: session, api: api, secretsProvider: secretsProvider)
}
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
self.database = SyncDatabase(databaseFilePath: databaseFilePath)
self.oauthAuthorizationClient = api.oauthAuthorizationClient
self.oauthAuthorizationClient = api.oauthAuthorizationClient(secretsProvider: secretsProvider)
self.caller.delegate = self
}
@ -539,7 +539,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
MainThreadOperationQueue.shared.add(logout)
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.")
completion(.success(credentials))
}

View File

@ -9,6 +9,7 @@
import Foundation
import AuthenticationServices
import RSCore
import Secrets
public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account)
@ -42,17 +43,19 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
private let accountType: AccountType
private let oauthClient: OAuthAuthorizationClient
private var session: ASWebAuthenticationSession?
public init(accountType: AccountType) {
private let secretsProvider: SecretsProvider
public init(accountType: AccountType, secretsProvider: SecretsProvider) {
self.accountType = accountType
self.oauthClient = Account.oauthAuthorizationClient(for: accountType)
self.secretsProvider = secretsProvider
self.oauthClient = Account.oauthAuthorizationClient(for: accountType, secretsProvider: secretsProvider)
}
public func run() {
assert(presentationAnchor != nil, "\(self) outlived presentation anchor.")
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType)
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, secretsProvider: secretsProvider)
guard let url = request.url else {
return DispatchQueue.main.async {
self.didEndAuthentication(url: nil, error: URLError(.badURL))
@ -113,7 +116,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
let response = try OAuthAuthorizationResponse(url: url, client: oauthClient)
Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completion: didEndRequestingAccessToken(_:))
Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, secretsProvider: secretsProvider, completion: didEndRequestingAccessToken(_:))
} catch is ASWebAuthenticationSessionError {
didFinish() // Primarily, cancellation.

View File

@ -11,14 +11,14 @@ import Secrets
extension OAuthAuthorizationClient {
static var feedlyCloudClient: OAuthAuthorizationClient {
static func feedlyCloudClient(secretsProvider: SecretsProvider) -> OAuthAuthorizationClient {
/// Models private NetNewsWire client secrets.
/// These placeholders are substituted 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: SecretsManager.provider.feedlyClientId,
return OAuthAuthorizationClient(id: secretsProvider.feedlyClientId,
redirectUri: "netnewswire://auth/feedly",
state: nil,
secret: SecretsManager.provider.feedlyClientSecret)
secret: secretsProvider.feedlyClientSecret)
}
static var feedlySandboxClient: OAuthAuthorizationClient {

View File

@ -17,7 +17,7 @@ public struct OAuthAuthorizationClient: Equatable {
public var redirectUri: String
public var state: String?
public var secret: String
public init(id: String, redirectUri: String, state: String?, secret: String) {
self.id = id
self.redirectUri = redirectUri
@ -167,7 +167,7 @@ public protocol OAuthAuthorizationCodeGrantRequesting {
protocol OAuthAuthorizationGranting: AccountDelegate {
static func oauthAuthorizationCodeGrantRequest() -> URLRequest
static func oauthAuthorizationCodeGrantRequest(secretsProvider: SecretsProvider) -> URLRequest
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, secretsProvider: SecretsProvider, completion: @escaping (Result<OAuthAuthorizationGrant, Error>) -> ())
}

View File

@ -197,7 +197,7 @@ final class LocalAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) {
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil))
}

View File

@ -612,7 +612,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
caller.logout() { _ in }
}
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> ()) {
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> ()) {
let caller = NewsBlurAPICaller(transport: transport)
caller.credentials = credentials
caller.validateCredentials() { result in

View File

@ -74,12 +74,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0)
init(dataFolder: String, transport: Transport?, variant: ReaderAPIVariant) {
init(dataFolder: String, transport: Transport?, variant: ReaderAPIVariant, secretsProvider: SecretsProvider) {
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
database = SyncDatabase(databaseFilePath: databaseFilePath)
if transport != nil {
caller = ReaderAPICaller(transport: transport!)
caller = ReaderAPICaller(transport: transport!, secretsProvider: secretsProvider)
} else {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
@ -94,7 +94,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
}
caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration))
caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration), secretsProvider: secretsProvider)
}
caller.variant = variant
@ -636,13 +636,13 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
func accountWillBeDeleted(_ account: Account) {
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider, completion: @escaping (Result<Credentials?, Error>) -> Void) {
guard let endpoint = endpoint else {
completion(.failure(TransportError.noURL))
return
}
let caller = ReaderAPICaller(transport: transport)
let caller = ReaderAPICaller(transport: transport, secretsProvider: secretsProvider)
caller.credentials = credentials
caller.validateCredentials(endpoint: endpoint) { result in
DispatchQueue.main.async {

View File

@ -48,6 +48,7 @@ final class ReaderAPICaller: NSObject {
}
private var transport: Transport!
private let secretsProvider: SecretsProvider
private let uriComponentAllowed: CharacterSet
private var accessToken: String?
@ -77,9 +78,10 @@ final class ReaderAPICaller: NSObject {
}
}
init(transport: Transport) {
init(transport: Transport, secretsProvider: SecretsProvider) {
self.transport = transport
self.secretsProvider = secretsProvider
var urlHostAllowed = CharacterSet.urlHostAllowed
urlHostAllowed.remove("+")
urlHostAllowed.remove("&")
@ -693,8 +695,8 @@ private extension ReaderAPICaller {
func addVariantHeaders(_ request: inout URLRequest) {
if variant == .inoreader {
request.addValue(SecretsManager.provider.inoreaderAppId, forHTTPHeaderField: "AppId")
request.addValue(SecretsManager.provider.inoreaderAppKey, forHTTPHeaderField: "AppKey")
request.addValue(secretsProvider.inoreaderAppId, forHTTPHeaderField: "AppId")
request.addValue(secretsProvider.inoreaderAppKey, forHTTPHeaderField: "AppKey")
}
}

View File

@ -19,10 +19,6 @@ class FeedlyTestSupport {
var refreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: "t3st-refresh-tok3n")
var transport = TestTransport()
init() {
SecretsManager.provider = FeedlyTestSecrets()
}
func makeMockNetworkStack() -> (TestTransport, FeedlyAPICaller) {
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
caller.credentials = accessToken

View File

@ -108,6 +108,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
#endif
private var themeImportPath: String?
private let secretsProvider = Secrets()
override init() {
NSWindow.allowsAutomaticWindowTabbing = false
@ -119,8 +120,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidat
crashReporter.enable()
#endif
SecretsManager.provider = Secrets()
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!, secretsProvider: secretsProvider)
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)

View File

@ -1257,7 +1257,7 @@ private extension MainWindowController {
}
func startArticleExtractorForCurrentLink() {
if let link = currentLink, let extractor = ArticleExtractor(link) {
if let link = currentLink, let extractor = ArticleExtractor(link, secretsProvider: Secrets()) {
extractor.delegate = self
extractor.process()
articleExtractor = extractor

View File

@ -79,8 +79,8 @@ class AccountsFeedbinWindowController: NSWindowController {
progressIndicator.startAnimation(self)
let credentials = Credentials(type: .basic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
Account.validateCredentials(type: .feedbin, credentials: credentials, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return }
self.actionButton.isEnabled = true

View File

@ -76,7 +76,7 @@ class AccountsNewsBlurWindowController: NSWindowController {
progressIndicator.startAnimation(self)
let credentials = Credentials(type: .newsBlurBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in
Account.validateCredentials(type: .newsBlur, credentials: credentials, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return }

View File

@ -174,7 +174,7 @@ extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelega
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
addAccountWindowController = accountsReaderAPIWindowController
case .feedly:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly, secretsProvider: Secrets())
addAccount.delegate = self
addAccount.presentationAnchor = self.view.window!
runAwaitingFeedlyLoginAlertModal(forLifetimeOf: addAccount)

View File

@ -131,8 +131,8 @@ class AccountsReaderAPIWindowController: NSWindowController {
progressIndicator.startAnimation(self)
let credentials = Credentials(type: .readerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL) { [weak self] result in
Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL, secretsProvider: Secrets()) { [weak self] result in
guard let self = self else { return }
self.actionButton.isEnabled = true

View File

@ -2,20 +2,23 @@
import PackageDescription
let package = Package(
name: "Secrets",
name: "Secrets",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Secrets",
products: [
.library(
name: "Secrets",
type: .dynamic,
targets: ["Secrets"]
)
],
dependencies: [],
targets: [
.target(
name: "Secrets",
dependencies: []
)
]
targets: ["Secrets"]
)
],
dependencies: [],
targets: [
.target(
name: "Secrets",
dependencies: [],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)

View File

@ -1,12 +0,0 @@
//
// SecretsManager.swift
//
//
// Created by Maurice Parker on 7/30/20.
//
import Foundation
public class SecretsManager {
public static var provider: SecretsProvider!
}

View File

@ -34,15 +34,15 @@ class ArticleExtractor {
private var url: URL!
public init?(_ articleLink: String) {
public init?(_ articleLink: String, secretsProvider: SecretsProvider) {
self.articleLink = articleLink
let clientURL = "https://extract.feedbin.com/parser"
let username = SecretsManager.provider.mercuryClientId
let signiture = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret)
let username = secretsProvider.mercuryClientId
let signature = articleLink.hmacUsingSHA1(key: secretsProvider.mercuryClientSecret)
if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() {
let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)"
let fullURL = "\(clientURL)/\(username)/\(signature)?base64_url=\(base64URL)"
if let url = URL(string: fullURL) {
self.url = url
return

View File

@ -30,6 +30,8 @@ class FeedbinAccountViewController: UITableViewController {
weak var account: Account?
weak var delegate: AddAccountDismissDelegate?
var secretsProvider: SecretsProvider!
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
@ -120,7 +122,7 @@ class FeedbinAccountViewController: UITableViewController {
setNavigationEnabled(to: false)
let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
Account.validateCredentials(type: .feedbin, credentials: credentials, secretsProvider: secretsProvider) { result in
self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true)

View File

@ -29,7 +29,7 @@ class NewsBlurAccountViewController: UITableViewController {
weak var account: Account?
weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
@ -105,7 +105,7 @@ class NewsBlurAccountViewController: UITableViewController {
disableNavigation()
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials, secretsProvider: Secrets()) { result in
self.stopAnimatingActivityIndicator()
self.enableNavigation()
@ -147,7 +147,6 @@ class NewsBlurAccountViewController: UITableViewController {
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}

View File

@ -157,7 +157,7 @@ class ReaderAPIAccountViewController: UITableViewController {
disableNavigation()
let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in
Account.validateCredentials(type: type, credentials: credentials, endpoint: url, secretsProvider: Secrets()) { result in
self.stopAnimatingActivityIndicator()
self.enableNavigation()
@ -199,7 +199,6 @@ class ReaderAPIAccountViewController: UITableViewController {
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}

View File

@ -58,16 +58,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
var isSyncArticleStatusRunning = false
var isWaitingForSyncTasks = false
private var secretsProvider = Secrets()
override init() {
super.init()
appDelegate = self
SecretsManager.provider = Secrets()
let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath, secretsProvider: secretsProvider)
let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString
let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7)))
ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath)

View File

@ -657,7 +657,7 @@ private extension WebViewController {
func startArticleExtractor() {
guard articleExtractor == nil else { return }
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) {
if let link = article?.preferredLink, let extractor = ArticleExtractor(link, secretsProvider: Secrets()) {
extractor.delegate = self
extractor.process()
articleExtractor = extractor

View File

@ -197,7 +197,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
addViewController.delegate = self
present(navController, animated: true)
case .feedly:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly, secretsProvider: Secrets())
addAccount.delegate = self
addAccount.presentationAnchor = self.view.window!
MainThreadOperationQueue.shared.add(addAccount)