From 6226595a4563e6f38cd22a85f4de6f4b56af0b24 Mon Sep 17 00:00:00 2001 From: FilippoZazzeroni <55580351+FilippoZazzeroni@users.noreply.github.com> Date: Tue, 6 May 2025 15:53:12 +0200 Subject: [PATCH] Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341) * Remove TabManagerNavigation, fix opening external apps * remove note * tests * Added tests for BrowserViewController+WebViewDelegates decide policy for navigation action * Add note around private policy * Fix failing tests * remove tests that depends on time * fix lint * Fix as per code review * Refactor TabManager as per code review * Fix error * fix BrowserViewController build issue * Update mocks * Removed unused file --------- Co-authored-by: Filippo (cherry picked from commit 8553936573c63f66fd7c336dd56e5e8e496ddc15) # Conflicts: # firefox-ios/Client.xcodeproj/project.pbxproj # firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift # firefox-ios/Client/TabManagement/TabManagerImplementation.swift # firefox-ios/firefox-ios-tests/Tests/ClientTests/TabManagement/Legacy/TabManagerNavDelegateTests.swift --- firefox-ios/Client.xcodeproj/project.pbxproj | 27 +- ...owserViewController+WebViewDelegates.swift | 77 +++++- .../Views/BrowserViewController.swift | 4 +- .../Client/TabManagement/TabManager.swift | 7 +- .../TabManagerImplementation.swift | 42 ++-- .../TabManagement/TabManagerNavDelegate.swift | 115 --------- ...erViewControllerWebViewDelegateTests.swift | 234 ++++++++++++++++-- .../ClientTests/Mocks/MockTabManager.swift | 18 +- 8 files changed, 349 insertions(+), 175 deletions(-) delete mode 100644 firefox-ios/Client/TabManagement/TabManagerNavDelegate.swift diff --git a/firefox-ios/Client.xcodeproj/project.pbxproj b/firefox-ios/Client.xcodeproj/project.pbxproj index e2b2924777cc..71b3e88a5261 100644 --- a/firefox-ios/Client.xcodeproj/project.pbxproj +++ b/firefox-ios/Client.xcodeproj/project.pbxproj @@ -187,6 +187,11 @@ 1DA710072AE7106B00677F6B /* AppDataUsageReportSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA710062AE7106B00677F6B /* AppDataUsageReportSetting.swift */; }; 1DAEEE002D93532900F4B42D /* UIImage+RenderingUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAEEDFF2D93532400F4B42D /* UIImage+RenderingUtilities.swift */; }; 1DC372022B23C80F000F96C8 /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC372012B23C80F000F96C8 /* WindowManager.swift */; }; +<<<<<<< HEAD +======= + 1DC598B02DC3EDD50037D263 /* BenchmarkSteps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC598AF2DC3EDD40037D263 /* BenchmarkSteps.swift */; }; + 1DC598B42DC52E3A0037D263 /* ForceRSSyncSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC598B32DC52E360037D263 /* ForceRSSyncSetting.swift */; }; +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) 1DCEC3F22D5D1FEC00129966 /* ASSearchEngineSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCEC3F12D5D1FDD00129966 /* ASSearchEngineSelector.swift */; }; 1DCEC3F42D600AE000129966 /* ASSearchEngineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCEC3F32D600ADC00129966 /* ASSearchEngineProvider.swift */; }; 1DCEC3F62D600C4400129966 /* ASSearchEngineIconRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DCEC3F52D600C3D00129966 /* ASSearchEngineIconRecord.swift */; }; @@ -694,8 +699,6 @@ 6669B5E2211418A200CA117B /* WebsiteDataSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6669B5E1211418A200CA117B /* WebsiteDataSearchResultsViewController.swift */; }; 66CE54A820FCF6CF00CC310B /* WebsiteDataManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CE54A720FCF6CF00CC310B /* WebsiteDataManagementViewController.swift */; }; 6A3E5D8A283831D1001E706E /* DownloadQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3E5D89283831D0001E706E /* DownloadQueueTests.swift */; }; - 6A5F591D28627C0100FABA92 /* TabManagerNavDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5F591C28627C0100FABA92 /* TabManagerNavDelegateTests.swift */; }; - 6ACB550C28633860007A6ABD /* TabManagerNavDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */; }; 6ADB651B285C03B100947EA4 /* DownloadHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADB651A285C03B100947EA4 /* DownloadHelperTests.swift */; }; 742A56391D80B54A00BDB803 /* PhotonActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742A56381D80B54A00BDB803 /* PhotonActionSheet.swift */; }; 742BD99E2A13AC9000BA6B15 /* OnboardingInstructionPopupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742BD99D2A13AC9000BA6B15 /* OnboardingInstructionPopupViewController.swift */; }; @@ -2723,6 +2726,11 @@ 1DA710062AE7106B00677F6B /* AppDataUsageReportSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDataUsageReportSetting.swift; sourceTree = ""; }; 1DAEEDFF2D93532400F4B42D /* UIImage+RenderingUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+RenderingUtilities.swift"; sourceTree = ""; }; 1DC372012B23C80F000F96C8 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; +<<<<<<< HEAD +======= + 1DC598AF2DC3EDD40037D263 /* BenchmarkSteps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkSteps.swift; sourceTree = ""; }; + 1DC598B32DC52E360037D263 /* ForceRSSyncSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceRSSyncSetting.swift; sourceTree = ""; }; +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) 1DCEC3F12D5D1FDD00129966 /* ASSearchEngineSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASSearchEngineSelector.swift; sourceTree = ""; }; 1DCEC3F32D600ADC00129966 /* ASSearchEngineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASSearchEngineProvider.swift; sourceTree = ""; }; 1DCEC3F52D600C3D00129966 /* ASSearchEngineIconRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASSearchEngineIconRecord.swift; sourceTree = ""; }; @@ -7889,11 +7897,9 @@ 6A3E5D89283831D0001E706E /* DownloadQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadQueueTests.swift; sourceTree = ""; }; 6A4646B4A929C5049ED3E0DB /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; 6A574F2FAB02381D22FD47B4 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/LoginManager.strings; sourceTree = ""; }; - 6A5F591C28627C0100FABA92 /* TabManagerNavDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerNavDelegateTests.swift; sourceTree = ""; }; 6A694C35BF15DBBE61D7DB77 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Search.strings; sourceTree = ""; }; 6A8B40A7B8B0933B706C84B0 /* or */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = or; path = or.lproj/InfoPlist.strings; sourceTree = ""; }; 6A954888A18B54ED87980F19 /* af */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = af; path = af.lproj/Intro.strings; sourceTree = ""; }; - 6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerNavDelegate.swift; sourceTree = ""; }; 6AD94362821BDAD94165D610 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Intro.strings"; sourceTree = ""; }; 6ADB651A285C03B100947EA4 /* DownloadHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadHelperTests.swift; sourceTree = ""; }; 6B0545D197CACC8F242FBBC4 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Search.strings; sourceTree = ""; }; @@ -12534,8 +12540,11 @@ 39F819C51FD70F5D009E31E4 /* GlobalTabEventHandlers.swift */, D3968F241A38FE8500CEFD3B /* TabManager.swift */, 5A8017DF29CE15D90047120D /* TabManagerImplementation.swift */, +<<<<<<< HEAD 6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */, 21BFEEF42A040EF40033048D /* TabMigrationUtility.swift */, +======= +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) 1D558A592BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift */, BD0D40792D83124E00C38511 /* TabWebViewPreview.swift */, ); @@ -13174,7 +13183,13 @@ 8AED868428CA7B1200351A50 /* TabManagement */ = { isa = PBXGroup; children = ( +<<<<<<< HEAD 5A475E8829DB87BF009C13FD /* Legacy */, +======= + 8AD08D1627E91AC800B8E907 /* TabsTelemetryTests.swift */, + AB7D4C3029ACAED100626427 /* Tab+ChangeUserAgentTests.swift */, + 39236E711FCC600200A38F1B /* TabEventHandlerTests.swift */, +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) 5A475E8B29DB87F6009C13FD /* Mocks */, 5A475E8929DB87F2009C13FD /* TabManagerTests.swift */, 21BFEEF62A05A0310033048D /* TabMigrationUtilityTests.swift */, @@ -17369,8 +17384,11 @@ 8A3EF8072A2FCFF700796E3A /* SentryIDSetting.swift in Sources */, ED390D202CF4DE2E00A775F9 /* URLActivityItemProvider.swift in Sources */, 8A03294E288F1F0800AD9B89 /* TopSitesDimension.swift in Sources */, +<<<<<<< HEAD E1516A3E2A7BC07E007819A4 /* ReliabilityGrade.swift in Sources */, 6ACB550C28633860007A6ABD /* TabManagerNavDelegate.swift in Sources */, +======= +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) E18259DF29B25E4F00E6BE76 /* UserNotificationCenterProtocol.swift in Sources */, DFFC9AD12A681FA0002A6AAD /* NimbusFakespotFeatureLayer.swift in Sources */, EDDB13872D3184F300C4B165 /* Site+createSponsoredSite.swift in Sources */, @@ -18680,7 +18698,6 @@ 8A2825352760399B00395E66 /* KeyboardPressesHandlerTests.swift in Sources */, 8AEF41622D15EDBC0013925D /* TopSitesMiddlewareTests.swift in Sources */, ABEF80D92A2F283E003F52C4 /* CreditCardBottomSheetViewModelTests.swift in Sources */, - 6A5F591D28627C0100FABA92 /* TabManagerNavDelegateTests.swift in Sources */, 8AABBD052A0041380089941E /* MockCoordinator.swift in Sources */, 8AABBCFF2A0017960089941E /* MockGleanWrapper.swift in Sources */, 21FA8FAE2AE856460013B815 /* TabsCoordinatorTests.swift in Sources */, diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift index 1db7c8ea9bdc..5719a3532e83 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+WebViewDelegates.swift @@ -452,6 +452,29 @@ extension BrowserViewController: WKUIDelegate { // MARK: - WKNavigationDelegate extension BrowserViewController: WKNavigationDelegate { + /// Called when the WKWebView's content process has gone away. If this happens for the currently selected tab + /// then we immediately reload it. + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + if let tab = tabManager.selectedTab, tab.webView == webView { + tab.consecutiveCrashes += 1 + + // Only automatically attempt to reload the crashed + // tab three times before giving up. + if tab.consecutiveCrashes < 3 { + logger.log("The webview has crashed, trying to reload.", + level: .warning, + category: .webview, + extra: ["Attempt number": "\(tab.consecutiveCrashes)"]) + + tabsTelemetry.trackConsecutiveCrashTelemetry(attemptNumber: tab.consecutiveCrashes) + + webView.reload() + } else { + tab.consecutiveCrashes = 0 + } + } + } + func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation?) { guard let tab = tabManager[webView] else { return } @@ -465,6 +488,11 @@ extension BrowserViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { if tabManager.selectedTab?.webView !== webView { return } + // Note the main frame JSContext (i.e. document, window) is not available yet. + if let tab = tabManager[webView], let blocker = tab.contentBlocker { + blocker.clearPageStats() + } + updateFindInPageVisibility(isVisible: false) // If we are going to navigate to a new page, hide the reader mode button. Unless we @@ -488,6 +516,9 @@ extension BrowserViewController: WKNavigationDelegate { decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { + // prevent the App from opening universal links + // https://stackoverflow.com/questions/38450586/prevent-universal-links-from-opening-in-wkwebview-uiwebview + let allowPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2) ?? .allow guard let url = navigationAction.request.url, let tab = tabManager[webView] else { @@ -645,15 +676,20 @@ extension BrowserViewController: WKNavigationDelegate { return } - if navigationAction.navigationType == .linkActivated && url != webView.url { - if profile.prefs.boolForKey(PrefsKeys.BlockOpeningExternalApps) ?? false { - decisionHandler(.cancel) - webView.load(navigationAction.request) - return - } + let shouldBlockExternalApps = profile.prefs.boolForKey(PrefsKeys.BlockOpeningExternalApps) ?? false + let isGoogleDomain = url.host?.contains("google") ?? false + let isPrivate = tab.isPrivate + + if navigationAction.navigationType == .linkActivated, + !shouldBlockExternalApps, + url != webView.url, + !isPrivate, + !isGoogleDomain { + decisionHandler(.allow) + return } - decisionHandler(.allow) + decisionHandler(allowPolicy) return } @@ -1047,6 +1083,13 @@ extension BrowserViewController: WKNavigationDelegate { let metadataManager = tab.metadataManager else { return } + // The main frame JSContext is available, and DOM parsing has begun. + // Do not execute JS at this point that requires running prior to DOM parsing. + if let tpHelper = tab.contentBlocker, !tpHelper.isEnabled { + let js = "window.__firefox__.TrackingProtectionStats.setEnabled(false, \(UserScriptManager.appIdToken))" + webView.evaluateJavascriptInDefaultContentWorld(js) + } + searchTelemetry.trackTabAndTopSiteSAP(tab, webView: webView) webviewTelemetry.start() tab.url = webView.url @@ -1090,6 +1133,20 @@ extension BrowserViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { webviewTelemetry.stop() +<<<<<<< HEAD +======= + + if let url = webView.url, InternalURL(url) == nil { + if let title = webView.title, + tabManager.selectedTab?.webView == webView { + tabManager.selectedTab?.lastTitle = title + tabManager.notifyCurrentTabDidFinishLoading() + } + + tabManager.commitChanges() + } + +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) if isPDFRefactorEnabled { scrollController.configureRefreshControl() navigationHandler?.removeDocumentLoading() @@ -1160,7 +1217,8 @@ private extension BrowserViewController { } // Use for sms and mailto, which do not show a confirmation before opening. - func showExternalAlert(withText text: String, completion: @escaping (UIAlertAction) -> Void) { + func showExternalAlert(withText text: String, + completion: @escaping (UIAlertAction) -> Void) { let alert = UIAlertController(title: nil, message: text, preferredStyle: .alert) @@ -1173,8 +1231,7 @@ private extension BrowserViewController { let cancelOption = UIAlertAction( title: .CancelString, - style: .cancel, - handler: nil + style: .cancel ) alert.addAction(okOption) diff --git a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index c499e767669d..7d3da046fb51 100644 --- a/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -221,6 +221,8 @@ class BrowserViewController: UIViewController, private(set) lazy var searchTelemetry = SearchTelemetry(tabManager: tabManager) private(set) lazy var webviewTelemetry = WebViewLoadMeasurementTelemetry() private(set) lazy var privateBrowsingTelemetry = PrivateBrowsingTelemetry() + private(set) lazy var tabsTelemetry = TabsTelemetry() + private lazy var appStartupTelemetry = AppStartupTelemetry() // location label actions @@ -380,7 +382,7 @@ class BrowserViewController: UIViewController, fileprivate func didInit() { tabManager.addDelegate(self) - tabManager.addNavigationDelegate(self) + tabManager.setNavigationDelegate(self) downloadQueue.addDelegate(self) let tabWindowUUID = tabManager.windowUUID AppEventQueue.wait(for: [.startupFlowComplete, .tabRestoration(tabWindowUUID)]) { [weak self] in diff --git a/firefox-ios/Client/TabManagement/TabManager.swift b/firefox-ios/Client/TabManagement/TabManager.swift index 4dd34126d862..c916d93005ed 100644 --- a/firefox-ios/Client/TabManagement/TabManager.swift +++ b/firefox-ios/Client/TabManagement/TabManager.swift @@ -32,7 +32,7 @@ protocol TabManager: AnyObject { // MARK: - Add/Remove Delegate func addDelegate(_ delegate: TabManagerDelegate) - func addNavigationDelegate(_ delegate: WKNavigationDelegate) + func setNavigationDelegate(_ delegate: WKNavigationDelegate) func removeDelegate(_ delegate: TabManagerDelegate, completion: (() -> Void)?) // MARK: - Select Tab @@ -94,6 +94,11 @@ protocol TabManager: AnyObject { func clearAllTabsHistory() func reorderTabs(isPrivate privateMode: Bool, fromIndex visibleFromIndex: Int, toIndex visibleToIndex: Int) func preserveTabs() + + /// Commits the pending changes to the persistent store. + func commitChanges() + + func notifyCurrentTabDidFinishLoading() func restoreTabs(_ forced: Bool) func startAtHomeCheck() -> Bool func expireLoginAlerts() diff --git a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift index 7dd60d481202..2fc726da549f 100644 --- a/firefox-ios/Client/TabManagement/TabManagerImplementation.swift +++ b/firefox-ios/Client/TabManagement/TabManagerImplementation.swift @@ -107,7 +107,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { private let windowManager: WindowManager private let windowIsNew: Bool private let profile: Profile - private let navDelegate: TabManagerNavDelegate + private weak var navigationDelegate: WKNavigationDelegate? private var backupCloseTabs = [Tab]() private var tabsTelemetry = TabsTelemetry() private var delegates = [WeakTabManagerDelegate]() @@ -159,7 +159,6 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { self.windowIsNew = uuid.isNew self.windowUUID = uuid.uuid self.profile = profile - self.navDelegate = TabManagerNavDelegate() self.logger = logger self.tabs = tabs @@ -167,7 +166,6 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { GlobalTabEventHandlers.configure(with: profile) - addNavigationDelegate(self) setupNotifications( forObserver: self, observing: [ @@ -235,8 +233,8 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { self.delegates.append(WeakTabManagerDelegate(value: delegate)) } - func addNavigationDelegate(_ delegate: WKNavigationDelegate) { - self.navDelegate.insert(delegate) + func setNavigationDelegate(_ delegate: WKNavigationDelegate) { + navigationDelegate = delegate } // MARK: - Remove Tab @@ -285,7 +283,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { for tab in tabs { self.removeTab(tab, flushToDisk: false) } - storeChanges() + commitChanges() } @MainActor @@ -321,7 +319,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { // Save the tab state that existed prior to removals (preserves original selected tab) backupCloseTab = currentSelectedTab - storeChanges() + commitChanges() } /// Remove a tab, will notify delegate of the tab removal @@ -363,7 +361,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { } if flushToDisk { - storeChanges() + commitChanges() } } @@ -408,7 +406,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { delegates.forEach { $0.get()?.tabManagerDidAddTabs(self) } // Flush. - storeChanges() + commitChanges() } private func addTab(_ request: URLRequest? = nil, @@ -459,13 +457,13 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { } delegates.forEach { $0.get()?.tabManagerUpdateCount() } - storeChanges() + commitChanges() } func undoCloseAllTabs() { guard !backupCloseTabs.isEmpty else { return } tabs = backupCloseTabs - storeChanges() + commitChanges() backupCloseTabs = [Tab]() if backupCloseTab != nil { selectTab(backupCloseTab?.tab) @@ -875,12 +873,17 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { return tabData } - /// storeChanges is called when a web view has finished loading a page, or when a tab is removed, and in other cases. - private func storeChanges() { + func commitChanges() { preserveTabs() saveSessionData(forTab: selectedTab) } + func notifyCurrentTabDidFinishLoading() { + delegates.forEach { + $0.get()?.tabManagerTabDidFinishLoading() + } + } + private func saveSessionData(forTab tab: Tab?) { guard let tab = tab, let tabSession = tab.webView?.interactionState as? Data, @@ -1092,13 +1095,13 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { for tab in currentModeTabs { await self.removeTab(tab.tabUUID) } - storeChanges() + commitChanges() } @MainActor func undoCloseInactiveTabs() async { tabs.append(contentsOf: backupCloseTabs) - storeChanges() + commitChanges() backupCloseTabs = [Tab]() } @@ -1141,7 +1144,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { selectedIndex = previousSelectedIndex } - storeChanges() + commitChanges() } func startAtHomeCheck() -> Bool { @@ -1264,7 +1267,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { } tab.createWebview(configuration: configuration) } - tab.navigationDelegate = self.navDelegate + tab.navigationDelegate = navigationDelegate if let request = request { tab.loadRequest(request) @@ -1293,7 +1296,7 @@ class TabManagerImplementation: NSObject, TabManager, FeatureFlaggable { tab.noImageMode = NoImageModeHelper.isActivated(profile.prefs) if flushToDisk { - storeChanges() + commitChanges() } } @@ -1377,6 +1380,7 @@ extension TabManagerImplementation: Notifiable { } } +<<<<<<< HEAD // MARK: - WKNavigationDelegate extension TabManagerImplementation: WKNavigationDelegate { // Note the main frame JSContext (i.e. document, window) is not available yet. @@ -1429,6 +1433,8 @@ extension TabManagerImplementation: WKNavigationDelegate { } } +======= +>>>>>>> 855393657 (Bugfix FXIOS-9909 [Content Sharing] Block open external apps (#26341)) // MARK: - WindowSimpleTabsProvider extension TabManagerImplementation: WindowSimpleTabsProvider { func windowSimpleTabs() -> [TabUUID: SimpleTab] { diff --git a/firefox-ios/Client/TabManagement/TabManagerNavDelegate.swift b/firefox-ios/Client/TabManagement/TabManagerNavDelegate.swift deleted file mode 100644 index 9c24158e03b7..000000000000 --- a/firefox-ios/Client/TabManagement/TabManagerNavDelegate.swift +++ /dev/null @@ -1,115 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/ - -import Foundation -@preconcurrency import WebKit -import Shared - -// WKNavigationDelegates must implement NSObjectProtocol -class TabManagerNavDelegate: NSObject, WKNavigationDelegate { - fileprivate var delegates = WeakList() - - func insert(_ delegate: WKNavigationDelegate) { - delegates.insert(delegate) - } - - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation?) { - for delegate in delegates { - delegate.webView?(webView, didCommit: navigation) - } - } - - func webView( - _ webView: WKWebView, - didFail navigation: WKNavigation?, - withError error: Error - ) { - for delegate in delegates { - delegate.webView?(webView, didFail: navigation, withError: error) - } - } - - func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation?, - withError error: Error - ) { - for delegate in delegates { - delegate.webView?(webView, didFailProvisionalNavigation: navigation, withError: error) - } - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { - for delegate in delegates { - delegate.webView?(webView, didFinish: navigation) - } - } - - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - for delegate in delegates { - delegate.webViewWebContentProcessDidTerminate?(webView) - } - } - - func webView( - _ webView: WKWebView, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - let authenticatingDelegates = delegates.filter { wv in - return wv.responds(to: #selector(webView(_:didReceive:completionHandler:))) - } - - guard let firstAuthenticatingDelegate = authenticatingDelegates.first else { - return completionHandler(.performDefaultHandling, nil) - } - - firstAuthenticatingDelegate.webView?(webView, didReceive: challenge) { (disposition, credential) in - completionHandler(disposition, credential) - } - } - - func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation?) { - for delegate in delegates { - delegate.webView?(webView, didReceiveServerRedirectForProvisionalNavigation: navigation) - } - } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { - for delegate in delegates { - delegate.webView?(webView, didStartProvisionalNavigation: navigation) - } - } - - func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - var res = WKNavigationActionPolicy.allow - for delegate in delegates { - delegate.webView?(webView, decidePolicyFor: navigationAction, decisionHandler: { policy in - if policy == .cancel { - res = policy - } - }) - } - decisionHandler(res) - } - - func webView(_ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse, - decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { - var res = WKNavigationResponsePolicy.allow - for delegate in delegates { - delegate.webView?(webView, decidePolicyFor: navigationResponse, decisionHandler: { policy in - if policy == .cancel { - res = policy - } - }) - } - - decisionHandler(res) - } -} diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/BrowserViewControllerWebViewDelegateTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/BrowserViewControllerWebViewDelegateTests.swift index d039b249df53..67d7d8d86166 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/BrowserViewControllerWebViewDelegateTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Browser/BrowserViewControllerWebViewDelegateTests.swift @@ -5,38 +5,195 @@ import XCTest import Common import WebKit +import Shared @testable import Client class BrowserViewControllerWebViewDelegateTests: XCTestCase { - var subject: BrowserViewController! - var profile: MockProfile! - var tabManager: TabManager! - var tabManagerDelegate: TabManagerNavDelegate! + private var profile: MockProfile! + private var tabManager: MockTabManager! + private var allowPolicyRawValue: Int { + return WKNavigationActionPolicy.allow.rawValue + } + private lazy var allowBlockingUniversalLinksPolicy = WKNavigationActionPolicy(rawValue: allowPolicyRawValue + 2) override func setUp() { super.setUp() DependencyHelperMock().bootstrapDependencies() profile = MockProfile() - tabManager = TabManagerImplementation(profile: profile, - uuid: ReservedWindowUUID(uuid: .XCTestDefaultUUID, isNew: false)) - subject = BrowserViewController(profile: profile, tabManager: tabManager) - tabManagerDelegate = TabManagerNavDelegate() + LegacyFeatureFlagsManager.shared.initializeDeveloperFeatures(with: profile) + tabManager = MockTabManager() } override func tearDown() { - super.tearDown() - AppContainer.shared.reset() profile = nil tabManager = nil - subject = nil - tabManagerDelegate = nil + super.tearDown() + } + + // MARK: - Decide policy + func testWebViewDecidePolicyForNavigationAction_cancelWhenTabNotInTabManager() { + let subject = createSubject() + let url = URL(string: "https://example.com")! + let tab = createTab() + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelFacetimeScheme() { + let subject = createSubject() + let url = URL(string: "facetime://testuser")! + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelFacetimeAudioScheme() { + let subject = createSubject() + let url = URL(string: "facetime-audio://testuser")! + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelTelScheme() { + let subject = createSubject() + let url = URL(string: "tel://3484563742")! + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelAppStoreScheme() { + let subject = createSubject() + let url = URL(string: "itms-apps://test-app")! + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelAppStoreURL() { + let subject = createSubject() + let url = URL(string: "https://apps.apple.com/test-app")! + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .cancel) + } + } + + func testWebViewDecidePolicyForNavigationAction_allowsGoogle_andBlockUniversalLink() { + let subject = createSubject() + let google = URL(string: "https://www.google.com") + let tab = createTab() + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: google!, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, self.allowBlockingUniversalLinksPolicy) + } + } + + func testWebViewDecidePolicyForNavigationAction_allowsAnyWebsite_withNormalTabs() { + let subject = createSubject() + let tab = createTab() + let url = URL(string: "https://www.example.com")! + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, .allow) + } + } + + func testWebViewDecidePolicyForNavigationAction_allowsAnyWebsiteBlockingUniversalLink_whenOptionEnabled() { + let subject = createSubject() + let tab = createTab() + let url = URL(string: "https://www.example.com")! + tabManager.tabs = [tab] + profile.prefs.setBool(true, forKey: PrefsKeys.BlockOpeningExternalApps) + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, self.allowBlockingUniversalLinksPolicy) + } + } + + func testWebViewDecidePolicyForNavigationAction_allowsAnyWebsite_andBlockUniversalLinksWithPrivateTab() { + let subject = createSubject() + let tab = createTab(isPrivate: true) + let url = URL(string: "https://www.example.com")! + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { policy in + XCTAssertEqual(policy, self.allowBlockingUniversalLinksPolicy) + } + } + + func testWebViewDecidePolicyForNavigationAction_addRequestToPending() { + let subject = createSubject() + let tab = createTab() + let url = URL(string: "https://www.example.com")! + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: url, + type: .linkActivated)) { _ in + XCTAssertNotNil(subject.pendingRequests[url.absoluteString]) + } + } + + func testWebViewDecidePolicyForNavigationAction_cancelLoading_whenBlobURLs() { + let subject = createSubject() + let tab = createTab() + let blob = URL(string: "blob://blobfile")! + tabManager.tabs = [tab] + + subject.webView(tab.webView!, + decidePolicyFor: MockNavigationAction(url: blob, + type: .other)) { policy in + XCTAssertEqual(policy, self.allowBlockingUniversalLinksPolicy) + } } + // MARK: - Authentication + func testWebViewDidReceiveChallenge_MethodServerTrust() { - tabManagerDelegate.insert(subject) + let subject = createSubject() - tabManagerDelegate.webView( + subject.webView( anyWebView(), didReceive: anyAuthenticationChallenge(for: "NSURLAuthenticationMethodServerTrust") ) { disposition, credential in @@ -46,9 +203,9 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { } func testWebViewDidReceiveChallenge_MethodHTTPDigest() { - tabManagerDelegate.insert(subject) + let subject = createSubject() - tabManagerDelegate.webView( + subject.webView( anyWebView(), didReceive: anyAuthenticationChallenge(for: "NSURLAuthenticationMethodHTTPDigest") ) { disposition, credential in @@ -58,9 +215,9 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { } func testWebViewDidReceiveChallenge_MethodHTTPNTLM() { - tabManagerDelegate.insert(subject) + let subject = createSubject() - tabManagerDelegate.webView( + subject.webView( anyWebView(), didReceive: anyAuthenticationChallenge(for: "NSURLAuthenticationMethodNTLM") ) { disposition, credential in @@ -70,9 +227,9 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { } func testWebViewDidReceiveChallenge_MethodHTTPBasic() { - tabManagerDelegate.insert(subject) + let subject = createSubject() - tabManagerDelegate.webView( + subject.webView( anyWebView(), didReceive: anyAuthenticationChallenge(for: "NSURLAuthenticationMethodHTTPBasic") ) { disposition, credential in @@ -81,8 +238,23 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { } } - private func anyWebView() -> WKWebView { - return WKWebView(frame: CGRect(width: 100, height: 100)) + private func createSubject() -> BrowserViewController { + let subject = BrowserViewController(profile: profile, tabManager: tabManager) + trackForMemoryLeaks(subject) + return subject + } + + private func anyWebView(url: URL? = nil) -> MockTabWebView { + let tab = MockTabWebView(frame: .zero, configuration: WKWebViewConfiguration(), windowUUID: .XCTestDefaultUUID) + tab.loadedURL = url + return tab + } + + private func createTab(isPrivate: Bool = false) -> Tab { + let tab = Tab(profile: profile, isPrivate: isPrivate, windowUUID: .XCTestDefaultUUID) + let webView = MockTabWebView(tab: tab) + tab.webView = webView + return tab } private func anyAuthenticationChallenge(for authenticationMethod: String) -> URLAuthenticationChallenge { @@ -106,6 +278,24 @@ class BrowserViewControllerWebViewDelegateTests: XCTestCase { } } +class MockNavigationAction: WKNavigationAction { + private var type: WKNavigationType? + private var urlRequest: URLRequest + + override var navigationType: WKNavigationType { + return type ?? .other + } + + override var request: URLRequest { + return urlRequest + } + + init(url: URL, type: WKNavigationType? = nil) { + self.type = type + self.urlRequest = URLRequest(url: url) + } +} + class MockURLAuthenticationChallengeSender: NSObject, URLAuthenticationChallengeSender { func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockTabManager.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockTabManager.swift index 66b497a51ae2..a4927927bd9f 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockTabManager.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockTabManager.swift @@ -36,6 +36,8 @@ class MockTabManager: TabManager { var removeTabsByURLCalled = 0 var addTabWasCalled = false + var notifyCurrentTabDidFinishLoadingCalled = 0 + var commitChangesCalled = 0 init( windowUUID: WindowUUID = WindowUUID.XCTestDefaultUUID, @@ -46,11 +48,13 @@ class MockTabManager: TabManager { } subscript(index: Int) -> Tab? { - return nil + return tabs[index] } subscript(webView: WKWebView) -> Tab? { - return nil + return tabs.first { + $0.webView === webView + } } func selectTab(_ tab: Tab?, previous: Tab?) { @@ -70,7 +74,7 @@ class MockTabManager: TabManager { func addDelegate(_ delegate: TabManagerDelegate) {} - func addNavigationDelegate(_ delegate: WKNavigationDelegate) {} + func setNavigationDelegate(_ delegate: WKNavigationDelegate) {} func removeDelegate(_ delegate: TabManagerDelegate, completion: (() -> Void)?) {} @@ -99,6 +103,10 @@ class MockTabManager: TabManager { func clearAllTabsHistory() {} + func commitChanges() { + commitChangesCalled += 1 + } + func willSwitchTabMode(leavingPBM: Bool) {} func reorderTabs(isPrivate privateMode: Bool, fromIndex visibleFromIndex: Int, toIndex visibleToIndex: Int) {} @@ -163,5 +171,9 @@ class MockTabManager: TabManager { func undoCloseInactiveTabs() async {} + func notifyCurrentTabDidFinishLoading() { + notifyCurrentTabDidFinishLoadingCalled += 1 + } + func tabDidSetScreenshot(_ tab: Client.Tab) {} }