diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 86e91dc461..dfc65d37f4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -755,6 +755,27 @@ 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; }; 9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; }; 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; }; + 9F254AAB2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */; }; + 9F254AAD2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */; }; + 9F254AB12CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */; }; + 9F254AB32CF4A1F10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */; }; + 9F254ACB2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */; }; + 9F254ACE2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */; }; + 9F254AD22CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */; }; + 9F254AD32CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */; }; + 9F254AD52CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */; }; + 9F254AD62CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */; }; + 9F254AD82CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */; }; + 9F254AD92CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */; }; + 9F254ADB2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */; }; + 9F254ADC2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */; }; + 9F254ADE2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */; }; + 9F254ADF2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */; }; + 9F254AFF2CF9FA1B0063B308 /* WebViewNavigationHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */; }; + 9F254B012CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */; }; + 9F254B032CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */; }; + 9F254B052CF9FB890063B308 /* SpecialErrorPageContextHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */; }; + 9F254B082CF9FC270063B308 /* SpecialErrorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */; }; 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */; }; 9F4CC5152C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */; }; 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */; }; @@ -989,6 +1010,7 @@ CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; + CBC88EE32C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */; }; CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */; }; @@ -2597,6 +2619,22 @@ 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = ""; }; 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = ""; }; 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = ""; }; + 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionManager.swift; sourceTree = ""; }; + 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandler.swift; sourceTree = ""; }; + 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SpecialErrorPageNavigationHandler+SSL.swift"; sourceTree = ""; }; + 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SpecialErrorPageNavigationHandler+MaliciousSite.swift"; sourceTree = ""; }; + 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandlerTests.swift; sourceTree = ""; }; + 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationHandlerIntegrationTests.swift; sourceTree = ""; }; + 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSpecialErrorWebView.swift; sourceTree = ""; }; + 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyMaliciousSiteProtectionNavigationHandler.swift; sourceTree = ""; }; + 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSSLErrorPageNavigationHandler.swift; sourceTree = ""; }; + 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSpecialErrorPageNavigationDelegate.swift; sourceTree = ""; }; + 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyWKNavigation.swift; sourceTree = ""; }; + 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewNavigationHandling.swift; sourceTree = ""; }; + 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageActionHandler.swift; sourceTree = ""; }; + 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageNavigationDelegate.swift; sourceTree = ""; }; + 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageContextHandling.swift; sourceTree = ""; }; + 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorModel.swift; sourceTree = ""; }; 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddToDockContent.swift"; sourceTree = ""; }; 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterMock.swift; sourceTree = ""; }; 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerDaxDialogTests.swift; sourceTree = ""; }; @@ -2837,6 +2875,7 @@ CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; + CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageNavigationHandlerTests.swift; sourceTree = ""; }; CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; @@ -4383,6 +4422,7 @@ 98B001B1251EABB40090EC07 /* InfoPlist.strings */, 85DFEDEB24C7CC7600973FE7 /* iPad */, F1C5ECFA1E37B15B00C599A4 /* Main */, + 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */, EECD94B22A28B8580085C66E /* NetworkProtection */, F13B4BF31F18C73A00814661 /* NewTabPage */, 85AE668C20971FCA0014CF04 /* Notifications */, @@ -4394,6 +4434,7 @@ C1B7B51D28941F160098FD6A /* RemoteMessaging */, F1AB2B401E3F75A000868554 /* Settings */, 0A6CC0EE23904D5400E4F627 /* Settings.bundle */, + 9F254AA62CF4777F0063B308 /* SpecialErrorPage */, D664C7922B289AA000CBFA76 /* Subscription */, 85F98F8C296F0ED100742F4A /* Sync */, F13B4BF41F18C74500814661 /* Tabs */, @@ -4968,6 +5009,69 @@ name = Onboarding; sourceTree = ""; }; + 9F254AA62CF4777F0063B308 /* SpecialErrorPage */ = { + isa = PBXGroup; + children = ( + 9F254B062CF9FC130063B308 /* Model */, + 9F254AFD2CF9F9EC0063B308 /* SpecialErrorPageInterfaces */, + 9F254AAC2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift */, + 9F254AB02CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift */, + 9F254AB22CF4A1E10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift */, + ); + path = SpecialErrorPage; + sourceTree = ""; + }; + 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */ = { + isa = PBXGroup; + children = ( + 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */, + ); + path = MaliciousSiteProtection; + sourceTree = ""; + }; + 9F254AC92CF5CA860063B308 /* SpecialErrorPage */ = { + isa = PBXGroup; + children = ( + 9F254AD02CF5D38E0063B308 /* TestDoubles */, + CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, + CBC88EE22C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift */, + 9F254ACA2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift */, + 9F254ACD2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift */, + ); + path = SpecialErrorPage; + sourceTree = ""; + }; + 9F254AD02CF5D38E0063B308 /* TestDoubles */ = { + isa = PBXGroup; + children = ( + 9F254AD12CF5D3A20063B308 /* MockSpecialErrorWebView.swift */, + 9F254AD42CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift */, + 9F254AD72CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift */, + 9F254ADA2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift */, + 9F254ADD2CF636CF0063B308 /* DummyWKNavigation.swift */, + ); + path = TestDoubles; + sourceTree = ""; + }; + 9F254AFD2CF9F9EC0063B308 /* SpecialErrorPageInterfaces */ = { + isa = PBXGroup; + children = ( + 9F254AFE2CF9FA1B0063B308 /* WebViewNavigationHandling.swift */, + 9F254B002CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift */, + 9F254B022CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift */, + 9F254B042CF9FB890063B308 /* SpecialErrorPageContextHandling.swift */, + ); + path = SpecialErrorPageInterfaces; + sourceTree = ""; + }; + 9F254B062CF9FC130063B308 /* Model */ = { + isa = PBXGroup; + children = ( + 9F254B072CF9FC270063B308 /* SpecialErrorModel.swift */, + ); + path = Model; + sourceTree = ""; + }; 9F4CC5252C4E22F9006A96EB /* ContextualOnboarding */ = { isa = PBXGroup; children = ( @@ -5979,6 +6083,7 @@ F1D477C71F2139210031ED49 /* OmniBar */, 9F23B8042C2BE20500950875 /* Onboarding */, 98EA2C3F218BB5140023E1DC /* Settings */, + 9F254AC92CF5CA860063B308 /* SpecialErrorPage */, F1BDDBFC2C340D9C00459306 /* Subscription */, 569437222BDD402600C0881B /* Sync */, F13B4BF71F18C9E800814661 /* Tabs */, @@ -6092,7 +6197,6 @@ F13B4BF81F18CA0600814661 /* TabsModelTests.swift */, F189AED61F18F6DE001EBAE1 /* TabTests.swift */, D625AAEA2BBEEFC900BC189A /* TabURLInterceptorTests.swift */, - CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, ); name = Tabs; sourceTree = ""; @@ -7597,6 +7701,7 @@ files = ( EE4FB1862A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift in Sources */, C17B59592A03AAD30055F2D1 /* PasswordGenerationPromptViewModel.swift in Sources */, + 9F254B082CF9FC270063B308 /* SpecialErrorModel.swift in Sources */, BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, @@ -7683,10 +7788,13 @@ B652DF12287C336E00C12A9C /* ContentBlockingUpdating.swift in Sources */, C1E42C7B2C5CD8AE00509204 /* AutofillCredentialsDebugViewController.swift in Sources */, 314C92BA27C3E7CB0042EC96 /* QuickLookContainerViewController.swift in Sources */, + 9F254B052CF9FB890063B308 /* SpecialErrorPageContextHandling.swift in Sources */, 855D914D2063EF6A00C4B448 /* TabSwitcherTransition.swift in Sources */, + 9F254AB32CF4A1F10063B308 /* SpecialErrorPageNavigationHandler+MaliciousSite.swift in Sources */, CB258D1229A4F24900DEBA24 /* ConfigurationManager.swift in Sources */, 8546A54A2A672959003929BF /* MainViewController+Email.swift in Sources */, F4F6DFB226E6AEC100ED7E12 /* AddOrEditBookmarkViewController.swift in Sources */, + 9F254B012CF9FA8D0063B308 /* SpecialErrorPageActionHandler.swift in Sources */, EE458D0D2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift in Sources */, F44D279F27F331BB0037F371 /* AutofillLoginPromptViewController.swift in Sources */, C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */, @@ -7733,6 +7841,7 @@ 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, + 9F254AFF2CF9FA1B0063B308 /* WebViewNavigationHandling.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, C12324C32C4697C900FBB26B /* AutofillBreakageReportTableViewCell.swift in Sources */, @@ -7848,6 +7957,7 @@ 316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, BDE91CDC2C62AA3A0005CB74 /* DefaultMetadataCollector.swift in Sources */, + 9F254AAD2CF480170063B308 /* SpecialErrorPageNavigationHandler.swift in Sources */, D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */, EE458D142ABB652900FC651A /* NetworkProtectionDebugUtilities.swift in Sources */, 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */, @@ -7918,6 +8028,7 @@ 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */, 9F8E0F382CCFAA8A001EA7C5 /* AddToDockPromoView.swift in Sources */, + 9F254B032CF9FB2E0063B308 /* SpecialErrorPageNavigationDelegate.swift in Sources */, 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, C160544129D6044D00B715A1 /* AutofillInterfaceUsernameTruncator.swift in Sources */, 31C70B5528045E3500FB6AD1 /* SecureVaultReporter.swift in Sources */, @@ -8059,6 +8170,7 @@ 1EEF387D285B1A1100383393 /* TrackerImageCache.swift in Sources */, 3151F0EC27357FEE00226F58 /* VoiceSearchFeedbackViewModel.swift in Sources */, 1E53508F2C7C9A1F00818DAA /* DefaultSubscriptionManager+AccountManagerKeychainAccessDelegate.swift in Sources */, + 9F254AB12CF48A6B0063B308 /* SpecialErrorPageNavigationHandler+SSL.swift in Sources */, 1DDF402D2BA09482006850D9 /* SettingsMainSettingsView.swift in Sources */, 85010502292FB1000033978F /* FireproofFaviconUpdater.swift in Sources */, F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */, @@ -8100,6 +8212,7 @@ 6FE127402C204D9B00EB5724 /* ShortcutsView.swift in Sources */, 85F98F92296F32BD00742F4A /* SyncSettingsViewController.swift in Sources */, 84E341961E2F7EFB00BDBA6F /* AppDelegate.swift in Sources */, + 9F254AAB2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift in Sources */, 310D091D2799F57200DC0060 /* Download.swift in Sources */, BDE91CDA2C62A70B0005CB74 /* UnifiedMetadataCollector.swift in Sources */, C13F3F6C2B7F88470083BE40 /* AuthConfirmationPromptViewModel.swift in Sources */, @@ -8219,9 +8332,11 @@ 8528AE84212FF9A100D0BD74 /* AppRatingPromptStorageTests.swift in Sources */, 569437312BE3F64400C0881B /* SyncErrorHandlerSyncPausedAlertsTests.swift in Sources */, 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */, + 9F254ADC2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */, 1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */, 31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */, 853A717820F645FB00FE60BC /* PixelTests.swift in Sources */, + 9F254AD92CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */, 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */, 984D036124AF49B80066CFB8 /* TabPreviewsSourceTests.swift in Sources */, 85D2187024BF24DB004373D2 /* FaviconRequestModifierTests.swift in Sources */, @@ -8262,7 +8377,9 @@ CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, + CBC88EE32C7F8B1700F0F8C5 /* SSLErrorPageNavigationHandlerTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, + 9F254AD52CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, 6F7FB8E12C660B3E00867DA7 /* NewTabPageFavoritesModelTests.swift in Sources */, @@ -8291,6 +8408,7 @@ 310E79BD2949CAA5007C49E8 /* FireButtonReferenceTests.swift in Sources */, 4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */, 31C7D71C27515A6300A95D0A /* MockVoiceSearchHelper.swift in Sources */, + 9F254ACE2CF5D3540063B308 /* SpecialErrorPageNavigationHandlerIntegrationTests.swift in Sources */, 8519526C2CE256BC00578553 /* AutocompleteSuggestionsDataSourceTests.swift in Sources */, 6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */, 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, @@ -8298,6 +8416,7 @@ 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */, C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */, + 9F254ADF2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */, 6F7BACD42CEE084B00F561D8 /* OmniBarEqualityCheckTests.swift in Sources */, 6F7FB8E72C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift in Sources */, 851CD674244D7E6000331B98 /* UserDefaultsExtension.swift in Sources */, @@ -8378,6 +8497,7 @@ 85D2187424BF25CD004373D2 /* FaviconsTests.swift in Sources */, 9F6933212C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift in Sources */, 9F6933192C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift in Sources */, + 9F254ACB2CF5CDC60063B308 /* SpecialErrorPageNavigationHandlerTests.swift in Sources */, CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */, 56D0602D2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, 31E77B272D038BBC006F1C9F /* OmnibarAccessoryHandlerTests.swift in Sources */, @@ -8402,6 +8522,7 @@ F1134ED61F40F29F00B73467 /* StatisticsUserDefaultsTests.swift in Sources */, 98629D342C21BE37001E6031 /* BookmarksStateValidationTests.swift in Sources */, C1FFBD462C761BE20073622B /* SyncPromoManagerTests.swift in Sources */, + 9F254AD32CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */, F1BDDBFF2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift in Sources */, 98EA2C3C218B9AAD0023E1DC /* ThemeManagerTests.swift in Sources */, 569437292BDD487600C0881B /* SyncCredentialsAdapterTests.swift in Sources */, @@ -8472,8 +8593,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F254AD62CF5E5B10063B308 /* DummyMaliciousSiteProtectionNavigationHandler.swift in Sources */, 85F21DB0210F5E32002631A6 /* AtbIntegrationTests.swift in Sources */, + 9F254AD22CF5D3A80063B308 /* MockSpecialErrorWebView.swift in Sources */, + 9F254ADE2CF636CF0063B308 /* DummyWKNavigation.swift in Sources */, 8551912724746EDC0010FDD0 /* SnapshotHelper.swift in Sources */, + 9F254ADB2CF6120E0063B308 /* MockSpecialErrorPageNavigationDelegate.swift in Sources */, + 9F254AD82CF605310063B308 /* MockSSLErrorPageNavigationHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift new file mode 100644 index 0000000000..5175a16879 --- /dev/null +++ b/DuckDuckGo/MaliciousSiteProtection/MaliciousSiteProtectionManager.swift @@ -0,0 +1,56 @@ +// +// MaliciousSiteProtectionManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +final class MaliciousSiteProtectionManager: MaliciousSiteDetecting { + + func evaluate(_ url: URL) async -> ThreatKind? { + try? await Task.sleep(interval: 0.3) + return .none + } + +} + +// MARK: - To Remove + +// These entities are copied from BSK and they will be used to mock the library +import SpecialErrorPages + +protocol MaliciousSiteDetecting { + func evaluate(_ url: URL) async -> ThreatKind? +} + +public enum ThreatKind: String, CaseIterable, CustomStringConvertible { + public var description: String { rawValue } + + case phishing + case malware +} + +public extension ThreatKind { + + var errorPageType: SpecialErrorKind { + switch self { + case .malware: .phishing // WIP in BSK + case .phishing: .phishing + } + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift b/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift new file mode 100644 index 0000000000..cd4b3b3411 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/Model/SpecialErrorModel.swift @@ -0,0 +1,31 @@ +// +// SpecialErrorModel.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SpecialErrorPages + +struct SpecialErrorModel: Equatable { + let url: URL + let errorData: SpecialErrorData +} + +struct SSLSpecialError { + let type: SSLErrorType + let error: SpecialErrorModel +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift new file mode 100644 index 0000000000..2d12540cde --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageActionHandler.swift @@ -0,0 +1,36 @@ +// +// SpecialErrorPageActionHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A type that defines actions for handling special error pages. +/// +/// This protocol is intended to be adopted by types that need to manage user interactions +/// with special error pages, such as navigating to a site, leaving a site, or presenting +/// advanced information related to the error. +protocol SpecialErrorPageActionHandler { + /// Handles the action of navigating to the site associated with the error page + func visitSite() + + /// Handles the action of leaving the site associated with the error page + func leaveSite() + + /// Handles the action of requesting more detailed information about the error + func advancedInfoPresented() +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift new file mode 100644 index 0000000000..0c3ce79206 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageContextHandling.swift @@ -0,0 +1,40 @@ +// +// SpecialErrorPageContextHandling.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +import SpecialErrorPages + +/// A type that defines the base functionality for handling navigation related to special error pages. +protocol SpecialErrorPageContextHandling: AnyObject { + /// The delegate that handles navigation actions for special error pages. + var delegate: SpecialErrorPageNavigationDelegate? { get set } + + /// A Boolean value indicating whether the special error page is currently visible. + var isSpecialErrorPageVisible: Bool { get } + + /// The URL that failed to load, if any. + var failedURL: URL? { get } + + /// Attaches a web view to the special error page handling. + func attachWebView(_ webView: WKWebView) + + /// Sets the user script for the special error page. + func setUserScript(_ userScript: SpecialErrorPageUserScript?) +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift new file mode 100644 index 0000000000..509a16c7a2 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/SpecialErrorPageNavigationDelegate.swift @@ -0,0 +1,26 @@ +// +// SpecialErrorPageNavigationDelegate.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A delegate for handling navigation actions related to special error pages. +protocol SpecialErrorPageNavigationDelegate: AnyObject { + /// Asks the delegate to close the special error page tab when the web view can't navigate back. + func closeSpecialErrorPageTab() +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift new file mode 100644 index 0000000000..31cb18a9bf --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces/WebViewNavigationHandling.swift @@ -0,0 +1,69 @@ +// +// WebViewNavigationHandling.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +// MARK: - WebViewNavigation + +/// For testing purposes. +protocol WebViewNavigation {} + +// Used in tests. WKNavigation() crashes on deinit when initialising it manually. +// As workaround we used to Swizzle the implementation of deinit in tests. +// The problem with that approach is that when running different test suites it is possible that unrelated tests re-set the original implementation of deinit while other tests are running. +// This cause the app to crash as the original implementation is executed. +// Defining a protocol for WKNavigation and using mocks such as DummyWKNavigation in tests resolves the problem. +extension WKNavigation: WebViewNavigation {} + +// MARK: - WebViewNavigationHandling + +/// A protocol that defines methods for handling navigation events of `WKWebView`. +protocol WebViewNavigationHandling: AnyObject { + /// Decides whether to cancel navigation to prevent opening a site and show a special error page based on the specified action information. + /// + /// - Parameters: + /// - navigationAction: Details about the action that triggered the navigation request. + /// - webView: The web view from which the navigation request began. + /// - Returns: A Boolean value that indicates whether the navigation action was handled. + func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool + + /// Handles authentication challenges received by the web view. + /// + /// - Parameters: + /// - webView: The web view that receives the authentication challenge. + /// - challenge: The authentication challenge. + /// - completionHandler: A completion handler block to execute with the response. + func handleWebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + + /// Handles failures during provisional navigation. + /// + /// - Parameters: + /// - webView: The `WKWebView` instance that failed the navigation. + /// - navigation: The navigation object for the operation. + /// - error: The error that occurred. + func handleWebView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WebViewNavigation, withError error: NSError) + + /// Handles the successful completion of a navigation in the web view. + /// + /// - Parameters: + /// - webView: The web view that loaded the content. + /// - navigation: The navigation object that finished. + func handleWebView(_ webView: WKWebView, didFinish navigation: WebViewNavigation) +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift new file mode 100644 index 0000000000..e49ea0e215 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+MaliciousSite.swift @@ -0,0 +1,83 @@ +// +// SpecialErrorPageNavigationHandler+MaliciousSite.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import BrowserServicesKit +import Core +import SpecialErrorPages +import WebKit + +enum MaliciousSiteProtectionNavigationResult: Equatable { + case navigationHandled(SpecialErrorModel) + case navigationNotHandled +} + +protocol MaliciousSiteProtectionNavigationHandling: AnyObject { + /// Decides whether to cancel navigation to prevent opening the YouTube app from the web view. + /// + /// - Parameters: + /// - navigationAction: The navigation action to evaluate. + /// - webView: The web view where navigation is occurring. + /// - Returns: `true` if the navigation should be canceled, `false` otherwise. + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult +} + +final class MaliciousSiteProtectionNavigationHandler { + private let maliciousSiteProtectionManager: MaliciousSiteDetecting + private let storageCache: StorageCache + + init( + maliciousSiteProtectionManager: MaliciousSiteDetecting = MaliciousSiteProtectionManager(), + storageCache: StorageCache = AppDependencyProvider.shared.storageCache + ) { + self.maliciousSiteProtectionManager = maliciousSiteProtectionManager + self.storageCache = storageCache + } +} + +// MARK: - MaliciousSiteProtectionNavigationHandling + +extension MaliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling { + + @MainActor + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult { + // Implement logic to use `maliciousSiteProtectionManager.evaluate(url)` + // Return navigationNotHandled for the time being + return .navigationNotHandled + } + +} + +// MARK: - SpecialErrorPageActionHandler + +extension MaliciousSiteProtectionNavigationHandler: SpecialErrorPageActionHandler { + + func visitSite() { + // Fire Pixel + } + + func leaveSite() { + // Fire Pixel + } + + func advancedInfoPresented() { + // Fire Pixel + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift new file mode 100644 index 0000000000..10d5b71f3a --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler+SSL.swift @@ -0,0 +1,101 @@ +// +// SpecialErrorPageNavigationHandler+SSL.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common +import BrowserServicesKit +import SpecialErrorPages +import Core + +protocol SSLSpecialErrorPageNavigationHandling { + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? + func errorPageVisited(errorType: SSLErrorType) +} + +final class SSLErrorPageNavigationHandler { + private var shouldBypassSSLError = false + + private let urlCredentialCreator: URLCredentialCreating + private let storageCache: StorageCache + private let featureFlagger: FeatureFlagger + + init( + urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), + storageCache: StorageCache = AppDependencyProvider.shared.storageCache, + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger + ) { + self.urlCredentialCreator = urlCredentialCreator + self.storageCache = storageCache + self.featureFlagger = featureFlagger + } +} + +// MARK: - SSLSpecialErrorPageNavigationHandling + +extension SSLErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling { + + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard shouldBypassSSLError, + let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { + completionHandler(.performDefaultHandling, nil) + return + } + shouldBypassSSLError = false + completionHandler(.useCredential, credential) + } + + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? { + guard featureFlagger.isFeatureOn(.sslCertificatesBypass), + error.isServerCertificateUntrusted, + let errorType = error.sslErrorType, + let failedURL = error.failedUrl, + let host = failedURL.host + else { + return nil + } + + let errorData = SpecialErrorData.ssl(type: errorType, domain: host, eTldPlus1: storageCache.tld.eTLDplus1(host)) + + return SSLSpecialError(type: errorType, error: SpecialErrorModel(url: failedURL, errorData: errorData)) + } + + func errorPageVisited(errorType: SSLErrorType) { + Pixel.fire(pixel: .certificateWarningDisplayed(errorType.pixelParameter)) + } + +} + +// MARK: - SSLErrorPageNavigationHandler + +extension SSLErrorPageNavigationHandler: SpecialErrorPageActionHandler { + + func leaveSite() { + Pixel.fire(pixel: .certificateWarningLeaveClicked) + } + + func visitSite() { + shouldBypassSSLError = true + } + + func advancedInfoPresented() { + Pixel.fire(pixel: .certificateWarningAdvancedClicked) + } + +} diff --git a/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift new file mode 100644 index 0000000000..57422c2610 --- /dev/null +++ b/DuckDuckGo/SpecialErrorPage/SpecialErrorPageNavigationHandler.swift @@ -0,0 +1,165 @@ +// +// SpecialErrorPageNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +import SpecialErrorPages +import Core + +typealias SpecialErrorPageManaging = SpecialErrorPageContextHandling & WebViewNavigationHandling & SpecialErrorPageUserScriptDelegate + +final class SpecialErrorPageNavigationHandler: SpecialErrorPageContextHandling { + private var webView: WKWebView? + private(set) var errorData: SpecialErrorData? + private var errorPageType: SpecialErrorKind? + private(set) var isSpecialErrorPageVisible = false + private(set) var failedURL: URL? + private weak var userScript: SpecialErrorPageUserScript? + weak var delegate: SpecialErrorPageNavigationDelegate? + + private let sslErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling & SpecialErrorPageActionHandler + private let maliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler + + init( + sslErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling & SpecialErrorPageActionHandler = SSLErrorPageNavigationHandler(), + maliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler = MaliciousSiteProtectionNavigationHandler() + ) { + self.sslErrorPageNavigationHandler = sslErrorPageNavigationHandler + self.maliciousSiteProtectionNavigationHandler = maliciousSiteProtectionNavigationHandler + } + + func attachWebView(_ webView: WKWebView) { + self.webView = webView + } + + func setUserScript(_ userScript: SpecialErrorPageUserScript?) { + self.userScript = userScript + userScript?.delegate = self + } +} + +// MARK: - WebViewNavigationHandling + +extension SpecialErrorPageNavigationHandler: WebViewNavigationHandling { + + func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool { + let result = await maliciousSiteProtectionNavigationHandler.handleMaliciousSiteProtectionNavigation(for: navigationAction, webView: webView) + + return await MainActor.run { + switch result { + case let .navigationHandled(model): + var request = navigationAction.request + request.url = model.url + failedURL = model.url + errorData = model.errorData + errorPageType = .phishing + loadSpecialErrorPage(request: request) + return true + case .navigationNotHandled: + return false + } + } + } + + func handleWebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return } + + sslErrorPageNavigationHandler.handleServerTrustChallenge(challenge, completionHandler: completionHandler) + } + + func handleWebView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WebViewNavigation, withError error: NSError) { + guard let sslSpecialError = sslErrorPageNavigationHandler.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error) else { return } + failedURL = sslSpecialError.error.url + sslErrorPageNavigationHandler.errorPageVisited(errorType: sslSpecialError.type) + errorData = sslSpecialError.error.errorData + errorPageType = .ssl + loadSpecialErrorPage(url: sslSpecialError.error.url) + } + + func handleWebView(_ webView: WKWebView, didFinish navigation: WebViewNavigation) { + userScript?.isEnabled = webView.url == failedURL + if webView.url != failedURL { + isSpecialErrorPageVisible = false + } + } + +} + +// MARK: - SpecialErrorPageUserScriptDelegate + +extension SpecialErrorPageNavigationHandler: SpecialErrorPageUserScriptDelegate { + + func leaveSiteAction() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.leaveSite() + case .phishing: + maliciousSiteProtectionNavigationHandler.leaveSite() + default: + break + } + + if webView?.canGoBack == true { + _ = webView?.goBack() + } else { + delegate?.closeSpecialErrorPageTab() + } + } + + func visitSiteAction() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.visitSite() + case .phishing: + maliciousSiteProtectionNavigationHandler.visitSite() + default: + break + } + + isSpecialErrorPageVisible = false + _ = webView?.reload() + } + + func advancedInfoPresented() { + switch errorPageType { + case .ssl: + sslErrorPageNavigationHandler.advancedInfoPresented() + case .phishing: + maliciousSiteProtectionNavigationHandler.advancedInfoPresented() + default: + break + } + } +} + +// MARK: Private + +private extension SpecialErrorPageNavigationHandler { + + func loadSpecialErrorPage(url: URL) { + loadSpecialErrorPage(request: URLRequest(url: url)) + } + + func loadSpecialErrorPage(request: URLRequest) { + let html = SpecialErrorPageHTMLTemplate.htmlFromTemplate + webView?.loadSimulatedRequest(request, responseHTML: html) + isSpecialErrorPageVisible = true + } + +} diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 19bb29eeec..4586314235 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -191,12 +191,7 @@ class TabViewController: UIViewController { private let refreshControl = UIRefreshControl() private let certificateTrustEvaluator: CertificateTrustEvaluating - private let urlCredentialCreator: URLCredentialCreating - private var shouldBypassSSLError = false - var errorData: SpecialErrorData? - var failedURL: URL? var storedSpecialErrorPageUserScript: SpecialErrorPageUserScript? - var isSpecialErrorPageVisible: Bool = false let syncService: DDGSyncing private let daxDialogsDebouncer = Debouncer(mode: .common) @@ -339,7 +334,6 @@ class TabViewController: UIViewController { contextualOnboardingPresenter: ContextualOnboardingPresenting, contextualOnboardingLogic: ContextualOnboardingLogic, onboardingPixelReporter: OnboardingCustomInteractionPixelReporting, - urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger, subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, @@ -359,7 +353,6 @@ class TabViewController: UIViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: onboardingPixelReporter, - urlCredentialCreator: urlCredentialCreator, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, textZoomCoordinator: textZoomCoordinator, @@ -386,6 +379,7 @@ class TabViewController: UIViewController { let textZoomCoordinator: TextZoomCoordinating let fireproofing: Fireproofing let websiteDataManager: WebsiteDataManaging + let specialErrorPageNavigationHandler: SpecialErrorPageManaging required init?(coder aDecoder: NSCoder, tabModel: Tab, @@ -404,7 +398,8 @@ class TabViewController: UIViewController { subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, fireproofing: Fireproofing, - websiteDataManager: WebsiteDataManaging) { + websiteDataManager: WebsiteDataManaging, + specialErrorPageNavigationHandler: SpecialErrorPageManaging = SpecialErrorPageNavigationHandler()) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase @@ -421,17 +416,21 @@ class TabViewController: UIViewController { self.contextualOnboardingPresenter = contextualOnboardingPresenter self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter - self.urlCredentialCreator = urlCredentialCreator self.featureFlagger = featureFlagger self.subscriptionCookieManager = subscriptionCookieManager self.textZoomCoordinator = textZoomCoordinator self.fireproofing = fireproofing self.websiteDataManager = websiteDataManager + self.specialErrorPageNavigationHandler = specialErrorPageNavigationHandler super.init(coder: aDecoder) // Assign itself as tabNavigationHandler for DuckPlayer duckPlayerNavigationHandler?.tabNavigationHandler = self + + // Assign itself as specialErrorPageNavigationDelegate for SpecialErrorPages + specialErrorPageNavigationHandler.delegate = self + } required init?(coder aDecoder: NSCoder) { @@ -569,6 +568,7 @@ class TabViewController: UIViewController { webView = WKWebView(frame: view.bounds, configuration: configuration) } textZoomCoordinator.onWebViewCreated(applyToWebView: webView) + specialErrorPageNavigationHandler.attachWebView(webView) webView.allowsLinkPreview = true webView.allowsBackForwardNavigationGestures = true @@ -1069,7 +1069,7 @@ class TabViewController: UIViewController { if let isValid { privacyInfo.serverTrust = isValid ? webView.serverTrust : nil } - privacyInfo.isSpecialErrorPageVisible = isSpecialErrorPageVisible + privacyInfo.isSpecialErrorPageVisible = specialErrorPageNavigationHandler.isSpecialErrorPageVisible previousPrivacyInfosByURL[url] = privacyInfo @@ -1284,7 +1284,8 @@ extension TabViewController: WKNavigationDelegate { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic { performBasicHTTPAuthentication(protectionSpace: challenge.protectionSpace, completionHandler: completionHandler) } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - handleServerTrustChallenge(challenge, completionHandler: completionHandler) + // Handle SSL challenge and present Special Error page if issues with SSL certificates are detected + specialErrorPageNavigationHandler.handleWebView(webView, didReceive: challenge, completionHandler: completionHandler) } else { completionHandler(.performDefaultHandling, nil) } @@ -1315,17 +1316,6 @@ extension TabViewController: WKNavigationDelegate { delegate?.tab(self, didRequestPresentingAlert: alert) } - private func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - guard shouldBypassSSLError, - let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { - completionHandler(.performDefaultHandling, nil) - return - } - shouldBypassSSLError = false - completionHandler(.useCredential, credential) - } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { if let url = webView.url { @@ -1490,10 +1480,8 @@ extension TabViewController: WKNavigationDelegate { includedParameters: [.appVersion, .atb]) } - specialErrorPageUserScript?.isEnabled = webView.url == failedURL - if webView.url != failedURL { - isSpecialErrorPageVisible = false - } + // Notify Special Error Page Navigation handler that webview successfully finished loading + specialErrorPageNavigationHandler.handleWebView(webView, didFinish: navigation) } var specialErrorPageUserScript: SpecialErrorPageUserScript? { @@ -1715,27 +1703,8 @@ extension TabViewController: WKNavigationDelegate { self.showErrorNow() } - loadSpecialErrorPageIfNeeded(error: error) - } - - private func loadSpecialErrorPageIfNeeded(error: NSError) { - guard featureFlagger.isFeatureOn(.sslCertificatesBypass), - error.isServerCertificateUntrusted, - let errorType = error.sslErrorType, - let failedURL = error.failedUrl, - let host = failedURL.host else { return } - - let tld = storageCache.tld - self.failedURL = failedURL - errorData = SpecialErrorData.ssl(type: errorType, domain: host, eTldPlus1: tld.eTLDplus1(host)) - loadSpecialErrorPage(url: failedURL) - Pixel.fire(pixel: .certificateWarningDisplayed(errorType.pixelParameter)) - } - - private func loadSpecialErrorPage(url: URL) { - let html = SpecialErrorPageHTMLTemplate.htmlFromTemplate - webView?.loadSimulatedRequest(URLRequest(url: url), responseHTML: html) - isSpecialErrorPageVisible = true + // Notify Special Error page that webview navigation failed and show special error page if needed. + specialErrorPageNavigationHandler.handleWebView(webView, didFailProvisionalNavigation: navigation, withError: error) } func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { @@ -1767,7 +1736,6 @@ extension TabViewController: WKNavigationDelegate { decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url { if !tabURLInterceptor.allowsNavigatingTo(url: url) { decisionHandler(.cancel) @@ -2571,7 +2539,9 @@ extension TabViewController: UserContentControllerDelegate { userScripts.printingUserScript.delegate = self userScripts.loginFormDetectionScript?.delegate = self userScripts.autoconsentUserScript.delegate = self - userScripts.specialErrorPageUserScript?.delegate = self + + // Special Error Page (SSL, Malicious Site protection) + specialErrorPageNavigationHandler.setUserScript(userScripts.specialErrorPageUserScript) // Setup DuckPlayer userScripts.duckPlayer = duckPlayerNavigationHandler?.duckPlayer @@ -3159,30 +3129,18 @@ extension UserContentController { } -extension TabViewController: SpecialErrorPageUserScriptDelegate { +// MARK: - SpecialErrorPageNavigationDelegate - func leaveSiteAction() { - Pixel.fire(pixel: .certificateWarningLeaveClicked) - guard webView?.canGoBack == true else { - delegate?.tabDidRequestClose(self) - return - } - _ = webView?.goBack() - } +extension TabViewController: SpecialErrorPageNavigationDelegate { - func visitSiteAction() { - Pixel.fire(pixel: .certificateWarningProceedClicked) - isSpecialErrorPageVisible = false - shouldBypassSSLError = true - _ = webView.reload() - } - - func advancedInfoPresented() { - Pixel.fire(pixel: .certificateWarningAdvancedClicked) + func closeSpecialErrorPageTab() { + delegate?.tabDidRequestClose(self) } } +// MARK: - DuckPlayerTabNavigationHandling + // This Protocol allows DuckPlayerHandler access tabs extension TabViewController: DuckPlayerTabNavigationHandling { diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index 47522ae542..84ac78bd22 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -145,7 +145,6 @@ extension TabViewController { contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: contextualOnboardingLogic, onboardingPixelReporter: contextualOnboardingPixelReporter, - urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger, subscriptionCookieManager: SubscriptionCookieManagerMock(), textZoomCoordinator: MockTextZoomCoordinator(), diff --git a/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift b/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift new file mode 100644 index 0000000000..d7b7ce923b --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SSLErrorPageNavigationHandlerTests.swift @@ -0,0 +1,198 @@ +// +// SSLErrorPageNavigationHandlerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Testing +import WebKit + +@testable import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SSLSpecialErrorPageTests Unit Tests", .serialized) +final class SSLSpecialErrorPageTests { + + private var sut: SSLErrorPageNavigationHandler! + + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + sut = SSLErrorPageNavigationHandler(urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger) + } + + deinit { + sut = nil + } + + @Test + func whenCertificateExpiredThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: URL(string: "https://expired.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://expired.badssl.com")!) + #expect(sslError.type == .expired) + #expect(sslError.error.errorData == .ssl(type: .expired, + domain: "expired.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenCertificateWrongHostThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, + NSURLErrorFailingURLErrorKey: URL(string: "https://wrong.host.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://wrong.host.badssl.com")!) + #expect(sslError.type == .wrongHost) + #expect(sslError.error.errorData == .ssl(type: .wrongHost, + domain: "wrong.host.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenCertificateSelfSignedThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, + NSURLErrorFailingURLErrorKey: URL(string: "https://self-signed.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://self-signed.badssl.com")!) + #expect(sslError.type == .selfSigned) + #expect(sslError.error.errorData == .ssl(type: .selfSigned, + domain: "self-signed.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenOtherCertificateIssueThenExpectedErrorPageIsShown() throws { + // GIVEN + let error = NSError(domain: NSURLErrorDomain, + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")!]) + + // WHEN + let sslError = try #require(sut.makeNewRequestURLAndSpecialErrorDataIfEnabled(error: error)) + + // THEN + #expect(sslError.error.url == URL(string: "https://untrusted-root.badssl.com")!) + #expect(sslError.type == .invalid) + #expect(sslError.error.errorData == .ssl(type: .invalid, + domain: "untrusted-root.badssl.com", + eTldPlus1: "badssl.com")) + } + + @Test + func whenDidReceiveChallengeIfChallengeForCertificateValidationAndNoBypassThenShouldNotReturnCredentials() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + var expectedCredential: URLCredential? + + // WHEN + sut.handleServerTrustChallenge(challenge) { _, credential in + expectedCredential = credential + } + + // THEN + #expect(expectedCredential == nil) + } + + @Test + func whenDidReceiveChallengeIfChallengeForCertificateValidationAndUserRequestBypassThenReturnsCredentials() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + var expectedCredential: URLCredential? + sut.visitSite() + + // WHEN + sut.handleServerTrustChallenge(challenge) { _, credential in + expectedCredential = credential + } + + // THEN + #expect(expectedCredential != nil) + } + +} + +final class ChallengeSender: URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} + func isEqual(_ object: Any?) -> Bool { + return false + } + var hash: Int = 0 + var superclass: AnyClass? + func `self`() -> Self { + self + } + func perform(_ aSelector: Selector!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { + return nil + } + func isProxy() -> Bool { + return false + } + func isKind(of aClass: AnyClass) -> Bool { + return false + } + func isMember(of aClass: AnyClass) -> Bool { + return false + } + func conforms(to aProtocol: Protocol) -> Bool { + return false + } + func responds(to aSelector: Selector!) -> Bool { + return false + } + var description: String = "" +} + +final class MockCredentialCreator: URLCredentialCreating { + + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } + +} diff --git a/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift new file mode 100644 index 0000000000..415386e733 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerIntegrationTests.swift @@ -0,0 +1,253 @@ +// +// SpecialErrorPageNavigationHandlerIntegrationTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Testing +import WebKit +import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SSL Integration Tests", .serialized) +final class SpecialErrorPageNavigationHandlerIntegrationTests { + private var sut: SpecialErrorPageNavigationHandler! + private var webView: MockSpecialErrorWebView! + private var sslErrorPageNavigationHandler: SSLErrorPageNavigationHandler! + + @MainActor + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + webView = MockSpecialErrorWebView(frame: CGRect(), configuration: .nonPersistent()) + sslErrorPageNavigationHandler = SSLErrorPageNavigationHandler(featureFlagger: featureFlagger) + sut = SpecialErrorPageNavigationHandler( + sslErrorPageNavigationHandler: sslErrorPageNavigationHandler, + maliciousSiteProtectionNavigationHandler: DummyMaliciousSiteProtectionNavigationHandler() + ) + } + + deinit { + sslErrorPageNavigationHandler = nil + sut = nil + webView = nil + } + + @MainActor + @Test + func whenCertificateExpiredThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://expired.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://expired.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is expired")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .expired, domain: "expired.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenCertificateWrongHostThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://wrong.host.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://wrong.host.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("does not match")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .wrongHost, domain: "wrong.host.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenCertificateSelfSignedThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://self-signed.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://self-signed.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is not trusted")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .selfSigned, domain: "self-signed.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenOtherCertificateIssueThenExpectedErrorPageIsShown() async throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://untrusted-root.badssl.com"))]) + sut.attachWebView(webView) + var expectedRequest: URLRequest? + var expectedHTML: String? + + try await confirmation { receivedHTML in + webView.loadRequestHandler = { request, html in + expectedRequest = request + expectedHTML = html + receivedHTML() + } + + // WHEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + + // THEN + let html = try #require(expectedHTML) + let url = try #require(expectedRequest?.url) + let expectedHost = try #require(URL(string: "https://untrusted-root.badssl.com")?.host) + #expect(html.contains("Warning: This site may be insecure")) + #expect(html.contains("is not trusted")) + #expect(url.host == expectedHost) + #expect(sut.failedURL == url) + #expect(sut.errorData == .ssl(type: .invalid, domain: "untrusted-root.badssl.com", eTldPlus1: "badssl.com")) + } + } + + @MainActor + @Test + func whenNavigationEndedIfNoSSLFailureSSLUserScriptIsNotEnabled() throws { + // GIVEN + webView.setCurrentURL(try #require(URL(string: "https://self-signed.badssl.com"))) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: DummyWKNavigation()) + + // THEN + #expect(script.isEnabled == false) + } + + @MainActor + @Test + func whenNavigationEndedIfSSLFailureButURLIsDifferentFromNavigationURLThenSSLUserScriptIsNotEnabled() throws { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: try #require(URL(string: "https://untrusted-root.badssl.com"))]) + + webView.setCurrentURL(try #require(URL(string: "https://self-signed.badssl.com"))) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + // Fail the request with a different URL. + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: error) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: DummyWKNavigation()) + + // THEN + #expect(script.isEnabled == false) + } + + @MainActor + @Test + func testWhenNavigationEndedIfSSLFailureAndNavigationURLIsTheSameAsFailingURLThenSSLUserScriptIsEnabled() throws { + // GIVEN + let url = try #require(URL(string: "https://self-signed.badssl.com")) + webView.setCurrentURL(url) + let script = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + sut.setUserScript(script) + sut.attachWebView(webView) + let navigation = DummyWKNavigation() + let error = NSError( + domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: [ + "_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: url] + ) + sut.handleWebView(webView, didFailProvisionalNavigation: navigation, withError: error) + #expect(script.isEnabled == false) + + // WHEN + sut.handleWebView(webView, didFinish: navigation) + + // THEN + #expect(script.isEnabled) + } + +} diff --git a/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift new file mode 100644 index 0000000000..d5ea9bd078 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageNavigationHandlerTests.swift @@ -0,0 +1,181 @@ +// +// SpecialErrorPageNavigationHandlerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Testing +import WebKit +import SpecialErrorPages +@testable import DuckDuckGo + +@Suite("Special Error Pages - SpecialErrorPageNavigationHandler Unit Tests", .serialized) +final class SpecialErrorPageNavigationHandlerTests { + private var sut: SpecialErrorPageNavigationHandler! + private var webView: MockSpecialErrorWebView! + private var sslErrorPageNavigationHandler: MockSSLErrorPageNavigationHandler! + + @MainActor + init() { + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + webView = MockSpecialErrorWebView(frame: CGRect(), configuration: .nonPersistent()) + sslErrorPageNavigationHandler = MockSSLErrorPageNavigationHandler() + sut = SpecialErrorPageNavigationHandler( + sslErrorPageNavigationHandler: sslErrorPageNavigationHandler, + maliciousSiteProtectionNavigationHandler: DummyMaliciousSiteProtectionNavigationHandler() + ) + } + + deinit { + sslErrorPageNavigationHandler = nil + sut = nil + webView = nil + } + + @Test("Receive Challenge forward event to SSL Error Page Navigation Handler") + func whenDidHandleWebViewReceiveChallengeIsCalledAskSSLErrorPageNavigationHandlerToHandleTheChallenge() { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + #expect(sslErrorPageNavigationHandler.didCallHandleServerTrustChallenge == false) + + // WHEN + sut.handleWebView(webView, didReceive: challenge) { _, _ in } + + // THEN + #expect(sslErrorPageNavigationHandler.didCallHandleServerTrustChallenge) + } + + @MainActor + @Test("Leave Site forward event to SSL Error Page Navigation Handler") + func whenLeaveSite_AndSSLError_ThenCallLeaveSiteOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCallLeaveSite) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(sslErrorPageNavigationHandler.didCallLeaveSite) + } + + @Test("Leave Site forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenLeaveSite_AndPhishingError_ThenCallLeaveSiteOnMaliciousSiteProtectioneNavigationHandler() { + + } + + @MainActor + @Test("Lave Site navigate Back") + func whenLeaveSite_AndWebViewCanNavigateBack_ThenNavigateBack() { + // GIVEN + webView.setCanGoBack(true) + sut.attachWebView(webView) + #expect(!webView.didCallGoBack) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(webView.didCallGoBack) + } + + @MainActor + @Test("Lave Site close Tab") + func whenLeaveSite_AndWebViewCannotNavigateBack_ThenAskDelegateToCloseTab() { + // GIVEN + webView.setCanGoBack(false) + let delegate = SpySpecialErrorPageNavigationDelegate() + sut.delegate = delegate + sut.attachWebView(webView) + #expect(!delegate.didCallCloseSpecialErrorPageTab) + + // WHEN + sut.leaveSiteAction() + + // THEN + #expect(delegate.didCallCloseSpecialErrorPageTab) + } + + @MainActor + @Test("Visit Site forward event to SSL Error Page Navigation Handler") + func whenVisitSite_AndSSLError_ThenCallVisitSiteOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCallVisitSite) + + // WHEN + sut.visitSiteAction() + + // THEN + #expect(sslErrorPageNavigationHandler.didCallVisitSite) + } + + @Test("Visit Site forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenVisitSite_AndPhishingError_ThenCallVisitSiteOnMaliciousSiteProtectioneNavigationHandler() { + + } + + @MainActor + @Test("Visit Site reset isSpecialErrorPageVisible and reload page") + func whenVisitSite_ThenSetIsSpecialErrorPageVisibleToFalseAndReloadPage() { + // GIVEN + sut.attachWebView(webView) + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(sut.isSpecialErrorPageVisible) + #expect(!webView.didCallReload) + + // WHEN + sut.visitSiteAction() + + // THEN + #expect(!sut.isSpecialErrorPageVisible) + #expect(webView.didCallReload) + } + + @MainActor + @Test("Advanced Info Presented forward event to SSL Error Page Navigation Handler") + func whenAdvancedInfoPresented_AndSSLError_ThenCallAdvancedInfoPresentedOnSSLErrorPageNavigationHandler() { + // GIVEN + sut.handleWebView(webView, didFailProvisionalNavigation: DummyWKNavigation(), withError: .genericSSL) + #expect(!sslErrorPageNavigationHandler.didCalladvancedInfoPresented) + + // WHEN + sut.advancedInfoPresented() + + // THEN + #expect(sslErrorPageNavigationHandler.didCalladvancedInfoPresented) + } + + @Test("Advanced Info Presented forward event to Malicious Site Protection Navigation Handler", .disabled("Will implement in upcoming PR")) + func whenAdvancedInfoPresented_AndPhishingError_ThenCallAdvancedInfoPresentedOnMaliciousSiteProtectionNavigationHandler() { + + } +} + +private extension NSError { + + static let genericSSL = NSError( + domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: [ + "_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")! + ] + ) + +} diff --git a/DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift similarity index 63% rename from DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift rename to DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift index d770f9cc3b..8b27e95c9c 100644 --- a/DuckDuckGoTests/SpecialErrorPageUserScriptTests.swift +++ b/DuckDuckGoTests/SpecialErrorPage/SpecialErrorPageUserScriptTests.swift @@ -17,13 +17,15 @@ // limitations under the License. // -import XCTest +import Testing import SpecialErrorPages @testable import DuckDuckGo -final class SpecialErrorPageUserScriptTests: XCTestCase { +@Suite("Special Error Pages - SpecialErrorPageUserScript Unit Tests", .serialized) +struct SpecialErrorPageUserScriptTests { - func testLocaleStringsForNotSupportedLanguage() { + @Test + func localeStringsForNotSupportedLanguage() { // Given let languageCode = "ko" @@ -31,20 +33,21 @@ final class SpecialErrorPageUserScriptTests: XCTestCase { let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) // Then - XCTAssertNil(result, "The result should be nil for Korean language") + #expect(result == nil, "The result should be nil for Korean language") } - func testLocaleStringsForPolishLanguage() { + @Test + func localeStringsForPolishLanguage() throws { // Given let languageCode = "pl" // When - let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) + let result = try #require(SpecialErrorPageUserScript.localeStrings(for: languageCode)) // Then - XCTAssertNotNil(result, "The result should not be nil for the Polish language code.") + #expect(result != nil, "The result should not be nil for the Polish language code.") let expectedSubstring = "Ostrzeżenie: ta witryna może być niebezpieczna" - XCTAssertTrue(result!.contains(expectedSubstring), "The result should contain the expected Polish string.") + #expect(result.contains(expectedSubstring), "The result should contain the expected Polish string.") } } diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift new file mode 100644 index 0000000000..e842c63cfa --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyMaliciousSiteProtectionNavigationHandler.swift @@ -0,0 +1,34 @@ +// +// DummyMaliciousSiteProtectionNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit +@testable import DuckDuckGo + +class DummyMaliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling & SpecialErrorPageActionHandler { + func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> DuckDuckGo.MaliciousSiteProtectionNavigationResult { + .navigationNotHandled + } + + func visitSite() {} + + func leaveSite() {} + + func advancedInfoPresented() {} +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift new file mode 100644 index 0000000000..7ec5952fbb --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/DummyWKNavigation.swift @@ -0,0 +1,23 @@ +// +// DummyWKNavigation.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo + +final class DummyWKNavigation: WebViewNavigation {} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift new file mode 100644 index 0000000000..0580bb6f37 --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSSLErrorPageNavigationHandler.swift @@ -0,0 +1,65 @@ +// +// MockSSLErrorPageNavigationHandler.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SpecialErrorPages +@testable import DuckDuckGo + +final class MockSSLErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling, SpecialErrorPageActionHandler { + private(set) var didCallHandleServerTrustChallenge = false + private(set) var capturedChallenge: URLAuthenticationChallenge? + var handleServerTrustChallengeHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void = { _, _ in } + + private(set) var didCallMakeNewRequestURLAndSpecialErrorDataIfEnabled = false + + private(set) var didCallErrorPageVisited = false + private(set) var capturedSpecialErrorType: SSLErrorType? + + private(set) var didCallLeaveSite = false + private(set) var didCallVisitSite = false + private(set) var didCalladvancedInfoPresented = false + + func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + didCallHandleServerTrustChallenge = true + capturedChallenge = challenge + handleServerTrustChallengeHandler(.performDefaultHandling, nil) + } + + func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? { + didCallMakeNewRequestURLAndSpecialErrorDataIfEnabled = true + return SSLSpecialError(type: .expired, error: SpecialErrorModel(url: URL(string: "www.example.com")!, errorData: .ssl(type: .expired, domain: "", eTldPlus1: nil))) + } + + func errorPageVisited(errorType: SSLErrorType) { + didCallErrorPageVisited = true + capturedSpecialErrorType = errorType + } + + func visitSite() { + didCallVisitSite = true + } + + func leaveSite() { + didCallLeaveSite = true + } + + func advancedInfoPresented() { + didCalladvancedInfoPresented = true + } +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift new file mode 100644 index 0000000000..6a2e58628e --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorPageNavigationDelegate.swift @@ -0,0 +1,29 @@ +// +// MockSpecialErrorPageNavigationDelegate.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo + +final class SpySpecialErrorPageNavigationDelegate: SpecialErrorPageNavigationDelegate { + private(set) var didCallCloseSpecialErrorPageTab: Bool = false + + func closeSpecialErrorPageTab() { + didCallCloseSpecialErrorPageTab = true + } +} diff --git a/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift new file mode 100644 index 0000000000..05558490cb --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPage/TestDoubles/MockSpecialErrorWebView.swift @@ -0,0 +1,63 @@ +// +// MockSpecialErrorWebView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +class MockSpecialErrorWebView: WKWebView { + + var loadRequestHandler: ((URLRequest, String) -> Void)? + var currentURL: URL? + private var _canGoBack: Bool = false + + private(set) var didCallGoBack = false + private(set) var didCallReload = false + + override func loadSimulatedRequest(_ request: URLRequest, responseHTML string: String) -> WKNavigation { + loadRequestHandler?(request, string) + return super.loadSimulatedRequest(request, responseHTML: string) + } + + override var url: URL? { + currentURL + } + + func setCurrentURL(_ url: URL) { + self.currentURL = url + } + + override var canGoBack: Bool { + _canGoBack + } + + func setCanGoBack(_ canGoBack: Bool) { + _canGoBack = canGoBack + } + + override func goBack() -> WKNavigation? { + didCallGoBack = true + return super.goBack() + } + + override func reload() -> WKNavigation? { + didCallReload = true + return super.reload() + } + +}