Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions Simplified.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

74 changes: 53 additions & 21 deletions Simplified/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey"
// MARK: AccountDetails
// Extra data that gets loaded from an OPDS2AuthenticationDocument,
@objcMembers final class AccountDetails: NSObject {
enum AuthType: String {
enum AuthType: String, Codable {
case basic = "http://opds-spec.org/auth/basic"
case coppa = "http://librarysimplified.org/terms/authentication/gate/coppa"
case anonymous = "http://librarysimplified.org/rel/auth/anonymous"
case oauthIntermediary = "http://librarysimplified.org/authtype/OAuth-with-intermediary"
case saml = "http://librarysimplified.org/authtype/SAML-2.0"
case none
}

struct Authentication {
@objc(AccountDetailsAuthentication)
@objcMembers
class Authentication: NSObject, Codable, NSCoding {
let authType:AuthType
let authPasscodeLength:UInt
let patronIDKeyboard:LoginKeyboard
Expand All @@ -34,23 +38,58 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey"
pinLabel = auth.labels?.password
supportsBarcodeScanner = auth.inputs?.login.barcodeFormat == "Codabar"
supportsBarcodeDisplay = supportsBarcodeScanner
coppaUnderUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-not-met" })?.href ?? "")
coppaOverUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-met" })?.href ?? "")

switch authType {
case .coppa:
coppaUnderUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-not-met" })?.href ?? "")
coppaOverUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-met" })?.href ?? "")

case .none, .basic, .anonymous:
coppaUnderUrl = nil
coppaOverUrl = nil

}
}

var isCatalogSecured: Bool {
return authType == .oauthIntermediary || authType == .saml
}

func encode(with coder: NSCoder) {
let jsonEncoder = JSONEncoder()
guard let data = try? jsonEncoder.encode(self) else { return }
coder.encode(data as NSData)
}

required init?(coder: NSCoder) {
guard let data = coder.decodeData() else { return nil }
let jsonDecoder = JSONDecoder()
guard let authentication = try? jsonDecoder.decode(Authentication.self, from: data) else { return nil }

authType = authentication.authType
authPasscodeLength = authentication.authPasscodeLength
patronIDKeyboard = authentication.patronIDKeyboard
pinKeyboard = authentication.pinKeyboard
patronIDLabel = authentication.patronIDLabel
pinLabel = authentication.pinLabel
supportsBarcodeScanner = authentication.supportsBarcodeScanner
supportsBarcodeDisplay = authentication.supportsBarcodeDisplay
coppaUnderUrl = authentication.coppaUnderUrl
coppaOverUrl = authentication.coppaOverUrl
}
}

let defaults:UserDefaults
let uuid:String
let supportsSimplyESync:Bool
let supportsCardCreator:Bool
let supportsReservations:Bool
let auths: [Authentication]

let mainColor:String?
let userProfileUrl:String?
let signUpUrl:URL?
let loansUrl:URL?

var authType: AuthType {
return auths.first?.authType ?? .none
}
Expand Down Expand Up @@ -83,22 +122,15 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey"
return auths.first?.coppaOverUrl
}

var patronIDLabel: String? {
return auths.first?.patronIDLabel
}

var pinLabel: String? {
return auths.first?.pinLabel
}

var needsAuth:Bool {
return authType == .basic
}

var needsAgeCheck:Bool {
return authType == .coppa
var defaultAuth: Authentication? {
guard auths.count > 1 else { return auths.first }
return auths.first(where: { !$0.isCatalogSecured }) ?? auths.first
}

fileprivate var urlAnnotations:URL?
fileprivate var urlAcknowledgements:URL?
fileprivate var urlContentLicenses:URL?
Expand Down Expand Up @@ -333,7 +365,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey"

NYPLNetworkExecutor.shared.GET(url) { result in
switch result {
case .success(let serverData):
case .success(let serverData, _):
do {
self.authenticationDocument = try
OPDS2AuthenticationDocument.fromData(serverData)
Expand All @@ -345,7 +377,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey"
""")
completion(false)
}
case .failure(let error):
case .failure(let error, _):
Log.error(#file, """
Failed to load authentication document at URL \(url). Error: \(error)
""")
Expand Down Expand Up @@ -386,7 +418,7 @@ extension Account {
}

// MARK: LoginKeyboard
@objc enum LoginKeyboard: Int {
@objc enum LoginKeyboard: Int, Codable {
case standard
case email
case numeric
Expand Down
13 changes: 6 additions & 7 deletions Simplified/AccountsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,11 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf
NotificationCenter.default.post(name: NSNotification.Name.NYPLCurrentAccountDidChange, object: nil)
completion(true)
}
if self.currentAccount?.details?.needsAgeCheck ?? false {

if self.currentAccount?.details?.defaultAuth?.needsAgeCheck ?? false {
AgeCheck.shared().verifyCurrentAccountAgeRequirement { meetsAgeRequirement in
DispatchQueue.main.async {
mainFeed = meetsAgeRequirement ? self.currentAccount?.details?.coppaOverUrl : self.currentAccount?.details?.coppaUnderUrl
mainFeed = meetsAgeRequirement ? self.currentAccount?.details?.defaultAuth?.coppaOverUrl : self.currentAccount?.details?.defaultAuth?.coppaUnderUrl
resolveFn()
}
}
Expand Down Expand Up @@ -186,18 +187,16 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf
.trimmingCharacters(in: ["="])

let wasAlreadyLoading = addLoadingCompletionHandler(key: hash, completion)
if wasAlreadyLoading {
return
}
guard !wasAlreadyLoading else { return }

NYPLNetworkExecutor.shared.GET(targetUrl) { result in
switch result {
case .success(let data):
case .success(let data, _):
self.loadAccountSetsAndAuthDoc(fromCatalogData: data, key: hash) { success in
self.callAndClearLoadingCompletionHandlers(key: hash, success)
NotificationCenter.default.post(name: NSNotification.Name.NYPLCatalogDidLoad, object: nil)
}
case .failure(let error):
case .failure(let error, _):
NYPLErrorLogger.logError(error,
message: "Catalog failed to load from \(targetUrl)")
self.callAndClearLoadingCompletionHandlers(key: hash, false)
Expand Down
116 changes: 116 additions & 0 deletions Simplified/KeychainStoredVariable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// KeychainStoredVariable.swift
// SimplyE
//
// Created by Jacek Szyja on 22/05/2020.
// Copyright © 2020 NYPL Labs. All rights reserved.
//

import Foundation

protocol Keyable {
var key: String { get set }
}

class KeychainVariable<VariableType>: Keyable {
var key: String {
didSet {
guard key != oldValue else { return }

// invalidate current cache if key changed
alreadyInited = false
}
}

fileprivate let transaction: KeychainVariableTransaction

// marks whether or not was the `cachedValue` initialized
fileprivate var alreadyInited = false

// stores the last variable that was written to the keychain, or read from it
// The stored value will also be invalidated once the key changes
fileprivate var cachedValue: VariableType?

init(key: String, accountInfoLock: NSRecursiveLock) {
self.key = key
self.transaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock)
}

func read() -> VariableType? {
// If currently cached value is valid, return from cache
guard !alreadyInited else { return cachedValue }

// Otherwise, obtain the latest value from keychain
cachedValue = NYPLKeychain.shared()?.object(forKey: key) as? VariableType

// set a flag indicating that current cache is good to use
alreadyInited = true

// return cached value
return cachedValue
}

func write(_ newValue: VariableType?) {
transaction.perform {
// set new value to cache
cachedValue = newValue

// set a flag indicating that current cache is good to use
alreadyInited = true

// write new data to keychain in background
DispatchQueue.global(qos: .userInitiated).async { [key] in
if let newValue = newValue {
// if there is a new value, set it
NYPLKeychain.shared()?.setObject(newValue, forKey: key)
} else {
// otherwise remove old value from keychain
NYPLKeychain.shared()?.removeObject(forKey: key)
}
}
}
}
}

class KeychainCodableVariable<VariableType: Codable>: KeychainVariable<VariableType> {
override func read() -> VariableType? {
guard !alreadyInited else { return cachedValue }
guard let data = NYPLKeychain.shared()?.object(forKey: key) as? Data else { cachedValue = nil; alreadyInited = true; return nil }
cachedValue = try? JSONDecoder().decode(VariableType.self, from: data)
alreadyInited = true
return cachedValue
}

override func write(_ newValue: VariableType?) {
transaction.perform {
cachedValue = newValue
alreadyInited = true
DispatchQueue.global(qos: .userInitiated).async { [key] in
if let newValue = newValue, let data = try? JSONEncoder().encode(newValue) {
NYPLKeychain.shared()?.setObject(data, forKey: key)
} else {
NYPLKeychain.shared()?.removeObject(forKey: key)
}
}
}
}
}

class KeychainVariableTransaction {
fileprivate let accountInfoLock: NSRecursiveLock

init(accountInfoLock: NSRecursiveLock) {
self.accountInfoLock = accountInfoLock
}

func perform(operations: () -> Void) {
guard NYPLKeychain.shared() != nil else { return }

accountInfoLock.lock()
defer {
accountInfoLock.unlock()
}

operations()
}
}
1 change: 1 addition & 0 deletions Simplified/NYPLAccountSignInViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// places in the app when necessary. Managing account sign in with settings is
/// NYPLSettingsAccountDetailViewController.
@interface NYPLAccountSignInViewController : UITableViewController
@property (nonatomic, copy) void (^completionHandler)(void);

- (id)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
Expand Down
Loading