diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index e49f6f94c..5d0e58ede 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -25,9 +25,14 @@ 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 */; }; + 086C45D624AE77CA00F5108E /* NYPLBasicAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C45D524AE77CA00F5108E /* NYPLBasicAuth.swift */; }; + 086C45D724AE77CA00F5108E /* NYPLBasicAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C45D524AE77CA00F5108E /* NYPLBasicAuth.swift */; }; + 086C45DC24AE77E600F5108E /* NYPLUserAccountFrontEndValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.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 */; }; @@ -35,6 +40,7 @@ 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, ); }; }; + 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 */; }; @@ -300,9 +306,7 @@ 7347F0B5245A4DE200558D7F /* NYPLBookAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DA7E9F1F2A718600CFBEC8 /* NYPLBookAuthor.swift */; }; 7347F0B6245A4DE200558D7F /* NYPLCatalogSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 118A0B1A19915BDF00792DDE /* NYPLCatalogSearchViewController.m */; }; 7347F0B7245A4DE200558D7F /* UIColor+LabelColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179699D024131BA500EC309F /* UIColor+LabelColor.swift */; }; - 7347F0B8245A4DE200558D7F /* NYPLUserAccountFrontEndValidation.m in Sources */ = {isa = PBXBuildFile; fileRef = 73E84FC524465083009071D8 /* NYPLUserAccountFrontEndValidation.m */; }; 7347F0B9245A4DE200558D7F /* NYPLNetworkResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735FED252427494900144C97 /* NYPLNetworkResponder.swift */; }; - 7347F0BA245A4DE200558D7F /* NYPLBasicAuth.m in Sources */ = {isa = PBXBuildFile; fileRef = A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */; }; 7347F0BB245A4DE200558D7F /* NYPLReaderSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 1188F3E01A1ECC4B006B2F36 /* NYPLReaderSettings.m */; }; 7347F0BC245A4DE200558D7F /* NYPLKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = 11C5DD15197727A6005A9945 /* NYPLKeychain.m */; }; 7347F0BD245A4DE200558D7F /* NYPLReaderContainerDelegateBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D7CF86322C19EBA007CAA34 /* NYPLReaderContainerDelegateBase.m */; }; @@ -422,7 +426,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 */; }; @@ -439,7 +442,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 */; }; @@ -567,16 +569,20 @@ 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 = ""; }; 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 = ""; }; + 086C45D524AE77CA00F5108E /* NYPLBasicAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLBasicAuth.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; }; 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 = ""; }; @@ -851,8 +857,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 = ""; }; @@ -878,8 +882,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; }; @@ -1400,10 +1402,8 @@ 73E84FBD24464FF3009071D8 /* Authentication */ = { isa = PBXGroup; children = ( - A4E254F81B0E610900193FE4 /* NYPLBasicAuth.h */, - A4E254F91B0E610900193FE4 /* NYPLBasicAuth.m */, - 73E84FC424465083009071D8 /* NYPLUserAccountFrontEndValidation.h */, - 73E84FC524465083009071D8 /* NYPLUserAccountFrontEndValidation.m */, + 08C481AD247CAEEB003A6723 /* NYPLUserAccountFrontEndValidation.swift */, + 086C45D524AE77CA00F5108E /* NYPLBasicAuth.swift */, ); path = Authentication; sourceTree = ""; @@ -1522,6 +1522,8 @@ 110AF8881961D652004887C3 /* My Books */, 7338755E2423E108000FEB67 /* Network */, 17CE5304243C020800315E63 /* NYPLUserAccount.swift */, + 0857A0F62478337D00C7984E /* NYPLSessionCredentials.swift */, + 0857A0FE247835FF00C7984E /* KeychainStoredVariable.swift */, 1158812C1A894F4E008672C3 /* NYPLAccountSignInViewController.h */, 1158812D1A894F4E008672C3 /* NYPLAccountSignInViewController.m */, E6BC315C1E009F3E0021B65E /* NYPLAgeCheck.swift */, @@ -2172,6 +2174,8 @@ 7347F087245A4DE200558D7F /* NYPLSession.m in Sources */, 7347F088245A4DE200558D7F /* NYPLSettingsEULAViewController.m in Sources */, 7347F089245A4DE200558D7F /* NYPLMyBooksDownloadInfo.m in Sources */, + 086C45DC24AE77E600F5108E /* NYPLUserAccountFrontEndValidation.swift in Sources */, + 086C45D724AE77CA00F5108E /* NYPLBasicAuth.swift in Sources */, 7347F08A245A4DE200558D7F /* NYPLBookLocation.m in Sources */, 7347F08B245A4DE200558D7F /* NYPLFacetView.m in Sources */, 7347F08C245A4DE200558D7F /* NYPLReaderContainerDelegate.mm in Sources */, @@ -2218,9 +2222,7 @@ 7347F0B5245A4DE200558D7F /* NYPLBookAuthor.swift in Sources */, 7347F0B6245A4DE200558D7F /* NYPLCatalogSearchViewController.m in Sources */, 7347F0B7245A4DE200558D7F /* UIColor+LabelColor.swift in Sources */, - 7347F0B8245A4DE200558D7F /* NYPLUserAccountFrontEndValidation.m in Sources */, 7347F0B9245A4DE200558D7F /* NYPLNetworkResponder.swift in Sources */, - 7347F0BA245A4DE200558D7F /* NYPLBasicAuth.m in Sources */, 7347F0BB245A4DE200558D7F /* NYPLReaderSettings.m in Sources */, 7347F0BC245A4DE200558D7F /* NYPLKeychain.m in Sources */, 7347F0BD245A4DE200558D7F /* NYPLReaderContainerDelegateBase.m in Sources */, @@ -2279,6 +2281,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 */, @@ -2327,6 +2330,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 */, @@ -2355,6 +2359,7 @@ 5D60D3542297353C001080D0 /* NYPLMigrationManager.swift in Sources */, 111E757A1A80165C00718AD7 /* NYPLSettingsSplitViewController.m in Sources */, 7384C802242BCC4800D5F960 /* Date+NYPLAdditions.swift in Sources */, + 086C45D624AE77CA00F5108E /* NYPLBasicAuth.swift in Sources */, B51C1E0422861C1A003B49A5 /* OPDS2AuthenticationDocument.swift in Sources */, 112C694D197301FE00C48F95 /* NYPLBookRegistryRecord.m in Sources */, 5DD5674522B303DF001F0C83 /* NYPLDeveloperSettingsTableViewController.swift in Sources */, @@ -2389,9 +2394,8 @@ 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 */, 11C5DD16197727A6005A9945 /* NYPLKeychain.m in Sources */, 5D7CF86422C19EBA007CAA34 /* NYPLReaderContainerDelegateBase.m in Sources */, diff --git a/Simplified/Account.swift b/Simplified/Account.swift index de8615070..ea433e76b 100644 --- a/Simplified/Account.swift +++ b/Simplified/Account.swift @@ -6,14 +6,18 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" // MARK: AccountDetails // Extra data that gets loaded from an OPDS2AuthenticationDocument, @objcMembers final class AccountDetails: NSObject { - enum AuthType: String { + enum AuthType: String, Codable { case basic = "http://opds-spec.org/auth/basic" case coppa = "http://librarysimplified.org/terms/authentication/gate/coppa" case anonymous = "http://librarysimplified.org/rel/auth/anonymous" + case oauthIntermediary = "http://librarysimplified.org/authtype/OAuth-with-intermediary" + case saml = "http://librarysimplified.org/authtype/SAML-2.0" case none } - struct Authentication { + @objc(AccountDetailsAuthentication) + @objcMembers + class Authentication: NSObject, Codable, NSCoding { let authType:AuthType let authPasscodeLength:UInt let patronIDKeyboard:LoginKeyboard @@ -34,23 +38,58 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" pinLabel = auth.labels?.password supportsBarcodeScanner = auth.inputs?.login.barcodeFormat == "Codabar" supportsBarcodeDisplay = supportsBarcodeScanner - coppaUnderUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-not-met" })?.href ?? "") - coppaOverUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-met" })?.href ?? "") + + switch authType { + case .coppa: + coppaUnderUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-not-met" })?.href ?? "") + coppaOverUrl = URL.init(string: auth.links?.first(where: { $0.rel == "http://librarysimplified.org/terms/rel/authentication/restriction-met" })?.href ?? "") + + case .none, .basic, .anonymous: + coppaUnderUrl = nil + coppaOverUrl = nil + + } + } + + var isCatalogSecured: Bool { + return authType == .oauthIntermediary || authType == .saml + } + + func encode(with coder: NSCoder) { + let jsonEncoder = JSONEncoder() + guard let data = try? jsonEncoder.encode(self) else { return } + coder.encode(data as NSData) + } + + required init?(coder: NSCoder) { + guard let data = coder.decodeData() else { return nil } + let jsonDecoder = JSONDecoder() + guard let authentication = try? jsonDecoder.decode(Authentication.self, from: data) else { return nil } + + authType = authentication.authType + authPasscodeLength = authentication.authPasscodeLength + patronIDKeyboard = authentication.patronIDKeyboard + pinKeyboard = authentication.pinKeyboard + patronIDLabel = authentication.patronIDLabel + pinLabel = authentication.pinLabel + supportsBarcodeScanner = authentication.supportsBarcodeScanner + supportsBarcodeDisplay = authentication.supportsBarcodeDisplay + coppaUnderUrl = authentication.coppaUnderUrl + coppaOverUrl = authentication.coppaOverUrl } } - + let defaults:UserDefaults let uuid:String let supportsSimplyESync:Bool let supportsCardCreator:Bool let supportsReservations:Bool let auths: [Authentication] - + let mainColor:String? let userProfileUrl:String? let signUpUrl:URL? let loansUrl:URL? - var authType: AuthType { return auths.first?.authType ?? .none } @@ -83,22 +122,15 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" return auths.first?.coppaOverUrl } - var patronIDLabel: String? { - return auths.first?.patronIDLabel - } - - var pinLabel: String? { - return auths.first?.pinLabel - } - var needsAuth:Bool { return authType == .basic } - var needsAgeCheck:Bool { - return authType == .coppa + var defaultAuth: Authentication? { + guard auths.count > 1 else { return auths.first } + return auths.first(where: { !$0.isCatalogSecured }) ?? auths.first } - + fileprivate var urlAnnotations:URL? fileprivate var urlAcknowledgements:URL? fileprivate var urlContentLicenses:URL? @@ -333,7 +365,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" NYPLNetworkExecutor.shared.GET(url) { result in switch result { - case .success(let serverData): + case .success(let serverData, _): do { self.authenticationDocument = try OPDS2AuthenticationDocument.fromData(serverData) @@ -345,7 +377,7 @@ private let accountSyncEnabledKey = "NYPLAccountSyncEnabledKey" """) completion(false) } - case .failure(let error): + case .failure(let error, _): Log.error(#file, """ Failed to load authentication document at URL \(url). Error: \(error) """) @@ -386,7 +418,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 833ed7aaf..26aaed6d5 100644 --- a/Simplified/AccountsManager.swift +++ b/Simplified/AccountsManager.swift @@ -152,10 +152,11 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf NotificationCenter.default.post(name: NSNotification.Name.NYPLCurrentAccountDidChange, object: nil) completion(true) } - if self.currentAccount?.details?.needsAgeCheck ?? false { + + if self.currentAccount?.details?.defaultAuth?.needsAgeCheck ?? false { AgeCheck.shared().verifyCurrentAccountAgeRequirement { meetsAgeRequirement in DispatchQueue.main.async { - mainFeed = meetsAgeRequirement ? self.currentAccount?.details?.coppaOverUrl : self.currentAccount?.details?.coppaUnderUrl + mainFeed = meetsAgeRequirement ? self.currentAccount?.details?.defaultAuth?.coppaOverUrl : self.currentAccount?.details?.defaultAuth?.coppaUnderUrl resolveFn() } } @@ -186,18 +187,16 @@ private let prodUrlHash = prodUrl.absoluteString.md5().base64EncodedStringUrlSaf .trimmingCharacters(in: ["="]) let wasAlreadyLoading = addLoadingCompletionHandler(key: hash, completion) - if wasAlreadyLoading { - return - } + guard !wasAlreadyLoading else { return } NYPLNetworkExecutor.shared.GET(targetUrl) { result in switch result { - case .success(let data): + case .success(let data, _): self.loadAccountSetsAndAuthDoc(fromCatalogData: data, key: hash) { success in self.callAndClearLoadingCompletionHandlers(key: hash, success) NotificationCenter.default.post(name: NSNotification.Name.NYPLCatalogDidLoad, object: nil) } - case .failure(let error): + case .failure(let error, _): NYPLErrorLogger.logError(error, message: "Catalog failed to load from \(targetUrl)") self.callAndClearLoadingCompletionHandlers(key: hash, false) diff --git a/Simplified/KeychainStoredVariable.swift b/Simplified/KeychainStoredVariable.swift new file mode 100644 index 000000000..d1b75b88a --- /dev/null +++ b/Simplified/KeychainStoredVariable.swift @@ -0,0 +1,116 @@ +// +// KeychainStoredVariable.swift +// SimplyE +// +// Created by Jacek Szyja on 22/05/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import Foundation + +protocol Keyable { + var key: String { get set } +} + +class KeychainVariable: Keyable { + var key: String { + didSet { + guard key != oldValue else { return } + + // invalidate current cache if key changed + alreadyInited = false + } + } + + fileprivate let transaction: KeychainVariableTransaction + + // marks whether or not was the `cachedValue` initialized + fileprivate var alreadyInited = false + + // stores the last variable that was written to the keychain, or read from it + // The stored value will also be invalidated once the key changes + fileprivate var cachedValue: VariableType? + + init(key: String, accountInfoLock: NSRecursiveLock) { + self.key = key + self.transaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) + } + + func read() -> VariableType? { + // If currently cached value is valid, return from cache + guard !alreadyInited else { return cachedValue } + + // Otherwise, obtain the latest value from keychain + cachedValue = NYPLKeychain.shared()?.object(forKey: key) as? VariableType + + // set a flag indicating that current cache is good to use + alreadyInited = true + + // return cached value + return cachedValue + } + + func write(_ newValue: VariableType?) { + transaction.perform { + // set new value to cache + cachedValue = newValue + + // set a flag indicating that current cache is good to use + alreadyInited = true + + // write new data to keychain in background + DispatchQueue.global(qos: .userInitiated).async { [key] in + if let newValue = newValue { + // if there is a new value, set it + NYPLKeychain.shared()?.setObject(newValue, forKey: key) + } else { + // otherwise remove old value from keychain + NYPLKeychain.shared()?.removeObject(forKey: key) + } + } + } + } +} + +class KeychainCodableVariable: 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) + alreadyInited = true + return cachedValue + } + + override func write(_ newValue: VariableType?) { + transaction.perform { + cachedValue = newValue + alreadyInited = true + DispatchQueue.global(qos: .userInitiated).async { [key] in + if let newValue = newValue, let data = try? JSONEncoder().encode(newValue) { + NYPLKeychain.shared()?.setObject(data, forKey: key) + } else { + NYPLKeychain.shared()?.removeObject(forKey: key) + } + } + } + } +} + +class KeychainVariableTransaction { + fileprivate let accountInfoLock: NSRecursiveLock + + init(accountInfoLock: NSRecursiveLock) { + self.accountInfoLock = accountInfoLock + } + + func perform(operations: () -> Void) { + guard NYPLKeychain.shared() != nil else { return } + + accountInfoLock.lock() + defer { + accountInfoLock.unlock() + } + + operations() + } +} diff --git a/Simplified/NYPLAccountSignInViewController.h b/Simplified/NYPLAccountSignInViewController.h index 90225eb3c..cca46e3ab 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 fd0af41e6..91b5e10fa 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,8 +17,8 @@ #import "NYPLOPDSFeed.h" #import "NYPLReachability.h" #import "NYPLRootTabBarController.h" +#import "NYPLSettingsAccountURLSessionChallengeHandler.h" #import "NYPLSettingsEULAViewController.h" -#import "NYPLUserAccountFrontEndValidation.h" #import "NYPLXML.h" #import "UIView+NYPLViewAdditions.h" #import "UIFont+NYPLSystemFontOverride.h" @@ -40,7 +39,7 @@ typedef NS_ENUM(NSInteger, Section) { SectionRegistration = 1 }; -@interface NYPLAccountSignInViewController () +@interface NYPLAccountSignInViewController () // state machine @property (nonatomic) BOOL isLoggingInAfterSignUp; @@ -60,7 +59,7 @@ @interface NYPLAccountSignInViewController () ())?) -> 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/NYPLAnnotations.swift b/Simplified/NYPLAnnotations.swift index 94b429e03..ff437091e 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 { 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/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/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/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 aa9153023..190d50f12 100644 --- a/Simplified/NYPLBookRegistry.m +++ b/Simplified/NYPLBookRegistry.m @@ -280,8 +280,8 @@ - (void)syncWithCompletionHandler:(void (^)(BOOL success))completion if(fetchHandler) fetchHandler(UIBackgroundFetchResultNoData); }]; return; - } else if (![[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { - NYPLLOG(@"[syncWithCompletionHandler] No barcode and PIN"); + } 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); diff --git a/Simplified/NYPLCatalogNavigationController.m b/Simplified/NYPLCatalogNavigationController.m index 7bf032ecc..ad8dfa2e3 100644 --- a/Simplified/NYPLCatalogNavigationController.m +++ b/Simplified/NYPLCatalogNavigationController.m @@ -198,10 +198,10 @@ - (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.coppaOverUrl : account.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }); }]; @@ -233,10 +233,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.coppaOverUrl : account.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }); }]; @@ -286,9 +286,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.coppaOverUrl : currentAccount.details.coppaUnderUrl; + mainFeedUrl = isOfAge ? NYPLUserAccount.sharedAccount.authDefinition.coppaOverUrl : NYPLUserAccount.sharedAccount.authDefinition.coppaUnderUrl; completion(); }]; } else { @@ -302,8 +302,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/NYPLHoldsViewController.m b/Simplified/NYPLHoldsViewController.m index 975d849df..ba18b057a 100644 --- a/Simplified/NYPLHoldsViewController.m +++ b/Simplified/NYPLHoldsViewController.m @@ -254,9 +254,8 @@ - (void)updateBadge - (void)didSelectSync { - Account *const account = [AccountsManager shared].currentAccount; - if (account.details.needsAuth) { - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + if ([NYPLUserAccount sharedAccount].needsAuth) { + 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..72b416acf 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]; @@ -322,7 +321,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 @@ -510,7 +509,7 @@ - (void)returnBookWithIdentifier:(NSString *)identifier NYPLLOG(@"Failed to create book from entry. Book not removed from registry."); } } else { - if([error[@"type"] isEqualToString:NYPLProblemDocument.TypeNoActiveLoan]) { + if ([error[@"type"] isEqualToString:NYPLProblemDocument.TypeNoActiveLoan]) { if(downloaded) { [self deleteLocalContentForBookIdentifier:identifier]; } @@ -689,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 @@ -719,7 +718,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..3e2f58d65 100644 --- a/Simplified/NYPLMyBooksViewController.m +++ b/Simplified/NYPLMyBooksViewController.m @@ -370,9 +370,8 @@ - (void)facetView:(NYPLFacetView *const)facetView - (void)didSelectSync { - Account *const account = [AccountsManager shared].currentAccount; - if (account.details.needsAuth) { - if([[NYPLUserAccount sharedAccount] hasBarcodeAndPIN]) { + if ([NYPLUserAccount sharedAccount].needsAuth) { + if([[NYPLUserAccount sharedAccount] hasCredentials]) { [[NYPLBookRegistry sharedRegistry] syncWithStandardAlertsOnCompletion]; } else { [NYPLAccountSignInViewController requestCredentialsUsingExistingBarcode:NO completionHandler:nil]; diff --git a/Simplified/NYPLOPDSFeed.m b/Simplified/NYPLOPDSFeed.m index affe9f607..9ba9a47d8 100644 --- a/Simplified/NYPLOPDSFeed.m +++ b/Simplified/NYPLOPDSFeed.m @@ -63,7 +63,9 @@ + (void)withURL:(NSURL *)URL completionHandler:(void (^)(NYPLOPDSFeed *feed, NSD } __block NSURLRequest *request = nil; - request = [[NYPLSession sharedSession] withURL:URL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + request = [[[NYPLNetworkExecutor shared] GET:URL + cachePolicy:NSURLRequestReloadIgnoringCacheData + completion:^(NSData *data, NSURLResponse *response, NSError *error) { if (error != nil) { // NYPLSession already logged this. @@ -126,7 +128,7 @@ + (void)withURL:(NSURL *)URL completionHandler:(void (^)(NYPLOPDSFeed *feed, NSD } NYPLAsyncDispatch(^{handler(feed, nil);}); - }]; + }] originalRequest]; } - (instancetype)initWithXML:(NYPLXML *const)feedXML diff --git a/Simplified/NYPLRemoteViewController.m b/Simplified/NYPLRemoteViewController.m index 79334312f..7681356ef 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,29 +49,103 @@ - (void)load [childViewController didMoveToParentViewController:nil]; } - [self.connection cancel]; + [self.dataTask cancel]; 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]; + + 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]; + request.cachePolicy = NSURLRequestReloadIgnoringCacheData; + self.dataTask = [NYPLNetworkExecutor.shared execute: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 @@ -97,7 +169,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]; } @@ -133,92 +205,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; - 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/NYPLSession.m b/Simplified/NYPLSession.m index 4b2e99fb5..b24626919 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,32 +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] originalRequest]; else - [req setHTTPMethod:@"GET"]; - - [[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 cachePolicy:NSURLRequestUseProtocolCachePolicy completion:completionWrapper] originalRequest]; return req; } diff --git a/Simplified/NYPLSessionCredentials.swift b/Simplified/NYPLSessionCredentials.swift new file mode 100644 index 000000000..95d01d480 --- /dev/null +++ b/Simplified/NYPLSessionCredentials.swift @@ -0,0 +1,128 @@ +// +// NYPLSessionCredentials.swift +// SimplyE +// +// Created by Jacek Szyja on 22/05/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import Foundation +import WebKit + +enum Credentials { +// case token(authToken: String, patron: [String:Any]) + case token(authToken: String) + case barcodeAndPin(barcode: String, pin: String) + case cookies([HTTPCookie]) + case open +} + +extension Credentials: Codable { + // warning, order is important for proper decoding! + enum TypeID: Int, Codable { + case token + case barcodeAndPin + case cookies + case open + } + + private var typeID: TypeID { + switch self { + case .token: return .token + case .barcodeAndPin: return .barcodeAndPin + case .cookies: return .cookies + case .open: return .open + } + } + + enum CodingKeys: String, CodingKey { + case type + case associatedTokenData + case associatedBarcodeAndPinData + case associatedCookiesData + } + + enum TokenKeys: String, CodingKey { + case authToken + } + + enum BarcodeAndPinKeys: String, CodingKey { + case barcode + case pin + } + + enum CookiesKeys: String, CodingKey { + case cookiesData + } + + 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 .cookies: + let additionalInfo = try values.nestedContainer(keyedBy: CookiesKeys.self, forKey: .associatedCookiesData) + let cookiesData = try additionalInfo.decode(Data.self, forKey: .cookiesData) + guard let properties = try JSONSerialization.jsonObject(with: cookiesData, options: .allowFragments) as? [[HTTPCookiePropertyKey : Any]] else { + throw NSError() + } + let cookies = properties.compactMap { HTTPCookie(properties: $0) } + self = .cookies(cookies) + + 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) + + case let .cookies(cookies): + var additionalInfo = container.nestedContainer(keyedBy: CookiesKeys.self, forKey: .associatedCookiesData) + let properties: [[HTTPCookiePropertyKey : Any]] = cookies.compactMap { $0.properties } + let data = try JSONSerialization.data(withJSONObject: properties, options: []) + try additionalInfo.encode(data, forKey: .cookiesData) + + case .open: break + } + } +} + +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 a7fd0447a..695ed090b 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" @@ -18,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" @@ -64,6 +62,9 @@ @interface NYPLSettingsAccountDetailViewController () 1 else { return auths.first } + + return nil + } + set { + _selectedAuthentication = newValue + } + } + + var userAccount: NYPLUserAccount { return NYPLUserAccount.sharedAccount(libraryUUID: libraryAccountID) } - @objc func librarySupportsBarcodeDisplay() -> Bool { + func librarySupportsBarcodeDisplay() -> Bool { // For now, only supports libraries granted access in Accounts.json, // 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 { return userAccount.hasBarcodeAndPIN() } - @objc func registrationIsPossible() -> Bool { + func registrationIsPossible() -> Bool { return !isSignedIn() && NYPLConfiguration.cardCreationEnabled() && libraryAccount?.details?.signUpUrl != nil } @@ -77,7 +93,7 @@ class NYPLSignInBusinessLogic: NSObject { return libraryDetails.supportsSimplyESync && libraryDetails.getLicenseURL(.annotations) != nil && - userAccount.hasBarcodeAndPIN() && + userAccount.hasCredentials() && libraryAccountID == AccountsManager.shared.currentAccount?.uuid } diff --git a/Simplified/NYPLUserAccount.swift b/Simplified/NYPLUserAccount.swift index d15e86280..fa84acd7a 100644 --- a/Simplified/NYPLUserAccount.swift +++ b/Simplified/NYPLUserAccount.swift @@ -10,21 +10,141 @@ 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" + case authDefinition = "NYPLAccountAuthDefinitionKey" + case cookies = "NYPLAccountAuthCookiesKey" + + 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 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 lazy var keychainTransaction = KeychainVariableTransaction(accountInfoLock: accountInfoLock) + + private var libraryUUID: String? { + didSet { + guard libraryUUID != oldValue else { return } + 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, + StorageKey.cookies: _cookies, + + // legacy + StorageKey.barcode: _barcode, + StorageKey.PIN: _pin, + StorageKey.authToken: _authToken, + ] + + for (key, var value) in variables { + value.key = key.keyForLibrary(uuid: libraryUUID) + } +// removeAll() // to clean keychain data for a library <- REMOVE ME + } + } + + 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.write(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 { + 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) + + 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) + } + } + } + + return credentials + } + set { + guard let newValue = newValue else { return } + + _credentials.write(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. + if case let .barcodeAndPin(barcode: userBarcode, pin: _) = newValue { + NYPLErrorLogger.setUserID(userBarcode) + } + + notifyAccountDidChange() + } + } @objc class func sharedAccount() -> NYPLUserAccount { @@ -36,46 +156,86 @@ extension Notification.Name { { shared.accountInfoLock.lock() defer { - shared.accountInfoLock.unlock() + shared.accountInfoLock.unlock() } - if let uuid = libraryUUID, uuid != AccountsManager.NYPLAccountUUID - { - 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)" + if let uuid = libraryUUID, uuid != AccountsManager.NYPLAccountUUID { + 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: KeychainCodableVariable = StorageKey.credentials + .keyForLibrary(uuid: libraryUUID) + .asKeychainCodableVariable(with: accountInfoLock) + private lazy var _authDefinition: KeychainCodableVariable = StorageKey.authDefinition + .keyForLibrary(uuid: libraryUUID) + .asKeychainCodableVariable(with: accountInfoLock) + private lazy var _cookies: KeychainVariable<[HTTPCookie]> = StorageKey.cookies + .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 { @@ -89,7 +249,18 @@ extension Notification.Name { func hasCredentials() -> Bool { return hasAuthToken() || hasBarcodeAndPIN() } - + + // Oauth requires login to load catalog + var isCatalogSecured: Bool { + return authDefinition?.isCatalogSecured ?? false + } + + // MARK: - Legacy + + private var legacyBarcode: String? { return _barcode.read() } + private var legacyPin: String? { return _pin.read() } + var legacyAuthToken: String? { _authToken.read() } + // MARK: - GET /// The barcode of this user; for NYPL, this is either an actual barcode @@ -102,7 +273,11 @@ extension Notification.Name { /// features of platform.nypl.org will work if you give them a 14-digit /// barcode but not a 7-letter username or a 16-digit NYC ID. var barcode: String? { - return NYPLKeychain.shared()?.object(forKey: barcodeKey) as? String + if let credentials = credentials, case let Credentials.barcodeAndPin(barcode: barcode, pin: _) = credentials { + return barcode + } else { + return nil + } } /// For any library but the NYPL, this identifier can be anything they want. @@ -118,35 +293,44 @@ extension Notification.Name { /// Authenticating with any of those barcodes should work. /// - Note: This is NOT the unique ILS ID. That's internal-only and it's not /// exposed to the public. - var authorizationIdentifier: String? { - return NYPLKeychain.shared()?.object(forKey: authorizationIdentifierKey) as? String - } + var authorizationIdentifier: String? { _authorizationIdentifier.read() } var PIN: String? { - return NYPLKeychain.shared()?.object(forKey: PINKey) as? String - } - - var deviceID: String? { - return NYPLKeychain.shared()?.object(forKey: deviceIDKey) as? String + if let credentials = credentials, case let Credentials.barcodeAndPin(barcode: _, pin: pin) = credentials { + return pin + } else { + return nil + } } - /// The user ID to use with Adobe DRM. - var userID: String? { - return NYPLKeychain.shared()?.object(forKey: userIDKey) as? String + var needsAuth:Bool { + let authType = authDefinition?.authType ?? .none + return authType == .basic || authType == .oauthIntermediary || authType == .saml } - - var adobeVendor: String? { - return NYPLKeychain.shared()?.object(forKey: adobeVendorKey) as? String - } - - var provider: String? { - return NYPLKeychain.shared()?.object(forKey: providerKey) as? String + + var needsAgeCheck:Bool { + let authType = authDefinition?.authType ?? .none + return authType == .coppa } - - var patron: [String:Any]? { - return NYPLKeychain.shared()?.object(forKey: patronKey) as? [String:Any] + + var deviceID: String? { _deviceID.read() } + /// The user ID to use with Adobe DRM. + 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 cookies: [HTTPCookie]? { _cookies.read() } + + 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] @@ -176,101 +360,40 @@ extension Notification.Name { return nil } - - 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 - } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() + keychainTransaction.perform { + _adobeToken.write(token) + _patron.write(patron) } - - 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.write(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.write(token) + notifyAccountDidChange() } @objc(setLicensor:) func setLicensor(_ licensor: [String : Any]) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(licensor, forKey: licensorKey) + _licensor.write(licensor) } /// This authorization identifier is returned by the circulation manager @@ -281,124 +404,72 @@ extension Notification.Name { /// this can be any string they want. @objc(setAuthorizationIdentifier:) func setAuthorizationIdentifier(_ identifier: String) { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - sharedKeychain.setObject(identifier, forKey: authorizationIdentifierKey) + _authorizationIdentifier.write(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.write(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(setCookies:) + func setCookies(_ cookies: [HTTPCookie]) { + _cookies.write(cookies) + notifyAccountDidChange() + } + @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.write(provider) + notifyAccountDidChange() } /// - parameter id: The user ID to use for Adobe DRM. @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.write(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.write(id) + notifyAccountDidChange() } // MARK: - Remove - func removeBarcodeAndPIN() { - guard let sharedKeychain = NYPLKeychain.shared() else { - return - } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() + keychainTransaction.perform { + _authDefinition.write(nil) + _credentials.write(nil) + _cookies.write(nil) + _authorizationIdentifier.write(nil) + + // remove legacy, just in case + _barcode.write(nil) + _pin.write(nil) + _authToken.write(nil) + + notifyAccountDidChange() } - - 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 - } - - accountInfoLock.lock() - defer { - accountInfoLock.unlock() + keychainTransaction.perform { + _adobeToken.write(nil) + _patron.write(nil) + _adobeVendor.write(nil) + _provider.write(nil) + _userID.write(nil) + _deviceID.write(nil) + + removeBarcodeAndPIN() } - - 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() } } diff --git a/Simplified/Network/NYPLNetworkExecutor.swift b/Simplified/Network/NYPLNetworkExecutor.swift index c1d93216f..e41950066 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,129 @@ 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"] + } + 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 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 + 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 GET(_ reqURL: URL, + cachePolicy: NSURLRequest.CachePolicy = .useProtocolCachePolicy, + completion: @escaping (_ result: Data?, _ response: URLResponse?, _ error: Error?) -> Void) -> URLSessionDataTask { + var req = request(for: reqURL) + req.cachePolicy = cachePolicy + 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..7305bb623 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,23 @@ 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); + + NYPLBasicAuth.authHandler(challenge: challenge, completionHandler: completionHandler) + } +} diff --git a/Simplified/RemoteHTMLViewController.swift b/Simplified/RemoteHTMLViewController.swift index c56292766..515b32fe1 100644 --- a/Simplified/RemoteHTMLViewController.swift +++ b/Simplified/RemoteHTMLViewController.swift @@ -73,21 +73,20 @@ import WebKit } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if navigationAction.navigationType == .linkActivated, - let url = navigationAction.request.url { - if !UIApplication.shared.canOpenURL(url) { - decisionHandler(.cancel) + + guard navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + + if UIApplication.shared.canOpenURL(url) { + if #available(iOS 10.0, *) { + UIApplication.shared.open(url) } else { - if #available(iOS 10.0, *) { - UIApplication.shared.open(url) - } else { - UIApplication.shared.openURL(url) - } - decisionHandler(.cancel) + UIApplication.shared.openURL(url) } - } else { - decisionHandler(.allow) } + decisionHandler(.cancel) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 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 db105a3df..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 00f50be2a..000000000 --- a/Simplified/Utilities/Authentication/NYPLBasicAuth.m +++ /dev/null @@ -1,59 +0,0 @@ -#import "SimplyE-Swift.h" - -#import "NYPLBasicAuth.h" - -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); - } -} diff --git a/Simplified/Utilities/Authentication/NYPLBasicAuth.swift b/Simplified/Utilities/Authentication/NYPLBasicAuth.swift new file mode 100644 index 000000000..56188dcf4 --- /dev/null +++ b/Simplified/Utilities/Authentication/NYPLBasicAuth.swift @@ -0,0 +1,58 @@ +// +// NYPLBasicAuth.swift +// Simplified +// +// Created by Jacek Szyja on 02/07/2020. +// Copyright © 2020 NYPL Labs. All rights reserved. +// + +import Foundation + +@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(.performDefaultHandling, 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(.performDefaultHandling, nil) + } + + default: + completionHandler(.rejectProtectionSpace, nil) + } + } + +} 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..47dab6ecf --- /dev/null +++ b/Simplified/Utilities/Authentication/NYPLUserAccountFrontEndValidation.swift @@ -0,0 +1,89 @@ +// +// 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 + private weak var businessLogic: NYPLSignInBusinessLogic? + + init(account: Account, businessLogic: NYPLSignInBusinessLogic?, inputProvider: NYPLUserAccountInputProvider) { + self.userInputProvider = inputProvider + self.account = account + self.businessLogic = businessLogic + } +} + +extension NYPLUserAccountFrontEndValidation: UITextFieldDelegate { + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + 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, + businessLogic?.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 = 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 > passcodeLength + } else { + abovePinCharLimit = false + } + + // PIN's support numeric or alphanumeric. + guard alphanumericPin || !containsNonNumeric else { return false } + + // PIN's character limit. Zero is unlimited. + if passcodeLength == 0 { + return true + } else if abovePinCharLimit { + return false + } + } + + return true + } +}