From dbc6ba6ce8d1b2fbb01d3bb98c965d82ef1e7d21 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Wed, 13 May 2020 02:50:45 +0200 Subject: [PATCH 01/19] login from settings and books listing --- .gitmodules | 6 - Cartfile | 1 - Cartfile.resolved | 1 - Simplified.xcconfig | 2 +- Simplified.xcodeproj/project.pbxproj | 36 +- Simplified/Account.swift | 15 +- Simplified/Accounts.json | 752 ++---------------- Simplified/CleverLoginViewcontroller.swift | 272 +++++++ Simplified/NYPLAccountSignInViewController.m | 8 +- Simplified/NYPLAnnotations.swift | 10 +- Simplified/NYPLAppDelegate.m | 14 + Simplified/NYPLBookCellDelegate.m | 2 +- Simplified/NYPLBookRegistry.m | 2 +- Simplified/NYPLHoldsViewController.m | 2 +- Simplified/NYPLMyBooksDownloadCenter.m | 2 +- Simplified/NYPLMyBooksViewController.m | 2 +- Simplified/NYPLRemoteViewController.m | 14 +- Simplified/NYPLSession.m | 8 +- .../NYPLSettingsAccountDetailViewController.m | 149 +++- Simplified/NYPLSignInBusinessLogic.swift | 67 +- Simplified/NYPLUserAccount.swift | 3 +- Simplified/SimplyE.entitlements | 4 +- .../Utilities/Authentication/NYPLBasicAuth.m | 2 + 23 files changed, 616 insertions(+), 758 deletions(-) create mode 100644 Simplified/CleverLoginViewcontroller.swift diff --git a/.gitmodules b/.gitmodules index 1529a86b3..943527552 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,3 @@ [submodule "tenprintcover-ios"] path = tenprintcover-ios url = git@github.com:NYPL/tenprintcover-ios.git -[submodule "adept-ios"] - path = adept-ios - url = git@github.com:NYPL/DRM-iOS-Adobe.git -[submodule "adobe-content-filter"] - path = adobe-content-filter - url = git@github.com:NYPL/Adobe-Content-Filter.git diff --git a/Cartfile b/Cartfile index 23eb8864d..514175605 100644 --- a/Cartfile +++ b/Cartfile @@ -1,7 +1,6 @@ binary "Crashlytics.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAnalyticsBinary.json" github "NYPL-Simplified/CardCreator-iOS" ~> 1.1.2 -github "NYPL-Simplified/NYPLAEToolkit" "master" github "NYPL-Simplified/NYPLAudiobookToolkit" "master" github "PureLayout/PureLayout" ~> 3.1.2 github "TheLevelUp/ZXingObjC" ~> 3.3 diff --git a/Cartfile.resolved b/Cartfile.resolved index afcc2f510..6154ed501 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,6 @@ binary "Crashlytics.json" "3.14.0" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAnalyticsBinary.json" "6.18.0" github "NYPL-Simplified/CardCreator-iOS" "v1.1.2" -github "NYPL-Simplified/NYPLAEToolkit" "35fd8c7e6c66ced9bd03fd9fde1d5be2a734b006" github "NYPL-Simplified/NYPLAudiobookToolkit" "f6a4e79224daacd10aac3cf851e5a7af216b8300" github "NYPL-Simplified/PDFRendererProvider-iOS" "b0ee13aa74e0d88193a401c29624dda8120a340d" github "PureLayout/PureLayout" "v3.1.5" diff --git a/Simplified.xcconfig b/Simplified.xcconfig index 189c24bd6..af76b59f4 100644 --- a/Simplified.xcconfig +++ b/Simplified.xcconfig @@ -1,4 +1,4 @@ -#include "Simplified+RMSDK.xcconfig" +//#include "Simplified+RMSDK.xcconfig" HEADER_SEARCH_PATHS = $(inherited) "$(SDKROOT)/usr/include/libxml2" "$(SRCROOT)/readium-sdk/Platform/Apple/RDServices" "$(SRCROOT)/readium-sdk/Platform/Apple/include" "$(SRCROOT)/tenprintcover-ios/TenPrintCover" "$(SRCROOT)/adept-ios" "$(SRCROOT)/adobe-content-filter/public" diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index 3cf84ad83..a1634514c 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -28,13 +28,12 @@ 085D31D71BE29E38007F7672 /* NYPLProblemReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31D61BE29E38007F7672 /* NYPLProblemReportViewController.m */; }; 085D31D91BE29ED4007F7672 /* NYPLProblemReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 085D31D81BE29ED4007F7672 /* NYPLProblemReportViewController.xib */; }; 085D31DF1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31DE1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m */; }; + 087FD174246B5B54004843EF /* CleverLoginViewcontroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */; }; 08A352201BDE8E410040BF1D /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A3521F1BDE8E410040BF1D /* CFNetwork.framework */; }; 08A352221BDE8E560040BF1D /* libicucore.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352211BDE8E560040BF1D /* libicucore.tbd */; }; 08A352241BDE8E640040BF1D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352231BDE8E640040BF1D /* Security.framework */; }; 08A352261BDE8E700040BF1D /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352251BDE8E700040BF1D /* SystemConfiguration.framework */; }; 08A352271BDE91B80040BF1D /* libRDServices.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A49C25461AE05A2600D63B89 /* libRDServices.a */; }; - 08C469C11BDAAEB1009D8AFD /* libADEPT.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A569A2C1B8351C6003B5B61 /* libADEPT.a */; settings = {ATTRIBUTES = (Weak, ); }; }; - 08C469C21BDAAEB1009D8AFD /* libAdobe Content Filter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A7048961B94A6710046FFF0 /* libAdobe Content Filter.a */; settings = {ATTRIBUTES = (Weak, ); }; }; 11068C55196DD37900E8A94B /* NYPLNull.m in Sources */ = {isa = PBXBuildFile; fileRef = 11068C54196DD37900E8A94B /* NYPLNull.m */; }; 11078357198160A50071AB1E /* NYPLBookDownloadFailedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 11078356198160A50071AB1E /* NYPLBookDownloadFailedCell.m */; }; 1107835E19816E3D0071AB1E /* UIView+NYPLViewAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1107835D19816E3D0071AB1E /* UIView+NYPLViewAdditions.m */; }; @@ -110,9 +109,7 @@ 145DA4922154464F0055DB93 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 145DA4912154464F0055DB93 /* SQLite.framework */; }; 145DA49421544A3F0055DB93 /* NYPLCardCreator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 145DA49321544A3F0055DB93 /* NYPLCardCreator.framework */; }; 145DA49621544A430055DB93 /* PureLayout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 145DA49521544A420055DB93 /* PureLayout.framework */; }; - 148B1C2D21768EAA00FF64AB /* NYPLAEToolkit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148B1C1721710C6800FF64AB /* NYPLAEToolkit.framework */; }; 148B1C2E21768EAA00FF64AB /* NYPLAudiobookToolkit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148B1C1C21710C6800FF64AB /* NYPLAudiobookToolkit.framework */; }; - 148B1C392176900F00FF64AB /* AudioEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148B1C342176900E00FF64AB /* AudioEngine.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 17071065242A923400E2648F /* NYPLSecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071060242A923400E2648F /* NYPLSecrets.swift */; }; 171966A924170819007BB87E /* NYPLBookState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171966A824170819007BB87E /* NYPLBookState.swift */; }; 173F0823241AAA4E00A64658 /* NYPLBookStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173F0822241AAA4E00A64658 /* NYPLBookStateTests.swift */; }; @@ -328,6 +325,7 @@ 085D31DD1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+NYPLURLRequestAdditions.h"; sourceTree = ""; }; 085D31DE1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+NYPLURLRequestAdditions.m"; sourceTree = ""; }; 085D31FB1BE7BE86007F7672 /* ReaderClientCert.sig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ReaderClientCert.sig; sourceTree = ""; }; + 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleverLoginViewcontroller.swift; sourceTree = ""; }; 08A3521F1BDE8E410040BF1D /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 08A352211BDE8E560040BF1D /* libicucore.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libicucore.tbd; path = usr/lib/libicucore.tbd; sourceTree = SDKROOT; }; 08A352231BDE8E640040BF1D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -719,7 +717,6 @@ buildActionMask = 2147483647; files = ( A9AD99402225D4AF009FF54A /* PDFRendererProvider.framework in Frameworks */, - 148B1C392176900F00FF64AB /* AudioEngine.framework in Frameworks */, 03B092301E78871A00AD338D /* MediaPlayer.framework in Frameworks */, 03B092281E78859E00AD338D /* CoreMedia.framework in Frameworks */, 03B0922E1E7886ED00AD338D /* CoreVideo.framework in Frameworks */, @@ -728,8 +725,6 @@ 03B092261E7883C400AD338D /* libiconv.tbd in Frameworks */, A823D813192BABA400B55DE2 /* CoreGraphics.framework in Frameworks */, 03B092241E78839D00AD338D /* QuartzCore.framework in Frameworks */, - 08C469C11BDAAEB1009D8AFD /* libADEPT.a in Frameworks */, - 08C469C21BDAAEB1009D8AFD /* libAdobe Content Filter.a in Frameworks */, 08A352271BDE91B80040BF1D /* libRDServices.a in Frameworks */, 08A352221BDE8E560040BF1D /* libicucore.tbd in Frameworks */, 5AEA9E011B947419009F71DB /* libc++.dylib in Frameworks */, @@ -747,7 +742,6 @@ 145DA49421544A3F0055DB93 /* NYPLCardCreator.framework in Frameworks */, 145DA49621544A430055DB93 /* PureLayout.framework in Frameworks */, 145DA4922154464F0055DB93 /* SQLite.framework in Frameworks */, - 148B1C2D21768EAA00FF64AB /* NYPLAEToolkit.framework in Frameworks */, 148B1C2E21768EAA00FF64AB /* NYPLAudiobookToolkit.framework in Frameworks */, 738EF2DB2405EA6000F388FB /* Firebase.framework in Frameworks */, 738EF2DC2405EA6000F388FB /* FirebaseCoreDiagnostics.framework in Frameworks */, @@ -1019,6 +1013,7 @@ 111E75791A80165C00718AD7 /* NYPLSettingsSplitViewController.m */, E6BA02B71DE4B6F600F76404 /* RemoteHTMLViewController.swift */, 731A5B1124621F2B00B5E663 /* NYPLSignInBusinessLogic.swift */, + 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */, ); name = Settings; sourceTree = ""; @@ -1466,9 +1461,9 @@ TestTargetID = A823D80C192BABA400B55DE2; }; A823D80C192BABA400B55DE2 = { - DevelopmentTeam = 7262U6ST2R; + DevelopmentTeam = 95MG2VV3YK; LastSwiftMigration = 1130; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; @@ -1611,9 +1606,7 @@ "$(SRCROOT)/Carthage/Build/iOS/SQLite.framework", "$(SRCROOT)/Carthage/Build/iOS/PureLayout.framework", "$(SRCROOT)/Carthage/Build/iOS/NYPLCardCreator.framework", - "$(SRCROOT)/Carthage/Build/iOS/NYPLAEToolkit.framework", "$(SRCROOT)/Carthage/Build/iOS/NYPLAudiobookToolkit.framework", - "$(SRCROOT)/Carthage/Build/iOS/AudioEngine.framework", "$(SRCROOT)/Carthage/Build/iOS/PDFRendererProvider.framework", ); name = "Copy Frameworks (Carthage)"; @@ -1624,9 +1617,7 @@ "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SQLite.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/PureLayout.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/NYPLCardCreator.framework", - "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/NYPLAEToolkit.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/NYPLAudiobookToolkit.framework", - "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/AudioEngine.framework", "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/PDFRendererProvider.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -1755,6 +1746,7 @@ 2D87909D20127AA300E2763F /* NYPLBookAcquisitionPath.m in Sources */, 2DCB71EE2017DFB5000E041A /* NYPLOPDSAcquisitionAvailability.m in Sources */, 2DB436381D4C049200F8E69D /* NYPLReachability.m in Sources */, + 087FD174246B5B54004843EF /* CleverLoginViewcontroller.swift in Sources */, 1195040B1994075B009FB788 /* NYPLReaderViewController.m in Sources */, 1183F33B194B775900DC322F /* NYPLCatalogLaneCell.m in Sources */, E6202A021DD4E6F300C99553 /* NYPLSettingsAccountDetailViewController.m in Sources */, @@ -2074,9 +2066,10 @@ CLANG_ENABLE_MODULES = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 0; - DEVELOPMENT_TEAM = 7262U6ST2R; + DEVELOPMENT_TEAM = 95MG2VV3YK; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2089,12 +2082,11 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 3.4.0; - PRODUCT_BUNDLE_IDENTIFIER = org.nypl.labs.SimplyE; + PRODUCT_BUNDLE_IDENTIFIER = "com.skyneck.SAML-Test-app"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "2e185b6c-271e-4b02-a05e-860b8c3831f6"; - PROVISIONING_PROFILE_SPECIFIER = "SimplyE Development"; + PROVISIONING_PROFILE_SPECIFIER = ""; RUN_CLANG_STATIC_ANALYZER = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG FEATURE_DRM_CONNECTOR"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Simplified/SimplyE-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; @@ -2143,9 +2135,9 @@ PRODUCT_BUNDLE_IDENTIFIER = org.nypl.labs.SimplyE; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "b3d9154d-70e1-48d6-a0c5-869431277a5c"; - PROVISIONING_PROFILE_SPECIFIER = "SimplyE Distribution"; + PROVISIONING_PROFILE_SPECIFIER = "SimplyE Distribution 2019"; RUN_CLANG_STATIC_ANALYZER = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = FEATURE_DRM_CONNECTOR; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_OBJC_BRIDGING_HEADER = "Simplified/SimplyE-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.2; diff --git a/Simplified/Account.swift b/Simplified/Account.swift index de8615070..a9d6f0b14 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -10,6 +10,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" 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 none } @@ -24,7 +25,8 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" let supportsBarcodeDisplay:Bool let coppaUnderUrl:URL? let coppaOverUrl:URL? - + let oauthIntermediaryUrl:URL? + init(auth: OPDS2AuthenticationDocument.Authentication) { authType = AuthType(rawValue: auth.type) ?? .none authPasscodeLength = auth.inputs?.password.maximumLength ?? 99 @@ -35,7 +37,8 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" 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 ?? "") + coppaOverUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-met" })?.href ?? "") + oauthIntermediaryUrl = URL.init(string: auth.links?.first(where: { $0.rel == "authenticate" })?.href ?? "") } } @@ -80,7 +83,11 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" } var coppaOverUrl: URL? { - return auths.first?.coppaOverUrl + return auths.first?.coppaOverUrl + } + + var oauthIntermediaryUrl: URL? { + return auths.first?.oauthIntermediaryUrl } var patronIDLabel: String? { @@ -92,7 +99,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" } var needsAuth:Bool { - return authType == .basic + return authType == .basic || authType == .oauthIntermediary } var needsAgeCheck:Bool { diff --git a/Simplified/Accounts.json b/Simplified/Accounts.json index 568e39503..19990e2ee 100644 --- a/Simplified/Accounts.json +++ b/Simplified/Accounts.json @@ -1,687 +1,71 @@ [ { - "id_numeric": 0, - "id_uuid": "urn:uuid:065c0c11-0d0f-42a3-82e4-277b18786949" - }, - { - "id_numeric": 1, - "id_uuid": "urn:uuid:edef2358-9f6a-4ce6-b64f-9b351ec68ac4" - }, - { - "id_numeric": 128, - "id_uuid": "urn:uuid:1a110ef6-3018-4359-84f9-e24abee9aeb3" - }, - { - "id_numeric": 7, - "id_uuid": "urn:uuid:bce4c73c-9d0b-4eac-92e1-1405bcee9367" - }, - { - "id_numeric": 21, - "id_uuid": "urn:uuid:1d80d5e5-d304-4be0-9110-49582a12162d" - }, - { - "id_numeric": 11, - "id_uuid": "urn:uuid:4a490a70-0bd2-4f14-a81f-aeca4dda620b" - }, - { - "id_numeric": 30, - "id_uuid": "urn:uuid:1ef19c4c-7380-4ab7-b85b-8487376e8e88" - }, - { - "id_numeric": 29, - "id_uuid": "urn:uuid:301d32db-ac98-43d0-a207-7112fd1d62f8" - }, - { - "id_numeric": 1231, - "id_uuid": "urn:uuid:f1b7a6c6-1c80-4422-8942-1df0b01236ab" - }, - { - "id_numeric": 31, - "id_uuid": "urn:uuid:2183a032-a4e6-4405-a4d3-7447d3c6886e" - }, - { - "id_numeric": 32, - "id_uuid": "urn:uuid:cb1d94fc-5c5c-4e38-9469-6182a6537881" - }, - { - "id_numeric": 60, - "id_uuid": "urn:uuid:d16d6a75-f4e5-40ff-a942-21b5ffb876c6" - }, - { - "id_numeric": 1232, - "id_uuid": "urn:uuid:0f5b790f-112e-4a24-8dc7-071f86892e24" - }, - { - "id_numeric": 12, - "id_uuid": "urn:uuid:f856053f-635b-48bd-b0b1-1982643fb3b4" - }, - { - "id_numeric": 61, - "id_uuid": "urn:uuid:5e1c8e01-2bb3-49d0-9b4e-7764f6da8811" - }, - { - "id_numeric": 66, - "id_uuid": "urn:uuid:725aed61-b893-4327-a768-ce54af0aee36" - }, - { - "id_numeric": 70, - "id_uuid": "urn:uuid:705afc58-a1ab-40f9-9a80-030be5c12d1e" - }, - { - "id_numeric": 1233, - "id_uuid": "urn:uuid:ec19e1c2-5fb5-47aa-b1ce-9d712507e34e" - }, - { - "id_numeric": 62, - "id_uuid": "urn:uuid:f4760d4d-bb1b-4ecc-a47a-f8c039c78158" - }, - { - "id_numeric": 1235, - "id_uuid": "urn:uuid:4759fc7d-0cb8-43af-a895-34449ba78257" - }, - { - "id_numeric": 63, - "id_uuid": "urn:uuid:9d26dbea-fcd1-4e4c-b00e-febab8931211" - }, - { - "id_numeric": 1234, - "id_uuid": "urn:uuid:8e5bda1a-c7d7-47b5-a783-2e8f4fdb54c3" - }, - { - "id_numeric": 17, - "id_uuid": "urn:uuid:6be885a0-d899-4b20-8c90-b8a6d1c8ab8e" - }, - { - "id_numeric": 14, - "id_uuid": "urn:uuid:4fa12865-a9d5-439b-8ed4-24ccabcdb9be" - }, - { - "id_numeric": 64, - "id_uuid": "urn:uuid:3f4783f5-a1c1-4f2f-8156-01af60d47742" - }, - { - "id_numeric": 36, - "id_uuid": "urn:uuid:a53993ce-91ca-4070-b965-b236e2ea146b" - }, - { - "id_numeric": 16, - "id_uuid": "urn:uuid:1ae0909b-e9a8-4fe3-8064-f3b2cfe52cea" - }, - { - "id_numeric": 129, - "id_uuid": "urn:uuid:750106ef-eceb-43b9-9a85-a8e505ac80ad" - }, - { - "id_numeric": 15, - "id_uuid": "urn:uuid:fe694ac0-d897-4bc8-9c71-bc103f2af810" - }, - { - "id_numeric": 76, - "id_uuid": "urn:uuid:37fbb5a4-617b-46ac-9fd1-4ac447d7697e" - }, - { - "id_numeric": 46, - "id_uuid": "urn:uuid:b4590bdb-160c-4025-9d73-af2382e7c898" - }, - { - "id_numeric": 87, - "id_uuid": "urn:uuid:e689ff0b-ba28-4e43-a598-2fe56964272b" - }, - { - "id_numeric": 138, - "id_uuid": "urn:uuid:15ff1a0d-8e1b-44bd-9c7a-c57d99ef841f" - }, - { - "id_numeric": 117, - "id_uuid": "urn:uuid:d2213bb8-544f-4912-832d-dc5a564fef85" - }, - { - "id_numeric": 33, - "id_uuid": "urn:uuid:f78e7d78-5342-4aa1-b954-7422998e914c" - }, - { - "id_numeric": 77, - "id_uuid": "urn:uuid:68a96afb-5f38-4ec6-bdb3-8ad706e5db42" - }, - { - "id_numeric": 78, - "id_uuid": "urn:uuid:6f6c6d79-b8fb-43ba-ae45-b9342954affa" - }, - { - "id_numeric": 1236, - "id_uuid": "urn:uuid:06f4e89b-7a64-4bd1-9787-c58361eab651" - }, - { - "id_numeric": 1205, - "id_uuid": "urn:uuid:4be05696-f269-4409-9754-7b9dce4e3dc5" - }, - { - "id_numeric": 34, - "id_uuid": "urn:uuid:0226f878-134d-42e0-bcb2-2b3ad3f13380" - }, - { - "id_numeric": 35, - "id_uuid": "urn:uuid:489402d9-97f1-419c-a267-f4cce4b6dd54" - }, - { - "id_numeric": 86, - "id_uuid": "urn:uuid:6b849570-070f-43b4-9dcc-7ebb4bca292e" - }, - { - "id_numeric": 55, - "id_uuid": "urn:uuid:10b84c15-8097-4f01-9da6-57c8b32b247c" - }, - { - "id_numeric": 56, - "id_uuid": "urn:uuid:33a32e9e-ea4b-404f-b4ab-0de7d9f6c4e4" - }, - { - "id_numeric": 48, - "id_uuid": "urn:uuid:2d1c8df1-fbfe-41e8-aa64-e0285ecd8599" - }, - { - "id_numeric": 81, - "id_uuid": "urn:uuid:f86aa852-ff5f-4970-88b4-d54ce97ef9a8" - }, - { - "id_numeric": 135, - "id_uuid": "urn:uuid:816199c6-1446-42a4-9a8f-8254fbc1017c" - }, - { - "id_numeric": 49, - "id_uuid": "urn:uuid:8bb90cee-5c31-4ebf-8c82-a541269d1dce" - }, - { - "id_numeric": 79, - "id_uuid": "urn:uuid:f74ff17c-d7a1-4657-9c4a-d6d09b9a69b8" - }, - { - "id_numeric": 50, - "id_uuid": "urn:uuid:3c9cbc42-db02-4119-9f11-31796aa84bcd" - }, - { - "id_numeric": 90, - "id_uuid": "urn:uuid:e0f433c7-5372-467a-bf5a-42ce614112e0" - }, - { - "id_numeric": 94, - "id_uuid": "urn:uuid:f7bc9a4d-aa0e-4d5a-bfb2-ccdfeecba207" - }, - { - "id_numeric": 110, - "id_uuid": "urn:uuid:0e4d8366-d7f6-48eb-b276-7574cd623344" - }, - { - "id_numeric": 51, - "id_uuid": "urn:uuid:2e1af0df-ee4d-4216-bcc7-266e521f1ec3" - }, - { - "id_numeric": 1259, - "id_uuid": "urn:uuid:fddd8eaa-c2ce-4eb3-a8b8-fcf100b47f05" - }, - { - "id_numeric": 1247, - "id_uuid": "urn:uuid:f1954241-99df-433d-badd-71c06c2f833d" - }, - { - "id_numeric": 10, - "id_uuid": "urn:uuid:90c35943-e6e6-4425-9315-73cea8fe7ded" - }, - { - "id_numeric": 1261, - "id_uuid": "urn:uuid:a7bddadc-91c7-45a3-a642-dfd137480a22" - }, - { - "id_numeric": 111, - "id_uuid": "urn:uuid:df2a9cd2-889a-440b-9ecc-cf60b92b6277" - }, - { - "id_numeric": 28, - "id_uuid": "urn:uuid:a73d1088-b462-47d4-bf6f-8a029d5fba0b" - }, - { - "id_numeric": 65, - "id_uuid": "urn:uuid:4ece1d0a-68ce-4637-ba47-6f7ed29548d1" - }, - { - "id_numeric": 1228, - "id_uuid": "urn:uuid:19b3fe47-4ac4-4a15-a1ac-eb52327c393f" - }, - { - "id_numeric": 57, - "id_uuid": "urn:uuid:02b1ff6c-55ce-40d6-af09-2d19a054514a" - }, - { - "id_numeric": 58, - "id_uuid": "urn:uuid:73d3f233-f332-4cb6-b319-738239f4f7fb" - }, - { - "id_numeric": 91, - "id_uuid": "urn:uuid:c728624e-bada-4953-b645-ece46c958157" - }, - { - "id_numeric": 67, - "id_uuid": "urn:uuid:45f7ea1a-2262-4af6-95ce-4cee2adb421f" - }, - { - "id_numeric": 1257, - "id_uuid": "urn:uuid:8bb7b978-4b62-41af-87e3-379df87f788b" - }, - { - "id_numeric": 1245, - "id_uuid": "urn:uuid:ead249d3-12c0-42d5-a0de-eefc0529606e" - }, - { - "id_numeric": 1206, - "id_uuid": "urn:uuid:654fd577-b1dc-4db2-9de3-75fcb908474d" - }, - { - "id_numeric": 1221, - "id_uuid": "urn:uuid:1d6b2000-fca2-4391-9eb7-ec4e43a28703" - }, - { - "id_numeric": 8, - "id_uuid": "urn:uuid:f60ee2fd-0734-4112-92e1-06063358a7d9" - }, - { - "id_numeric": 59, - "id_uuid": "urn:uuid:63bfea56-0d89-4d5d-80af-e6fc7044b8e5" - }, - { - "id_numeric": 5, - "id_uuid": "urn:uuid:f60b644e-4955-4996-a4e5-a192feb4e7f8" - }, - { - "id_numeric": 68, - "id_uuid": "urn:uuid:a3f7a338-7266-4f0b-8433-d976a2f384cf" - }, - { - "id_numeric": 47, - "id_uuid": "urn:uuid:36b5a718-0b90-4921-841b-87992355e814" - }, - { - "id_numeric": 71, - "id_uuid": "urn:uuid:37957a24-0f6e-4740-a560-6599e8314caf" - }, - { - "id_numeric": 1248, - "id_uuid": "urn:uuid:964392a7-8f23-47f3-bafc-5b67996b4bdb" - }, - { - "id_numeric": 72, - "id_uuid": "urn:uuid:0c9c1c7f-13fc-4f5a-ac58-3bd155e86e25" - }, - { - "id_numeric": 1253, - "id_uuid": "urn:uuid:23c0910c-bde2-4da5-9f42-c25616128353" - }, - { - "id_numeric": 73, - "id_uuid": "urn:uuid:9e881a55-45e3-427d-aa8e-22c4cf4c5996" - }, - { - "id_numeric": 1244, - "id_uuid": "urn:uuid:89d41123-c7f6-48cf-ab1c-735df71f44e4" - }, - { - "id_numeric": 74, - "id_uuid": "urn:uuid:2443ac65-55f4-43cb-a07d-60682f82bab1" - }, - { - "id_numeric": 69, - "id_uuid": "urn:uuid:b1a7b2ae-98d5-45d7-a531-82938c9fe694" - }, - { - "id_numeric": 1249, - "id_uuid": "urn:uuid:cc0eba87-1487-495f-94f1-2c363b9e28f8" - }, - { - "id_numeric": 83, - "id_uuid": "urn:uuid:122d9720-640d-4cf3-9931-819ea091a158" - }, - { - "id_numeric": 80, - "id_uuid": "urn:uuid:f8aaaa27-e4d5-4604-9404-697cfc79ba70" - }, - { - "id_numeric": 18, - "id_uuid": "urn:uuid:5fbf105c-bebb-495a-b0ee-50ba8ef0fda1" - }, - { - "id_numeric": 114, - "id_uuid": "urn:uuid:43044b1e-5b1b-4d6d-b2af-75542c272b01" - }, - { - "id_numeric": 112, - "id_uuid": "urn:uuid:471ef85e-b35c-4880-932b-ba91b785740c" - }, - { - "id_numeric": 1243, - "id_uuid": "urn:uuid:d2d8bfbd-7144-4dc7-b44c-3684151afa33" - }, - { - "id_numeric": 100, - "id_uuid": "urn:uuid:7e6c7eb6-b915-45cf-a19a-a6d339da8fc2" - }, - { - "id_numeric": 82, - "id_uuid": "urn:uuid:2922723f-5c4c-4e25-81b8-81fad628b528" - }, - { - "id_numeric": 93, - "id_uuid": "urn:uuid:bb86001e-c810-4c48-98cd-f4ae84e85db4" - }, - { - "id_numeric": 75, - "id_uuid": "urn:uuid:a5748a8b-ffa5-46e4-908e-3278f691b856" - }, - { - "id_numeric": 9, - "id_uuid": "urn:uuid:6e4575eb-abaf-4ddb-a246-52065ae3de8c" - }, - { - "id_numeric": 95, - "id_uuid": "urn:uuid:77320694-95ff-467f-8abb-a8a7c606d44f" - }, - { - "id_numeric": 136, - "id_uuid": "urn:uuid:4ddbed61-b789-4eac-957d-f93ff204798f" - }, - { - "id_numeric": 44, - "id_uuid": "urn:uuid:7d6f67d1-d4a3-4ff1-a6d2-db6d7af585a9" - }, - { - "id_numeric": 1255, - "id_uuid": "urn:uuid:389bf866-2dae-4b46-a320-789a48772990" - }, - { - "id_numeric": 1258, - "id_uuid": "urn:uuid:9d365be3-abac-40e5-bb5f-1df66e5109f2" - }, - { - "id_numeric": 1199, - "id_uuid": "urn:uuid:acdcf485-a920-4210-8354-c0504d96532a" - }, - { - "id_numeric": 1254, - "id_uuid": "urn:uuid:00603c60-a261-4faf-a5eb-79107f3d7f9e" - }, - { - "id_numeric": 96, - "id_uuid": "urn:uuid:cccc6e9a-e938-40f7-aeff-2cfd2dd5bff2" - }, - { - "id_numeric": 124, - "id_uuid": "urn:uuid:ddb50957-6453-4bd3-a3ce-bd5b7333930c" - }, - { - "id_numeric": 125, - "id_uuid": "urn:uuid:f3ded2c4-b182-42e1-a486-c9e6f1de61ff" - }, - { - "id_numeric": 1260, - "id_uuid": "urn:uuid:4e547dac-3203-427f-9fb1-0b74e81b5e22" - }, - { - "id_numeric": 127, - "id_uuid": "urn:uuid:c62ecb1d-99e8-438d-9bca-13536ef216ac" - }, - { - "id_numeric": 97, - "id_uuid": "urn:uuid:400f87c9-1ac6-40e3-adf9-fe5321d6ca8b" - }, - { - "id_numeric": 1263, - "id_uuid": "urn:uuid:5278562c-d642-4fda-ad7e-1613077cfb8d" - }, - { - "id_numeric": 126, - "id_uuid": "urn:uuid:724811c4-7af7-47e7-aace-bf6a35ba25f7" - }, - { - "id_numeric": 137, - "id_uuid": "urn:uuid:0006e04c-f47d-46ca-aba6-f5848d210e27" - }, - { - "id_numeric": 98, - "id_uuid": "urn:uuid:339aa64f-7d2b-4f64-a4bc-bb4320419681" - }, - { - "id_numeric": 1250, - "id_uuid": "urn:uuid:2fc0dc8d-e697-41c7-b925-588a7b00b0a5" - }, - { - "id_numeric": 115, - "id_uuid": "urn:uuid:b0065fa8-6719-4f7c-9022-b8ed77e5cbca" - }, - { - "id_numeric": 19, - "id_uuid": "urn:uuid:84106637-891f-4544-b638-dfa8bd0be92b" - }, - { - "id_numeric": 116, - "id_uuid": "urn:uuid:643be35c-27be-4c98-acb3-e01031a10655" - }, - { - "id_numeric": 123, - "id_uuid": "urn:uuid:4711ac2e-49d9-47e5-9aef-6896166c291d" - }, - { - "id_numeric": 99, - "id_uuid": "urn:uuid:ef52fc30-ca51-4c1f-9b7d-cf525a29ac04" - }, - { - "id_numeric": 113, - "id_uuid": "urn:uuid:0a09f0b4-e978-49ee-b6b4-efc42b4cc755" - }, - { - "id_numeric": 101, - "id_uuid": "urn:uuid:f8272064-bc72-429d-8b22-758c5999caf3" - }, - { - "id_numeric": 102, - "id_uuid": "urn:uuid:378d0def-bea5-4845-9f65-4864344f15ed" - }, - { - "id_numeric": 1256, - "id_uuid": "urn:uuid:b9a27dd4-a4f4-4ebe-826c-cf4b63d32745" - }, - { - "id_numeric": 103, - "id_uuid": "urn:uuid:81a20070-869c-4810-b883-bab77bbf3403" - }, - { - "id_numeric": 84, - "id_uuid": "urn:uuid:130f48a8-4b71-413f-a71e-9b8f58138ced" - }, - { - "id_numeric": 22, - "id_uuid": "urn:uuid:dbb59ed3-a9b4-45e9-be00-934c02b21f3f" - }, - { - "id_numeric": 104, - "id_uuid": "urn:uuid:88ecde03-b1a6-4ed7-be62-6bc22776f748" - }, - { - "id_numeric": 1242, - "id_uuid": "urn:uuid:2a0812a0-5eb1-4abc-a7a3-262b124c9b14" - }, - { - "id_numeric": 88, - "id_uuid": "urn:uuid:16ba4234-ee15-47d9-9c70-fb699ebd48d2" - }, - { - "id_numeric": 1246, - "id_uuid": "urn:uuid:db931a14-59da-4543-91fb-697fb766d4ec" - }, - { - "id_numeric": 20, - "id_uuid": "urn:uuid:bd2baa3c-98f0-49de-8a62-3bc98065faa2" - }, - { - "id_numeric": 105, - "id_uuid": "urn:uuid:2c48ad29-6eb8-4ba1-a058-a3491a28d522" - }, - { - "id_numeric": 106, - "id_uuid": "urn:uuid:9626096b-3604-4f76-8885-84a655204f5f" - }, - { - "id_numeric": 107, - "id_uuid": "urn:uuid:2003c69e-3b79-42d2-818c-5ec1f0e72ad0" - }, - { - "id_numeric": 108, - "id_uuid": "urn:uuid:972888b7-7abe-407d-835d-9610c43580f3" - }, - { - "id_numeric": 1251, - "id_uuid": "urn:uuid:639a814f-18a4-4d1a-9c18-6284448ae9e6" - }, - { - "id_numeric": 109, - "id_uuid": "urn:uuid:1fb8d6ea-9072-4686-abed-fa0f9479e635" - }, - { - "id_numeric": 2, - "id_uuid": "urn:uuid:56906f26-2c9a-4ae9-bd02-552557720b99" - }, - { - "id_numeric": 45, - "id_uuid": "urn:uuid:306436c4-f024-41aa-bc28-9c58f877ee24" - }, - { - "id_numeric": 92, - "id_uuid": "urn:uuid:944a14a1-3d07-4ddf-a9c1-94435ec16315" - }, - { - "id_numeric": 1198, - "id_uuid": "urn:uuid:6c92feac-a2a5-4b53-82f5-265f40c9187d" - }, - { - "id_numeric": 118, - "id_uuid": "urn:uuid:f6eb4e88-c9ed-442f-ae17-988c25b6c23d" - }, - { - "id_numeric": 1222, - "id_uuid": "urn:uuid:3a2e8b39-ef65-4acf-a17c-d662b2888cf1" - }, - { - "id_numeric": 119, - "id_uuid": "urn:uuid:414b7738-2abd-493a-874f-ad4191b04a61" - }, - { - "id_numeric": 1201, - "id_uuid": "urn:uuid:625a725d-2324-4407-9407-20d230e2648a" - }, - { - "id_numeric": 13, - "id_uuid": "urn:uuid:3e4028e1-4882-4e9a-a6c1-aafbd585f4f5" - }, - { - "id_numeric": 1202, - "id_uuid": "urn:uuid:4c0fcc4b-777b-42f3-a3d0-2577b6a23ba7" - }, - { - "id_numeric": 1203, - "id_uuid": "urn:uuid:dea97614-8838-4339-a4a3-2340af1ad582" - }, - { - "id_numeric": 139, - "id_uuid": "urn:uuid:fd692093-00c0-4885-b0e8-0365f82244f1" - }, - { - "id_numeric": 1223, - "id_uuid": "urn:uuid:d3fdbdc9-d129-46fc-bb4e-f7422b3f3725" - }, - { - "id_numeric": 1207, - "id_uuid": "urn:uuid:6f0e930f-25c2-41de-b6c4-feb66ab2df34" - }, - { - "id_numeric": 1208, - "id_uuid": "urn:uuid:6e206037-215e-4d1a-b89f-0f69af6f5ec3" - }, - { - "id_numeric": 1224, - "id_uuid": "urn:uuid:3a2f7d77-fc34-4ba4-a046-73e80b4f8bee" - }, - { - "id_numeric": 1225, - "id_uuid": "urn:uuid:733f5c08-0d60-4e18-a80d-db15bb1e35e1" - }, - { - "id_numeric": 131, - "id_uuid": "urn:uuid:c6a50365-e90e-4b8a-8f36-a1eaf26e751d" - }, - { - "id_numeric": 1226, - "id_uuid": "urn:uuid:015a8c7c-d188-4773-b839-1ceaedb67a70" - }, - { - "id_numeric": 23, - "id_uuid": "urn:uuid:6263da4e-723f-417a-9d87-9ca4cbc2da6d" - }, - { - "id_numeric": 41, - "id_uuid": "urn:uuid:f160318e-2d32-4821-97ed-e8bf03828b51" - }, - { - "id_numeric": 42, - "id_uuid": "urn:uuid:bdbd1482-1336-4684-8267-9d48f80f00f9" - }, - { - "id_numeric": 132, - "id_uuid": "urn:uuid:ba6e4b4b-0c34-47c2-ab83-df1a820eba54" - }, - { - "id_numeric": 133, - "id_uuid": "urn:uuid:74a72251-75ca-4813-b982-ce28ad8036b5" - }, - { - "id_numeric": 1219, - "id_uuid": "urn:uuid:8c5a57c2-6c98-4b57-a7d9-1cced4afdab2" - }, - { - "id_numeric": 120, - "id_uuid": "urn:uuid:825b934f-5cf9-4bd8-91ef-dbc80b84d78b" - }, - { - "id_numeric": 1252, - "id_uuid": "urn:uuid:ea7e5398-7091-420d-9560-378d479847c0" - }, - { - "id_numeric": 1218, - "id_uuid": "urn:uuid:0557a7e4-58c3-41ef-9d8f-fcb81affa262" - }, - { - "id_numeric": 130, - "id_uuid": "urn:uuid:8a773572-178a-4fae-920a-26af646dbbb6" - }, - { - "id_numeric": 1227, - "id_uuid": "urn:uuid:8a9b61df-36f3-41de-bcbb-e821cf6559c2" - }, - { - "id_numeric": 121, - "id_uuid": "urn:uuid:d74acd09-e42d-44da-a468-e2d98509642c" - }, - { - "id_numeric": 122, - "id_uuid": "urn:uuid:88fbd66a-4a91-4a90-949b-75a4d2b46c53" - }, - { - "id_numeric": 1229, - "id_uuid": "urn:uuid:1374649b-86ee-4779-a0b2-0142598ce418" - }, - { - "id_numeric": 134, - "id_uuid": "urn:uuid:f09d725a-1e1d-42ba-8f95-8fd3eedd4d5f" - }, - { - "id_numeric": 1230, - "id_uuid": "urn:uuid:fb5e191b-2779-4b66-8e54-91abf7fa535f" + "authPasscodeAllowsLetters": false, + "authPasscodeLength": 4, + "catalogUrl": "https://circulation.librarysimplified.org/NYNYPL/", + "eulaUrl": "http://www.librarysimplified.org/EULA.html", + "id_numeric": 0, + "id_uuid": "urn:uuid:065c0c11-0d0f-42a3-82e4-277b18786949", + "inProduction": true, + "licenseUrl": "http://www.librarysimplified.org/license.html", + "logo": "", + "mainColor": "red", + "name": "The New York Public Library", + "needsAuth": true, + "pinRequired": true, + "privacyUrl": "http://www.librarysimplified.org/privacypolicy.html", + "subtitle": "Inspiring lifelong learning, advancing knowledge, and strengthening our communities.", + "supportEmail": "simplyehelp@nypl.org", + "supportsBarcodeDisplay": true, + "supportsBarcodeScanner": true, + "supportsCardCreator": true, + "supportsReservations": true, + "supportsSimplyESync": true + }, + { + "authLoginLength": 4, + "authPasscodeAllowsLetters": false, + "authPasscodeLength": 4, + "cardCreatorUrl": "https://go.bklynlibrary.org/ecard", + "catalogUrl": "https://bplsimplye.bklynlibrary.org/NYBKLYN/", + "eulaUrl": "http://www.bklynlibrary.org/policy/terms-conditions", + "id_numeric": 1, + "id_uuid": "urn:uuid:edef2358-9f6a-4ce6-b64f-9b351ec68ac4", + "inProduction": true, + "licenseUrl": "http://www.librarysimplified.org/bklynlicenses.html", + "logo": "", + "mainColor": "blue", + "name": "Brooklyn Public Library", + "needsAuth": true, + "pinRequired": true, + "privacyUrl": "http://www.librarysimplified.org/bklynprivacypolicy.html", + "subtitle": "Connecting people to resources to achieve their goals.", + "supportEmail": "askus@bklynlibrary.org", + "supportsBarcodeDisplay": false, + "supportsBarcodeScanner": false, + "supportsCardCreator": false, + "supportsReservations": true, + "supportsSimplyESync": true + }, + { + "authPasscodeAllowsLetters": false, + "catalogUrl": "https://circulation.librarysimplified.org/CLASSICS/", + "catalogUrl13": "http://simplye-collection-redirects.librarysimplified.org/all-books.xml", + "catalogUrlUnder13": "http://simplye-collection-redirects.librarysimplified.org/childrens-books.xml", + "id_numeric": 2, + "id_uuid": "urn:uuid:56906f26-2c9a-4ae9-bd02-552557720b99", + "inProduction": true, + "licenseUrl": "http://www.librarysimplified.org/iclicenses.html", + "logo": null, + "mainColor": "teal", + "name": "The SimplyE Collection", + "needsAuth": false, + "pinRequired": true, + "subtitle": "A selection of classics and modern material available to anyone, with no library card necessary.", + "supportsBarcodeDisplay": false, + "supportsBarcodeScanner": false, + "supportsCardCreator": false, + "supportsReservations": false, + "supportsSimplyESync": false } - ] - \ No newline at end of file +] diff --git a/Simplified/CleverLoginViewcontroller.swift b/Simplified/CleverLoginViewcontroller.swift new file mode 100644 index 000000000..fdb148d2d --- /dev/null +++ b/Simplified/CleverLoginViewcontroller.swift @@ -0,0 +1,272 @@ +//// +//// CleverLoginViewController.swift +//// Simplified +//// +//// Created by Aferdita Muriqi on 8/13/16. +//// Copyright © 2016 NYPL Labs. All rights reserved. +//// +// +//import UIKit +//import WebKit +// +//typealias CompletionHandler = () -> Void +// +//class CleverLoginViewController: UIViewController, WKNavigationDelegate { +// +// var libraryAccountID: String! +// private let permissionsCheckLock = NSLock() +// +// @objc var libraryAccount: Account? { +// guard let libraryAccountID = libraryAccountID else { return nil } +// return AccountsManager.shared.account(libraryAccountID) +// } +// +// @objc var userAccount: NYPLUserAccount { +// return NYPLUserAccount.sharedAccount(libraryUUID: libraryAccountID) +// } +// +// var cleverAuth:(authToken:String,patron:AnyObject,adobeToken:String)? +// var completionHandler:CompletionHandler! +// +// let instructionLabel = UILabel() +// +// let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView() +// +// override func viewDidLoad() { +// super.viewDidLoad() +// +// self.view.backgroundColor = UIColor.white +// +// self.view.addSubview(self.instructionLabel) +// self.instructionLabel.textColor = UIColor.gray +// self.instructionLabel.text = +// NSLocalizedString("Log in with Safari to continue.", +// comment: "An instruction for the user to log into Clever via Safari.") +// self.instructionLabel.sizeToFit() +// +// let cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(CleverLoginViewController.didSelectCancel)) +// +// self.navigationItem.leftBarButtonItem = cancelBarButtonItem +// self.navigationController?.navigationBar.barStyle = .default +// self.navigationController?.navigationBar.isTranslucent = false +// +// let url = NYPLUserAccount.sharedAccount().adobeToken +// +// var urlComponents = URLComponents() +// urlComponents.scheme = url.scheme; +// urlComponents.host = url.host; +// urlComponents.path = url.path; +// +// // add params +// let provider = URLQueryItem(name: "provider", value: "Clever") +// let redirect_uri = URLQueryItem(name: "redirect_uri", value: "open-ebooks-clever://oauth") +// urlComponents.queryItems = [provider, redirect_uri] +// +// NotificationCenter.default.addObserver( +// forName: NSNotification.Name.NYPLAppDelegateDidReceiveCleverRedirectURL, +// object: nil, +// queue: OperationQueue.main, +// using: { notification in self.handleRedirectURL(url: notification.object as! URL) }) +// +// +// UIApplication.shared.openURL(urlComponents.url!) +// } +// +// deinit { +// NotificationCenter.default.removeObserver(self) +// } +// +// override func viewWillLayoutSubviews() { +// self.instructionLabel.centerInSuperview(); +// self.instructionLabel.integralizeFrame(); +// } +// +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// } +// +// func handleRedirectURL(url: URL) { +// +// self.navigationItem.leftBarButtonItem?.isEnabled = false +// +// LoadingIndicatorView.show( +// self.view, +// loadingText: NSLocalizedString("Logging in, please wait…", +// comment: "An instruction to wait while a Clever login is being finalized.")) +// +// if(!url.absoluteString.hasPrefix("open-ebooks-clever") +// || !(url.absoluteString.contains("error") || url.absoluteString.contains("access_token"))) +// { +// // The server did not give us what we expected (e.g. we received a 500), +// // thus we show an error message and stop handling the result. +// self.showErrorMessage(nil) +// return +// } +// +// let fragment = url.fragment +// var kvpairs:[String:String] = [String:String]() +// let components = fragment?.components(separatedBy: "&") +// for component in components! +// { +// var kv = component.components(separatedBy: "=") +// if kv.count == 2 +// { +// kvpairs[kv[0]] = kv[1] +// } +// } +// +// if let error = kvpairs["error"] +// { +// if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +// { +// debugPrint(errorJson) +// +// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) +// +// } +// } +// +// if let auth_token = kvpairs["access_token"], +// let patron_info = kvpairs["patron_info"] +// { +// if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +// { +// var request = URLRequest(url: NYPLConfiguration.circulationURL().appendingPathComponent("AdobeAuth/authdata")) +// request.httpMethod = "GET" +// request.setValue("Bearer \(auth_token)", forHTTPHeaderField: "Authorization") +// +// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in +// if let stringData = data +// { +// if let adobe_token:String = NSString(data: stringData, encoding: String.Encoding.utf8.rawValue) as String? +// { +// self.cleverAuth = (auth_token,patronJson,adobe_token) +// +// debugPrint(auth_token) +// debugPrint(adobe_token) +// debugPrint(patronJson) +// +// self.validateCredentials() +// } +// } +// }) +// dataTask.resume() +// } +// } +// } +// +// @objc func didSelectCancel() +// { +// self.dismiss(animated: true, completion: nil) +// } +// +// func showErrorMessage(_ message: String?) +// { +// let title = NSLocalizedString( +// "Clever Sign-In Failed", +// comment: "An alert title telling the user that signing into Clever failed.") +// +// let message = message != nil ? message : NSLocalizedString("UnknownRequestError", comment: "") +// +// let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) +// alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) +// +// self.dismiss(animated: true) { +// NYPLRootTabBarController.shared()?.safelyPresentViewController(alert, animated: true, completion: nil) +// } +// } +// +// +// +// func validateCredentials() +// { +// if let authToken = self.cleverAuth?.authToken, +// let adobeToken = self.cleverAuth?.adobeToken +// { +// var request = URLRequest(url: NYPLConfiguration.loanURL()) +// request.httpMethod = "HEAD" +// request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") +// +// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, responseError) in +// +// #if FEATURE_DRM_CONNECTOR +// NYPLADEPT.sharedInstance().authorize(withVendorID: "OEI", username: adobeToken, password: "", completion: { (success, error) in +// self.authorizationAttemptDidFinish(success, error: error) +// }) +// #else +// self.authorizationAttemptDidFinish(true, error: nil) +// #endif +// +// }) +// dataTask.resume() +// } +// } +// +// func authorizationAttemptDidFinish(_ success:Bool, error:Error?) +// { +// +// OperationQueue.main.addOperation { +// +// if success +// { +// if let adobeToken = self.cleverAuth?.adobeToken, +// let patron = self.cleverAuth?.patron as? [AnyHashable: Any], +// let authToken = self.cleverAuth?.authToken +// { +// NYPLAccount.shared().setAdobeToken(adobeToken, patron: patron) +// NYPLAccount.shared().setAuthToken(authToken) +// NYPLAccount.shared().setProvider(NSLocalizedString("Clever", comment: "")) +// +// self.dismiss(animated: false, completion: nil) +// +// let handler = self.completionHandler +// self.completionHandler = nil; +// if(handler != nil) +// { +// handler!() +// } +// +// NYPLBookRegistry.shared().sync(completionHandler: nil) +// +// OperationQueue.main.addOperation({ +// NotificationCenter.default.post(name: Notification.Name(rawValue: NYPLAccountLoginDidChangeNotification), object: self) +// }) +// +// } +// } +// } +// } +// +// +// @objc class func loginWith(libraryAccountID: String, completionHandler: @escaping CompletionHandler) +// { +// +// let controller = CleverLoginViewController() +// controller.libraryAccountID = libraryAccountID +// +// controller.completionHandler = completionHandler +// +// let viewController = UINavigationController(rootViewController: controller) +// +// viewController.modalPresentationStyle = .formSheet; +// +// NYPLRootTabBarController.shared()?.safelyPresentViewController(viewController, animated: true, completion: nil) +// } +// +//} +// +//extension String { +// +// var parseJSONString: AnyObject? { +// +// let data = self.data(using: String.Encoding.utf8, allowLossyConversion: false) +// +// if let jsonData = data { +// // Will return an object or nil if JSON decoding fails +// return try! JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject? +// } else { +// // Lossless conversion of the string was not possible +// return nil +// } +// } +//} diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index 29154130a..16034a2f4 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -253,7 +253,7 @@ - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (![[NYPLADEPT sharedInstance] isUserAuthorized:[[NYPLUserAccount sharedAccount] userID] withDevice:[[NYPLUserAccount sharedAccount] deviceID]]) { - if ([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN] && !self.isCurrentlySigningIn) { + if ([[NYPLUserAccount sharedAccount] hasCredentials] && !self.isCurrentlySigningIn) { self.usernameTextField.text = [NYPLUserAccount sharedAccount].barcode; self.PINTextField.text = [NYPLUserAccount sharedAccount].PIN; [self logIn]; @@ -573,7 +573,7 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session animated:YES completion:nil]; - if (authorizeImmediately && [NYPLUserAccount sharedAccount].hasBarcodeAndPIN) { + if (authorizeImmediately && [NYPLUserAccount sharedAccount].hasCredentials) { accountViewController.PINTextField.text = [NYPLUserAccount sharedAccount].PIN; [accountViewController logIn]; } else { @@ -706,7 +706,7 @@ - (void)togglePINShowHideState - (void)accountDidChange { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if([NYPLUserAccount sharedAccount].hasBarcodeAndPIN) { + if([NYPLUserAccount sharedAccount].hasCredentials) { self.usernameTextField.text = [NYPLUserAccount sharedAccount].barcode; self.usernameTextField.enabled = NO; self.usernameTextField.textColor = [UIColor grayColor]; @@ -738,7 +738,7 @@ - (void)updateLoginLogoutCellAppearance if (self.isCurrentlySigningIn) { return; } - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + if([[NYPLUserAccount sharedAccount] hasCredentials]) { self.logInSignOutCell.textLabel.text = NSLocalizedString(@"SignOut", nil); self.logInSignOutCell.textLabel.textAlignment = NSTextAlignmentCenter; self.logInSignOutCell.textLabel.textColor = [NYPLConfiguration mainColor]; diff --git a/Simplified/NYPLAnnotations.swift b/Simplified/NYPLAnnotations.swift index 94b429e03..78c4b682c 100644 --- a/Simplified/NYPLAnnotations.swift +++ b/Simplified/NYPLAnnotations.swift @@ -80,7 +80,7 @@ import UIKit /// is guaranteed to be called on the Main queue. Otherwise, this is called /// on the same thread the function was invoked on. class func updateServerSyncSetting(toEnabled enabled: Bool, completion:@escaping (Bool)->()) { - if (NYPLUserAccount.sharedAccount().hasBarcodeAndPIN() && + if (NYPLUserAccount.sharedAccount().hasCredentials() && AccountsManager.shared.currentAccount?.details?.supportsSimplyESync == true) { guard let userProfileUrl = URL(string: AccountsManager.shared.currentAccount?.details?.userProfileUrl ?? "") else { Log.error(#file, "Could not create user profile URL from string. Abandoning attempt to update sync setting.") @@ -645,7 +645,7 @@ import UIKit /// and if the currently selected library supports it. class func syncIsPossible(_ account: NYPLUserAccount) -> Bool { let library = AccountsManager.shared.currentAccount - return account.hasBarcodeAndPIN() && library?.details?.supportsSimplyESync == true + return account.hasCredentials() && library?.details?.supportsSimplyESync == true } class func syncIsPossibleAndPermitted() -> Bool { @@ -669,8 +669,12 @@ import UIKit } else { Log.error(#file, "Error formatting auth headers.") } + } else if let authToken = NYPLUserAccount.sharedAccount().authToken { + let authenticationValue = "Bearer \(authToken)" + return ["Authorization" : "\(authenticationValue)", + "Content-Type" : "application/json"] } else { - Log.error(#file, "Attempted to create authorization header without a barcode or pin.") + Log.error(#file, "Attempted to create authorization header with neither an oauth token nor a barcode and pin pair.") } return ["Authorization" : "", "Content-Type" : "application/json"] diff --git a/Simplified/NYPLAppDelegate.m b/Simplified/NYPLAppDelegate.m index 3c15f229e..87378c981 100644 --- a/Simplified/NYPLAppDelegate.m +++ b/Simplified/NYPLAppDelegate.m @@ -103,10 +103,24 @@ - (void)application:(__attribute__((unused)) UIApplication *)application } } +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler +{ + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] && [userActivity.webpageURL.host isEqualToString:@"skyneck.pl"]) { + [[NSNotificationCenter defaultCenter] + postNotificationName:@"NYPLAppDelegateDidReceiveCleverRedirectURL" + object:userActivity.webpageURL]; + + return YES; + } + + return NO; +} + - (BOOL)application:(__unused UIApplication *)app openURL:(NSURL *)url options:(__unused NSDictionary *)options { + // URLs should be a permalink to a feed URL NSURL *entryURL = [url URLBySwappingForScheme:@"http"]; NSData *data = [NSData dataWithContentsOfURL:entryURL]; diff --git a/Simplified/NYPLBookCellDelegate.m b/Simplified/NYPLBookCellDelegate.m index db5d717cb..41cdeaf8e 100644 --- a/Simplified/NYPLBookCellDelegate.m +++ b/Simplified/NYPLBookCellDelegate.m @@ -74,7 +74,7 @@ - (void)didSelectReadForBook:(NYPLBook *)book // Try to prevent blank books bug if ((![[NYPLADEPT sharedInstance] isUserAuthorized:[[NYPLUserAccount sharedAccount] userID] withDevice:[[NYPLUserAccount sharedAccount] deviceID]]) && - ([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN])) { + ([[NYPLUserAccount sharedAccount] hasCredentials])) { [NYPLAccountSignInViewController authorizeUsingExistingBarcodeAndPinWithCompletionHandler:^{ [self openBook:book]; // with successful DRM activation }]; diff --git a/Simplified/NYPLBookRegistry.m b/Simplified/NYPLBookRegistry.m index aa9153023..d2ebd26bb 100644 --- a/Simplified/NYPLBookRegistry.m +++ b/Simplified/NYPLBookRegistry.m @@ -280,7 +280,7 @@ - (void)syncWithCompletionHandler:(void (^)(BOOL success))completion if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); }]; return; - } else if (![[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + } else if (![[NYPLUserAccount sharedAccount] hasCredentials]) { NYPLLOG(@"[syncWithCompletionHandler] No barcode and PIN"); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if(completion) completion(NO); diff --git a/Simplified/NYPLHoldsViewController.m b/Simplified/NYPLHoldsViewController.m index 975d849df..49dcd519f 100644 --- a/Simplified/NYPLHoldsViewController.m +++ b/Simplified/NYPLHoldsViewController.m @@ -256,7 +256,7 @@ - (void)didSelectSync { Account *const account = [AccountsManager shared].currentAccount; if (account.details.needsAuth) { - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + if([[NYPLUserAccount sharedAccount] hasCredentials]) { [[NYPLBookRegistry sharedRegistry] syncWithStandardAlertsOnCompletion]; } else { [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:NO completionHandler:nil]; diff --git a/Simplified/NYPLMyBooksDownloadCenter.m b/Simplified/NYPLMyBooksDownloadCenter.m index 507450782..6fa5cae86 100644 --- a/Simplified/NYPLMyBooksDownloadCenter.m +++ b/Simplified/NYPLMyBooksDownloadCenter.m @@ -719,7 +719,7 @@ - (void)startDownloadForBook:(NYPLBook *const)book return; } - if([NYPLUserAccount sharedAccount].hasBarcodeAndPIN || !loginRequired) { + if([NYPLUserAccount sharedAccount].hasCredentials || !loginRequired) { if(state == NYPLBookStateUnregistered || state == NYPLBookStateHolding) { // Check out the book [self startBorrowForBook:book attemptDownload:YES borrowCompletion:nil]; diff --git a/Simplified/NYPLMyBooksViewController.m b/Simplified/NYPLMyBooksViewController.m index 1cbd4c188..ab84dd3b1 100644 --- a/Simplified/NYPLMyBooksViewController.m +++ b/Simplified/NYPLMyBooksViewController.m @@ -372,7 +372,7 @@ - (void)didSelectSync { Account *const account = [AccountsManager shared].currentAccount; if (account.details.needsAuth) { - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + if([[NYPLUserAccount sharedAccount] hasCredentials]) { [[NYPLBookRegistry sharedRegistry] syncWithStandardAlertsOnCompletion]; } else { [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:NO completionHandler:nil]; diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index 79334312f..f7f1f2251 100644 --- a/Simplified/NYPLRemoteViewController.m +++ b/Simplified/NYPLRemoteViewController.m @@ -56,10 +56,16 @@ - (void)load NSTimeInterval timeoutInterval = 30.0; NSTimeInterval activityLabelTimer = 10.0; - NSURLRequest *const request = [NSURLRequest requestWithURL:self.URL - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:timeoutInterval]; - + NSMutableURLRequest *const request = [[NSURLRequest requestWithURL:self.URL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:timeoutInterval] mutableCopy]; + + if([[NYPLUserAccount sharedAccount] hasAuthToken]) + { + NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; + [request setValue:authValue forHTTPHeaderField:@"Authorization"]; + } + self.activityIndicatorLabel.hidden = YES; [NSTimer scheduledTimerWithTimeInterval: activityLabelTimer target: self selector: @selector(addActivityIndicatorLabel:) userInfo: nil repeats: NO]; diff --git a/Simplified/NYPLSession.m b/Simplified/NYPLSession.m index 4b2e99fb5..aa7cd0396 100644 --- a/Simplified/NYPLSession.m +++ b/Simplified/NYPLSession.m @@ -97,7 +97,13 @@ - (NSURLRequest*)withURL:(NSURL *const)URL [req setHTTPMethod:@"PUT"]; else [req setHTTPMethod:@"GET"]; - + + if([[NYPLUserAccount sharedAccount] hasAuthToken]) + { + NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; + [req setValue:authValue forHTTPHeaderField:@"Authorization"]; + } + [[self.session dataTaskWithRequest:req completionHandler:^(NSData *const data, diff --git a/Simplified/NYPLSettingsAccountDetailViewController.m b/Simplified/NYPLSettingsAccountDetailViewController.m index 5ec6c13b2..547cbddce 100644 --- a/Simplified/NYPLSettingsAccountDetailViewController.m +++ b/Simplified/NYPLSettingsAccountDetailViewController.m @@ -68,6 +68,7 @@ @interface NYPLSettingsAccountDetailViewController () 0); - assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); - - [self.usernameTextField resignFirstResponder]; - [self.PINTextField resignFirstResponder]; - - [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; - - [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; - - [self validateCredentials]; + if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + // oauth + NSURL *oauthURL = self.selectedAccount.details.oauthIntermediaryUrl; + + NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; + + // add params + NSURLQueryItem *redirect_uri = [[NSURLQueryItem alloc] initWithName:@"redirect_uri" value:@"https://skyneck.pl/login"]; + urlComponents.queryItems = [urlComponents.queryItems arrayByAddingObject:redirect_uri]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRedirectURL:) + name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" + object:nil]; + + [UIApplication.sharedApplication openURL: urlComponents.URL]; + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + } else { + // bar and pin + assert(self.usernameTextField.text.length > 0); + assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); + + [self.usernameTextField resignFirstResponder]; + [self.PINTextField resignFirstResponder]; + + [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; + + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + + [self validateCredentials]; + } } +- (void) handleRedirectURL: (NSNotification *) notification +{ + [NSNotificationCenter.defaultCenter removeObserver: self name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" object: nil]; + + NSURL *url = notification.object; + if (![url.absoluteString hasPrefix:@"https://skyneck.pl/login"] + || !([url.absoluteString containsString:@"error"] || [url.absoluteString containsString:@"access_token"])) + { + [self displayErrorMessage:nil]; + return; + } + + NSMutableDictionary *kvpairs = [[NSMutableDictionary alloc] init]; + for (NSString *param in [url.fragment componentsSeparatedByString:@"&"]) { + NSArray *elts = [param componentsSeparatedByString:@"="]; + if([elts count] < 2) continue; + [kvpairs setObject:[elts lastObject] forKey:[elts firstObject]]; + } + +// if (kvpairs[@"error"] != nil) { +// NSString *error = [[kvpairs[@"error"] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; +// +// +// if ([error parseJSONString] != nil) { +// +// } +//// if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +//// { +//// debugPrint(errorJson) +//// +//// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) +//// +//// } +// +// } + + + NSString *auth_token = kvpairs[@"access_token"]; + NSString *patron_info = kvpairs[@"patron_info"]; + + if (auth_token != nil && patron_info != nil) { + NSString *patron = [[patron_info stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; + + if ([patron parseJSONString] != nil) { + self.authToken = auth_token; + [self validateCredentials]; + } + +// if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +// { +// var request = URLRequest(url: NYPLConfiguration.circulationURL().appendingPathComponent("AdobeAuth/authdata")) +// request.httpMethod = "GET" +// request.setValue("Bearer \(auth_token)", forHTTPHeaderField: "Authorization") +// +// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in +// if let stringData = data +// { +// if let adobe_token:String = NSString(data: stringData, encoding: String.Encoding.utf8.rawValue) as String? +// { +// self.cleverAuth = (auth_token,patronJson,adobe_token) +// +// debugPrint(auth_token) +// debugPrint(adobe_token) +// debugPrint(patronJson) +// +// self.validateCredentials() +// } +// } +// }) +// dataTask.resume() +// } + } + +} + + + - (void)logOut { @@ -481,7 +581,7 @@ - (void)logOut } else { [[NYPLMyBooksDownloadCenter sharedDownloadCenter] reset:self.selectedAccountId]; [[NYPLBookRegistry sharedRegistry] reset:self.selectedAccountId]; - [[NYPLAccount sharedAccount:self.selectedAccountId] removeAll]; + [[NYPLUserAccount sharedAccount:self.selectedAccountId] removeAll]; [self setupTableData]; [self.tableView reloadData]; [self removeActivityTitle]; @@ -559,6 +659,14 @@ - (void)validateCredentials request.timeoutInterval = sRequestTimeoutInterval; + if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + NSString *authToken = self.authToken; + if (authToken != nil) { + NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; + [request addValue:authenticationValue forHTTPHeaderField:@"Authorization"]; + } + } + __weak __auto_type weakSelf = self; NSURLSessionDataTask *const task = [self.session @@ -714,7 +822,13 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error [[UIApplication sharedApplication] endIgnoringInteractionEvents]; if (success) { - [self.selectedUserAccount setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; + // szyjson tu authtoken + if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + [self.selectedUserAccount setAuthToken:self.authToken]; +// [self.selectedUserAccount setPatron:self.patron]; // szyjson + } else { + [self.selectedUserAccount setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; + } if ([self.selectedAccountId isEqualToString:[AccountsManager shared].currentAccount.uuid]) { [[NYPLBookRegistry sharedRegistry] syncWithCompletionHandler:^(BOOL success) { @@ -775,7 +889,7 @@ - (void)tableView:(__attribute__((unused)) UITableView *)tableView case CellKindLogInSignOut: { [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; NSString *logoutString; - if([self.selectedUserAccount hasBarcodeAndPIN]) { + if([self.selectedUserAccount hasCredentials]) { if ([self.businessLogic shouldShowSyncButton] && !self.syncSwitch.on) { logoutString = NSLocalizedString(@"SettingsAccountViewControllerLogoutMessageSync", nil); } else { @@ -1376,7 +1490,7 @@ - (void)togglePINShowHideState - (void)accountDidChange { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if(self.selectedUserAccount.hasBarcodeAndPIN) { + if(self.selectedUserAccount.hasCredentials) { [self checkSyncPermissionForCurrentPatron]; self.usernameTextField.text = self.selectedUserAccount.barcode; self.usernameTextField.enabled = NO; @@ -1402,7 +1516,7 @@ - (void)accountDidChange - (void)updateLoginLogoutCellAppearance { - if([self.selectedUserAccount hasBarcodeAndPIN]) { + if([self.selectedUserAccount hasCredentials]) { self.logInSignOutCell.textLabel.text = NSLocalizedString(@"SignOut", nil); self.logInSignOutCell.textLabel.textAlignment = NSTextAlignmentCenter; self.logInSignOutCell.textLabel.textColor = [NYPLConfiguration mainColor]; @@ -1415,7 +1529,8 @@ - (void)updateLoginLogoutCellAppearance BOOL const pinHasText = [self.PINTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; BOOL const pinIsNotRequired = self.selectedAccount.details.pinKeyboard == LoginKeyboardNone; - if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired)) { + BOOL const oauthLogin = self.selectedAccount.details.oauthIntermediaryUrl != nil; + if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired) || oauthLogin) { self.logInSignOutCell.userInteractionEnabled = YES; self.logInSignOutCell.textLabel.textColor = [NYPLConfiguration mainColor]; } else { diff --git a/Simplified/NYPLSignInBusinessLogic.swift b/Simplified/NYPLSignInBusinessLogic.swift index 63ff67d8b..bb11fda01 100644 --- a/Simplified/NYPLSignInBusinessLogic.swift +++ b/Simplified/NYPLSignInBusinessLogic.swift @@ -35,7 +35,7 @@ class NYPLSignInBusinessLogic: NSObject { } @objc func isSignedIn() -> Bool { - return userAccount.hasBarcodeAndPIN() + return userAccount.hasCredentials() } @objc func registrationIsPossible() -> Bool { @@ -64,7 +64,7 @@ class NYPLSignInBusinessLogic: NSObject { return libraryDetails.supportsSimplyESync && libraryDetails.getLicenseURL(.annotations) != nil && - userAccount.hasBarcodeAndPIN() && + userAccount.hasCredentials() && libraryAccountID == AccountsManager.shared.currentAccount?.uuid } @@ -123,4 +123,67 @@ class NYPLSignInBusinessLogic: NSObject { self.permissionsCheckLock.unlock() } } + + @objc func handleRedirectURL(url: URL) { + + if(!url.absoluteString.hasPrefix("open-ebooks-clever") + || !(url.absoluteString.contains("error") || url.absoluteString.contains("access_token"))) + { + // The server did not give us what we expected (e.g. we received a 500), + // thus we show an error message and stop handling the result. + +// self.showErrorMessage(nil) + return + } + + let fragment = url.fragment + var kvpairs:[String:String] = [String:String]() + let components = fragment?.components(separatedBy: "&") + for component in components! + { + var kv = component.components(separatedBy: "=") + if kv.count == 2 + { + kvpairs[kv[0]] = kv[1] + } + } + + if let error = kvpairs["error"] + { + if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString + { + debugPrint(errorJson) + +// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) + + } + } + + if let auth_token = kvpairs["access_token"], + let patron_info = kvpairs["patron_info"] + { + if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString + { + + } + } + } + + +} + +extension NSString { + + @objc var parseJSONString: AnyObject? { + + let data = self.data(using: String.Encoding.utf8.rawValue, allowLossyConversion: false) + + if let jsonData = data { + // Will return an object or nil if JSON decoding fails + return try! JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject? + } else { + // Lossless conversion of the string was not possible + return nil + } + } } diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index f0c18a8c6..dc9b99fa8 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -154,7 +154,8 @@ extension Notification.Name { return nil } - + + // szyjson set token here var authToken: String? { return NYPLKeychain.shared()?.object(forKey: authTokenKey) as? String } diff --git a/Simplified/SimplyE.entitlements b/Simplified/SimplyE.entitlements index b88ac2098..ecd5b6022 100644 --- a/Simplified/SimplyE.entitlements +++ b/Simplified/SimplyE.entitlements @@ -2,9 +2,9 @@ - keychain-access-groups + com.apple.developer.associated-domains - 7262U6ST2R.org.nypl.labs.SharedKeychainGroup + applinks:skyneck.pl diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.m b/Simplified/Utilities/Authentication/NYPLBasicAuth.m index 00f50be2a..a661132bf 100644 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.m +++ b/Simplified/Utilities/Authentication/NYPLBasicAuth.m @@ -2,6 +2,8 @@ #import "NYPLBasicAuth.h" +// szyjson + void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *const challenge, void (^completionHandler) (NSURLSessionAuthChallengeDisposition disposition, From 3b4676f8728d16fd35262cf141d85cfdf4850de0 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Wed, 13 May 2020 12:33:47 +0200 Subject: [PATCH 02/19] note to self --- Simplified/NYPLRemoteViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index f7f1f2251..309b9ffda 100644 --- a/Simplified/NYPLRemoteViewController.m +++ b/Simplified/NYPLRemoteViewController.m @@ -60,6 +60,7 @@ - (void)load cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeoutInterval] mutableCopy]; + // szyjson this makes books list loading possible if([[NYPLUserAccount sharedAccount] hasAuthToken]) { NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; From 7193fc15e4379969c8b3acd6e5bad4ef00c52cf7 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Wed, 20 May 2020 16:14:12 +0200 Subject: [PATCH 03/19] Clever alpha version --- Simplified/Account.swift | 4 +- Simplified/AccountsManager.swift | 8 +- Simplified/NYPLAccountSignInViewController.h | 2 + Simplified/NYPLAccountSignInViewController.m | 205 +++++++++++++++--- Simplified/NYPLAnnotations.swift | 16 +- Simplified/NYPLBook.m | 2 +- Simplified/NYPLBookRegistry.m | 26 ++- Simplified/NYPLCatalogNavigationController.m | 15 +- Simplified/NYPLMyBooksDownloadCenter.m | 17 +- Simplified/NYPLSession.m | 50 ++--- .../NYPLSettingsAccountDetailViewController.m | 1 - ...ettingsAccountURLSessionChallengeHandler.m | 14 +- Simplified/Network/NYPLNetworkExecutor.swift | 109 +++++++++- Simplified/Network/NYPLNetworkResponder.swift | 79 ++++++- .../Utilities/Authentication/NYPLBasicAuth.h | 24 +- .../Utilities/Authentication/NYPLBasicAuth.m | 112 +++++----- 16 files changed, 505 insertions(+), 179 deletions(-) diff --git a/Simplified/Account.swift b/Simplified/Account.swift index a9d6f0b14..42eb10c7a 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -340,7 +340,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) @@ -352,7 +352,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) """) diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index f249af013..149fa4efd 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -83,7 +83,9 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf object: nil ) - self.loadCatalogs(completion: {_ in }) + DispatchQueue.main.async { + self.loadCatalogs(completion: {_ in }) + } } let completionHandlerAccessQueue = DispatchQueue(label: "libraryListCompletionHandlerAccessQueue") @@ -204,12 +206,12 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf 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) diff --git a/Simplified/NYPLAccountSignInViewController.h b/Simplified/NYPLAccountSignInViewController.h index 90225eb3c..e8e941e61 100644 --- a/Simplified/NYPLAccountSignInViewController.h +++ b/Simplified/NYPLAccountSignInViewController.h @@ -17,4 +17,6 @@ // and pin. + (void)authorizeUsingExistingBarcodeAndPinWithCompletionHandler:(void (^)(void))handler; ++ (void)authorizeUsingIntermediaryWithCompletionHandler:(void (^)(void))handler; + @end diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index 16034a2f4..795806fae 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -9,7 +9,6 @@ #import "NYPLAccountSignInViewController.h" #import "NYPLAppDelegate.h" #import "NYPLBarcodeScanningViewController.h" -#import "NYPLBasicAuth.h" #import "NYPLBookCoverRegistry.h" #import "NYPLBookRegistry.h" #import "NYPLConfiguration.h" @@ -18,6 +17,7 @@ #import "NYPLOPDSFeed.h" #import "NYPLReachability.h" #import "NYPLRootTabBarController.h" +#import "NYPLSettingsAccountURLSessionChallengeHandler.h" #import "NYPLSettingsEULAViewController.h" #import "NYPLUserAccountFrontEndValidation.h" #import "NYPLXML.h" @@ -40,7 +40,7 @@ typedef NS_ENUM(NSInteger, Section) { SectionRegistration = 1 }; -@interface NYPLAccountSignInViewController () +@interface NYPLAccountSignInViewController () // state machine @property (nonatomic) BOOL isLoggingInAfterSignUp; @@ -57,9 +57,11 @@ @interface NYPLAccountSignInViewController () 0); - assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); - [self.usernameTextField resignFirstResponder]; - [self.PINTextField resignFirstResponder]; + if (self.currentAccount.details.oauthIntermediaryUrl) { + // oauth + NSURL *oauthURL = self.currentAccount.details.oauthIntermediaryUrl; - [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; - - [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; - - [self validateCredentials]; + NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; + + // add params + NSURLQueryItem *redirect_uri = [[NSURLQueryItem alloc] initWithName:@"redirect_uri" value:@"https://skyneck.pl/login"]; + urlComponents.queryItems = [urlComponents.queryItems arrayByAddingObject:redirect_uri]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRedirectURL:) + name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" + object:nil]; + + [UIApplication.sharedApplication openURL: urlComponents.URL]; + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + } else { + // bar and pin + assert(self.usernameTextField.text.length > 0); + assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); + + [self.usernameTextField resignFirstResponder]; + [self.PINTextField resignFirstResponder]; + + [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; + + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + + [self validateCredentials]; + } +} + +- (void) handleRedirectURL: (NSNotification *) notification +{ + [NSNotificationCenter.defaultCenter removeObserver: self name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" object: nil]; + + NSURL *url = notification.object; + if (![url.absoluteString hasPrefix:@"https://skyneck.pl/login"] + || !([url.absoluteString containsString:@"error"] || [url.absoluteString containsString:@"access_token"])) + { + [self displayErrorMessage:nil]; + return; + } + + NSMutableDictionary *kvpairs = [[NSMutableDictionary alloc] init]; + for (NSString *param in [url.fragment componentsSeparatedByString:@"&"]) { + NSArray *elts = [param componentsSeparatedByString:@"="]; + if([elts count] < 2) continue; + [kvpairs setObject:[elts lastObject] forKey:[elts firstObject]]; + } + +// if (kvpairs[@"error"] != nil) { +// NSString *error = [[kvpairs[@"error"] stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; +// +// +// if ([error parseJSONString] != nil) { +// +// } +//// if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +//// { +//// debugPrint(errorJson) +//// +//// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) +//// +//// } +// +// } + + + NSString *auth_token = kvpairs[@"access_token"]; + NSString *patron_info = kvpairs[@"patron_info"]; + + if (auth_token != nil && patron_info != nil) { + NSString *patron = [[patron_info stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByRemovingPercentEncoding]; + + if ([patron parseJSONString] != nil) { + self.authToken = auth_token; + [self validateCredentials]; + } + +// if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString +// { +// var request = URLRequest(url: NYPLConfiguration.circulationURL().appendingPathComponent("AdobeAuth/authdata")) +// request.httpMethod = "GET" +// request.setValue("Bearer \(auth_token)", forHTTPHeaderField: "Authorization") +// +// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in +// if let stringData = data +// { +// if let adobe_token:String = NSString(data: stringData, encoding: String.Encoding.utf8.rawValue) as String? +// { +// self.cleverAuth = (auth_token,patronJson,adobe_token) +// +// debugPrint(auth_token) +// debugPrint(adobe_token) +// debugPrint(patronJson) +// +// self.validateCredentials() +// } +// } +// }) +// dataTask.resume() +// } + } + +} + +- (void)displayErrorMessage:(NSString *)errorMessage { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero]; + label.text = errorMessage; + [label sizeToFit]; + label.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2); + [self.view addSubview:label]; } - (void)setActivityTitleWithText:(NSString *)text @@ -822,7 +945,15 @@ - (void)validateCredentials [self.currentAccount.details userProfileUrl]]]; request.timeoutInterval = 20.0; - + + if (self.currentAccount.details.oauthIntermediaryUrl != nil) { + NSString *authToken = self.authToken; + if (authToken != nil) { + NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; + [request addValue:authenticationValue forHTTPHeaderField:@"Authorization"]; + } + } + self.isCurrentlySigningIn = YES; NSURLSessionDataTask *const task = [self.session @@ -1011,6 +1142,14 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error [[UIApplication sharedApplication] endIgnoringInteractionEvents]; if(success) { + // szyjson tu authtoken + if (AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl) { + [[NYPLUserAccount sharedAccount] setAuthToken:self.authToken]; +// [[NYPLUserAccount sharedAccount] setPatron:self.patron]; // szyjson + } else { + [[NYPLUserAccount sharedAccount] setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; + } + [[NYPLUserAccount sharedAccount] setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; if (!self.isLoggingInAfterSignUp) { diff --git a/Simplified/NYPLAnnotations.swift b/Simplified/NYPLAnnotations.swift index 78c4b682c..cef7645fb 100644 --- a/Simplified/NYPLAnnotations.swift +++ b/Simplified/NYPLAnnotations.swift @@ -653,12 +653,20 @@ import UIKit return syncIsPossible(NYPLUserAccount.sharedAccount()) && acct?.details?.syncPermissionGranted == true } - class func setDefaultAnnotationHeaders(forRequest request: inout URLRequest) { - for (headerKey, headerValue) in NYPLAnnotations.headers { - request.setValue(headerValue, forHTTPHeaderField: headerKey) + @objc class func addingDefaultAnnotationHeaders(to request: URLRequest) -> URLRequest { + var request = request + for (headerKey, headerValue) in NYPLAnnotations.headers { + request.setValue(headerValue, forHTTPHeaderField: headerKey) + } + return request } + + class func setDefaultAnnotationHeaders(forRequest request: inout URLRequest) { + for (headerKey, headerValue) in NYPLAnnotations.headers { + request.setValue(headerValue, forHTTPHeaderField: headerKey) + } } - + class var headers: [String:String] { if let barcode = NYPLUserAccount.sharedAccount().barcode, let pin = NYPLUserAccount.sharedAccount().PIN { let authenticationString = "\(barcode):\(pin)" diff --git a/Simplified/NYPLBook.m b/Simplified/NYPLBook.m index 74d4e9cd4..91eaa2440 100644 --- a/Simplified/NYPLBook.m +++ b/Simplified/NYPLBook.m @@ -315,7 +315,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary NSMutableArray *const mutableAcquisitions = [NSMutableArray array]; - NSString *const applicationEPUBZIP = @"application/epub+zip"; + NSString *const applicationEPUBZIP = ContentTypeEpubZip; NSString *const genericString = NYPLNullToNil(dictionary[DeprecatedAcquisitionKey][@"generic"]); NSURL *const genericURL = genericString ? [NSURL URLWithString:genericString] : nil; diff --git a/Simplified/NYPLBookRegistry.m b/Simplified/NYPLBookRegistry.m index d2ebd26bb..b699a5c35 100644 --- a/Simplified/NYPLBookRegistry.m +++ b/Simplified/NYPLBookRegistry.m @@ -281,13 +281,25 @@ - (void)syncWithCompletionHandler:(void (^)(BOOL success))completion }]; return; } else if (![[NYPLUserAccount sharedAccount] hasCredentials]) { - NYPLLOG(@"[syncWithCompletionHandler] No barcode and PIN"); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if(completion) completion(NO); - if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); - [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; - }]; - return; +// AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl + if (AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl) { + // sign in + NYPLLOG(@"[syncWithCompletionHandler] No valid authorization header"); + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + if(completion) completion(NO); + if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); + [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; + }]; + return; + } else { + NYPLLOG(@"[syncWithCompletionHandler] No barcode and PIN"); + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + if(completion) completion(NO); + if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); + [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; + }]; + return; + } } else { self.syncing = YES; self.syncShouldCommit = YES; diff --git a/Simplified/NYPLCatalogNavigationController.m b/Simplified/NYPLCatalogNavigationController.m index 7bf032ecc..8524203ab 100644 --- a/Simplified/NYPLCatalogNavigationController.m +++ b/Simplified/NYPLCatalogNavigationController.m @@ -3,6 +3,7 @@ #import "NYPLCatalogNavigationController.h" +#import "NYPLAccountSignInViewController.h" #import "NYPLBookRegistry.h" #import "NYPLRootTabBarController.h" #import "NYPLMyBooksNavigationController.h" @@ -205,6 +206,13 @@ - (void)updateFeedAndRegistryOnAccountChange completion(); }); }]; + } else if (account.details.oauthIntermediaryUrl && !NYPLUserAccount.sharedAccount.hasAuthToken) { + // sign in + [NYPLAccountSignInViewController authorizeUsingIntermediaryWithCompletionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + completion(); + }); + }]; } else { if (![NSThread isMainThread]) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -302,8 +310,11 @@ -(void) welcomeScreenCompletionHandlerForAccount:(Account *const)account [[NYPLSettings sharedSettings] setUserHasSeenWelcomeScreen:YES]; [[NYPLBookRegistry sharedRegistry] save]; [AccountsManager sharedInstance].currentAccount = account; - [self updateFeedAndRegistryOnAccountChange]; - [self dismissViewControllerAnimated:YES completion:nil]; + [self dismissViewControllerAnimated:YES completion:^{ + [self updateFeedAndRegistryOnAccountChange]; + }]; +// [self updateFeedAndRegistryOnAccountChange]; +// [self dismissViewControllerAnimated:YES completion:nil]; } @end diff --git a/Simplified/NYPLMyBooksDownloadCenter.m b/Simplified/NYPLMyBooksDownloadCenter.m index 6fa5cae86..12aac10e0 100644 --- a/Simplified/NYPLMyBooksDownloadCenter.m +++ b/Simplified/NYPLMyBooksDownloadCenter.m @@ -2,7 +2,6 @@ #import "NSString+NYPLStringAdditions.h" #import "NYPLAccountSignInViewController.h" -#import "NYPLBasicAuth.h" #import "NYPLBook.h" #import "NYPLBookCoverRegistry.h" #import "NYPLBookRegistry.h" @@ -120,16 +119,16 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session // We update the rights management status based on the MIME type given to us by the server. We do // this only once at the point when we first start receiving data. if(bytesWritten == totalBytesWritten) { - if([downloadTask.response.MIMEType isEqualToString:@"application/vnd.adobe.adept+xml"]) { + if([downloadTask.response.MIMEType isEqualToString:ContentTypeAdobeAdept]) { self.bookIdentifierToDownloadInfo[book.identifier] = [[self downloadInfoForBookIdentifier:book.identifier] withRightsManagement:NYPLMyBooksDownloadRightsManagementAdobe]; - } else if([downloadTask.response.MIMEType isEqualToString:@"application/epub+zip"]) { + } else if([downloadTask.response.MIMEType isEqualToString:ContentTypeEpubZip]) { self.bookIdentifierToDownloadInfo[book.identifier] = [[self downloadInfoForBookIdentifier:book.identifier] withRightsManagement:NYPLMyBooksDownloadRightsManagementNone]; } else if ([downloadTask.response.MIMEType - isEqualToString:@"application/vnd.librarysimplified.bearer-token+json"]) { + isEqualToString:ContentTypeBearerToken]) { self.bookIdentifierToDownloadInfo[book.identifier] = [[self downloadInfoForBookIdentifier:book.identifier] withRightsManagement:NYPLMyBooksDownloadRightsManagementSimplifiedBearerTokenJSON]; @@ -225,6 +224,7 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session break; } case NYPLMyBooksDownloadRightsManagementSimplifiedBearerTokenJSON: { + // szyjson it looks like it firstly downloads json with a bearer token, and then downloads the book itself NSData *const data = [NSData dataWithContentsOfURL:location]; if (!data) { [self failDownloadForBook:book]; @@ -322,7 +322,7 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - NYPLBasicAuthHandler(challenge, completionHandler); + [NYPLBasicAuth authHandlerWithChallenge:challenge completionHandler:completionHandler]; } // This is implemented in order to be able to handle redirects when using @@ -345,6 +345,7 @@ - (void)URLSession:(__unused NSURLSession *)session self.taskIdentifierToRedirectAttempts[@(task.taskIdentifier)] = @(redirectAttempts + 1); + // szyjson appends bearer token to redirected requests NSString *const authorizationKey = @"Authorization"; // Since any "Authorization" header will be dropped on redirection for security @@ -726,7 +727,7 @@ - (void)startDownloadForBook:(NYPLBook *const)book } else { // Actually download the book. NSURL *URL = book.defaultAcquisition.hrefURL; - NSURLRequest *const request = [NSURLRequest requestWithURL:URL]; + NSURLRequest *const request = [NYPLNetworkExecutor bearerAuthorizedWithRequest:[NSURLRequest requestWithURL:URL]]; if(!request.URL) { // Originally this code just let the request fail later on, but apparently resuming an @@ -736,7 +737,8 @@ - (void)startDownloadForBook:(NYPLBook *const)book [self failDownloadForBook:book]; return; } - + + // szyjson book downloading, insert header and maybe set a type? NSURLSessionDownloadTask *const task = [self.session downloadTaskWithRequest:request]; self.bookIdentifierToDownloadInfo[book.identifier] = @@ -766,6 +768,7 @@ - (void)startDownloadForBook:(NYPLBook *const)book } } else { + // szyjson show login screen here [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:NO completionHandler:^{ diff --git a/Simplified/NYPLSession.m b/Simplified/NYPLSession.m index aa7cd0396..e67101602 100644 --- a/Simplified/NYPLSession.m +++ b/Simplified/NYPLSession.m @@ -1,5 +1,4 @@ #import "NYPLAsync.h" -#import "NYPLBasicAuth.h" #import "SimplyE-Swift.h" #import "NYPLSession.h" @@ -69,7 +68,7 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session task.currentRequest.URL.absoluteString, challenge.protectionSpace.authenticationMethod); - NYPLBasicAuthHandler(challenge, completionHandler); + [NYPLBasicAuth authHandlerWithChallenge:challenge completionHandler:completionHandler]; } #pragma mark - @@ -89,38 +88,27 @@ - (NSURLRequest*)withURL:(NSURL *const)URL if(!handler) { @throw NSInvalidArgumentException; } - - NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:URL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; - + + NSURLRequest *req; + void (^completionWrapper)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^ void (NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error){ + if (error) { + NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [NYPLErrorLogger logNetworkError:error + request:req + response:response + message:dataString]; + handler(nil, response, error); + return; + } + + handler(data, response, nil); + }; + NSString *lpe = [URL lastPathComponent]; if ([lpe isEqualToString:@"borrow"]) - [req setHTTPMethod:@"PUT"]; + req = [NYPLNetworkExecutor.shared PUT:URL completion:completionWrapper]; else - [req setHTTPMethod:@"GET"]; - - if([[NYPLUserAccount sharedAccount] hasAuthToken]) - { - NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; - [req setValue:authValue forHTTPHeaderField:@"Authorization"]; - } - - [[self.session - dataTaskWithRequest:req - completionHandler:^(NSData *const data, - NSURLResponse *response, - NSError *const error) { - if (error) { - NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - [NYPLErrorLogger logNetworkError:error - request:req - response:response - message:dataString]; - handler(nil, response, error); - return; - } - - handler(data, response, nil); - }] resume]; + req = [NYPLNetworkExecutor.shared GET:URL completion:completionWrapper]; return req; } diff --git a/Simplified/NYPLSettingsAccountDetailViewController.m b/Simplified/NYPLSettingsAccountDetailViewController.m index 547cbddce..3c3572184 100644 --- a/Simplified/NYPLSettingsAccountDetailViewController.m +++ b/Simplified/NYPLSettingsAccountDetailViewController.m @@ -6,7 +6,6 @@ #import #import -#import "NYPLBasicAuth.h" #import "NYPLBookCoverRegistry.h" #import "NYPLBookRegistry.h" #import "NYPLCatalogNavigationController.h" diff --git a/Simplified/NYPLSettingsAccountURLSessionChallengeHandler.m b/Simplified/NYPLSettingsAccountURLSessionChallengeHandler.m index 9d90d58ee..355d0e04f 100644 --- a/Simplified/NYPLSettingsAccountURLSessionChallengeHandler.m +++ b/Simplified/NYPLSettingsAccountURLSessionChallengeHandler.m @@ -7,7 +7,7 @@ // #import "NYPLSettingsAccountURLSessionChallengeHandler.h" -#import "NYPLBasicAuth.h" +#import "SimplyE-Swift.h" @implementation NYPLSettingsAccountURLSessionChallengeHandler @@ -30,10 +30,14 @@ - (void) URLSession:(__attribute__((unused)) NSURLSession *)session completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - NYPLBasicAuthCustomHandler(challenge, - completionHandler, - self.uiDelegate.username, - self.uiDelegate.pin); + if (self.uiDelegate.username && self.uiDelegate.pin) { + [NYPLBasicAuth authCustomHandlerWithChallenge:challenge + completionHandler:completionHandler + username:self.uiDelegate.username + password:self.uiDelegate.pin]; + } else { + @throw NSInvalidArgumentException; + } } @end diff --git a/Simplified/Network/NYPLNetworkExecutor.swift b/Simplified/Network/NYPLNetworkExecutor.swift index c1d93216f..ff8f1d7c4 100644 --- a/Simplified/Network/NYPLNetworkExecutor.swift +++ b/Simplified/Network/NYPLNetworkExecutor.swift @@ -9,8 +9,8 @@ import Foundation enum NYPLResult { - case success(SuccessInfo) - case failure(Error) + case success(SuccessInfo, URLResponse?) + case failure(Error, URLResponse?) } @@ -22,7 +22,7 @@ enum NYPLResult { /// header if present, otherwise defaults to 3 hours. /// /// The cache lives on both memory and disk. -class NYPLNetworkExecutor { +@objc class NYPLNetworkExecutor: NSObject { private let urlSession: URLSession /// The delegate of the URLSession. @@ -49,7 +49,7 @@ class NYPLNetworkExecutor { } /// A shared generic executor with enabled fallback caching. - static let shared = NYPLNetworkExecutor(shouldEnableFallbackCaching: true) + @objc static let shared = NYPLNetworkExecutor(shouldEnableFallbackCaching: true) /// Performs a GET request using the specified URL /// - Parameters: @@ -67,21 +67,112 @@ class NYPLNetworkExecutor { /// - req: The request to perform. /// - completion: Always called when the resource is either fetched from /// the network or from the cache. + @discardableResult func executeRequest(_ req: URLRequest, - completion: @escaping (_ result: NYPLResult) -> Void) { + completion: @escaping (_ result: NYPLResult) -> Void) -> URLSessionDataTask { let task = urlSession.dataTask(with: req) responder.addCompletion(completion, taskID: task.taskIdentifier) task.resume() + return task } } extension NYPLNetworkExecutor { - private func request(for url: URL) -> URLRequest { - return URLRequest(url: url, - cachePolicy: urlSession.configuration.requestCachePolicy) - } + func request(for url: URL) -> URLRequest { + + var urlRequest = URLRequest(url: url, + cachePolicy: urlSession.configuration.requestCachePolicy) + + if let authToken = NYPLUserAccount.sharedAccount().authToken { + let headers = [ + "Authorization" : "Bearer \(authToken)", + "Content-Type" : "application/json" + ] + + headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } + } + + return urlRequest + } func clearCache() { urlSession.configuration.urlCache?.removeAllCachedResponses() } } + +// Objective-C compatibility +extension NYPLNetworkExecutor { + @objc class func bearerAuthorized(request: URLRequest) -> URLRequest { + var request = request + var headers: [String: String] = ["Authorization" : "", + "Content-Type" : "application/json"] + if let authToken = NYPLUserAccount.sharedAccount().authToken { + let authenticationValue = "Bearer \(authToken)" + headers = ["Authorization" : "\(authenticationValue)", + "Content-Type" : "application/json"] + } else { + Log.error(#file, "Attempted to create authorization header with neither an oauth token nor a barcode and pin pair.") + } + for (headerKey, headerValue) in headers { + request.setValue(headerValue, forHTTPHeaderField: headerKey) + } + return request + } + + /// Performs a GET request using the specified URL + /// - Parameters: + /// - reqURL: URL of the resource to GET. + /// - completion: Always called when the resource is either fetched from + /// the network or from the cache. + @objc func download(_ reqURL: URL, + completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDownloadTask { + let req = request(for: reqURL) + let completionWrapper: (_ result: NYPLResult) -> Void = { result in + switch result { + case let .success(data, response): completion(data, response, nil) + case let .failure(error, response): completion(nil, response, error) + } + } + + let task = urlSession.downloadTask(with: req) + responder.addCompletion(completionWrapper, taskID: task.taskIdentifier) + task.resume() + + return task + } + + /// Performs a GET request using the specified URL + /// - Parameters: + /// - reqURL: URL of the resource to GET. + /// - completion: Always called when the resource is either fetched from + /// the network or from the cache. + @objc func GET(_ reqURL: URL, + completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDataTask { + let req = request(for: reqURL) + let completionWrapper: (_ result: NYPLResult) -> Void = { result in + switch result { + case let .success(data, response): completion(data, response, nil) + case let .failure(error, response): completion(nil, response, error) + } + } + return executeRequest(req, completion: completionWrapper) + } + + /// Performs a GET request using the specified URL + /// - Parameters: + /// - reqURL: URL of the resource to GET. + /// - completion: Always called when the resource is either fetched from + /// the network or from the cache. + @objc func PUT(_ reqURL: URL, + completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDataTask { + var req = request(for: reqURL) + req.httpMethod = "PUT" + let completionWrapper: (_ result: NYPLResult) -> Void = { result in + switch result { + case let .success(data, response): completion(data, response, nil) + case let .failure(error, response): completion(nil, response, error) + } + } + return executeRequest(req, completion: completionWrapper) + } +} diff --git a/Simplified/Network/NYPLNetworkResponder.swift b/Simplified/Network/NYPLNetworkResponder.swift index a2bc28953..59f015cb2 100644 --- a/Simplified/Network/NYPLNetworkResponder.swift +++ b/Simplified/Network/NYPLNetworkResponder.swift @@ -22,7 +22,7 @@ fileprivate struct NYPLNetworkTaskInfo { } //------------------------------------------------------------------------------ -class NYPLNetworkResponder: NSObject, URLSessionDelegate, URLSessionDataDelegate { +class NYPLNetworkResponder: NSObject { typealias TaskID = Int private var taskInfo: [TaskID: NYPLNetworkTaskInfo] @@ -45,10 +45,11 @@ class NYPLNetworkResponder: NSObject, URLSessionDelegate, URLSessionDataDelegate taskInfo[taskID] = NYPLNetworkTaskInfo(completion: completion) } +} //---------------------------------------------------------------------------- // MARK: - URLSessionDelegate - +extension NYPLNetworkResponder: URLSessionDelegate { func urlSession(_ session: URLSession, didBecomeInvalidWithError err: Error?) { if let err = err { NYPLErrorLogger.logError(err, message: "URLSession became invalid") @@ -65,10 +66,11 @@ class NYPLNetworkResponder: NSObject, URLSessionDelegate, URLSessionDataDelegate taskInfo.removeAll() } +} //---------------------------------------------------------------------------- // MARK: - URLSessionDataDelegate - +extension NYPLNetworkResponder: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { @@ -122,7 +124,7 @@ class NYPLNetworkResponder: NSObject, URLSessionDelegate, URLSessionDataDelegate Log.debug(#file, "Task \(taskID) completed, elapsed time: \(elapsed) sec") if let error = error { - currentTaskInfo.completion(.failure(error)) + currentTaskInfo.completion(.failure(error, task.response)) // logging the error after the completion call so that the error report // will include any eventual logging done in the completion handler. @@ -134,7 +136,74 @@ class NYPLNetworkResponder: NSObject, URLSessionDelegate, URLSessionDataDelegate return } - currentTaskInfo.completion(.success(currentTaskInfo.progressData)) + currentTaskInfo.completion(.success(currentTaskInfo.progressData, task.response)) } } + +//---------------------------------------------------------------------------- +// MARK: - URLSessionTaskDelegate +extension NYPLNetworkResponder: URLSessionTaskDelegate { + func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { +// NYPLLOG_F(@"NSURLSessionTask: %@. Challenge Received: %@", +// task.currentRequest.URL.absoluteString, +// challenge.protectionSpace.authenticationMethod); +// +// NYPLBasicAuthHandler(challenge, completionHandler); + + NYPLBasicAuth.authHandler(challenge: challenge, completionHandler: completionHandler) + } +} + +@objc class NYPLBasicAuth: NSObject { + typealias BasicAuthCompletionHandler = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + + @objc static func authHandler(challenge: URLAuthenticationChallenge, + completionHandler: @escaping BasicAuthCompletionHandler) + { + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodHTTPBasic: + let account = NYPLUserAccount.sharedAccount() + if let barcode = account.barcode, let pin = account.PIN { + authCustomHandler(challenge: challenge, + completionHandler: completionHandler, + username: barcode, + password: pin) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + + case NSURLAuthenticationMethodServerTrust: + completionHandler(.performDefaultHandling, nil) + + default: + completionHandler(.rejectProtectionSpace, nil) + } + } + + @objc static func authCustomHandler(challenge: URLAuthenticationChallenge!, + completionHandler: @escaping BasicAuthCompletionHandler, + username: String, + password: String) + { + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodHTTPBasic: + if challenge.previousFailureCount == 0 { + let credentials = URLCredential(user: username, + password: password, + persistence: .none) + completionHandler(.useCredential, credentials) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + + default: + completionHandler(.rejectProtectionSpace, nil) + } + } + +} diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.h b/Simplified/Utilities/Authentication/NYPLBasicAuth.h index db105a3df..73cdcc682 100644 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.h +++ b/Simplified/Utilities/Authentication/NYPLBasicAuth.h @@ -1,14 +1,14 @@ // This function is used to implement basic authentication handling for NSURLSessionTaskDelegate // instances by pulling credentials from NYPLAccount. -void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *challenge, - void (^completionHandler) - (NSURLSessionAuthChallengeDisposition disposition, - NSURLCredential *credential)); - -// Neither |username| nor |password| may be nil. -void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, - void (^completionHandler) - (NSURLSessionAuthChallengeDisposition disposition, - NSURLCredential *credential), - NSString *username, - NSString *password); +//void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *challenge, +// void (^completionHandler) +// (NSURLSessionAuthChallengeDisposition disposition, +// NSURLCredential *credential)); +// +//// Neither |username| nor |password| may be nil. +//void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, +// void (^completionHandler) +// (NSURLSessionAuthChallengeDisposition disposition, +// NSURLCredential *credential), +// NSString *username, +// NSString *password); diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.m b/Simplified/Utilities/Authentication/NYPLBasicAuth.m index a661132bf..3c2078ac9 100644 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.m +++ b/Simplified/Utilities/Authentication/NYPLBasicAuth.m @@ -2,60 +2,58 @@ #import "NYPLBasicAuth.h" -// szyjson - -void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *const challenge, - void (^completionHandler) - (NSURLSessionAuthChallengeDisposition disposition, - NSURLCredential *credential)) -{ - if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { - NSString *const barcode = [NYPLUserAccount sharedAccount].barcode; - NSString *const PIN = [NYPLUserAccount sharedAccount].PIN; - NYPLBasicAuthCustomHandler(challenge, completionHandler, barcode, PIN); - } else { - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); - } - return; - } else if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { - completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); - return; - } else { - NYPLLOG(@"NSURLAuthenticationChallenge rejected."); - completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); - return; - } -} - -void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, - void (^completionHandler) - (NSURLSessionAuthChallengeDisposition disposition, - NSURLCredential *credential), - NSString *const username, - NSString *const password) -{ - if(!(username && password)) { - @throw NSInvalidArgumentException; - } - - if(![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { - completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); - return; - } - - if([challenge.protectionSpace.authenticationMethod - isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { - if(challenge.previousFailureCount == 0) { - completionHandler(NSURLSessionAuthChallengeUseCredential, - [NSURLCredential - credentialWithUser:username - password:password - persistence:NSURLCredentialPersistenceNone]); - } else { - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); - } - } else { - completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); - } -} +// szyjson wherever this is used, I probably need to use auth token +//void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *const challenge, +// void (^completionHandler) +// (NSURLSessionAuthChallengeDisposition disposition, +// NSURLCredential *credential)) +//{ +// if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { +// if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { +// NSString *const barcode = [NYPLUserAccount sharedAccount].barcode; +// NSString *const PIN = [NYPLUserAccount sharedAccount].PIN; +// NYPLBasicAuthCustomHandler(challenge, completionHandler, barcode, PIN); +// } else { +// completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); +// } +// return; +// } else if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { +// completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); +// return; +// } else { +// NYPLLOG(@"NSURLAuthenticationChallenge rejected."); +// completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); +// return; +// } +//} +// +//void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, +// void (^completionHandler) +// (NSURLSessionAuthChallengeDisposition disposition, +// NSURLCredential *credential), +// NSString *const username, +// NSString *const password) +//{ +// if(!(username && password)) { +// @throw NSInvalidArgumentException; +// } +// +// if(![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { +// completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); +// return; +// } +// +// if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { +// if(challenge.previousFailureCount == 0) { +// completionHandler(NSURLSessionAuthChallengeUseCredential, +// [NSURLCredential +// credentialWithUser:username +// password:password +// persistence:NSURLCredentialPersistenceNone]); +// } else { +// completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); +// } +// } else { +// completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); +// } +//} From b13aac13533433504075e472525e76823bd6cf8d Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 22 May 2020 21:18:40 +0200 Subject: [PATCH 04/19] refactored keychain storage --- Simplified.xcodeproj/project.pbxproj | 8 + Simplified/KeychainStoredVariable.swift | 56 ++++ Simplified/NYPLSessionCredentials.swift | 100 ++++++ Simplified/NYPLUserAccount.swift | 419 +++++++++++------------- 4 files changed, 347 insertions(+), 236 deletions(-) create mode 100644 Simplified/KeychainStoredVariable.swift create mode 100644 Simplified/NYPLSessionCredentials.swift diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index a1634514c..9268a46c1 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 081387571BC574DA003DEA6A /* UILabel+NYPLAppearanceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 081387561BC574DA003DEA6A /* UILabel+NYPLAppearanceAdditions.m */; }; 0813875A1BC5767F003DEA6A /* UIButton+NYPLAppearanceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 081387591BC5767F003DEA6A /* UIButton+NYPLAppearanceAdditions.m */; }; 085640CE1BB99FC30088BDBF /* NSURL+NYPLURLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 085640CD1BB99FC30088BDBF /* NSURL+NYPLURLAdditions.m */; }; + 0857A0F72478337D00C7984E /* NYPLSessionCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857A0F62478337D00C7984E /* NYPLSessionCredentials.swift */; }; + 0857A0FF247835FF00C7984E /* KeychainStoredVariable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857A0FE247835FF00C7984E /* KeychainStoredVariable.swift */; }; 085D31D71BE29E38007F7672 /* NYPLProblemReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31D61BE29E38007F7672 /* NYPLProblemReportViewController.m */; }; 085D31D91BE29ED4007F7672 /* NYPLProblemReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 085D31D81BE29ED4007F7672 /* NYPLProblemReportViewController.xib */; }; 085D31DF1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31DE1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m */; }; @@ -319,6 +321,8 @@ 081387591BC5767F003DEA6A /* UIButton+NYPLAppearanceAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIButton+NYPLAppearanceAdditions.m"; sourceTree = ""; }; 085640CC1BB99FC30088BDBF /* NSURL+NYPLURLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+NYPLURLAdditions.h"; sourceTree = ""; }; 085640CD1BB99FC30088BDBF /* NSURL+NYPLURLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+NYPLURLAdditions.m"; sourceTree = ""; }; + 0857A0F62478337D00C7984E /* NYPLSessionCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLSessionCredentials.swift; sourceTree = ""; }; + 0857A0FE247835FF00C7984E /* KeychainStoredVariable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStoredVariable.swift; sourceTree = ""; }; 085D31D51BE29E38007F7672 /* NYPLProblemReportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLProblemReportViewController.h; sourceTree = ""; }; 085D31D61BE29E38007F7672 /* NYPLProblemReportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLProblemReportViewController.m; sourceTree = ""; }; 085D31D81BE29ED4007F7672 /* NYPLProblemReportViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NYPLProblemReportViewController.xib; sourceTree = ""; }; @@ -1214,6 +1218,8 @@ 110AF8881961D652004887C3 /* My Books */, 7338755E2423E108000FEB67 /* Network */, 17CE5304243C020800315E63 /* NYPLUserAccount.swift */, + 0857A0F62478337D00C7984E /* NYPLSessionCredentials.swift */, + 0857A0FE247835FF00C7984E /* KeychainStoredVariable.swift */, 1158812C1A894F4E008672C3 /* NYPLAccountSignInViewController.h */, 1158812D1A894F4E008672C3 /* NYPLAccountSignInViewController.m */, E6BC315C1E009F3E0021B65E /* NYPLAgeCheck.swift */, @@ -1691,6 +1697,7 @@ E671FF7D1E3A7068002AB13F /* NYPLNetworkQueue.swift in Sources */, 116A5EAF194767B200491A21 /* NYPLMyBooksNavigationController.m in Sources */, 1120749319D20BF9008203A4 /* NYPLBookCellCollectionViewController.m in Sources */, + 0857A0FF247835FF00C7984E /* KeychainStoredVariable.swift in Sources */, 111559ED19B8FA590003BE94 /* NYPLXML.m in Sources */, 731A5B1224621F2B00B5E663 /* NYPLSignInBusinessLogic.swift in Sources */, 11580AC81986A77B00949A15 /* NYPLBookCell.m in Sources */, @@ -1739,6 +1746,7 @@ E6207B652118973800864143 /* NYPLAppTheme.swift in Sources */, 11A14DF41A1BF94F00D6C510 /* UIColor+NYPLColorAdditions.m in Sources */, 73CDA121243EDAD8009CC6A6 /* URLRequest+NYPL.swift in Sources */, + 0857A0F72478337D00C7984E /* NYPLSessionCredentials.swift in Sources */, A42E0DF41B40F4E00095EBAE /* NYPLCatalogFeedViewController.m in Sources */, 11B6020519806CD300800DA9 /* NYPLBookDownloadingCell.m in Sources */, 114B7F161A3644CF00B8582B /* NYPLTenPrintCoverView+NYPLImageAdditions.m in Sources */, diff --git a/Simplified/KeychainStoredVariable.swift b/Simplified/KeychainStoredVariable.swift new file mode 100644 index 000000000..47ffc7d06 --- /dev/null +++ b/Simplified/KeychainStoredVariable.swift @@ -0,0 +1,56 @@ +// +// KeychainStoredVariable.swift +// SimplyE +// +// Created by Jacek Szyja on 22/05/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import Foundation + +class KeychainVariable { + let transaction: KeychainVariableTransaction + var key: String + + init(key: String, accountInfoLock: NSRecursiveLock) { + self.key = key + self.transaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) + } + + func read() -> VariableType? { + return NYPLKeychain.shared()?.object(forKey: key) as? VariableType + } + + func write(_ newValue: VariableType?) { + if let newValue = newValue { + NYPLKeychain.shared()?.setObject(newValue, forKey: key) + } else { + NYPLKeychain.shared()?.removeObject(forKey: key) + } + } + + func safeWrite(_ newValue: VariableType?) { + transaction.write { + write(newValue) + } + } +} + +class KeychainVariableTransaction { + let accountInfoLock: NSRecursiveLock + + init(accountInfoLock: NSRecursiveLock) { + self.accountInfoLock = accountInfoLock + } + + func write(transaction: () -> Void) { + guard NYPLKeychain.shared() != nil else { return } + + accountInfoLock.lock() + defer { + accountInfoLock.unlock() + } + + transaction() + } +} diff --git a/Simplified/NYPLSessionCredentials.swift b/Simplified/NYPLSessionCredentials.swift new file mode 100644 index 000000000..fdbc10fba --- /dev/null +++ b/Simplified/NYPLSessionCredentials.swift @@ -0,0 +1,100 @@ +// +// NYPLSessionCredentials.swift +// SimplyE +// +// Created by Jacek Szyja on 22/05/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import Foundation + +enum Credentials { +// case token(authToken: String, patron: [String:Any]) + case token(authToken: String) + case barcodeAndPin(barcode: String, pin: String) + case open +} + +extension Credentials: Codable { + enum TypeID: Int, Codable { + case token + case barcodeAndPin + case open + } + + private var typeID: TypeID { + switch self { + case .token: return .token + case .barcodeAndPin: return .barcodeAndPin + case .open: return .open + } + } + + enum CodingKeys: String, CodingKey { + case type + case associatedTokenData + case associatedBarcodeAndPinData + } + + enum TokenKeys: String, CodingKey { + case authToken + case patron + } + + enum BarcodeAndPinKeys: String, CodingKey { + case barcode + case pin + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let type = try values.decode(TypeID.self, forKey: .type) + + switch type { + case .token: + let additionalInfo = try values.nestedContainer(keyedBy: TokenKeys.self, forKey: .associatedTokenData) + let token = try additionalInfo.decode(String.self, forKey: .authToken) +// let patronData = try additionalInfo.decode(Data.self, forKey: .patron) +// guard let patron = try JSONSerialization.jsonObject(with: patronData, options: .allowFragments) as? [String: Any] else { +// throw NSError() +// } +// self = .token(authToken: token, patron: patron) + self = .token(authToken: token) + + case .barcodeAndPin: + let additionalInfo = try values.nestedContainer(keyedBy: BarcodeAndPinKeys.self, forKey: .associatedBarcodeAndPinData) + let barcode = try additionalInfo.decode(String.self, forKey: .barcode) + let pin = try additionalInfo.decode(String.self, forKey: .pin) + self = .barcodeAndPin(barcode: barcode, pin: pin) + + case .open: self = .open + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(typeID, forKey: .type) + + switch self { +// case let .token(authToken: token, patron: info): + case let .token(authToken: token): + var additionalInfo = container.nestedContainer(keyedBy: TokenKeys.self, forKey: .associatedTokenData) + try additionalInfo.encode(token, forKey: .authToken) +// let data = try JSONSerialization.data(withJSONObject: info, options: []) +// try additionalInfo.encode(data, forKey: .patron) + + case let .barcodeAndPin(barcode: barcode, pin: pin): + var additionalInfo = container.nestedContainer(keyedBy: BarcodeAndPinKeys.self, forKey: .associatedBarcodeAndPinData) + try additionalInfo.encode(barcode, forKey: .barcode) + try additionalInfo.encode(pin, forKey: .pin) + + default: break + } + } +} + +extension String { + func asKeychainVariable(with accountInfoLock: NSRecursiveLock) -> KeychainVariable { + return KeychainVariable(key: self, accountInfoLock: accountInfoLock) + } +} diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index dc9b99fa8..2f5538de4 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -10,21 +10,61 @@ extension Notification.Name { public static let NYPLUserAccountLoginDidChange = Notification.Name.NYPLUserAccountLoginDidChange } +private enum StorageKey: String { + case authorizationIdentifier = "NYPLAccountAuthorization" + case barcode = "NYPLAccountBarcode" // legacy + case PIN = "NYPLAccountPIN" // legacy + case adobeToken = "NYPLAccountAdobeTokenKey" + case licensor = "NYPLAccountLicensorKey" + case patron = "NYPLAccountPatronKey" + case authToken = "NYPLAccountAuthTokenKey" // legacy + case adobeVendor = "NYPLAccountAdobeVendorKey" + case provider = "NYPLAccountProviderKey" + case userID = "NYPLAccountUserIDKey" + case deviceID = "NYPLAccountDeviceIDKey" + case credentials = "NYPLAccountCredentialsKey" + + func keyForLibrary(uuid libraryUUID: String?) -> String { + guard let libraryUUID = libraryUUID else { return self.rawValue } + return "\(self.rawValue)_\(libraryUUID)" + } +} + @objcMembers class NYPLUserAccount : NSObject { static private let shared = NYPLUserAccount() private let accountInfoLock = NSRecursiveLock() + private lazy var keychainTransaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) - private var authorizationIdentifierKey = "NYPLAccountAuthorization" - private var barcodeKey = "NYPLAccountBarcode" - private var PINKey = "NYPLAccountPIN" - private var adobeTokenKey = "NYPLAccountAdobeTokenKey" - private var licensorKey = "NYPLAccountLicensorKey" - private var patronKey = "NYPLAccountPatronKey" - private var authTokenKey = "NYPLAccountAuthTokenKey" - private var adobeVendorKey = "NYPLAccountAdobeVendorKey" - private var providerKey = "NYPLAccountProviderKey" - private var userIDKey = "NYPLAccountUserIDKey" - private var deviceIDKey = "NYPLAccountDeviceIDKey" + private var libraryUUID: String? + + public private(set) var credentials: Credentials? { + get { + var credentials = _credentials.read() + + if credentials == nil { + // try to load legacy values + if let barcode = legacyBarcode, let pin = legacyPin { + credentials = .barcodeAndPin(barcode: barcode, pin: pin) + } else if let authToken = legacyAuthToken { + credentials = .token(authToken: authToken) + } + } + + return credentials + } + set { + guard let newValue = newValue else { return } + + _credentials.safeWrite(newValue) + + // make sure to set the barcode related to the current account (aka library) + // not the one we just signed in to, because we could have signed in into + // library A, but still browsing the catalog of library B. + NYPLErrorLogger.setUserID(NYPLUserAccount.sharedAccount().barcode) + + notifyAccountDidChange() + } + } @objc class func sharedAccount() -> NYPLUserAccount { @@ -36,47 +76,82 @@ extension Notification.Name { { shared.accountInfoLock.lock() defer { - shared.accountInfoLock.unlock() + shared.accountInfoLock.unlock() } if let uuid = libraryUUID, - uuid != AccountsManager.NYPLAccountUUIDs[0] + uuid != AccountsManager.NYPLAccountUUIDs[0] { - shared.barcodeKey = "NYPLAccountBarcode_\(uuid)" - shared.authorizationIdentifierKey = "NYPLAccountAuthorization_\(uuid)" - shared.PINKey = "NYPLAccountPIN_\(uuid)" - shared.adobeTokenKey = "NYPLAccountAdobeTokenKey_\(uuid)" - shared.patronKey = "NYPLAccountPatronKey_\(uuid)" - shared.authTokenKey = "NYPLAccountAuthTokenKey_\(uuid)" - shared.adobeVendorKey = "NYPLAccountAdobeVendorKey_\(uuid)" - shared.providerKey = "NYPLAccountProviderKey_\(uuid)" - shared.userIDKey = "NYPLAccountUserIDKey_\(uuid)" - shared.deviceIDKey = "NYPLAccountDeviceIDKey_\(uuid)" - shared.licensorKey = "NYPLAccountLicensorKey_\(uuid)" + shared.libraryUUID = uuid } else { - shared.barcodeKey = "NYPLAccountBarcode" - shared.authorizationIdentifierKey = "NYPLAccountAuthorization" - shared.PINKey = "NYPLAccountPIN" - shared.adobeTokenKey = "NYPLAccountAdobeTokenKey" - shared.patronKey = "NYPLAccountPatronKey" - shared.authTokenKey = "NYPLAccountAuthTokenKey" - shared.adobeVendorKey = "NYPLAccountAdobeVendorKey" - shared.providerKey = "NYPLAccountProviderKey" - shared.userIDKey = "NYPLAccountUserIDKey" - shared.deviceIDKey = "NYPLAccountDeviceIDKey" - shared.licensorKey = "NYPLAccountLicensorKey" + shared.libraryUUID = nil } - + return shared } - + + private func notifyAccountDidChange() { + NotificationCenter.default.post( + name: Notification.Name.NYPLUserAccountDidChange, + object: self + ) + } + + // MARK: - Storage + private lazy var _authorizationIdentifier: KeychainVariable = StorageKey.authorizationIdentifier + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _adobeToken: KeychainVariable = StorageKey.adobeToken + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _licensor: KeychainVariable<[String:Any]> = StorageKey.licensor + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _patron: KeychainVariable<[String:Any]> = StorageKey.patron + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _adobeVendor: KeychainVariable = StorageKey.adobeVendor + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _provider: KeychainVariable = StorageKey.provider + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _userID: KeychainVariable = StorageKey.userID + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _deviceID: KeychainVariable = StorageKey.deviceID + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _credentials: KeychainVariable = StorageKey.credentials + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + + // Legacy + private lazy var _barcode: KeychainVariable = StorageKey.barcode + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _pin: KeychainVariable = StorageKey.PIN + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + private lazy var _authToken: KeychainVariable = StorageKey.authToken + .keyForLibrary(uuid: libraryUUID) + .asKeychainVariable(with: accountInfoLock) + // MARK: - Check func hasBarcodeAndPIN() -> Bool { - return barcode != nil && PIN != nil + if let credentials = credentials, case Credentials.barcodeAndPin = credentials { + return true + } else { + return false + } } func hasAuthToken() -> Bool { - return authToken != nil + if let credentials = credentials, case Credentials.token = credentials { + return true + } else { + return false + } } func hasAdobeToken() -> Bool { @@ -90,41 +165,47 @@ extension Notification.Name { func hasCredentials() -> Bool { return hasAuthToken() || hasBarcodeAndPIN() } - + + // MARK: - Legacy + private var legacyBarcode: String? { return _barcode.read() } + private var legacyPin: String? { return _pin.read() } + var legacyAuthToken: String? { _authToken.read() } + // MARK: - GET - + var authorizationIdentifier: String? { _authorizationIdentifier.read() } + var deviceID: String? { _deviceID.read() } + var userID: String? { _userID.read() } + var adobeVendor: String? { _adobeVendor.read() } + var provider: String? { _provider.read() } + var patron: [String:Any]? { _patron.read() } + var adobeToken: String? { _adobeToken.read() } + var licensor: [String:Any]? { _licensor.read() } + var barcode: String? { - return NYPLKeychain.shared()?.object(forKey: barcodeKey) as? String - } - - var authorizationIdentifier: String? { - return NYPLKeychain.shared()?.object(forKey: authorizationIdentifierKey) as? String + if let credentials = credentials, case let Credentials.barcodeAndPin(barcode: barcode, pin: _) = credentials { + return barcode + } else { + return nil + } } - + var PIN: String? { - return NYPLKeychain.shared()?.object(forKey: PINKey) as? String - } - - var deviceID: String? { - return NYPLKeychain.shared()?.object(forKey: deviceIDKey) as? String - } - - var userID: String? { - return NYPLKeychain.shared()?.object(forKey: userIDKey) as? String - } - - var adobeVendor: String? { - return NYPLKeychain.shared()?.object(forKey: adobeVendorKey) as? String - } - - var provider: String? { - return NYPLKeychain.shared()?.object(forKey: providerKey) as? String + if let credentials = credentials, case let Credentials.barcodeAndPin(barcode: _, pin: pin) = credentials { + return pin + } else { + return nil + } } - - var patron: [String:Any]? { - return NYPLKeychain.shared()?.object(forKey: patronKey) as? [String:Any] + + var authToken: String? { + if let credentials = credentials, case let Credentials.token(authToken: token) = credentials { + return token + } else { + return nil + } } - + + var patronFullName: String? { if let patron = patron, let name = patron["name"] as? [String:String] @@ -155,222 +236,88 @@ extension Notification.Name { return nil } - // szyjson set token here - var authToken: String? { - return NYPLKeychain.shared()?.object(forKey: authTokenKey) as? String - } - - var adobeToken: String? { - return NYPLKeychain.shared()?.object(forKey: adobeTokenKey) as? String - } - - var licensor: [String:Any]? { - return NYPLKeychain.shared()?.object(forKey: licensorKey) as? [String:Any] - } - + + // MARK: - SET - @objc(setBarcode:PIN:) func setBarcode(_ barcode: String, PIN: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() - } - - sharedKeychain.setObject(barcode, forKey: barcodeKey) - sharedKeychain.setObject(PIN, forKey: PINKey) - - // make sure to set the barcode related to the current account (aka library) - // not the one we just signed in to, because we could have signed in into - // library A, but still browsing the catalog of library B. - NYPLErrorLogger.setUserID(NYPLUserAccount.sharedAccount().barcode) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + credentials = .barcodeAndPin(barcode: barcode, pin: PIN) } @objc(setAdobeToken:patron:) func setAdobeToken(_ token: String, patron: [String : Any]) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return + keychainTransaction.write { + _adobeToken.write(token) + _patron.write(patron) } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() - } - - sharedKeychain.setObject(token, forKey: adobeTokenKey) - sharedKeychain.setObject(patron, forKey: patronKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + + notifyAccountDidChange() } @objc(setAdobeVendor:) func setAdobeVendor(_ vendor: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(vendor, forKey: adobeVendorKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _adobeVendor.safeWrite(vendor) + notifyAccountDidChange() } @objc(setAdobeToken:) func setAdobeToken(_ token: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(token, forKey: adobeTokenKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _adobeToken.safeWrite(token) + notifyAccountDidChange() } @objc(setLicensor:) func setLicensor(_ licensor: [String : Any]) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(licensor, forKey: licensorKey) + _licensor.safeWrite(licensor) } @objc(setAuthorizationIdentifier:) func setAuthorizationIdentifier(_ identifier: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(identifier, forKey: authorizationIdentifierKey) + _authorizationIdentifier.safeWrite(identifier) } @objc(setPatron:) func setPatron(_ patron: [String : Any]) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(patron, forKey: patronKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _patron.safeWrite(patron) + notifyAccountDidChange() } @objc(setAuthToken:) func setAuthToken(_ token: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(token, forKey: authTokenKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + credentials = .token(authToken: token) } @objc(setProvider:) func setProvider(_ provider: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(provider, forKey: providerKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _provider.safeWrite(provider) + notifyAccountDidChange() } @objc(setUserID:) func setUserID(_ id: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(id, forKey: userIDKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _userID.safeWrite(id) + notifyAccountDidChange() } @objc(setDeviceID:) func setDeviceID(_ id: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(id, forKey: deviceIDKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) + _deviceID.safeWrite(id) + notifyAccountDidChange() } // MARK: - Remove - - func removeBarcodeAndPIN() { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() - } - - sharedKeychain.removeObject(forKey: barcodeKey) - sharedKeychain.removeObject(forKey: authorizationIdentifierKey) - sharedKeychain.removeObject(forKey: PINKey) - - NotificationCenter.default.post( - name: Notification.Name.NYPLUserAccountDidChange, - object: self - ) - } - func removeAll() { - guard let sharedKeychain = NYPLKeychain.shared() else { - return + keychainTransaction.write { + _credentials.write(nil) + _authorizationIdentifier.write(nil) + _adobeToken.write(nil) + _patron.write(nil) + _authToken.write(nil) + _adobeVendor.write(nil) + _provider.write(nil) + _userID.write(nil) + _deviceID.write(nil) } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() - } - - sharedKeychain.removeObject(forKey: adobeTokenKey) - sharedKeychain.removeObject(forKey: patronKey) - sharedKeychain.removeObject(forKey: authTokenKey) - sharedKeychain.removeObject(forKey: adobeVendorKey) - sharedKeychain.removeObject(forKey: providerKey) - sharedKeychain.removeObject(forKey: userIDKey) - sharedKeychain.removeObject(forKey: deviceIDKey) - - removeBarcodeAndPIN() + notifyAccountDidChange() } } From 411852595fcff57af9a9ff668461ac80fdfb4602 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Tue, 26 May 2020 14:37:51 +0200 Subject: [PATCH 05/19] swift front end validator + refactore logic lying behind account authentication details to use a specific authentication method --- Simplified.xcodeproj/project.pbxproj | 25 +++--- Simplified/Account.swift | 57 +++--------- Simplified/AccountsManager.swift | 2 +- Simplified/NYPLAccountSignInViewController.m | 32 +++---- Simplified/NYPLBookRegistry.m | 2 +- Simplified/NYPLCatalogNavigationController.m | 8 +- .../NYPLSettingsAccountDetailViewController.m | 33 +++---- Simplified/NYPLSignInBusinessLogic.swift | 6 +- .../NYPLUserAccountFrontEndValidation.h | 38 -------- .../NYPLUserAccountFrontEndValidation.m | 86 ------------------ .../NYPLUserAccountFrontEndValidation.swift | 88 +++++++++++++++++++ 11 files changed, 160 insertions(+), 217 deletions(-) delete mode 100644 Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.h delete mode 100644 Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.m create mode 100644 Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index 604b6d397..c93a15ea0 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 08A352241BDE8E640040BF1D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352231BDE8E640040BF1D /* Security.framework */; }; 08A352261BDE8E700040BF1D /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352251BDE8E700040BF1D /* SystemConfiguration.framework */; }; 08A352271BDE91B80040BF1D /* libRDServices.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A49C25461AE05A2600D63B89 /* libRDServices.a */; }; + 08C481AE247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift */; }; 11068C55196DD37900E8A94B /* NYPLNull.m in Sources */ = {isa = PBXBuildFile; fileRef = 11068C54196DD37900E8A94B /* NYPLNull.m */; }; 11078357198160A50071AB1E /* NYPLBookDownloadFailedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 11078356198160A50071AB1E /* NYPLBookDownloadFailedCell.m */; }; 1107835E19816E3D0071AB1E /* UIView+NYPLViewAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1107835D19816E3D0071AB1E /* UIView+NYPLViewAdditions.m */; }; @@ -188,7 +189,6 @@ 738EF2EC2405EC5900F388FB /* PromisesObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 738EF2EB2405EC5900F388FB /* PromisesObjC.framework */; }; 73A3EAED2400A9560061A7FB /* NYPLSettingsAccountURLSessionChallengeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 73A3EAEC2400A9560061A7FB /* NYPLSettingsAccountURLSessionChallengeHandler.m */; }; 73CDA121243EDAD8009CC6A6 /* URLRequest+NYPL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CDA120243EDAD8009CC6A6 /* URLRequest+NYPL.swift */; }; - 73E84FC624465083009071D8 /* NYPLUserAccountFrontEndValidation.m in Sources */ = {isa = PBXBuildFile; fileRef = 73E84FC524465083009071D8 /* NYPLUserAccountFrontEndValidation.m */; }; 841B55431B740F2700FAC1AF /* NYPLSettingsEULAViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 841B55421B740F2700FAC1AF /* NYPLSettingsEULAViewController.m */; }; 84B7A3461B84E8FE00584FB2 /* OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 84B7A3431B84E8FE00584FB2 /* OFL.txt */; }; 84B7A3471B84E8FE00584FB2 /* OpenDyslexic3-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 84B7A3441B84E8FE00584FB2 /* OpenDyslexic3-Bold.ttf */; }; @@ -333,6 +333,7 @@ 08A352211BDE8E560040BF1D /* libicucore.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libicucore.tbd; path = usr/lib/libicucore.tbd; sourceTree = SDKROOT; }; 08A352231BDE8E640040BF1D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 08A352251BDE8E700040BF1D /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLUserAccountFrontEndValidation.swift; sourceTree = ""; }; 11068C53196DD37900E8A94B /* NYPLNull.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLNull.h; sourceTree = ""; }; 11068C54196DD37900E8A94B /* NYPLNull.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLNull.m; sourceTree = ""; }; 11078355198160A50071AB1E /* NYPLBookDownloadFailedCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLBookDownloadFailedCell.h; sourceTree = ""; }; @@ -603,8 +604,6 @@ 73DA43A62404BA5600985482 /* build-carthage.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-carthage.sh"; sourceTree = SOURCE_ROOT; }; 73DA43A72404BA5600985482 /* build_curl.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_curl.sh; sourceTree = SOURCE_ROOT; }; 73DA43A82404BA5600985482 /* build-openssl-curl.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-openssl-curl.sh"; sourceTree = SOURCE_ROOT; }; - 73E84FC424465083009071D8 /* NYPLUserAccountFrontEndValidation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NYPLUserAccountFrontEndValidation.h; sourceTree = ""; }; - 73E84FC524465083009071D8 /* NYPLUserAccountFrontEndValidation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NYPLUserAccountFrontEndValidation.m; sourceTree = ""; }; 841B55411B740F2700FAC1AF /* NYPLSettingsEULAViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLSettingsEULAViewController.h; sourceTree = ""; }; 841B55421B740F2700FAC1AF /* NYPLSettingsEULAViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLSettingsEULAViewController.m; sourceTree = ""; }; 84B7A3431B84E8FE00584FB2 /* OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = OFL.txt; sourceTree = ""; }; @@ -773,6 +772,13 @@ name = CardCreator; sourceTree = ""; }; + 08C481A8247CAEDA003A6723 /* Temporary */ = { + isa = PBXGroup; + children = ( + ); + name = Temporary; + sourceTree = ""; + }; 110AF8881961D652004887C3 /* My Books */ = { isa = PBXGroup; children = ( @@ -1096,10 +1102,9 @@ 73E84FBD24464FF3009071D8 /* Authentication */ = { isa = PBXGroup; children = ( + 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift */, A4E254F81B0E610900193FE4 /* NYPLBasicAuth.h */, A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */, - 73E84FC424465083009071D8 /* NYPLUserAccountFrontEndValidation.h */, - 73E84FC524465083009071D8 /* NYPLUserAccountFrontEndValidation.m */, ); path = Authentication; sourceTree = ""; @@ -1207,6 +1212,7 @@ A823D816192BABA400B55DE2 /* Simplified */ = { isa = PBXGroup; children = ( + 08C481A8247CAEDA003A6723 /* Temporary */, 110AF8891961D66C004887C3 /* Additions */, 110AF8901961D822004887C3 /* Book */, 0345BFEE1DBF031B00398B6F /* CardCreator */, @@ -1464,7 +1470,7 @@ TestTargetID = A823D80C192BABA400B55DE2; }; A823D80C192BABA400B55DE2 = { - DevelopmentTeam = 95MG2VV3YK; + DevelopmentTeam = 7262U6ST2R; LastSwiftMigration = 1130; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -1805,7 +1811,7 @@ E6DA7EA01F2A718600CFBEC8 /* NYPLBookAuthor.swift in Sources */, 118A0B1B19915BDF00792DDE /* NYPLCatalogSearchViewController.m in Sources */, 179699D124131BA500EC309F /* UIColor+LabelColor.swift in Sources */, - 73E84FC624465083009071D8 /* NYPLUserAccountFrontEndValidation.m in Sources */, + 08C481AE247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift in Sources */, 735FED262427494900144C97 /* NYPLNetworkResponder.swift in Sources */, A4E254FA1B0E610900193FE4 /* NYPLBasicAuth.m in Sources */, 1188F3E11A1ECC4B006B2F36 /* NYPLReaderSettings.m in Sources */, @@ -2070,11 +2076,8 @@ CLANG_ENABLE_MODULES = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; - DEVELOPMENT_TEAM = 95MG2VV3YK; CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; diff --git a/Simplified/Account.swift b/Simplified/Account.swift index 42eb10c7a..c8146d9ac 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -14,7 +14,9 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" case none } - struct Authentication { + @objc(AccountDetailsAuthentication) + @objcMembers + class Authentication: NSObject { let authType:AuthType let authPasscodeLength:UInt let patronIDKeyboard:LoginKeyboard @@ -53,56 +55,25 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" let userProfileUrl:String? let signUpUrl:URL? let loansUrl:URL? - - var authType: AuthType { - return auths.first?.authType ?? .none - } - - var authPasscodeLength: UInt { - return auths.first?.authPasscodeLength ?? 99 - } - - var patronIDKeyboard: LoginKeyboard { - return auths.first?.patronIDKeyboard ?? .standard - } - - var pinKeyboard: LoginKeyboard { - return auths.first?.pinKeyboard ?? .standard - } - - var supportsBarcodeScanner: Bool { - return auths.first?.supportsBarcodeScanner ?? false - } - - var supportsBarcodeDisplay: Bool { - return auths.first?.supportsBarcodeDisplay ?? false - } - - var coppaUnderUrl: URL? { - return auths.first?.coppaUnderUrl - } - - var coppaOverUrl: URL? { - return auths.first?.coppaOverUrl - } - - var oauthIntermediaryUrl: URL? { - return auths.first?.oauthIntermediaryUrl - } - var patronIDLabel: String? { - return auths.first?.patronIDLabel - } - - var pinLabel: String? { - return auths.first?.pinLabel + // szyjson set it properly + private var _selectedAuth: Authentication? + var selectedAuth: Authentication? { + get { + return _selectedAuth ?? auths.first + } + set { + _selectedAuth = newValue + } } var needsAuth:Bool { + let authType = selectedAuth?.authType ?? .none return authType == .basic || authType == .oauthIntermediary } var needsAgeCheck:Bool { + let authType = selectedAuth?.authType ?? .none return authType == .coppa } diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index 8012ec398..14e65a9f8 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -157,7 +157,7 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf if self.currentAccount?.details?.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?.selectedAuth?.coppaOverUrl : self.currentAccount?.details?.selectedAuth?.coppaUnderUrl resolveFn() } } diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index 985626e27..d8c6f8a78 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -19,7 +19,6 @@ #import "NYPLRootTabBarController.h" #import "NYPLSettingsAccountURLSessionChallengeHandler.h" #import "NYPLSettingsEULAViewController.h" -#import "NYPLUserAccountFrontEndValidation.h" #import "NYPLXML.h" #import "UIView+NYPLViewAdditions.h" #import "UIFont+NYPLSystemFontOverride.h" @@ -127,9 +126,10 @@ - (instancetype)init sessionWithConfiguration:configuration delegate:_urlSessionDelegate delegateQueue:[NSOperationQueue mainQueue]]; - + self.frontEndValidator = [[NYPLUserAccountFrontEndValidation alloc] initWithAccount:self.currentAccount + selectedAuthentication:self.currentAccount.details.auths.firstObject inputProvider:self]; return self; } @@ -152,7 +152,7 @@ - (void)viewDidLoad self.usernameTextField.delegate = self.frontEndValidator; self.usernameTextField.placeholder = NSLocalizedString(@"BarcodeOrUsername", nil); - switch (self.currentAccount.details.patronIDKeyboard) { + switch (self.businessLogic.selectedAuthentication.patronIDKeyboard) { case LoginKeyboardStandard: case LoginKeyboardNone: self.usernameTextField.keyboardType = UIKeyboardTypeASCIICapable; @@ -175,7 +175,7 @@ - (void)viewDidLoad self.PINTextField = [[UITextField alloc] initWithFrame:CGRectZero]; self.PINTextField.placeholder = NSLocalizedString(@"PIN", nil); - switch (self.currentAccount.details.pinKeyboard) { + switch (self.businessLogic.selectedAuthentication.pinKeyboard) { case LoginKeyboardStandard: case LoginKeyboardNone: self.PINTextField.keyboardType = UIKeyboardTypeASCIICapable; @@ -218,9 +218,9 @@ - (void)viewDidLoad - (void)setupTableData { NSArray *section0; - if (self.currentAccount.details.oauthIntermediaryUrl != nil) { + if (self.currentAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { section0 = @[@(CellKindLogInSignOut)].mutableCopy; - } else if (self.currentAccount.details.pinKeyboard != LoginKeyboardNone) { + } else if (self.businessLogic.selectedAuthentication.pinKeyboard != LoginKeyboardNone) { section0 = @[@(CellKindBarcode), @(CellKindPIN), @(CellKindLogInSignOut)]; @@ -298,8 +298,8 @@ - (void)tableView:(__attribute__((unused)) UITableView *)tableView [[CardCreatorConfiguration alloc] initWithEndpointURL:self.currentAccount.details.signUpUrl ?: APIKeys.cardCreatorEndpointURL endpointVersion:[APIKeys cardCreatorVersion] - endpointUsername:NYPLSecrets.cardCreatorUsername - endpointPassword:NYPLSecrets.cardCreatorPassword + endpointUsername:APIKeys.cardCreatorUsername + endpointPassword:APIKeys.cardCreatorPassword requestTimeoutInterval:20.0 completionHandler:^(NSString *const username, NSString *const PIN, BOOL const userInitiated) { if (userInitiated) { @@ -385,7 +385,7 @@ - (UITableViewCell *)tableView:(__attribute__((unused)) UITableView *)tableView ofView:[self.usernameTextField superview] withOffset:-marginPadding]; - if (self.currentAccount.details.supportsBarcodeScanner) { + if (self.businessLogic.selectedAuthentication.supportsBarcodeScanner) { [cell.contentView addSubview:self.barcodeScanButton]; CGFloat rightMargin = cell.layoutMargins.right; self.barcodeScanButton.contentEdgeInsets = UIEdgeInsetsMake(0, rightMargin * 2, 0, rightMargin); @@ -581,7 +581,7 @@ - (NSString *)pin if (authorizeImmediately && [NYPLUserAccount sharedAccount].hasBarcodeAndPIN) { accountViewController.PINTextField.text = [NYPLUserAccount sharedAccount].PIN; [accountViewController logIn]; - } else if (authorizeImmediately && AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl) { + } else if (authorizeImmediately && AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { [accountViewController logIn]; } else { if(useExistingBarcode) { @@ -762,8 +762,8 @@ - (void)updateLoginLogoutCellAppearance stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; BOOL const pinHasText = [self.PINTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; - BOOL const pinIsNotRequired = self.currentAccount.details.pinKeyboard == LoginKeyboardNone; - BOOL const oauthLogin = self.currentAccount.details.oauthIntermediaryUrl != nil; + BOOL const pinIsNotRequired = self.businessLogic.selectedAuthentication.pinKeyboard == LoginKeyboardNone; + BOOL const oauthLogin = self.currentAccount.details.selectedAuth.oauthIntermediaryUrl != nil; if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired) || oauthLogin) { self.logInSignOutCell.userInteractionEnabled = YES; @@ -782,9 +782,9 @@ - (void)updateLoginLogoutCellAppearance - (void)logIn { - if (self.currentAccount.details.oauthIntermediaryUrl) { + if (self.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { // oauth - NSURL *oauthURL = self.currentAccount.details.oauthIntermediaryUrl; + NSURL *oauthURL = self.currentAccount.details.selectedAuth.oauthIntermediaryUrl; NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; @@ -946,7 +946,7 @@ - (void)validateCredentials request.timeoutInterval = 20.0; - if (self.currentAccount.details.oauthIntermediaryUrl != nil) { + if (self.currentAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { NSString *authToken = self.authToken; if (authToken != nil) { NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; @@ -1143,7 +1143,7 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error if(success) { // szyjson tu authtoken - if (AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl) { + if (AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { [[NYPLUserAccount sharedAccount] setAuthToken:self.authToken]; // [[NYPLUserAccount sharedAccount] setPatron:self.patron]; // szyjson } else { diff --git a/Simplified/NYPLBookRegistry.m b/Simplified/NYPLBookRegistry.m index b699a5c35..9056396eb 100644 --- a/Simplified/NYPLBookRegistry.m +++ b/Simplified/NYPLBookRegistry.m @@ -282,7 +282,7 @@ - (void)syncWithCompletionHandler:(void (^)(BOOL success))completion return; } else if (![[NYPLUserAccount sharedAccount] hasCredentials]) { // AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl - if (AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl) { + if (AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { // sign in NYPLLOG(@"[syncWithCompletionHandler] No valid authorization header"); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ diff --git a/Simplified/NYPLCatalogNavigationController.m b/Simplified/NYPLCatalogNavigationController.m index 8524203ab..c62abc352 100644 --- a/Simplified/NYPLCatalogNavigationController.m +++ b/Simplified/NYPLCatalogNavigationController.m @@ -202,11 +202,11 @@ - (void)updateFeedAndRegistryOnAccountChange if (account.details.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { dispatch_async(dispatch_get_main_queue(), ^{ - mainFeedUrl = isOfAge ? account.details.coppaOverUrl : account.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? account.details.selectedAuth.coppaOverUrl : account.details.selectedAuth.coppaUnderUrl; completion(); }); }]; - } else if (account.details.oauthIntermediaryUrl && !NYPLUserAccount.sharedAccount.hasAuthToken) { + } else if (account.details.selectedAuth.oauthIntermediaryUrl && !NYPLUserAccount.sharedAccount.hasAuthToken) { // sign in [NYPLAccountSignInViewController authorizeUsingIntermediaryWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ @@ -244,7 +244,7 @@ - (void)viewDidLoad if (account.details.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { dispatch_async(dispatch_get_main_queue(), ^{ - mainFeedUrl = isOfAge ? account.details.coppaOverUrl : account.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? account.details.selectedAuth.coppaOverUrl : account.details.selectedAuth.coppaUnderUrl; completion(); }); }]; @@ -296,7 +296,7 @@ - (void)viewDidAppear:(BOOL)animated }; if (currentAccount.details.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { - mainFeedUrl = isOfAge ? currentAccount.details.coppaOverUrl : currentAccount.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? currentAccount.details.selectedAuth.coppaOverUrl : currentAccount.details.selectedAuth.coppaUnderUrl; completion(); }]; } else { diff --git a/Simplified/NYPLSettingsAccountDetailViewController.m b/Simplified/NYPLSettingsAccountDetailViewController.m index dd0e1f56c..b3d689405 100644 --- a/Simplified/NYPLSettingsAccountDetailViewController.m +++ b/Simplified/NYPLSettingsAccountDetailViewController.m @@ -17,7 +17,6 @@ #import "NYPLSettingsAccountDetailViewController.h" #import "NYPLSettingsAccountURLSessionChallengeHandler.h" #import "NYPLSettingsEULAViewController.h" -#import "NYPLUserAccountFrontEndValidation.h" #import "NYPLXML.h" #import "UIFont+NYPLSystemFontOverride.h" #import "UIView+NYPLViewAdditions.h" @@ -124,6 +123,7 @@ - (instancetype)initWithLibraryAccountID:(NSString *)libraryUUID self.frontEndValidator = [[NYPLUserAccountFrontEndValidation alloc] initWithAccount:self.selectedAccount + selectedAuthentication:self.selectedAccount.details.auths.firstObject inputProvider:self]; [[NSNotificationCenter defaultCenter] @@ -217,9 +217,10 @@ - (void)displayErrorMessage:(NSString *)errorMessage { - (void)setupViews { self.usernameTextField = [[UITextField alloc] initWithFrame:CGRectZero]; self.usernameTextField.delegate = self.frontEndValidator; - self.usernameTextField.placeholder = self.selectedAccount.details.patronIDLabel ?: NSLocalizedString(@"BarcodeOrUsername", nil); + self.usernameTextField.placeholder = + self.businessLogic.selectedAuthentication.patronIDLabel ?: NSLocalizedString(@"BarcodeOrUsername", nil); - switch (self.selectedAccount.details.patronIDKeyboard) { + switch (self.businessLogic.selectedAuthentication.patronIDKeyboard) { case LoginKeyboardStandard: case LoginKeyboardNone: self.usernameTextField.keyboardType = UIKeyboardTypeASCIICapable; @@ -245,9 +246,9 @@ - (void)setupViews { forControlEvents:UIControlEventTouchUpInside]; self.PINTextField = [[UITextField alloc] initWithFrame:CGRectZero]; - self.PINTextField.placeholder = self.selectedAccount.details.pinLabel ?: NSLocalizedString(@"PIN", nil); + self.PINTextField.placeholder = self.businessLogic.selectedAuthentication.pinLabel ?: NSLocalizedString(@"PIN", nil); - switch (self.selectedAccount.details.pinKeyboard) { + switch (self.businessLogic.selectedAuthentication.pinKeyboard) { case LoginKeyboardStandard: case LoginKeyboardNone: self.PINTextField.keyboardType = UIKeyboardTypeASCIICapable; @@ -288,9 +289,9 @@ - (void)setupTableData section0AcctInfo = @[@(CellKindAgeCheck)].mutableCopy; } else if (!self.selectedAccount.details.needsAuth) { section0AcctInfo = [NSMutableArray new]; - } else if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + } else if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { section0AcctInfo = @[@(CellKindLogInSignOut)].mutableCopy; - } else if (self.selectedAccount.details.pinKeyboard != LoginKeyboardNone) { + } else if (self.businessLogic.selectedAuthentication.pinKeyboard != LoginKeyboardNone) { section0AcctInfo = @[@(CellKindBarcode), @(CellKindPIN), @(CellKindLogInSignOut)].mutableCopy; } else { //Server expects a blank string. Passes local textfield validation. @@ -392,9 +393,9 @@ - (NSString *)pin - (void)logIn { - if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { // oauth - NSURL *oauthURL = self.selectedAccount.details.oauthIntermediaryUrl; + NSURL *oauthURL = self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl; NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; @@ -650,7 +651,7 @@ - (void)validateCredentials request.timeoutInterval = self.businessLogic.requestTimeoutInterval; - if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { NSString *authToken = self.authToken; if (authToken != nil) { NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; @@ -814,7 +815,7 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error if (success) { // szyjson tu authtoken - if (self.selectedAccount.details.oauthIntermediaryUrl != nil) { + if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { [self.selectedUserAccount setAuthToken:self.authToken]; // [self.selectedUserAccount setPatron:self.patron]; // szyjson } else { @@ -925,8 +926,8 @@ - (void)tableView:(__attribute__((unused)) UITableView *)tableView [[CardCreatorConfiguration alloc] initWithEndpointURL:self.selectedAccount.details.signUpUrl ?: APIKeys.cardCreatorEndpointURL endpointVersion:[APIKeys cardCreatorVersion] - endpointUsername:NYPLSecrets.cardCreatorUsername - endpointPassword:NYPLSecrets.cardCreatorPassword + endpointUsername:APIKeys.cardCreatorUsername + endpointPassword:APIKeys.cardCreatorPassword requestTimeoutInterval:self.businessLogic.requestTimeoutInterval completionHandler:^(NSString *const username, NSString *const PIN, BOOL const userInitiated) { if (userInitiated) { @@ -1070,7 +1071,7 @@ - (UITableViewCell *)tableView:(__attribute__((unused)) UITableView *)tableView ofView:[self.usernameTextField superview] withOffset:-sVerticalMarginPadding]; - if (self.selectedAccount.details.supportsBarcodeScanner) { + if (self.businessLogic.selectedAuthentication.supportsBarcodeScanner) { [cell.contentView addSubview:self.barcodeScanButton]; CGFloat rightMargin = cell.layoutMargins.right; self.barcodeScanButton.contentEdgeInsets = UIEdgeInsetsMake(0, rightMargin * 2, 0, rightMargin); @@ -1508,8 +1509,8 @@ - (void)updateLoginLogoutCellAppearance stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; BOOL const pinHasText = [self.PINTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; - BOOL const pinIsNotRequired = self.selectedAccount.details.pinKeyboard == LoginKeyboardNone; - BOOL const oauthLogin = self.selectedAccount.details.oauthIntermediaryUrl != nil; + BOOL const pinIsNotRequired = self.businessLogic.selectedAuthentication.pinKeyboard == LoginKeyboardNone; + BOOL const oauthLogin = self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil; if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired) || oauthLogin) { self.logInSignOutCell.userInteractionEnabled = YES; self.logInSignOutCell.textLabel.textColor = [NYPLConfiguration mainColor]; diff --git a/Simplified/NYPLSignInBusinessLogic.swift b/Simplified/NYPLSignInBusinessLogic.swift index 91c2215fd..63a0a3225 100644 --- a/Simplified/NYPLSignInBusinessLogic.swift +++ b/Simplified/NYPLSignInBusinessLogic.swift @@ -23,6 +23,10 @@ class NYPLSignInBusinessLogic: NSObject { return AccountsManager.shared.account(libraryAccountID) } + @objc var selectedAuthentication: AccountDetails.Authentication? { + libraryAccount?.details?.auths.first + } + @objc var userAccount: NYPLUserAccount { return NYPLUserAccount.sharedAccount(libraryUUID: libraryAccountID) } @@ -32,7 +36,7 @@ class NYPLSignInBusinessLogic: NSObject { // is signed in, and has an authorization ID returned from the loans feed. return userAccount.hasBarcodeAndPIN() && userAccount.authorizationIdentifier != nil && - (libraryAccount?.details?.supportsBarcodeDisplay ?? false) + (selectedAuthentication?.supportsBarcodeDisplay ?? false) } @objc func isSignedIn() -> Bool { diff --git a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.h b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.h deleted file mode 100644 index 0a4281012..000000000 --- a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// NYPLUserAccountFrontEndValidation.h -// SimplyE -// -// Created by Ettore Pasquini on 4/14/20. -// Copyright © 2020 NYPL Labs. All rights reserved. -// - -@import UIKit; - -@class Account; - -/** - Protocol that represents the input sources / UI requirements for performing - front-end validation. - */ -@protocol NYPLUserAccountInputProvider -@property(nonatomic) UITextField *usernameTextField; -@property(nonatomic) UITextField *PINTextField; -@end - -/** - Performs front-end validation of user input in the context of user account - sign-in. It acts as the @p UITextFieldDelegate, so the UI elements defined in - @p NYPLUserAccountInputProvider should define this class as their @p delegate. - */ -@interface NYPLUserAccountFrontEndValidation : NSObject - -/** - The designated initializer. - @param account The library account to use for performing validation. - @param inputProvider The object providing the input fields from which to - perform validation. - */ -- (instancetype)initWithAccount:(Account*)account - inputProvider:(id)inputProvider; - -@end diff --git a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.m b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.m deleted file mode 100644 index 99877f13a..000000000 --- a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.m +++ /dev/null @@ -1,86 +0,0 @@ -// -// NYPLUserAccountFrontEndValidation.m -// SimplyE -// -// Created by Ettore Pasquini on 4/14/20. -// Copyright © 2020 NYPL Labs. All rights reserved. -// - -#import "SimplyE-Swift.h" -#import "NYPLUserAccountFrontEndValidation.h" - - -@interface NYPLUserAccountFrontEndValidation () -@property(weak) id userInputProvider; -@property Account *account; -@end - -@implementation NYPLUserAccountFrontEndValidation - -- (instancetype)initWithAccount:(Account*)account - inputProvider:(id)inputProvider -{ - self = [super init]; - if (self) { - self.userInputProvider = inputProvider; - self.account = account; - } - - return self; -} - -#pragma mark - UITextFieldDelegate - -- (BOOL)textFieldShouldBeginEditing:(__unused UITextField *)textField -{ - return ![[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]; -} - -- (BOOL)textField:(UITextField *)textField -shouldChangeCharactersInRange:(NSRange)range -replacementString:(NSString *)string -{ - if(![string canBeConvertedToEncoding:NSASCIIStringEncoding]) { - return NO; - } - - if (textField == self.userInputProvider.usernameTextField && - self.account.details.patronIDKeyboard != LoginKeyboardEmail) { - - // Barcodes are numeric and usernames are alphanumeric including punctuation - NSMutableCharacterSet *allowedChars = [NSMutableCharacterSet alphanumericCharacterSet]; - [allowedChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; - - if ([string stringByTrimmingCharactersInSet:allowedChars].length > 0) { - return NO; - } - - // Usernames cannot be longer than 25 characters. - if ([textField.text stringByReplacingCharactersInRange:range withString:string].length > 25) { - return NO; - } - } - - if (textField == self.userInputProvider.PINTextField) { - NSCharacterSet *charSet = [NSCharacterSet decimalDigitCharacterSet]; - bool alphanumericPin = self.account.details.pinKeyboard != LoginKeyboardNumeric; - bool containsNonNumericChar = [string stringByTrimmingCharactersInSet:charSet].length > 0; - bool abovePinCharLimit = [textField.text stringByReplacingCharactersInRange:range withString:string].length > self.account.details.authPasscodeLength; - - // PIN's support numeric or alphanumeric. - if (!alphanumericPin && containsNonNumericChar) { - return NO; - } - - // PIN's character limit. Zero is unlimited. - if (self.account.details.authPasscodeLength == 0) { - return YES; - } else if (abovePinCharLimit) { - return NO; - } - } - - return YES; -} - -@end diff --git a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift new file mode 100644 index 000000000..06edeb1c4 --- /dev/null +++ b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift @@ -0,0 +1,88 @@ +// +// NYPLUserAccountFrontEndValidation.swift +// SimplyE +// +// Created by Jacek Szyja on 26/05/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import UIKit + +/** + Protocol that represents the input sources / UI requirements for performing + front-end validation. + */ +@objc +protocol NYPLUserAccountInputProvider { + var usernameTextField: UITextField! { get set } + var PINTextField: UITextField! { get set } +} + +@objcMembers class NYPLUserAccountFrontEndValidation: NSObject { + let userInputProvider: NYPLUserAccountInputProvider + let account: Account + let selectedAuthentication: AccountDetails.Authentication + + init(account: Account, selectedAuthentication: AccountDetails.Authentication, inputProvider: NYPLUserAccountInputProvider) { + self.userInputProvider = inputProvider + self.account = account + self.selectedAuthentication = selectedAuthentication + } +} + +extension NYPLUserAccountFrontEndValidation: UITextFieldDelegate { + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + return !NYPLUserAccount.sharedAccount().hasBarcodeAndPIN() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard string.canBeConverted(to: .ascii) else { return false } + + if textField == userInputProvider.usernameTextField, + selectedAuthentication.patronIDKeyboard != .email { + + // Barcodes are numeric and usernames are alphanumeric including punctuation + let allowedCharacters = CharacterSet.alphanumerics.union(.punctuationCharacters) + let bannedCharacters = allowedCharacters.inverted + + guard string.rangeOfCharacter(from: bannedCharacters) == nil else { return false } + + if let text = textField.text, + let textRange = Range(range, in: text) { + + let updatedText = text.replacingCharacters(in: textRange, with: string) + guard updatedText.count <= 25 else { return false } + } + } + + if textField == userInputProvider.PINTextField { + let allowedCharacters = CharacterSet.decimalDigits + let bannedCharacters = allowedCharacters.inverted + + let alphanumericPin = selectedAuthentication.pinKeyboard != .numeric + let containsNonNumeric = !(string.rangeOfCharacter(from: bannedCharacters)?.isEmpty ?? true) + let abovePinCharLimit: Bool + + if let text = textField.text, + let textRange = Range(range, in: text) { + + let updatedText = text.replacingCharacters(in: textRange, with: string) + abovePinCharLimit = updatedText.count > selectedAuthentication.authPasscodeLength + } else { + abovePinCharLimit = false + } + + // PIN's support numeric or alphanumeric. + guard alphanumericPin || !containsNonNumeric else { return false } + + // PIN's character limit. Zero is unlimited. + if selectedAuthentication.authPasscodeLength == 0 { + return true + } else if abovePinCharLimit { + return false + } + } + + return true + } +} From ea2ecd0e886a1d92b2b2c333608899a9d5e80ff8 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 29 May 2020 01:40:48 +0200 Subject: [PATCH 06/19] Open ebooks beta --- Simplified.xcodeproj/project.pbxproj | 6 - Simplified/Account.swift | 122 ++++++++--- Simplified/AccountsManager.swift | 10 +- Simplified/KeychainStoredVariable.swift | 71 ++++++- Simplified/NYPLAccountSignInViewController.h | 1 + Simplified/NYPLAccountSignInViewController.m | 193 +++++++++++++----- Simplified/NYPLAgeCheck.swift | 3 +- Simplified/NYPLBookButtonsView.m | 16 +- Simplified/NYPLBookDetailButtonsView.m | 16 +- Simplified/NYPLBookRegistry.m | 28 +-- Simplified/NYPLCatalogNavigationController.m | 14 +- Simplified/NYPLHoldsViewController.m | 3 +- Simplified/NYPLMyBooksDownloadCenter.m | 6 +- Simplified/NYPLMyBooksViewController.m | 3 +- Simplified/NYPLRemoteViewController.m | 20 +- Simplified/NYPLSessionCredentials.swift | 4 + .../NYPLSettingsAccountDetailViewController.m | 153 +++++++++++--- Simplified/NYPLSignInBusinessLogic.swift | 14 +- Simplified/NYPLUserAccount.swift | 81 +++++++- Simplified/NYPLWelcomeScreen.swift | 15 +- Simplified/Network/NYPLNetworkResponder.swift | 2 - Simplified/SimplyE-Bridging-Header.h | 1 + .../Utilities/Authentication/NYPLBasicAuth.h | 14 -- .../Utilities/Authentication/NYPLBasicAuth.m | 59 ------ 24 files changed, 594 insertions(+), 261 deletions(-) delete mode 100644 Simplified/Utilities/Authentication/NYPLBasicAuth.h delete mode 100644 Simplified/Utilities/Authentication/NYPLBasicAuth.m diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index c93a15ea0..042f73702 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -205,7 +205,6 @@ A499BF261B39EFC7002F8B8B /* NYPLOPDSEntryGroupAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = A499BF251B39EFC7002F8B8B /* NYPLOPDSEntryGroupAttributes.m */; }; A4BA1D0E1B430341006F83DF /* NYPLCatalogGroupedFeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A4BA1D0D1B430341006F83DF /* NYPLCatalogGroupedFeedViewController.m */; }; A4BA1D131B43046B006F83DF /* NYPLCatalogGroupedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = A4BA1D121B43046B006F83DF /* NYPLCatalogGroupedFeed.m */; }; - A4E254FA1B0E610900193FE4 /* NYPLBasicAuth.m in Sources */ = {isa = PBXBuildFile; fileRef = A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */; }; A823D811192BABA400B55DE2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A823D810192BABA400B55DE2 /* Foundation.framework */; }; A823D813192BABA400B55DE2 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A823D812192BABA400B55DE2 /* CoreGraphics.framework */; }; A823D815192BABA400B55DE2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A823D814192BABA400B55DE2 /* UIKit.framework */; }; @@ -629,8 +628,6 @@ A4BA1D0D1B430341006F83DF /* NYPLCatalogGroupedFeedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLCatalogGroupedFeedViewController.m; sourceTree = ""; }; A4BA1D111B43046B006F83DF /* NYPLCatalogGroupedFeed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLCatalogGroupedFeed.h; sourceTree = ""; }; A4BA1D121B43046B006F83DF /* NYPLCatalogGroupedFeed.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLCatalogGroupedFeed.m; sourceTree = ""; }; - A4E254F81B0E610900193FE4 /* NYPLBasicAuth.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NYPLBasicAuth.h; sourceTree = ""; }; - A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLBasicAuth.m; sourceTree = ""; }; A823D80D192BABA400B55DE2 /* SimplyE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimplyE.app; sourceTree = BUILT_PRODUCTS_DIR; }; A823D810192BABA400B55DE2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; A823D812192BABA400B55DE2 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -1103,8 +1100,6 @@ isa = PBXGroup; children = ( 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift */, - A4E254F81B0E610900193FE4 /* NYPLBasicAuth.h */, - A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */, ); path = Authentication; sourceTree = ""; @@ -1813,7 +1808,6 @@ 179699D124131BA500EC309F /* UIColor+LabelColor.swift in Sources */, 08C481AE247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift in Sources */, 735FED262427494900144C97 /* NYPLNetworkResponder.swift in Sources */, - A4E254FA1B0E610900193FE4 /* NYPLBasicAuth.m in Sources */, 1188F3E11A1ECC4B006B2F36 /* NYPLReaderSettings.m in Sources */, 11C5DD16197727A6005A9945 /* NYPLKeychain.m in Sources */, 5D7CF86422C19EBA007CAA34 /* NYPLReaderContainerDelegateBase.m in Sources */, diff --git a/Simplified/Account.swift b/Simplified/Account.swift index c8146d9ac..63e56b1a1 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -6,7 +6,7 @@ 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" @@ -16,7 +16,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" @objc(AccountDetailsAuthentication) @objcMembers - class Authentication: NSObject { + class Authentication: NSObject, Codable, NSCoding { let authType:AuthType let authPasscodeLength:UInt let patronIDKeyboard:LoginKeyboard @@ -28,6 +28,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" let coppaUnderUrl:URL? let coppaOverUrl:URL? let oauthIntermediaryUrl:URL? + let methodDescription: String? init(auth: OPDS2AuthenticationDocument.Authentication) { authType = AuthType(rawValue: auth.type) ?? .none @@ -36,47 +37,118 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" pinKeyboard = LoginKeyboard.init(auth.inputs?.password.keyboard) ?? .standard patronIDLabel = auth.labels?.login pinLabel = auth.labels?.password + methodDescription = auth.description 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 ?? "") oauthIntermediaryUrl = URL.init(string: auth.links?.first(where: { $0.rel == "authenticate" })?.href ?? "") } + + var needsAuth:Bool { + return authType == .basic || authType == .oauthIntermediary + } + + var needsAgeCheck:Bool { + return authType == .coppa + } + + var isCatalogSecured: Bool { + return authType == .oauthIntermediary + } + + enum CodingKeys: String, CodingKey { + case authType + case authPasscodeLength + case patronIDKeyboard + case pinKeyboard + case patronIDLabel + case pinLabel + case supportsBarcodeScanner + case supportsBarcodeDisplay + case coppaUnderUrl + case coppaOverUrl + case oauthIntermediaryUrl + case methodDescription + } + + required init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + authType = try values.decode(AuthType.self, forKey: .authType) + authPasscodeLength = try values.decode(UInt.self, forKey: .authPasscodeLength) + patronIDKeyboard = try values.decode(LoginKeyboard.self, forKey: .patronIDKeyboard) + pinKeyboard = try values.decode(LoginKeyboard.self, forKey: .pinKeyboard) + patronIDLabel = try values.decodeIfPresent(String.self, forKey: .patronIDLabel) + pinLabel = try values.decodeIfPresent(String.self, forKey: .pinLabel) + supportsBarcodeScanner = try values.decode(Bool.self, forKey: .supportsBarcodeScanner) + supportsBarcodeDisplay = try values.decode(Bool.self, forKey: .supportsBarcodeDisplay) + coppaUnderUrl = try values.decodeIfPresent(URL.self, forKey: .coppaUnderUrl) + coppaOverUrl = try values.decodeIfPresent(URL.self, forKey: .coppaOverUrl) + oauthIntermediaryUrl = try values.decodeIfPresent(URL.self, forKey: .oauthIntermediaryUrl) + methodDescription = try values.decodeIfPresent(String.self, forKey: .methodDescription) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(authType, forKey: .authType) + try container.encode(authPasscodeLength, forKey: .authPasscodeLength) + try container.encode(patronIDKeyboard, forKey: .patronIDKeyboard) + try container.encode(pinKeyboard, forKey: .pinKeyboard) + try container.encode(patronIDLabel, forKey: .patronIDLabel) + try container.encode(pinLabel, forKey: .pinLabel) + try container.encode(supportsBarcodeScanner, forKey: .supportsBarcodeScanner) + try container.encode(supportsBarcodeDisplay, forKey: .supportsBarcodeDisplay) + try container.encode(coppaUnderUrl, forKey: .coppaUnderUrl) + try container.encode(coppaOverUrl, forKey: .coppaOverUrl) + try container.encode(oauthIntermediaryUrl, forKey: .oauthIntermediaryUrl) + try container.encode(methodDescription, forKey: .methodDescription) + } + + 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 + oauthIntermediaryUrl = authentication.oauthIntermediaryUrl + methodDescription = authentication.methodDescription + } } - + 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? - // szyjson set it properly - private var _selectedAuth: Authentication? - var selectedAuth: Authentication? { - get { - return _selectedAuth ?? auths.first - } - set { - _selectedAuth = newValue - } - } - - var needsAuth:Bool { - let authType = selectedAuth?.authType ?? .none - return authType == .basic || authType == .oauthIntermediary + // szyjson is it needed? + var defaultAuth: Authentication? { + guard auths.count > 1 else { return auths.first } + return auths.first(where: { !$0.isCatalogSecured }) ?? auths.first } - - var needsAgeCheck:Bool { - let authType = selectedAuth?.authType ?? .none - return authType == .coppa - } - + fileprivate var urlAnnotations:URL? fileprivate var urlAcknowledgements:URL? fileprivate var urlContentLicenses:URL? @@ -364,7 +436,7 @@ extension Account { } // MARK: LoginKeyboard -@objc enum LoginKeyboard: Int { +@objc enum LoginKeyboard: Int, Codable { case standard case email case numeric diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index 14e65a9f8..b9ab82399 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -146,6 +146,7 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf """) } + // szyjson move age check after login (some auth methods may or may not require it) DispatchQueue.main.async { var mainFeed = URL(string: self.currentAccount?.catalogUrl ?? "") let resolveFn = { @@ -154,10 +155,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?.selectedAuth?.coppaOverUrl : self.currentAccount?.details?.selectedAuth?.coppaUnderUrl + mainFeed = meetsAgeRequirement ? self.currentAccount?.details?.defaultAuth?.coppaOverUrl : self.currentAccount?.details?.defaultAuth?.coppaUnderUrl resolveFn() } } @@ -188,9 +190,7 @@ 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 { diff --git a/Simplified/KeychainStoredVariable.swift b/Simplified/KeychainStoredVariable.swift index 47ffc7d06..728147dab 100644 --- a/Simplified/KeychainStoredVariable.swift +++ b/Simplified/KeychainStoredVariable.swift @@ -10,7 +10,58 @@ import Foundation class KeychainVariable { let transaction: KeychainVariableTransaction - var key: String + var key: String { + didSet { + guard key != oldValue else { return } + alreadyInited = false + } + } + + var alreadyInited = false + var cachedValue: VariableType? + + init(key: String, accountInfoLock: NSRecursiveLock) { + self.key = key + self.transaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) + } + + func read() -> VariableType? { + guard !alreadyInited else { return cachedValue } + cachedValue = NYPLKeychain.shared()?.object(forKey: key) as? VariableType + alreadyInited = true + return cachedValue + } + + func write(_ newValue: VariableType?) { + cachedValue = newValue + alreadyInited = true + DispatchQueue.global(qos: .userInitiated).async { [key] in + if let newValue = newValue { + NYPLKeychain.shared()?.setObject(newValue, forKey: key) + } else { + NYPLKeychain.shared()?.removeObject(forKey: key) + } + } + } + + func safeWrite(_ newValue: VariableType?) { + transaction.write { + write(newValue) + } + } +} + +class KeychainCodableVariable { + let transaction: KeychainVariableTransaction + var key: String { + didSet { + guard key != oldValue else { return } + alreadyInited = false + } + } + + var alreadyInited = false + var cachedValue: VariableType? init(key: String, accountInfoLock: NSRecursiveLock) { self.key = key @@ -18,14 +69,22 @@ class KeychainVariable { } func read() -> VariableType? { - return NYPLKeychain.shared()?.object(forKey: key) as? 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 } func write(_ newValue: VariableType?) { - if let newValue = newValue { - NYPLKeychain.shared()?.setObject(newValue, forKey: key) - } else { - NYPLKeychain.shared()?.removeObject(forKey: key) + 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) + } } } diff --git a/Simplified/NYPLAccountSignInViewController.h b/Simplified/NYPLAccountSignInViewController.h index e8e941e61..b7346edcd 100644 --- a/Simplified/NYPLAccountSignInViewController.h +++ b/Simplified/NYPLAccountSignInViewController.h @@ -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; diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index d8c6f8a78..002165985 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -28,6 +28,7 @@ #endif typedef NS_ENUM(NSInteger, CellKind) { + CellKindAuthenticationMethodName, CellKindBarcode, CellKindPIN, CellKindLogInSignOut, @@ -61,7 +62,8 @@ @interface NYPLAccountSignInViewController () 1) { + // multiple authentication methods + if (self.businessLogic.selectedAuthentication) { + // show all possible login methods + // and unfold the selected one + for (AccountDetailsAuthentication *authenticationMethod in self.businessLogic.libraryAccount.details.auths) { + [section0AcctInfo addObject:@(CellKindAuthenticationMethodName)]; + if (authenticationMethod.methodDescription == self.businessLogic.selectedAuthentication.methodDescription) { + // selected method, unfold + insertCredentials(authenticationMethod, section0AcctInfo); + } + } + } else { + // show all possible login methods + // with none of them unfolded + for (AccountDetailsAuthentication *authenticationMethod in self.businessLogic.libraryAccount.details.auths) { + [section0AcctInfo addObject:@(CellKindAuthenticationMethodName)]; + } + } + } else if (self.businessLogic.selectedAuthentication) { + // only 1 authentication method + // no header needed + section0AcctInfo = @[].mutableCopy; + insertCredentials(self.businessLogic.selectedAuthentication, section0AcctInfo); + } else { + section0AcctInfo = @[].mutableCopy; + } } else { - //Server expects a blank string. Passes local textfield validation. - self.PINTextField.text = @""; - section0 = @[@(CellKindBarcode), - @(CellKindLogInSignOut)]; + section0AcctInfo = @[].mutableCopy; + insertCredentials(self.businessLogic.selectedAuthentication, section0AcctInfo); } + NSArray *section1; if ([self.businessLogic registrationIsPossible]) { section1 = @[@(CellKindRegistration)]; } else { section1 = @[]; } - self.tableData = @[section0, section1]; + self.tableData = @[section0AcctInfo, section1]; } - (void)viewWillAppear:(BOOL)animated @@ -355,6 +401,14 @@ - (void)tableView:(__attribute__((unused)) UITableView *)tableView } break; } + case CellKindAuthenticationMethodName: { + AccountDetailsAuthentication * _Nullable cellAuthentication = [self authenticationFor:indexPath]; + self.businessLogic.selectedAuthentication = cellAuthentication; + [self setupTableData]; + [self.tableView reloadData]; + + break; + } } } @@ -425,7 +479,36 @@ - (UITableViewCell *)tableView:(__attribute__((unused)) UITableView *)tableView case CellKindRegistration: { return [self createRegistrationCell]; } + case CellKindAuthenticationMethodName: { + AccountDetailsAuthentication * _Nullable cellAuthentication = [self authenticationFor:indexPath]; + + UITableViewCell *cell = [[UITableViewCell alloc] + initWithStyle:UITableViewCellStyleDefault + reuseIdentifier:nil]; + cell.textLabel.font = [UIFont customFontForTextStyle:UIFontTextStyleBody]; + cell.textLabel.text = cellAuthentication.methodDescription; + return cell; + } + } +} + +- (AccountDetailsAuthentication * _Nullable)authenticationFor:(NSIndexPath *)indexPath { + NSArray *sectionArray = (NSArray *)self.tableData[indexPath.section]; + AccountDetailsAuthentication * _Nullable cellAuthentication; + NSInteger authTypeIndex = -1; + + for (NSInteger index = 0; index <= indexPath.row; ++index) { + CellKind kind = (CellKind)[sectionArray[index] intValue]; + if (kind == CellKindAuthenticationMethodName) { + authTypeIndex += 1; + } + } + + if (authTypeIndex >= 0 && authTypeIndex < ((NSInteger) self.businessLogic.libraryAccount.details.auths.count)) { + cellAuthentication = self.businessLogic.libraryAccount.details.auths[authTypeIndex]; } + + return cellAuthentication; } - (UITableViewCell *)createRegistrationCell @@ -535,7 +618,6 @@ - (NSString *)pin #pragma mark - Class Methods - + (void) requestCredentialsUsingExistingBarcode:(BOOL const)useExistingBarcode authorizeImmediately:(BOOL)authorizeImmediately @@ -581,7 +663,7 @@ - (NSString *)pin if (authorizeImmediately && [NYPLUserAccount sharedAccount].hasBarcodeAndPIN) { accountViewController.PINTextField.text = [NYPLUserAccount sharedAccount].PIN; [accountViewController logIn]; - } else if (authorizeImmediately && AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { + } else if (authorizeImmediately && NYPLUserAccount.sharedAccount.authDefinition.oauthIntermediaryUrl) { [accountViewController logIn]; } else { if(useExistingBarcode) { @@ -606,7 +688,7 @@ + (void)authorizeUsingExistingBarcodeAndPinWithCompletionHandler:(void (^)(void) + (void)authorizeUsingIntermediaryWithCompletionHandler:(void (^)(void))handler { - [self requestCredentialsUsingExistingBarcode:NO authorizeImmediately:NO completionHandler:handler]; + [self requestCredentialsUsingExistingBarcode:NO authorizeImmediately:YES completionHandler:handler]; } @@ -763,7 +845,7 @@ - (void)updateLoginLogoutCellAppearance BOOL const pinHasText = [self.PINTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; BOOL const pinIsNotRequired = self.businessLogic.selectedAuthentication.pinKeyboard == LoginKeyboardNone; - BOOL const oauthLogin = self.currentAccount.details.selectedAuth.oauthIntermediaryUrl != nil; + BOOL const oauthLogin = self.businessLogic.selectedAuthentication.oauthIntermediaryUrl != nil; if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired) || oauthLogin) { self.logInSignOutCell.userInteractionEnabled = YES; @@ -782,37 +864,37 @@ - (void)updateLoginLogoutCellAppearance - (void)logIn { - if (self.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { - // oauth - NSURL *oauthURL = self.currentAccount.details.selectedAuth.oauthIntermediaryUrl; + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { + // oauth + NSURL *oauthURL = self.businessLogic.selectedAuthentication.oauthIntermediaryUrl; - NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; + NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; - // add params - NSURLQueryItem *redirect_uri = [[NSURLQueryItem alloc] initWithName:@"redirect_uri" value:@"https://skyneck.pl/login"]; - urlComponents.queryItems = [urlComponents.queryItems arrayByAddingObject:redirect_uri]; + // add params + NSURLQueryItem *redirect_uri = [[NSURLQueryItem alloc] initWithName:@"redirect_uri" value:@"https://skyneck.pl/login"]; + urlComponents.queryItems = [urlComponents.queryItems arrayByAddingObject:redirect_uri]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleRedirectURL:) - name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" - object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRedirectURL:) + name: @"NYPLAppDelegateDidReceiveCleverRedirectURL" + object:nil]; - [UIApplication.sharedApplication openURL: urlComponents.URL]; - [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; - } else { - // bar and pin - assert(self.usernameTextField.text.length > 0); - assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); + [UIApplication.sharedApplication openURL: urlComponents.URL]; + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + } else { + // bar and pin + assert(self.usernameTextField.text.length > 0); + assert(self.PINTextField.text.length > 0 || [self.PINTextField.text isEqualToString:@""]); - [self.usernameTextField resignFirstResponder]; - [self.PINTextField resignFirstResponder]; + [self.usernameTextField resignFirstResponder]; + [self.PINTextField resignFirstResponder]; - [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; + [self setActivityTitleWithText:NSLocalizedString(@"Verifying", nil)]; - [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; + [[UIApplication sharedApplication] beginIgnoringInteractionEvents]; - [self validateCredentials]; - } + [self validateCredentials]; + } } - (void) handleRedirectURL: (NSNotification *) notification @@ -946,13 +1028,13 @@ - (void)validateCredentials request.timeoutInterval = 20.0; - if (self.currentAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { - NSString *authToken = self.authToken; - if (authToken != nil) { - NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; - [request addValue:authenticationValue forHTTPHeaderField:@"Authorization"]; - } + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { + NSString *authToken = self.authToken; + if (authToken != nil) { + NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; + [request addValue:authenticationValue forHTTPHeaderField:@"Authorization"]; } + } self.isCurrentlySigningIn = YES; NSURLSessionDataTask *const task = @@ -1142,16 +1224,17 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error [[UIApplication sharedApplication] endIgnoringInteractionEvents]; if(success) { - // szyjson tu authtoken - if (AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { - [[NYPLUserAccount sharedAccount] setAuthToken:self.authToken]; -// [[NYPLUserAccount sharedAccount] setPatron:self.patron]; // szyjson - } else { - [[NYPLUserAccount sharedAccount] setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; - } + // szyjson handle patron + + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { + [self.businessLogic.userAccount setAuthToken:self.authToken]; +// [self.selectedUserAccount setPatron:self.patron]; + } else { + [self.businessLogic.userAccount setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; + } + + self.businessLogic.userAccount.authDefinition = self.businessLogic.selectedAuthentication; - [[NYPLUserAccount sharedAccount] setBarcode:self.usernameTextField.text - PIN:self.PINTextField.text]; if (!self.isLoggingInAfterSignUp) { [self dismissViewControllerAnimated:YES completion:nil]; } diff --git a/Simplified/NYPLAgeCheck.swift b/Simplified/NYPLAgeCheck.swift index 51f0376b7..c306d411e 100644 --- a/Simplified/NYPLAgeCheck.swift +++ b/Simplified/NYPLAgeCheck.swift @@ -18,12 +18,13 @@ import Foundation func verifyCurrentAccountAgeRequirement(_ completion: ((Bool) -> ())?) -> Void { serialQueue.async { + let userAccount = NYPLUserAccount.sharedAccount() guard let accountDetails = AccountsManager.shared.currentAccount?.details else { completion?(false) return } - if accountDetails.needsAuth == true || accountDetails.userAboveAgeLimit { + if userAccount.needsAuth == true || accountDetails.userAboveAgeLimit { completion?(true) return } diff --git a/Simplified/NYPLBookButtonsView.m b/Simplified/NYPLBookButtonsView.m index 1a7833c45..e515722df 100644 --- a/Simplified/NYPLBookButtonsView.m +++ b/Simplified/NYPLBookButtonsView.m @@ -188,8 +188,8 @@ - (void)updateButtons if (self.showReturnButtonIfApplicable) { - NSString *title = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); - NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; + NSString *title = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); + NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; visibleButtonInfo = @[@{ButtonKey: self.downloadButton, TitleKey: NSLocalizedString(@"Download", nil), @@ -230,8 +230,8 @@ - (void)updateButtons if (self.showReturnButtonIfApplicable) { - NSString *title = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); - NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; + NSString *title = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); + NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; visibleButtonInfo = @[buttonInfo, @{ButtonKey: self.deleteButton, @@ -267,7 +267,7 @@ - (void)updateButtons for (NSDictionary *buttonInfo in visibleButtonInfo) { NYPLRoundedButton *button = buttonInfo[ButtonKey]; if(button == self.deleteButton && (!fulfillmentId && fulfillmentIdRequired) && !hasRevokeLink) { - if(!self.book.defaultAcquisitionIfOpenAccess && [[AccountsManager sharedInstance] currentAccount].details.needsAuth) { + if(!self.book.defaultAcquisitionIfOpenAccess && NYPLUserAccount.sharedAccount.authDefinition.needsAuth) { continue; } } @@ -341,13 +341,13 @@ - (void)didSelectReturn case NYPLBookStateDownloadFailed: case NYPLBookStateDownloadNeeded: case NYPLBookStateDownloadSuccessful: - title = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + title = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitle", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitle", nil)); - message = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + message = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitleMessageFormat", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitleMessageFormat", nil)); - confirmButtonTitle = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + confirmButtonTitle = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitle", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitle", nil)); break; diff --git a/Simplified/NYPLBookDetailButtonsView.m b/Simplified/NYPLBookDetailButtonsView.m index ae134916e..dbd193e9e 100644 --- a/Simplified/NYPLBookDetailButtonsView.m +++ b/Simplified/NYPLBookDetailButtonsView.m @@ -170,8 +170,8 @@ - (void)updateButtons if (self.showReturnButtonIfApplicable) { - NSString *title = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); - NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; + NSString *title = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); + NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; visibleButtonInfo = @[@{ButtonKey: self.downloadButton, TitleKey: NSLocalizedString(@"Download", nil), @@ -212,8 +212,8 @@ - (void)updateButtons if (self.showReturnButtonIfApplicable) { - NSString *title = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); - NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; + NSString *title = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"Delete", nil) : NSLocalizedString(@"Return", nil); + NSString *hint = (self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? [NSString stringWithFormat:NSLocalizedString(@"Deletes %@", nil), self.book.title] : [NSString stringWithFormat:NSLocalizedString(@"Returns %@", nil), self.book.title]; visibleButtonInfo = @[buttonInfo, @{ButtonKey: self.deleteButton, @@ -271,7 +271,7 @@ - (void)updateButtons for (NSDictionary *buttonInfo in visibleButtonInfo) { NYPLRoundedButton *button = buttonInfo[ButtonKey]; if(button == self.deleteButton && (!fulfillmentId && fulfillmentIdRequired) && !hasRevokeLink) { - if(!self.book.defaultAcquisitionIfOpenAccess && [[AccountsManager sharedInstance] currentAccount].details.needsAuth) { + if(!self.book.defaultAcquisitionIfOpenAccess && NYPLUserAccount.sharedAccount.authDefinition.needsAuth) { continue; } } @@ -344,13 +344,13 @@ - (void)didSelectReturn case NYPLBookStateDownloadFailed: case NYPLBookStateDownloadNeeded: case NYPLBookStateDownloadSuccessful: - title = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + title = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitle", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitle", nil)); - message = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + message = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitleMessageFormat", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitleMessageFormat", nil)); - confirmButtonTitle = ((self.book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth) + confirmButtonTitle = ((self.book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth) ? NSLocalizedString(@"MyBooksDownloadCenterConfirmDeleteTitle", nil) : NSLocalizedString(@"MyBooksDownloadCenterConfirmReturnTitle", nil)); break; diff --git a/Simplified/NYPLBookRegistry.m b/Simplified/NYPLBookRegistry.m index 9056396eb..190d50f12 100644 --- a/Simplified/NYPLBookRegistry.m +++ b/Simplified/NYPLBookRegistry.m @@ -280,26 +280,14 @@ - (void)syncWithCompletionHandler:(void (^)(BOOL success))completion if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); }]; return; - } else if (![[NYPLUserAccount sharedAccount] hasCredentials]) { -// AccountsManager.shared.currentAccount.details.oauthIntermediaryUrl - if (AccountsManager.shared.currentAccount.details.selectedAuth.oauthIntermediaryUrl) { - // sign in - NYPLLOG(@"[syncWithCompletionHandler] No valid authorization header"); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if(completion) completion(NO); - if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); - [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; - }]; - return; - } else { - NYPLLOG(@"[syncWithCompletionHandler] No barcode and PIN"); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if(completion) completion(NO); - if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); - [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; - }]; - return; - } + } else if (!NYPLUserAccount.sharedAccount.hasCredentials || !AccountsManager.shared.currentAccount.loansUrl) { + NYPLLOG(@"[syncWithCompletionHandler] No valid credentials"); + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + if(completion) completion(NO); + if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); + [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NYPLSyncEnded object:nil]; + }]; + return; } else { self.syncing = YES; self.syncShouldCommit = YES; diff --git a/Simplified/NYPLCatalogNavigationController.m b/Simplified/NYPLCatalogNavigationController.m index c62abc352..4951a4de1 100644 --- a/Simplified/NYPLCatalogNavigationController.m +++ b/Simplified/NYPLCatalogNavigationController.m @@ -199,14 +199,14 @@ - (void)updateFeedAndRegistryOnAccountChange postNotificationName:NSNotification.NYPLCurrentAccountDidChange object:nil]; }; - if (account.details.needsAgeCheck) { + if (NYPLUserAccount.sharedAccount.authDefinition.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { dispatch_async(dispatch_get_main_queue(), ^{ - mainFeedUrl = isOfAge ? account.details.selectedAuth.coppaOverUrl : account.details.selectedAuth.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }); }]; - } else if (account.details.selectedAuth.oauthIntermediaryUrl && !NYPLUserAccount.sharedAccount.hasAuthToken) { + } else if (NYPLUserAccount.sharedAccount.isCatalogSecured && !NYPLUserAccount.sharedAccount.hasCredentials) { // sign in [NYPLAccountSignInViewController authorizeUsingIntermediaryWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ @@ -241,10 +241,10 @@ - (void)viewDidLoad object:nil]; }; - if (account.details.needsAgeCheck) { + if (NYPLUserAccount.sharedAccount.authDefinition.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { dispatch_async(dispatch_get_main_queue(), ^{ - mainFeedUrl = isOfAge ? account.details.selectedAuth.coppaOverUrl : account.details.selectedAuth.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }); }]; @@ -294,9 +294,9 @@ - (void)viewDidAppear:(BOOL)animated NYPLRootTabBarController *vc = [NYPLRootTabBarController sharedController]; [vc safelyPresentViewController:navController animated:YES completion:nil]; }; - if (currentAccount.details.needsAgeCheck) { + if (NYPLUserAccount.sharedAccount.authDefinition.needsAgeCheck) { [[AgeCheck shared] verifyCurrentAccountAgeRequirement:^(BOOL isOfAge) { - mainFeedUrl = isOfAge ? currentAccount.details.selectedAuth.coppaOverUrl : currentAccount.details.selectedAuth.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }]; } else { diff --git a/Simplified/NYPLHoldsViewController.m b/Simplified/NYPLHoldsViewController.m index 49dcd519f..ba18b057a 100644 --- a/Simplified/NYPLHoldsViewController.m +++ b/Simplified/NYPLHoldsViewController.m @@ -254,8 +254,7 @@ - (void)updateBadge - (void)didSelectSync { - Account *const account = [AccountsManager shared].currentAccount; - if (account.details.needsAuth) { + if ([NYPLUserAccount sharedAccount].needsAuth) { if([[NYPLUserAccount sharedAccount] hasCredentials]) { [[NYPLBookRegistry sharedRegistry] syncWithStandardAlertsOnCompletion]; } else { diff --git a/Simplified/NYPLMyBooksDownloadCenter.m b/Simplified/NYPLMyBooksDownloadCenter.m index 12aac10e0..b0fcd5eb7 100644 --- a/Simplified/NYPLMyBooksDownloadCenter.m +++ b/Simplified/NYPLMyBooksDownloadCenter.m @@ -224,7 +224,6 @@ - (void)URLSession:(__attribute__((unused)) NSURLSession *)session break; } case NYPLMyBooksDownloadRightsManagementSimplifiedBearerTokenJSON: { - // szyjson it looks like it firstly downloads json with a bearer token, and then downloads the book itself NSData *const data = [NSData dataWithContentsOfURL:location]; if (!data) { [self failDownloadForBook:book]; @@ -345,7 +344,6 @@ - (void)URLSession:(__unused NSURLSession *)session self.taskIdentifierToRedirectAttempts[@(task.taskIdentifier)] = @(redirectAttempts + 1); - // szyjson appends bearer token to redirected requests NSString *const authorizationKey = @"Authorization"; // Since any "Authorization" header will be dropped on redirection for security @@ -690,7 +688,7 @@ - (void)startDownloadForBook:(NYPLBook *const)book switch(state) { case NYPLBookStateUnregistered: - if(!book.defaultAcquisitionIfBorrow && (book.defaultAcquisitionIfOpenAccess || ![[AccountsManager sharedInstance] currentAccount].details.needsAuth)) { + if(!book.defaultAcquisitionIfBorrow && (book.defaultAcquisitionIfOpenAccess || !NYPLUserAccount.sharedAccount.authDefinition.needsAuth)) { [[NYPLBookRegistry sharedRegistry] addBook:book location:nil @@ -738,7 +736,6 @@ - (void)startDownloadForBook:(NYPLBook *const)book return; } - // szyjson book downloading, insert header and maybe set a type? NSURLSessionDownloadTask *const task = [self.session downloadTaskWithRequest:request]; self.bookIdentifierToDownloadInfo[book.identifier] = @@ -768,7 +765,6 @@ - (void)startDownloadForBook:(NYPLBook *const)book } } else { - // szyjson show login screen here [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:NO completionHandler:^{ diff --git a/Simplified/NYPLMyBooksViewController.m b/Simplified/NYPLMyBooksViewController.m index ab84dd3b1..3e2f58d65 100644 --- a/Simplified/NYPLMyBooksViewController.m +++ b/Simplified/NYPLMyBooksViewController.m @@ -370,8 +370,7 @@ - (void)facetView:(NYPLFacetView *const)facetView - (void)didSelectSync { - Account *const account = [AccountsManager shared].currentAccount; - if (account.details.needsAuth) { + if ([NYPLUserAccount sharedAccount].needsAuth) { if([[NYPLUserAccount sharedAccount] hasCredentials]) { [[NYPLBookRegistry sharedRegistry] syncWithStandardAlertsOnCompletion]; } else { diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index 309b9ffda..2ce7c576e 100644 --- a/Simplified/NYPLRemoteViewController.m +++ b/Simplified/NYPLRemoteViewController.m @@ -60,12 +60,11 @@ - (void)load cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeoutInterval] mutableCopy]; - // szyjson this makes books list loading possible - if([[NYPLUserAccount sharedAccount] hasAuthToken]) - { - NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; - [request setValue:authValue forHTTPHeaderField:@"Authorization"]; - } + if([[NYPLUserAccount sharedAccount] hasAuthToken]) + { + NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; + [request setValue:authValue forHTTPHeaderField:@"Authorization"]; + } self.activityIndicatorLabel.hidden = YES; [NSTimer scheduledTimerWithTimeInterval: activityLabelTimer target: self @@ -157,6 +156,15 @@ - (void)connectionDidFinishLoading:(__attribute__((unused)) NSURLConnection *)co { [self.activityIndicatorView stopAnimating]; self.activityIndicatorLabel.hidden = YES; + + if ([self.response.MIMEType isEqualToString:@"application/vnd.opds.authentication.v1.0+json"]) { + self.reloadView.hidden = false; + [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:([NYPLUserAccount sharedAccount].barcode) completionHandler:^{ + [self load]; + }]; + return; + } + BOOL mimeTypeMatches = [self.response.MIMEType isEqualToString:@"application/problem+json"] || [self.response.MIMEType isEqualToString:@"application/api-problem+json"]; diff --git a/Simplified/NYPLSessionCredentials.swift b/Simplified/NYPLSessionCredentials.swift index fdbc10fba..3f4253d86 100644 --- a/Simplified/NYPLSessionCredentials.swift +++ b/Simplified/NYPLSessionCredentials.swift @@ -97,4 +97,8 @@ extension String { func asKeychainVariable(with accountInfoLock: NSRecursiveLock) -> KeychainVariable { return KeychainVariable(key: self, accountInfoLock: accountInfoLock) } + + func asKeychainCodableVariable(with accountInfoLock: NSRecursiveLock) -> KeychainCodableVariable { + return KeychainCodableVariable(key: self, accountInfoLock: accountInfoLock) + } } diff --git a/Simplified/NYPLSettingsAccountDetailViewController.m b/Simplified/NYPLSettingsAccountDetailViewController.m index b3d689405..6b67be54a 100644 --- a/Simplified/NYPLSettingsAccountDetailViewController.m +++ b/Simplified/NYPLSettingsAccountDetailViewController.m @@ -27,6 +27,7 @@ #endif typedef NS_ENUM(NSInteger, CellKind) { + CellKindAuthenticationMethodName, CellKindAdvancedSettings, CellKindAgeCheck, CellKindBarcodeImage, @@ -61,6 +62,9 @@ @interface NYPLSettingsAccountDetailViewController () 1) { + // multiple authentication methods + if (self.businessLogic.selectedAuthentication) { + // show all possible login methods + // and unfold the selected one + for (AccountDetailsAuthentication *authenticationMethod in self.businessLogic.libraryAccount.details.auths) { + [section0AcctInfo addObject:@(CellKindAuthenticationMethodName)]; + if (authenticationMethod.methodDescription == self.businessLogic.selectedAuthentication.methodDescription) { + // selected method, unfold + insertCredentials(authenticationMethod, section0AcctInfo); + } + } + } else { + // show all possible login methods + // with none of them unfolded + for (AccountDetailsAuthentication *authenticationMethod in self.businessLogic.libraryAccount.details.auths) { + [section0AcctInfo addObject:@(CellKindAuthenticationMethodName)]; + } + } + } else if (self.businessLogic.selectedAuthentication) { + // only 1 authentication method + // no header needed + section0AcctInfo = @[].mutableCopy; + insertCredentials(self.businessLogic.selectedAuthentication, section0AcctInfo); + } else { + section0AcctInfo = @[].mutableCopy; + } } else { - //Server expects a blank string. Passes local textfield validation. - self.PINTextField.text = @""; - section0AcctInfo = @[@(CellKindBarcode), @(CellKindLogInSignOut)].mutableCopy; + section0AcctInfo = @[].mutableCopy; + insertCredentials(self.businessLogic.selectedAuthentication, section0AcctInfo); } if ([self.businessLogic librarySupportsBarcodeDisplay]) { [section0AcctInfo insertObject:@(CellKindBarcodeImage) atIndex: 0]; @@ -393,9 +444,9 @@ - (NSString *)pin - (void)logIn { - if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { // oauth - NSURL *oauthURL = self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl; + NSURL *oauthURL = self.businessLogic.selectedAuthentication.oauthIntermediaryUrl; NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:oauthURL resolvingAgainstBaseURL:true]; @@ -651,7 +702,7 @@ - (void)validateCredentials request.timeoutInterval = self.businessLogic.requestTimeoutInterval; - if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { NSString *authToken = self.authToken; if (authToken != nil) { NSString *authenticationValue = [@"Bearer " stringByAppendingString: authToken]; @@ -814,14 +865,16 @@ - (void)authorizationAttemptDidFinish:(BOOL)success error:(NSError *)error [[UIApplication sharedApplication] endIgnoringInteractionEvents]; if (success) { - // szyjson tu authtoken - if (self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil) { - [self.selectedUserAccount setAuthToken:self.authToken]; -// [self.selectedUserAccount setPatron:self.patron]; // szyjson + // szyjson handle patron + if (self.businessLogic.selectedAuthentication.oauthIntermediaryUrl) { + [self.businessLogic.userAccount setAuthToken:self.authToken]; +// [self.selectedUserAccount setPatron:self.patron]; } else { - [self.selectedUserAccount setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; + [self.businessLogic.userAccount setBarcode:self.usernameTextField.text PIN:self.PINTextField.text]; } + self.businessLogic.userAccount.authDefinition = self.businessLogic.selectedAuthentication; + if ([self.selectedAccountId isEqualToString:[AccountsManager shared].currentAccount.uuid]) { [[NYPLBookRegistry sharedRegistry] syncWithCompletionHandler:^(BOOL success) { if (success) { @@ -1038,6 +1091,14 @@ - (void)tableView:(__attribute__((unused)) UITableView *)tableView [self.navigationController pushViewController:vc animated:YES]; break; } + case CellKindAuthenticationMethodName: { + AccountDetailsAuthentication * _Nullable cellAuthentication = [self authenticationFor:indexPath]; + self.businessLogic.selectedAuthentication = cellAuthentication; + [self setupTableData]; + [self.tableView reloadData]; + + break; + } } } @@ -1230,12 +1291,38 @@ - (UITableViewCell *)tableView:(__attribute__((unused)) UITableView *)tableView cell.textLabel.text = NSLocalizedString(@"Advanced", nil); return cell; } - default: { - return nil; + case CellKindAuthenticationMethodName: { + AccountDetailsAuthentication * _Nullable cellAuthentication = [self authenticationFor:indexPath]; + + UITableViewCell *cell = [[UITableViewCell alloc] + initWithStyle:UITableViewCellStyleDefault + reuseIdentifier:nil]; + cell.textLabel.font = [UIFont customFontForTextStyle:UIFontTextStyleBody]; + cell.textLabel.text = cellAuthentication.methodDescription; + return cell; } } } +- (AccountDetailsAuthentication * _Nullable)authenticationFor:(NSIndexPath *)indexPath { + NSArray *sectionArray = (NSArray *)self.tableData[indexPath.section]; + AccountDetailsAuthentication * _Nullable cellAuthentication; + NSInteger authTypeIndex = -1; + + for (NSInteger index = 0; index <= indexPath.row; ++index) { + CellKind kind = (CellKind)[sectionArray[index] intValue]; + if (kind == CellKindAuthenticationMethodName) { + authTypeIndex += 1; + } + } + + if (authTypeIndex >= 0 && authTypeIndex < ((NSInteger) self.businessLogic.libraryAccount.details.auths.count)) { + cellAuthentication = self.businessLogic.libraryAccount.details.auths[authTypeIndex]; + } + + return cellAuthentication; +} + - (UITableViewCell *)createRegistrationCell { UIView *containerView = [[UIView alloc] init]; @@ -1291,7 +1378,12 @@ - (CGFloat)tableView:(__unused UITableView *)tableView heightForHeaderInSection: } - (CGFloat)tableView:(__unused UITableView *)tableView heightForFooterInSection:(__unused NSInteger)section { - return UITableViewAutomaticDimension; + if ((section == sSection0AccountInfo && [self.businessLogic shouldShowEULALink]) || + (section == sSection1Sync && [self.businessLogic shouldShowSyncButton])) { + return UITableViewAutomaticDimension; + } else { + return 0; + } } -(CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section { @@ -1303,7 +1395,12 @@ -(CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForHeaderIn } - (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForFooterInSection:(__unused NSInteger)section { - return 44; + if ((section == sSection0AccountInfo && [self.businessLogic shouldShowEULALink]) || + (section == sSection1Sync && [self.businessLogic shouldShowSyncButton])) { + return 44; + } else { + return 0; + } } - (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForRowAtIndexPath:(__unused NSIndexPath *)indexPath @@ -1350,7 +1447,8 @@ - (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:( [subtitleLabel autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:titleLabel]; [subtitleLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:titleLabel withOffset:0]; [subtitleLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:20]; - + + self.accountInfoHeaderView = containerView; return containerView; } else { return nil; @@ -1359,6 +1457,7 @@ - (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:( - (UIView *)tableView:(UITableView *)__unused tableView viewForFooterInSection:(NSInteger)section { + // something's wrong, it gets called every refresh cycle when scrolling if ((section == sSection0AccountInfo && [self.businessLogic shouldShowEULALink]) || (section == sSection1Sync && [self.businessLogic shouldShowSyncButton])) { @@ -1395,6 +1494,12 @@ - (UIView *)tableView:(UITableView *)__unused tableView viewForFooterInSection:( [footerLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:8.0]; [footerLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:16.0 relation:NSLayoutRelationGreaterThanOrEqual]; + if (section == sSection0AccountInfo) { + self.accountInfoFooterView = container; + } else if (section == sSection1Sync) { + self.syncFooterView = container; + } + return container; } else { return nil; @@ -1510,7 +1615,7 @@ - (void)updateLoginLogoutCellAppearance BOOL const pinHasText = [self.PINTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length; BOOL const pinIsNotRequired = self.businessLogic.selectedAuthentication.pinKeyboard == LoginKeyboardNone; - BOOL const oauthLogin = self.selectedAccount.details.selectedAuth.oauthIntermediaryUrl != nil; + BOOL const oauthLogin = self.businessLogic.selectedAuthentication.oauthIntermediaryUrl != nil; if((barcodeHasText && pinHasText) || (barcodeHasText && pinIsNotRequired) || oauthLogin) { self.logInSignOutCell.userInteractionEnabled = YES; self.logInSignOutCell.textLabel.textColor = [NYPLConfiguration mainColor]; diff --git a/Simplified/NYPLSignInBusinessLogic.swift b/Simplified/NYPLSignInBusinessLogic.swift index 63a0a3225..dd9562254 100644 --- a/Simplified/NYPLSignInBusinessLogic.swift +++ b/Simplified/NYPLSignInBusinessLogic.swift @@ -8,6 +8,7 @@ import UIKit +@objcMembers class NYPLSignInBusinessLogic: NSObject { @objc let libraryAccountID: String @@ -23,8 +24,19 @@ class NYPLSignInBusinessLogic: NSObject { return AccountsManager.shared.account(libraryAccountID) } + private var _selectedAuthentication: AccountDetails.Authentication? @objc var selectedAuthentication: AccountDetails.Authentication? { - libraryAccount?.details?.auths.first + get { + guard _selectedAuthentication == nil else { return _selectedAuthentication } + guard userAccount.authDefinition == nil else { return userAccount.authDefinition } + guard let auths = libraryAccount?.details?.auths else { return nil } + guard auths.count > 1 else { return auths.first } + + return nil + } + set { + _selectedAuthentication = newValue + } } @objc var userAccount: NYPLUserAccount { diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index 2f5538de4..b7a199f3c 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -23,6 +23,7 @@ private enum StorageKey: String { case userID = "NYPLAccountUserIDKey" case deviceID = "NYPLAccountDeviceIDKey" case credentials = "NYPLAccountCredentialsKey" + case authDefinition = "NYPLAccountAuthDefinitionKey" func keyForLibrary(uuid libraryUUID: String?) -> String { guard let libraryUUID = libraryUUID else { return self.rawValue } @@ -35,7 +36,63 @@ private enum StorageKey: String { private let accountInfoLock = NSRecursiveLock() private lazy var keychainTransaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) - private var libraryUUID: String? + private var libraryUUID: String? { + didSet { + guard libraryUUID != oldValue else { return } + _authorizationIdentifier.key = StorageKey.authorizationIdentifier.keyForLibrary(uuid: libraryUUID) + _adobeToken.key = StorageKey.adobeToken.keyForLibrary(uuid: libraryUUID) + _licensor.key = StorageKey.licensor.keyForLibrary(uuid: libraryUUID) + _patron.key = StorageKey.patron.keyForLibrary(uuid: libraryUUID) + _adobeVendor.key = StorageKey.adobeVendor.keyForLibrary(uuid: libraryUUID) + _provider.key = StorageKey.provider.keyForLibrary(uuid: libraryUUID) + _userID.key = StorageKey.userID.keyForLibrary(uuid: libraryUUID) + _deviceID.key = StorageKey.deviceID.keyForLibrary(uuid: libraryUUID) + _credentials.key = StorageKey.credentials.keyForLibrary(uuid: libraryUUID) + _authDefinition.key = StorageKey.authDefinition.keyForLibrary(uuid: libraryUUID) + + _barcode.key = StorageKey.barcode.keyForLibrary(uuid: libraryUUID) + _pin.key = StorageKey.PIN.keyForLibrary(uuid: libraryUUID) + _authToken.key = StorageKey.authToken.keyForLibrary(uuid: libraryUUID) + } + } + + var authDefinition: AccountDetails.Authentication? { + get { + let legacyDefinition: AccountDetails.Authentication? + if let libraryUUID = self.libraryUUID { + legacyDefinition = AccountsManager.shared.account(libraryUUID)?.details?.auths.first + } else { + legacyDefinition = AccountsManager.shared.currentAccount?.details?.auths.first + } + return _authDefinition.read() ?? legacyDefinition + } + set { + guard let newValue = newValue else { return } + _authDefinition.safeWrite(newValue) + + DispatchQueue.main.async { + var mainFeed = URL(string: AccountsManager.shared.currentAccount?.catalogUrl ?? "") + let resolveFn = { + NYPLSettings.shared.accountMainFeedURL = mainFeed + UIApplication.shared.delegate?.window??.tintColor = NYPLConfiguration.mainColor() + NotificationCenter.default.post(name: NSNotification.Name.NYPLCurrentAccountDidChange, object: nil) + } + + if self.needsAgeCheck { + AgeCheck.shared().verifyCurrentAccountAgeRequirement { [weak self] meetsAgeRequirement in + DispatchQueue.main.async { + mainFeed = meetsAgeRequirement ? self?.authDefinition?.coppaOverUrl : self?.authDefinition?.coppaUnderUrl + resolveFn() + } + } + } else { + resolveFn() + } + } + + notifyAccountDidChange() + } + } public private(set) var credentials: Credentials? { get { @@ -121,9 +178,12 @@ private enum StorageKey: String { private lazy var _deviceID: KeychainVariable = StorageKey.deviceID .keyForLibrary(uuid: libraryUUID) .asKeychainVariable(with: accountInfoLock) - private lazy var _credentials: KeychainVariable = StorageKey.credentials + private lazy var _credentials: KeychainCodableVariable = StorageKey.credentials .keyForLibrary(uuid: libraryUUID) - .asKeychainVariable(with: accountInfoLock) + .asKeychainCodableVariable(with: accountInfoLock) + private lazy var _authDefinition: KeychainCodableVariable = StorageKey.authDefinition + .keyForLibrary(uuid: libraryUUID) + .asKeychainCodableVariable(with: accountInfoLock) // Legacy private lazy var _barcode: KeychainVariable = StorageKey.barcode @@ -166,6 +226,21 @@ private enum StorageKey: String { return hasAuthToken() || hasBarcodeAndPIN() } + // Oauth requires login to load catalog + var isCatalogSecured: Bool { + return authDefinition?.isCatalogSecured ?? false + } + + var needsAuth:Bool { + let authType = authDefinition?.authType ?? .none + return authType == .basic || authType == .oauthIntermediary + } + + var needsAgeCheck:Bool { + let authType = authDefinition?.authType ?? .none + return authType == .coppa + } + // MARK: - Legacy private var legacyBarcode: String? { return _barcode.read() } private var legacyPin: String? { return _pin.read() } diff --git a/Simplified/NYPLWelcomeScreen.swift b/Simplified/NYPLWelcomeScreen.swift index 85211c899..f7a973d0c 100644 --- a/Simplified/NYPLWelcomeScreen.swift +++ b/Simplified/NYPLWelcomeScreen.swift @@ -1,7 +1,6 @@ import UIKit import PureLayout - /// Welcome screen for a first-time user @objcMembers final class NYPLWelcomeScreenViewController: UIViewController { @@ -186,6 +185,19 @@ import PureLayout let pickLibrary = { let listVC = NYPLWelcomeScreenAccountList { account in + +// let completion: ((Account) -> ()) = { account in +// if account.details?.defaultAuth?.isCatalogSecured == true && NYPLUserAccount.sharedAccount().hasCredentials() { +// let signInVC = NYPLAccountSignInViewController() +// signInVC.completionHandler = { +// self.completion?(account) +// } +// self.navigationController?.pushViewController(signInVC, animated: true) +// } else { +// self.completion?(account) +// } +// } + if account.details != nil { self.completion?(account) } else { @@ -278,7 +290,6 @@ import PureLayout } } - /// List of available Libraries/Accounts to select as patron's primary /// when going through Welcome Screen flow. final class NYPLWelcomeScreenAccountList: UIViewController, UITableViewDelegate, UITableViewDataSource { diff --git a/Simplified/Network/NYPLNetworkResponder.swift b/Simplified/Network/NYPLNetworkResponder.swift index 59f015cb2..a1f7493ce 100644 --- a/Simplified/Network/NYPLNetworkResponder.swift +++ b/Simplified/Network/NYPLNetworkResponder.swift @@ -152,8 +152,6 @@ extension NYPLNetworkResponder: URLSessionTaskDelegate { // NYPLLOG_F(@"NSURLSessionTask: %@. Challenge Received: %@", // task.currentRequest.URL.absoluteString, // challenge.protectionSpace.authenticationMethod); -// -// NYPLBasicAuthHandler(challenge, completionHandler); NYPLBasicAuth.authHandler(challenge: challenge, completionHandler: completionHandler) } diff --git a/Simplified/SimplyE-Bridging-Header.h b/Simplified/SimplyE-Bridging-Header.h index 8053417b4..530010f24 100644 --- a/Simplified/SimplyE-Bridging-Header.h +++ b/Simplified/SimplyE-Bridging-Header.h @@ -30,6 +30,7 @@ #import "NYPLHoldsNavigationController.h" #import "NYPLMyBooksDownloadCenter.h" #import "NYPLBookLocation.h" +#import "NYPLAccountSignInViewController.h" #if FEATURE_DRM_CONNECTOR #import "ADEPT/NYPLADEPTErrors.h" #import "ADEPT/NYPLADEPT.h" diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.h b/Simplified/Utilities/Authentication/NYPLBasicAuth.h deleted file mode 100644 index 73cdcc682..000000000 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.h +++ /dev/null @@ -1,14 +0,0 @@ -// This function is used to implement basic authentication handling for NSURLSessionTaskDelegate -// instances by pulling credentials from NYPLAccount. -//void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *challenge, -// void (^completionHandler) -// (NSURLSessionAuthChallengeDisposition disposition, -// NSURLCredential *credential)); -// -//// Neither |username| nor |password| may be nil. -//void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, -// void (^completionHandler) -// (NSURLSessionAuthChallengeDisposition disposition, -// NSURLCredential *credential), -// NSString *username, -// NSString *password); diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.m b/Simplified/Utilities/Authentication/NYPLBasicAuth.m deleted file mode 100644 index 3c2078ac9..000000000 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.m +++ /dev/null @@ -1,59 +0,0 @@ -#import "SimplyE-Swift.h" - -#import "NYPLBasicAuth.h" - -// szyjson wherever this is used, I probably need to use auth token -//void NYPLBasicAuthHandler(NSURLAuthenticationChallenge *const challenge, -// void (^completionHandler) -// (NSURLSessionAuthChallengeDisposition disposition, -// NSURLCredential *credential)) -//{ -// if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { -// if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { -// NSString *const barcode = [NYPLUserAccount sharedAccount].barcode; -// NSString *const PIN = [NYPLUserAccount sharedAccount].PIN; -// NYPLBasicAuthCustomHandler(challenge, completionHandler, barcode, PIN); -// } else { -// completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); -// } -// return; -// } else if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { -// completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); -// return; -// } else { -// NYPLLOG(@"NSURLAuthenticationChallenge rejected."); -// completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); -// return; -// } -//} -// -//void NYPLBasicAuthCustomHandler(NSURLAuthenticationChallenge *challenge, -// void (^completionHandler) -// (NSURLSessionAuthChallengeDisposition disposition, -// NSURLCredential *credential), -// NSString *const username, -// NSString *const password) -//{ -// if(!(username && password)) { -// @throw NSInvalidArgumentException; -// } -// -// if(![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { -// completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); -// return; -// } -// -// if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]) { -// if(challenge.previousFailureCount == 0) { -// completionHandler(NSURLSessionAuthChallengeUseCredential, -// [NSURLCredential -// credentialWithUser:username -// password:password -// persistence:NSURLCredentialPersistenceNone]); -// } else { -// completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); -// } -// } else { -// completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); -// } -//} From 86cc543a2845233b010e03f37a82be42d2a70857 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 29 May 2020 19:26:51 +0200 Subject: [PATCH 07/19] beta 2 --- Simplified/NYPLAccountSignInViewController.m | 2 +- Simplified/NYPLRemoteViewController.m | 202 ++++++++---------- .../NYPLSettingsAccountDetailViewController.m | 6 +- Simplified/NYPLUserAccount.swift | 4 +- Simplified/NYPLWelcomeScreen.swift | 13 -- Simplified/Network/NYPLNetworkExecutor.swift | 19 +- Simplified/Network/NYPLNetworkResponder.swift | 4 +- .../NYPLUserAccountFrontEndValidation.swift | 17 +- 8 files changed, 120 insertions(+), 147 deletions(-) diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index 002165985..ce8da1246 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -131,7 +131,7 @@ - (instancetype)init self.frontEndValidator = [[NYPLUserAccountFrontEndValidation alloc] initWithAccount:self.currentAccount - selectedAuthentication:self.currentAccount.details.auths.firstObject + businessLogic:self.businessLogic inputProvider:self]; return self; } diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index 2ce7c576e..5afa59035 100644 --- a/Simplified/NYPLRemoteViewController.m +++ b/Simplified/NYPLRemoteViewController.m @@ -11,11 +11,9 @@ @interface NYPLRemoteViewController () @property (nonatomic) UIActivityIndicatorView *activityIndicatorView; @property (nonatomic) UILabel *activityIndicatorLabel; -@property (nonatomic) NSURLConnection *connection; -@property (nonatomic) NSMutableData *data; @property (nonatomic, copy) UIViewController *(^handler)(NYPLRemoteViewController *remoteViewController, NSData *data, NSURLResponse *response); @property (nonatomic) NYPLReloadView *reloadView; -@property (nonatomic, strong) NSURLResponse *response; +@property (nonatomic, strong) NSURLSessionDataTask *dataTask; @end @@ -51,7 +49,7 @@ - (void)load [childViewController didMoveToParentViewController:nil]; } - [self.connection cancel]; + [self.dataTask cancel]; NSTimeInterval timeoutInterval = 30.0; NSTimeInterval activityLabelTimer = 10.0; @@ -60,26 +58,93 @@ - (void)load cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeoutInterval] mutableCopy]; - if([[NYPLUserAccount sharedAccount] hasAuthToken]) - { - NSString *authValue = [NSString stringWithFormat:@"Bearer %@", [[NYPLUserAccount sharedAccount] authToken]]; - [request setValue:authValue forHTTPHeaderField:@"Authorization"]; - } self.activityIndicatorLabel.hidden = YES; [NSTimer scheduledTimerWithTimeInterval: activityLabelTimer target: self selector: @selector(addActivityIndicatorLabel:) userInfo: nil repeats: NO]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO: SIMPLY-2589 Replace with NSURLSession - self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; -#pragma clang diagnostic pop - self.data = [NSMutableData data]; - - [self.activityIndicatorView startAnimating]; - - [self.connection start]; + self.dataTask = [NYPLNetworkExecutor.shared resolve:request + completion:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.activityIndicatorView stopAnimating]; + self.activityIndicatorLabel.hidden = YES; + self.dataTask = nil; + + if ([response.MIMEType isEqualToString:@"application/vnd.opds.authentication.v1.0+json"]) { + self.reloadView.hidden = false; + [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:([NYPLUserAccount sharedAccount].barcode) completionHandler:^{ + [self load]; + }]; + return; + } + + if (error) { + self.reloadView.hidden = NO; + [NYPLErrorLogger logCatalogLoadError:error url:self.URL]; + return; + } + + BOOL mimeTypeMatches = [response.MIMEType isEqualToString:@"application/problem+json"] || + [response.MIMEType isEqualToString:@"application/api-problem+json"]; + + if ([(NSHTTPURLResponse *)response statusCode] != 200 && mimeTypeMatches) { + NSError *problemDocumentParseError = nil; + NYPLProblemDocument *pDoc = [NYPLProblemDocument fromData:data error:&problemDocumentParseError]; + UIAlertController *alert; + if (problemDocumentParseError) { + [NYPLErrorLogger logProblemDocumentParseError:problemDocumentParseError + url:[response URL] + context:@"RemoteVC-errorResponse"]; + alert = [NYPLAlertUtils + alertWithTitle:NSLocalizedString(@"Error", @"Title for a generic error") + message:NSLocalizedString(@"Unknown error parsing problem document", + @"Message for a problem document error")]; + } else { + alert = [NYPLAlertUtils alertWithTitle:pDoc.title message:pDoc.detail]; + } + [self presentViewController:alert animated:YES completion:nil]; + } + + UIViewController *const viewController = self.handler(self, data, response); + + if (viewController) { + [self addChildViewController:viewController]; + viewController.view.frame = self.view.bounds; + [self.view addSubview:viewController.view]; + + // If `viewController` has its own bar button items or title, use whatever + // has been set by default. + if(viewController.navigationItem.rightBarButtonItems) { + self.navigationItem.rightBarButtonItems = viewController.navigationItem.rightBarButtonItems; + } + if(viewController.navigationItem.leftBarButtonItems) { + self.navigationItem.leftBarButtonItems = viewController.navigationItem.leftBarButtonItems; + } + if(viewController.navigationItem.backBarButtonItem) { + self.navigationItem.backBarButtonItem = viewController.navigationItem.backBarButtonItem; + } + if(viewController.navigationItem.title) { + self.navigationItem.title = viewController.navigationItem.title; + } + + [viewController didMoveToParentViewController:self]; + } else { + self.reloadView.hidden = NO; + } + }); + }]; + +//#pragma clang diagnostic push +//#pragma clang diagnostic ignored "-Wdeprecated-declarations" +// // TODO: SIMPLY-2589 Replace with NSURLSession +// self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; +//#pragma clang diagnostic pop +// self.data = [NSMutableData data]; +// +// [self.activityIndicatorView startAnimating]; +// +// [self.connection start]; } #pragma mark UIViewController @@ -103,7 +168,7 @@ - (void)viewDidLoad [self.activityIndicatorLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.activityIndicatorView withOffset:8.0]; // We always nil out the connection when not in use so this is reliable. - if(self.connection) { + if(self.dataTask) { [self.activityIndicatorView startAnimating]; } @@ -139,101 +204,4 @@ - (void)addActivityIndicatorLabel:(NSTimer*)timer [timer invalidate]; } -#pragma mark NSURLConnectionDataDelegate - -- (void)connection:(__attribute__((unused)) NSURLConnection *)connection - didReceiveData:(NSData *const)data -{ - [self.data appendData:data]; -} - -- (void)connection:(__attribute__((unused)) NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response -{ - self.response = response; -} - -- (void)connectionDidFinishLoading:(__attribute__((unused)) NSURLConnection *)connection -{ - [self.activityIndicatorView stopAnimating]; - self.activityIndicatorLabel.hidden = YES; - - if ([self.response.MIMEType isEqualToString:@"application/vnd.opds.authentication.v1.0+json"]) { - self.reloadView.hidden = false; - [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:([NYPLUserAccount sharedAccount].barcode) completionHandler:^{ - [self load]; - }]; - return; - } - - BOOL mimeTypeMatches = [self.response.MIMEType isEqualToString:@"application/problem+json"] || - [self.response.MIMEType isEqualToString:@"application/api-problem+json"]; - - if ([(NSHTTPURLResponse *)self.response statusCode] != 200 && mimeTypeMatches) { - NSError *problemDocumentParseError = nil; - NYPLProblemDocument *pDoc = [NYPLProblemDocument fromData:self.data error:&problemDocumentParseError]; - UIAlertController *alert; - if (problemDocumentParseError) { - [NYPLErrorLogger logProblemDocumentParseError:problemDocumentParseError - url:[self.response URL] - context:@"RemoteVC-errorResponse"]; - alert = [NYPLAlertUtils - alertWithTitle:NSLocalizedString(@"Error", @"Title for a generic error") - message:NSLocalizedString(@"Unknown error parsing problem document", - @"Message for a problem document error")]; - } else { - alert = [NYPLAlertUtils alertWithTitle:pDoc.title message:pDoc.detail]; - } - [self presentViewController:alert animated:YES completion:nil]; - } - - UIViewController *const viewController = self.handler(self, self.data, self.response); - - if (viewController) { - [self addChildViewController:viewController]; - viewController.view.frame = self.view.bounds; - [self.view addSubview:viewController.view]; - - // If `viewController` has its own bar button items or title, use whatever - // has been set by default. - if(viewController.navigationItem.rightBarButtonItems) { - self.navigationItem.rightBarButtonItems = viewController.navigationItem.rightBarButtonItems; - } - if(viewController.navigationItem.leftBarButtonItems) { - self.navigationItem.leftBarButtonItems = viewController.navigationItem.leftBarButtonItems; - } - if(viewController.navigationItem.backBarButtonItem) { - self.navigationItem.backBarButtonItem = viewController.navigationItem.backBarButtonItem; - } - if(viewController.navigationItem.title) { - self.navigationItem.title = viewController.navigationItem.title; - } - - [viewController didMoveToParentViewController:self]; - } else { - self.reloadView.hidden = NO; - } - - self.response = nil; - self.connection = nil; - self.data = [NSMutableData data]; -} - -#pragma mark NSURLConnectionDelegate - -- (void)connection:(NSURLConnection *)connection - didFailWithError:(NSError *)error -{ - [self.activityIndicatorView stopAnimating]; - self.activityIndicatorLabel.hidden = YES; - - if (connection.currentRequest.URL) { - self.reloadView.hidden = NO; - [NYPLErrorLogger logCatalogLoadError:error url:self.URL]; - } - - self.connection = nil; - self.data = [NSMutableData data]; - self.response = nil; -} - @end diff --git a/Simplified/NYPLSettingsAccountDetailViewController.m b/Simplified/NYPLSettingsAccountDetailViewController.m index 6b67be54a..5a775417d 100644 --- a/Simplified/NYPLSettingsAccountDetailViewController.m +++ b/Simplified/NYPLSettingsAccountDetailViewController.m @@ -129,7 +129,7 @@ - (instancetype)initWithLibraryAccountID:(NSString *)libraryUUID self.frontEndValidator = [[NYPLUserAccountFrontEndValidation alloc] initWithAccount:self.selectedAccount - selectedAuthentication:self.selectedAccount.details.auths.firstObject + businessLogic:self.businessLogic inputProvider:self]; [[NSNotificationCenter defaultCenter] @@ -624,7 +624,7 @@ - (void)logOut } else { [[NYPLMyBooksDownloadCenter sharedDownloadCenter] reset:self.selectedAccountId]; [[NYPLBookRegistry sharedRegistry] reset:self.selectedAccountId]; - [[NYPLUserAccount sharedAccount:self.selectedAccountId] removeAll]; + [self.businessLogic.userAccount removeAll]; [self setupTableData]; [self.tableView reloadData]; [self removeActivityTitle]; @@ -748,7 +748,7 @@ - (void)processCredsValidationSuccessUsingDRMWithData:(NSData*)data } if (pDoc.authorizationIdentifier) { - [[NYPLUserAccount sharedAccount:self.selectedAccountId] setAuthorizationIdentifier:pDoc.authorizationIdentifier]; + [self.businessLogic.userAccount setAuthorizationIdentifier:pDoc.authorizationIdentifier]; } else { NYPLLOG(@"Authorization ID (Barcode String) was nil."); } diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index b7a199f3c..9ed2cc0a3 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -117,7 +117,9 @@ private enum StorageKey: String { // make sure to set the barcode related to the current account (aka library) // not the one we just signed in to, because we could have signed in into // library A, but still browsing the catalog of library B. - NYPLErrorLogger.setUserID(NYPLUserAccount.sharedAccount().barcode) + if case let .barcodeAndPin(barcode: userBarcode, pin: _) = newValue { + NYPLErrorLogger.setUserID(userBarcode) + } notifyAccountDidChange() } diff --git a/Simplified/NYPLWelcomeScreen.swift b/Simplified/NYPLWelcomeScreen.swift index f7a973d0c..983531ce4 100644 --- a/Simplified/NYPLWelcomeScreen.swift +++ b/Simplified/NYPLWelcomeScreen.swift @@ -185,19 +185,6 @@ import PureLayout let pickLibrary = { let listVC = NYPLWelcomeScreenAccountList { account in - -// let completion: ((Account) -> ()) = { account in -// if account.details?.defaultAuth?.isCatalogSecured == true && NYPLUserAccount.sharedAccount().hasCredentials() { -// let signInVC = NYPLAccountSignInViewController() -// signInVC.completionHandler = { -// self.completion?(account) -// } -// self.navigationController?.pushViewController(signInVC, animated: true) -// } else { -// self.completion?(account) -// } -// } - if account.details != nil { self.completion?(account) } else { diff --git a/Simplified/Network/NYPLNetworkExecutor.swift b/Simplified/Network/NYPLNetworkExecutor.swift index ff8f1d7c4..364dba034 100644 --- a/Simplified/Network/NYPLNetworkExecutor.swift +++ b/Simplified/Network/NYPLNetworkExecutor.swift @@ -110,8 +110,6 @@ extension NYPLNetworkExecutor { let authenticationValue = "Bearer \(authToken)" headers = ["Authorization" : "\(authenticationValue)", "Content-Type" : "application/json"] - } else { - Log.error(#file, "Attempted to create authorization header with neither an oauth token nor a barcode and pin pair.") } for (headerKey, headerValue) in headers { request.setValue(headerValue, forHTTPHeaderField: headerKey) @@ -141,6 +139,23 @@ extension NYPLNetworkExecutor { return task } + /// Performs a GET request using the specified URL + /// - Parameters: + /// - reqURL: URL of the resource to GET. + /// - completion: Always called when the resource is either fetched from + /// the network or from the cache. + @objc func resolve(_ request: URLRequest, + completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDataTask { + let req = NYPLNetworkExecutor.bearerAuthorized(request: request) + let completionWrapper: (_ result: NYPLResult) -> Void = { result in + switch result { + case let .success(data, response): completion(data, response, nil) + case let .failure(error, response): completion(nil, response, error) + } + } + return executeRequest(req, completion: completionWrapper) + } + /// Performs a GET request using the specified URL /// - Parameters: /// - reqURL: URL of the resource to GET. diff --git a/Simplified/Network/NYPLNetworkResponder.swift b/Simplified/Network/NYPLNetworkResponder.swift index a1f7493ce..3ccac5055 100644 --- a/Simplified/Network/NYPLNetworkResponder.swift +++ b/Simplified/Network/NYPLNetworkResponder.swift @@ -172,7 +172,7 @@ extension NYPLNetworkResponder: URLSessionTaskDelegate { username: barcode, password: pin) } else { - completionHandler(.cancelAuthenticationChallenge, nil) + completionHandler(.performDefaultHandling, nil) } case NSURLAuthenticationMethodServerTrust: @@ -196,7 +196,7 @@ extension NYPLNetworkResponder: URLSessionTaskDelegate { persistence: .none) completionHandler(.useCredential, credentials) } else { - completionHandler(.cancelAuthenticationChallenge, nil) + completionHandler(.performDefaultHandling, nil) } default: diff --git a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift index 06edeb1c4..47dab6ecf 100644 --- a/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift +++ b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift @@ -21,25 +21,25 @@ protocol NYPLUserAccountInputProvider { @objcMembers class NYPLUserAccountFrontEndValidation: NSObject { let userInputProvider: NYPLUserAccountInputProvider let account: Account - let selectedAuthentication: AccountDetails.Authentication + private weak var businessLogic: NYPLSignInBusinessLogic? - init(account: Account, selectedAuthentication: AccountDetails.Authentication, inputProvider: NYPLUserAccountInputProvider) { + init(account: Account, businessLogic: NYPLSignInBusinessLogic?, inputProvider: NYPLUserAccountInputProvider) { self.userInputProvider = inputProvider self.account = account - self.selectedAuthentication = selectedAuthentication + self.businessLogic = businessLogic } } extension NYPLUserAccountFrontEndValidation: UITextFieldDelegate { func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - return !NYPLUserAccount.sharedAccount().hasBarcodeAndPIN() + return !(businessLogic?.userAccount.hasBarcodeAndPIN() ?? false) } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard string.canBeConverted(to: .ascii) else { return false } if textField == userInputProvider.usernameTextField, - selectedAuthentication.patronIDKeyboard != .email { + businessLogic?.selectedAuthentication?.patronIDKeyboard != .email { // Barcodes are numeric and usernames are alphanumeric including punctuation let allowedCharacters = CharacterSet.alphanumerics.union(.punctuationCharacters) @@ -59,15 +59,16 @@ extension NYPLUserAccountFrontEndValidation: UITextFieldDelegate { let allowedCharacters = CharacterSet.decimalDigits let bannedCharacters = allowedCharacters.inverted - let alphanumericPin = selectedAuthentication.pinKeyboard != .numeric + let alphanumericPin = businessLogic?.selectedAuthentication?.pinKeyboard != .numeric let containsNonNumeric = !(string.rangeOfCharacter(from: bannedCharacters)?.isEmpty ?? true) let abovePinCharLimit: Bool + let passcodeLength = businessLogic?.selectedAuthentication?.authPasscodeLength ?? 0 if let text = textField.text, let textRange = Range(range, in: text) { let updatedText = text.replacingCharacters(in: textRange, with: string) - abovePinCharLimit = updatedText.count > selectedAuthentication.authPasscodeLength + abovePinCharLimit = updatedText.count > passcodeLength } else { abovePinCharLimit = false } @@ -76,7 +77,7 @@ extension NYPLUserAccountFrontEndValidation: UITextFieldDelegate { guard alphanumericPin || !containsNonNumeric else { return false } // PIN's character limit. Zero is unlimited. - if selectedAuthentication.authPasscodeLength == 0 { + if passcodeLength == 0 { return true } else if abovePinCharLimit { return false From 9b0f1a3ea78f97bf8b5d68dc04a4da715d93015d Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Mon, 1 Jun 2020 15:36:11 +0200 Subject: [PATCH 08/19] added support for patron info from oauth login --- Simplified/AccountsManager.swift | 3 +- Simplified/NYPLAccountSignInViewController.m | 104 ++++++------------ .../NYPLSettingsAccountDetailViewController.m | 57 +++------- 3 files changed, 51 insertions(+), 113 deletions(-) diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index b9ab82399..f8b7508e1 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -3,6 +3,7 @@ import Foundation let currentAccountIdentifierKey = "NYPLCurrentAccountIdentifier" private let betaUrl = URL(string: "https://libraryregistry.librarysimplified.org/libraries/qa")! +//private let prodUrl = URL(string: "https://libraryregistry.librarysimplified.org/libraries/qa")! private let prodUrl = URL(string: "https://libraryregistry.librarysimplified.org/libraries")! private let betaUrlHash = betaUrl.absoluteString.md5().base64EncodedStringUrlSafe().trimmingCharacters(in: ["="]) private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSafe().trimmingCharacters(in: ["="]) @@ -146,7 +147,7 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf """) } - // szyjson move age check after login (some auth methods may or may not require it) + // FIXME: move age check after login (some auth methods may or may not require it) DispatchQueue.main.async { var mainFeed = URL(string: self.currentAccount?.catalogUrl ?? "") let resolveFn = { diff --git a/Simplified/NYPLAccountSignInViewController.m b/Simplified/NYPLAccountSignInViewController.m index ce8da1246..326baa0ec 100644 --- a/Simplified/NYPLAccountSignInViewController.m +++ b/Simplified/NYPLAccountSignInViewController.m @@ -58,6 +58,7 @@ @interface NYPLAccountSignInViewController () Date: Tue, 2 Jun 2020 14:09:48 +0200 Subject: [PATCH 09/19] uncommited comment --- Simplified/Account.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simplified/Account.swift b/Simplified/Account.swift index 63e56b1a1..60720b9e2 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -143,7 +143,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" let signUpUrl:URL? let loansUrl:URL? - // szyjson is it needed? + // FIXME: Is it needed? var defaultAuth: Authentication? { guard auths.count > 1 else { return auths.first } return auths.first(where: { !$0.isCatalogSecured }) ?? auths.first From 069f33a6cb2582c3cc996fe804b4ca089c2aa8f9 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Wed, 3 Jun 2020 14:59:56 +0200 Subject: [PATCH 10/19] removed cleverloginviewcontroller --- Simplified.xcodeproj/project.pbxproj | 4 - Simplified/CleverLoginViewcontroller.swift | 272 --------------------- 2 files changed, 276 deletions(-) delete mode 100644 Simplified/CleverLoginViewcontroller.swift diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index 042f73702..7cb4c8f01 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -30,7 +30,6 @@ 085D31D71BE29E38007F7672 /* NYPLProblemReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31D61BE29E38007F7672 /* NYPLProblemReportViewController.m */; }; 085D31D91BE29ED4007F7672 /* NYPLProblemReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 085D31D81BE29ED4007F7672 /* NYPLProblemReportViewController.xib */; }; 085D31DF1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 085D31DE1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m */; }; - 087FD174246B5B54004843EF /* CleverLoginViewcontroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */; }; 08A352201BDE8E410040BF1D /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A3521F1BDE8E410040BF1D /* CFNetwork.framework */; }; 08A352221BDE8E560040BF1D /* libicucore.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352211BDE8E560040BF1D /* libicucore.tbd */; }; 08A352241BDE8E640040BF1D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A352231BDE8E640040BF1D /* Security.framework */; }; @@ -327,7 +326,6 @@ 085D31DD1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+NYPLURLRequestAdditions.h"; sourceTree = ""; }; 085D31DE1BE3CD3C007F7672 /* NSURLRequest+NYPLURLRequestAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+NYPLURLRequestAdditions.m"; sourceTree = ""; }; 085D31FB1BE7BE86007F7672 /* ReaderClientCert.sig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ReaderClientCert.sig; sourceTree = ""; }; - 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleverLoginViewcontroller.swift; sourceTree = ""; }; 08A3521F1BDE8E410040BF1D /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 08A352211BDE8E560040BF1D /* libicucore.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libicucore.tbd; path = usr/lib/libicucore.tbd; sourceTree = SDKROOT; }; 08A352231BDE8E640040BF1D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -1018,7 +1016,6 @@ 111E75791A80165C00718AD7 /* NYPLSettingsSplitViewController.m */, E6BA02B71DE4B6F600F76404 /* RemoteHTMLViewController.swift */, 731A5B1124621F2B00B5E663 /* NYPLSignInBusinessLogic.swift */, - 087FD173246B5B54004843EF /* CleverLoginViewcontroller.swift */, ); name = Settings; sourceTree = ""; @@ -1751,7 +1748,6 @@ 2D87909D20127AA300E2763F /* NYPLBookAcquisitionPath.m in Sources */, 2DCB71EE2017DFB5000E041A /* NYPLOPDSAcquisitionAvailability.m in Sources */, 2DB436381D4C049200F8E69D /* NYPLReachability.m in Sources */, - 087FD174246B5B54004843EF /* CleverLoginViewcontroller.swift in Sources */, 1195040B1994075B009FB788 /* NYPLReaderViewController.m in Sources */, 1183F33B194B775900DC322F /* NYPLCatalogLaneCell.m in Sources */, E6202A021DD4E6F300C99553 /* NYPLSettingsAccountDetailViewController.m in Sources */, diff --git a/Simplified/CleverLoginViewcontroller.swift b/Simplified/CleverLoginViewcontroller.swift deleted file mode 100644 index fdb148d2d..000000000 --- a/Simplified/CleverLoginViewcontroller.swift +++ /dev/null @@ -1,272 +0,0 @@ -//// -//// CleverLoginViewController.swift -//// Simplified -//// -//// Created by Aferdita Muriqi on 8/13/16. -//// Copyright © 2016 NYPL Labs. All rights reserved. -//// -// -//import UIKit -//import WebKit -// -//typealias CompletionHandler = () -> Void -// -//class CleverLoginViewController: UIViewController, WKNavigationDelegate { -// -// var libraryAccountID: String! -// private let permissionsCheckLock = NSLock() -// -// @objc var libraryAccount: Account? { -// guard let libraryAccountID = libraryAccountID else { return nil } -// return AccountsManager.shared.account(libraryAccountID) -// } -// -// @objc var userAccount: NYPLUserAccount { -// return NYPLUserAccount.sharedAccount(libraryUUID: libraryAccountID) -// } -// -// var cleverAuth:(authToken:String,patron:AnyObject,adobeToken:String)? -// var completionHandler:CompletionHandler! -// -// let instructionLabel = UILabel() -// -// let activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView() -// -// override func viewDidLoad() { -// super.viewDidLoad() -// -// self.view.backgroundColor = UIColor.white -// -// self.view.addSubview(self.instructionLabel) -// self.instructionLabel.textColor = UIColor.gray -// self.instructionLabel.text = -// NSLocalizedString("Log in with Safari to continue.", -// comment: "An instruction for the user to log into Clever via Safari.") -// self.instructionLabel.sizeToFit() -// -// let cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(CleverLoginViewController.didSelectCancel)) -// -// self.navigationItem.leftBarButtonItem = cancelBarButtonItem -// self.navigationController?.navigationBar.barStyle = .default -// self.navigationController?.navigationBar.isTranslucent = false -// -// let url = NYPLUserAccount.sharedAccount().adobeToken -// -// var urlComponents = URLComponents() -// urlComponents.scheme = url.scheme; -// urlComponents.host = url.host; -// urlComponents.path = url.path; -// -// // add params -// let provider = URLQueryItem(name: "provider", value: "Clever") -// let redirect_uri = URLQueryItem(name: "redirect_uri", value: "open-ebooks-clever://oauth") -// urlComponents.queryItems = [provider, redirect_uri] -// -// NotificationCenter.default.addObserver( -// forName: NSNotification.Name.NYPLAppDelegateDidReceiveCleverRedirectURL, -// object: nil, -// queue: OperationQueue.main, -// using: { notification in self.handleRedirectURL(url: notification.object as! URL) }) -// -// -// UIApplication.shared.openURL(urlComponents.url!) -// } -// -// deinit { -// NotificationCenter.default.removeObserver(self) -// } -// -// override func viewWillLayoutSubviews() { -// self.instructionLabel.centerInSuperview(); -// self.instructionLabel.integralizeFrame(); -// } -// -// override func viewWillAppear(_ animated: Bool) { -// super.viewWillAppear(animated) -// } -// -// func handleRedirectURL(url: URL) { -// -// self.navigationItem.leftBarButtonItem?.isEnabled = false -// -// LoadingIndicatorView.show( -// self.view, -// loadingText: NSLocalizedString("Logging in, please wait…", -// comment: "An instruction to wait while a Clever login is being finalized.")) -// -// if(!url.absoluteString.hasPrefix("open-ebooks-clever") -// || !(url.absoluteString.contains("error") || url.absoluteString.contains("access_token"))) -// { -// // The server did not give us what we expected (e.g. we received a 500), -// // thus we show an error message and stop handling the result. -// self.showErrorMessage(nil) -// return -// } -// -// let fragment = url.fragment -// var kvpairs:[String:String] = [String:String]() -// let components = fragment?.components(separatedBy: "&") -// for component in components! -// { -// var kv = component.components(separatedBy: "=") -// if kv.count == 2 -// { -// kvpairs[kv[0]] = kv[1] -// } -// } -// -// if let error = kvpairs["error"] -// { -// if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString -// { -// debugPrint(errorJson) -// -// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) -// -// } -// } -// -// if let auth_token = kvpairs["access_token"], -// let patron_info = kvpairs["patron_info"] -// { -// if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString -// { -// var request = URLRequest(url: NYPLConfiguration.circulationURL().appendingPathComponent("AdobeAuth/authdata")) -// request.httpMethod = "GET" -// request.setValue("Bearer \(auth_token)", forHTTPHeaderField: "Authorization") -// -// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in -// if let stringData = data -// { -// if let adobe_token:String = NSString(data: stringData, encoding: String.Encoding.utf8.rawValue) as String? -// { -// self.cleverAuth = (auth_token,patronJson,adobe_token) -// -// debugPrint(auth_token) -// debugPrint(adobe_token) -// debugPrint(patronJson) -// -// self.validateCredentials() -// } -// } -// }) -// dataTask.resume() -// } -// } -// } -// -// @objc func didSelectCancel() -// { -// self.dismiss(animated: true, completion: nil) -// } -// -// func showErrorMessage(_ message: String?) -// { -// let title = NSLocalizedString( -// "Clever Sign-In Failed", -// comment: "An alert title telling the user that signing into Clever failed.") -// -// let message = message != nil ? message : NSLocalizedString("UnknownRequestError", comment: "") -// -// let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) -// alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) -// -// self.dismiss(animated: true) { -// NYPLRootTabBarController.shared()?.safelyPresentViewController(alert, animated: true, completion: nil) -// } -// } -// -// -// -// func validateCredentials() -// { -// if let authToken = self.cleverAuth?.authToken, -// let adobeToken = self.cleverAuth?.adobeToken -// { -// var request = URLRequest(url: NYPLConfiguration.loanURL()) -// request.httpMethod = "HEAD" -// request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") -// -// let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, responseError) in -// -// #if FEATURE_DRM_CONNECTOR -// NYPLADEPT.sharedInstance().authorize(withVendorID: "OEI", username: adobeToken, password: "", completion: { (success, error) in -// self.authorizationAttemptDidFinish(success, error: error) -// }) -// #else -// self.authorizationAttemptDidFinish(true, error: nil) -// #endif -// -// }) -// dataTask.resume() -// } -// } -// -// func authorizationAttemptDidFinish(_ success:Bool, error:Error?) -// { -// -// OperationQueue.main.addOperation { -// -// if success -// { -// if let adobeToken = self.cleverAuth?.adobeToken, -// let patron = self.cleverAuth?.patron as? [AnyHashable: Any], -// let authToken = self.cleverAuth?.authToken -// { -// NYPLAccount.shared().setAdobeToken(adobeToken, patron: patron) -// NYPLAccount.shared().setAuthToken(authToken) -// NYPLAccount.shared().setProvider(NSLocalizedString("Clever", comment: "")) -// -// self.dismiss(animated: false, completion: nil) -// -// let handler = self.completionHandler -// self.completionHandler = nil; -// if(handler != nil) -// { -// handler!() -// } -// -// NYPLBookRegistry.shared().sync(completionHandler: nil) -// -// OperationQueue.main.addOperation({ -// NotificationCenter.default.post(name: Notification.Name(rawValue: NYPLAccountLoginDidChangeNotification), object: self) -// }) -// -// } -// } -// } -// } -// -// -// @objc class func loginWith(libraryAccountID: String, completionHandler: @escaping CompletionHandler) -// { -// -// let controller = CleverLoginViewController() -// controller.libraryAccountID = libraryAccountID -// -// controller.completionHandler = completionHandler -// -// let viewController = UINavigationController(rootViewController: controller) -// -// viewController.modalPresentationStyle = .formSheet; -// -// NYPLRootTabBarController.shared()?.safelyPresentViewController(viewController, animated: true, completion: nil) -// } -// -//} -// -//extension String { -// -// var parseJSONString: AnyObject? { -// -// let data = self.data(using: String.Encoding.utf8, allowLossyConversion: false) -// -// if let jsonData = data { -// // Will return an object or nil if JSON decoding fails -// return try! JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject? -// } else { -// // Lossless conversion of the string was not possible -// return nil -// } -// } -//} From d3d4ba63c917855271a63986e341f782277f3f9d Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Thu, 4 Jun 2020 15:59:45 +0200 Subject: [PATCH 11/19] removed unnecessary Codable code --- Simplified/Account.swift | 48 ---------------------------------------- 1 file changed, 48 deletions(-) diff --git a/Simplified/Account.swift b/Simplified/Account.swift index 60720b9e2..5d5f81e28 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -57,54 +57,6 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" return authType == .oauthIntermediary } - enum CodingKeys: String, CodingKey { - case authType - case authPasscodeLength - case patronIDKeyboard - case pinKeyboard - case patronIDLabel - case pinLabel - case supportsBarcodeScanner - case supportsBarcodeDisplay - case coppaUnderUrl - case coppaOverUrl - case oauthIntermediaryUrl - case methodDescription - } - - required init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - authType = try values.decode(AuthType.self, forKey: .authType) - authPasscodeLength = try values.decode(UInt.self, forKey: .authPasscodeLength) - patronIDKeyboard = try values.decode(LoginKeyboard.self, forKey: .patronIDKeyboard) - pinKeyboard = try values.decode(LoginKeyboard.self, forKey: .pinKeyboard) - patronIDLabel = try values.decodeIfPresent(String.self, forKey: .patronIDLabel) - pinLabel = try values.decodeIfPresent(String.self, forKey: .pinLabel) - supportsBarcodeScanner = try values.decode(Bool.self, forKey: .supportsBarcodeScanner) - supportsBarcodeDisplay = try values.decode(Bool.self, forKey: .supportsBarcodeDisplay) - coppaUnderUrl = try values.decodeIfPresent(URL.self, forKey: .coppaUnderUrl) - coppaOverUrl = try values.decodeIfPresent(URL.self, forKey: .coppaOverUrl) - oauthIntermediaryUrl = try values.decodeIfPresent(URL.self, forKey: .oauthIntermediaryUrl) - methodDescription = try values.decodeIfPresent(String.self, forKey: .methodDescription) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(authType, forKey: .authType) - try container.encode(authPasscodeLength, forKey: .authPasscodeLength) - try container.encode(patronIDKeyboard, forKey: .patronIDKeyboard) - try container.encode(pinKeyboard, forKey: .pinKeyboard) - try container.encode(patronIDLabel, forKey: .patronIDLabel) - try container.encode(pinLabel, forKey: .pinLabel) - try container.encode(supportsBarcodeScanner, forKey: .supportsBarcodeScanner) - try container.encode(supportsBarcodeDisplay, forKey: .supportsBarcodeDisplay) - try container.encode(coppaUnderUrl, forKey: .coppaUnderUrl) - try container.encode(coppaOverUrl, forKey: .coppaOverUrl) - try container.encode(oauthIntermediaryUrl, forKey: .oauthIntermediaryUrl) - try container.encode(methodDescription, forKey: .methodDescription) - } - func encode(with coder: NSCoder) { let jsonEncoder = JSONEncoder() guard let data = try? jsonEncoder.encode(self) else { return } From 5f49fe4b0cb68be81f0b41a2f2add77ac02825b5 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Thu, 4 Jun 2020 16:13:26 +0200 Subject: [PATCH 12/19] cleaned unnecessary swift function --- Simplified/NYPLSignInBusinessLogic.swift | 47 ------------------------ 1 file changed, 47 deletions(-) diff --git a/Simplified/NYPLSignInBusinessLogic.swift b/Simplified/NYPLSignInBusinessLogic.swift index dd9562254..633a169cd 100644 --- a/Simplified/NYPLSignInBusinessLogic.swift +++ b/Simplified/NYPLSignInBusinessLogic.swift @@ -140,53 +140,6 @@ class NYPLSignInBusinessLogic: NSObject { self.permissionsCheckLock.unlock() } } - - @objc func handleRedirectURL(url: URL) { - - if(!url.absoluteString.hasPrefix("open-ebooks-clever") - || !(url.absoluteString.contains("error") || url.absoluteString.contains("access_token"))) - { - // The server did not give us what we expected (e.g. we received a 500), - // thus we show an error message and stop handling the result. - -// self.showErrorMessage(nil) - return - } - - let fragment = url.fragment - var kvpairs:[String:String] = [String:String]() - let components = fragment?.components(separatedBy: "&") - for component in components! - { - var kv = component.components(separatedBy: "=") - if kv.count == 2 - { - kvpairs[kv[0]] = kv[1] - } - } - - if let error = kvpairs["error"] - { - if let errorJson = error.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString - { - debugPrint(errorJson) - -// self.showErrorMessage((errorJson as? [String : Any])?["title"] as? String) - - } - } - - if let auth_token = kvpairs["access_token"], - let patron_info = kvpairs["patron_info"] - { - if let patronJson = patron_info.replacingOccurrences(of: "+", with: " ").removingPercentEncoding?.parseJSONString - { - - } - } - } - - } extension NSString { From da7c2dabe1284d08f45135b876ab6bc323091d9b Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Thu, 4 Jun 2020 17:14:56 +0200 Subject: [PATCH 13/19] renaming keychain functions --- Simplified/KeychainStoredVariable.swift | 8 ++++---- Simplified/NYPLUserAccount.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Simplified/KeychainStoredVariable.swift b/Simplified/KeychainStoredVariable.swift index 728147dab..8ce46b63e 100644 --- a/Simplified/KeychainStoredVariable.swift +++ b/Simplified/KeychainStoredVariable.swift @@ -45,7 +45,7 @@ class KeychainVariable { } func safeWrite(_ newValue: VariableType?) { - transaction.write { + transaction.perform { write(newValue) } } @@ -89,7 +89,7 @@ class KeychainCodableVariable { } func safeWrite(_ newValue: VariableType?) { - transaction.write { + transaction.perform { write(newValue) } } @@ -102,7 +102,7 @@ class KeychainVariableTransaction { self.accountInfoLock = accountInfoLock } - func write(transaction: () -> Void) { + func perform(operations: () -> Void) { guard NYPLKeychain.shared() != nil else { return } accountInfoLock.lock() @@ -110,6 +110,6 @@ class KeychainVariableTransaction { accountInfoLock.unlock() } - transaction() + operations() } } diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index 9ed2cc0a3..caf2689ca 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -323,7 +323,7 @@ private enum StorageKey: String { @objc(setAdobeToken:patron:) func setAdobeToken(_ token: String, patron: [String : Any]) { - keychainTransaction.write { + keychainTransaction.perform { _adobeToken.write(token) _patron.write(patron) } @@ -384,7 +384,7 @@ private enum StorageKey: String { // MARK: - Remove func removeAll() { - keychainTransaction.write { + keychainTransaction.perform { _credentials.write(nil) _authorizationIdentifier.write(nil) _adobeToken.write(nil) From 369e357557088fa85d76da6cfd19dfb86bfb1e45 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Thu, 4 Jun 2020 17:17:52 +0200 Subject: [PATCH 14/19] changed thread used to perform catalog loading in accounts manager --- Simplified/AccountsManager.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index f8b7508e1..ddf0a2718 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -72,8 +72,11 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf object: nil ) - DispatchQueue.main.async { - self.loadCatalogs(completion: {_ in }) + // It needs to be done asynchronously, so that init returns prior to calling it + // Otherwise it would try to access itself before intialization is finished + // Network executor will try to access shared accounts manager, as it needs it to get headers data + OperationQueue.current?.underlyingQueue?.async { + self.loadCatalogs(completion: {_ in }) } } From 33a007450305848e8b7eedf8866c655344b90c7d Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Thu, 4 Jun 2020 17:57:57 +0200 Subject: [PATCH 15/19] split account removing logic --- Simplified/NYPLUserAccount.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index caf2689ca..9944ba4c5 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -383,18 +383,30 @@ private enum StorageKey: String { } // MARK: - Remove - func removeAll() { + func removeBarcodeAndPIN() { keychainTransaction.perform { _credentials.write(nil) _authorizationIdentifier.write(nil) + + // remove legacy, just in case + _barcode.write(nil) + _pin.write(nil) + _authToken.write(nil) + + notifyAccountDidChange() + } + } + + func removeAll() { + keychainTransaction.perform { _adobeToken.write(nil) _patron.write(nil) - _authToken.write(nil) _adobeVendor.write(nil) _provider.write(nil) _userID.write(nil) _deviceID.write(nil) + + removeBarcodeAndPIN() } - notifyAccountDidChange() } } From 6ceee81ea9c22a561fc6090e8127e10923e02096 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 5 Jun 2020 14:37:59 +0200 Subject: [PATCH 16/19] renamed network executor function name --- Simplified/NYPLRemoteViewController.m | 2 +- Simplified/Network/NYPLNetworkExecutor.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index 5afa59035..963699e45 100644 --- a/Simplified/NYPLRemoteViewController.m +++ b/Simplified/NYPLRemoteViewController.m @@ -63,7 +63,7 @@ - (void)load [NSTimer scheduledTimerWithTimeInterval: activityLabelTimer target: self selector: @selector(addActivityIndicatorLabel:) userInfo: nil repeats: NO]; - self.dataTask = [NYPLNetworkExecutor.shared resolve:request + self.dataTask = [NYPLNetworkExecutor.shared execute:request completion:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Simplified/Network/NYPLNetworkExecutor.swift b/Simplified/Network/NYPLNetworkExecutor.swift index 364dba034..7d7691dbd 100644 --- a/Simplified/Network/NYPLNetworkExecutor.swift +++ b/Simplified/Network/NYPLNetworkExecutor.swift @@ -144,7 +144,7 @@ extension NYPLNetworkExecutor { /// - reqURL: URL of the resource to GET. /// - completion: Always called when the resource is either fetched from /// the network or from the cache. - @objc func resolve(_ request: URLRequest, + @objc func execute(_ request: URLRequest, completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDataTask { let req = NYPLNetworkExecutor.bearerAuthorized(request: request) let completionWrapper: (_ result: NYPLResult) -> Void = { result in From 2f3ca8237cd5eb6ae8caf9685f9a4fd0941c9a17 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 5 Jun 2020 16:08:50 +0200 Subject: [PATCH 17/19] convenience refactor to keychain storage and documentation comments --- Simplified/KeychainStoredVariable.swift | 63 +++++++++++++------------ Simplified/NYPLUserAccount.swift | 35 ++++++++------ 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/Simplified/KeychainStoredVariable.swift b/Simplified/KeychainStoredVariable.swift index 8ce46b63e..c1c88f5e3 100644 --- a/Simplified/KeychainStoredVariable.swift +++ b/Simplified/KeychainStoredVariable.swift @@ -8,17 +8,28 @@ import Foundation -class KeychainVariable { - let transaction: KeychainVariableTransaction +protocol Keyable { + var key: String { get set } +} + +class KeychainVariable: Keyable { var key: String { didSet { guard key != oldValue else { return } + + // invalidate current cache if key changed alreadyInited = false } } - var alreadyInited = false - var cachedValue: VariableType? + 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 @@ -26,19 +37,33 @@ class KeychainVariable { } 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?) { + // 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) } } @@ -51,24 +76,8 @@ class KeychainVariable { } } -class KeychainCodableVariable { - let transaction: KeychainVariableTransaction - var key: String { - didSet { - guard key != oldValue else { return } - alreadyInited = false - } - } - - var alreadyInited = false - var cachedValue: VariableType? - - init(key: String, accountInfoLock: NSRecursiveLock) { - self.key = key - self.transaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) - } - - func read() -> VariableType? { +class KeychainCodableVariable: KeychainVariable { + 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) @@ -76,7 +85,7 @@ class KeychainCodableVariable { return cachedValue } - func write(_ newValue: VariableType?) { + override func write(_ newValue: VariableType?) { cachedValue = newValue alreadyInited = true DispatchQueue.global(qos: .userInitiated).async { [key] in @@ -87,16 +96,10 @@ class KeychainCodableVariable { } } } - - func safeWrite(_ newValue: VariableType?) { - transaction.perform { - write(newValue) - } - } } class KeychainVariableTransaction { - let accountInfoLock: NSRecursiveLock + fileprivate let accountInfoLock: NSRecursiveLock init(accountInfoLock: NSRecursiveLock) { self.accountInfoLock = accountInfoLock diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index 9944ba4c5..df13a9b8a 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -39,20 +39,27 @@ private enum StorageKey: String { private var libraryUUID: String? { didSet { guard libraryUUID != oldValue else { return } - _authorizationIdentifier.key = StorageKey.authorizationIdentifier.keyForLibrary(uuid: libraryUUID) - _adobeToken.key = StorageKey.adobeToken.keyForLibrary(uuid: libraryUUID) - _licensor.key = StorageKey.licensor.keyForLibrary(uuid: libraryUUID) - _patron.key = StorageKey.patron.keyForLibrary(uuid: libraryUUID) - _adobeVendor.key = StorageKey.adobeVendor.keyForLibrary(uuid: libraryUUID) - _provider.key = StorageKey.provider.keyForLibrary(uuid: libraryUUID) - _userID.key = StorageKey.userID.keyForLibrary(uuid: libraryUUID) - _deviceID.key = StorageKey.deviceID.keyForLibrary(uuid: libraryUUID) - _credentials.key = StorageKey.credentials.keyForLibrary(uuid: libraryUUID) - _authDefinition.key = StorageKey.authDefinition.keyForLibrary(uuid: libraryUUID) - - _barcode.key = StorageKey.barcode.keyForLibrary(uuid: libraryUUID) - _pin.key = StorageKey.PIN.keyForLibrary(uuid: libraryUUID) - _authToken.key = StorageKey.authToken.keyForLibrary(uuid: libraryUUID) + let variables: [StorageKey: Keyable] = [ + StorageKey.authorizationIdentifier: _authorizationIdentifier, + StorageKey.adobeToken: _adobeToken, + StorageKey.licensor: _licensor, + StorageKey.patron: _patron, + StorageKey.adobeVendor: _adobeVendor, + StorageKey.provider: _provider, + StorageKey.userID: _userID, + StorageKey.deviceID: _deviceID, + StorageKey.credentials: _credentials, + StorageKey.authDefinition: _authDefinition, + + // legacy + StorageKey.barcode: _barcode, + StorageKey.PIN: _pin, + StorageKey.authToken: _authToken, + ] + + for (key, var value) in variables { + value.key = key.keyForLibrary(uuid: libraryUUID) + } } } From 85e1f66965459f89c19db5a33e62240d9f0f6e15 Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Fri, 5 Jun 2020 17:06:26 +0200 Subject: [PATCH 18/19] accounts manager comment --- Simplified/AccountsManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Simplified/AccountsManager.swift b/Simplified/AccountsManager.swift index ddf0a2718..6e8f8e22b 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -75,6 +75,7 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf // It needs to be done asynchronously, so that init returns prior to calling it // Otherwise it would try to access itself before intialization is finished // Network executor will try to access shared accounts manager, as it needs it to get headers data + // Thik of this async block as you would about viewDidLoad which is triggered after a view is loaded OperationQueue.current?.underlyingQueue?.async { self.loadCatalogs(completion: {_ in }) } From 7426d233ce6430393cc11b62de6042ebaa73b03f Mon Sep 17 00:00:00 2001 From: Jacek Szyja Date: Mon, 8 Jun 2020 15:34:55 +0200 Subject: [PATCH 19/19] remove legacy keychain values --- Simplified/NYPLUserAccount.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index df13a9b8a..52b834282 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -109,8 +109,19 @@ private enum StorageKey: String { // try to load legacy values if let barcode = legacyBarcode, let pin = legacyPin { credentials = .barcodeAndPin(barcode: barcode, pin: pin) + + keychainTransaction.perform { + _credentials.write(credentials) + _barcode.write(nil) + _pin.write(nil) + } } else if let authToken = legacyAuthToken { credentials = .token(authToken: authToken) + + keychainTransaction.perform { + _credentials.write(credentials) + _authToken.write(nil) + } } }