/* 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 import WebKit import Shared private let log = Logger.browserLogger extension WKNavigationAction { /// Allow local requests only if the request is privileged. var isAllowed: Bool { guard let url = request.url else { return true } return !url.isWebPage(includeDataURIs: false) || !url.isLocal || request.isPrivileged } } extension BrowserViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { if tabManager.selectedTab?.webView !== webView { return } updateFindInPageVisibility(visible: false) // If we are going to navigate to a new page, hide the reader mode button. Unless we // are going to a about:reader page. Then we keep it on screen: it will change status // (orange color) as soon as the page has loaded. if let url = webView.url { if !url.isReaderModeURL { urlBar.updateReaderModeState(ReaderModeState.unavailable) hideReaderModeBar(animated: false) } // remove the open in overlay view if it is present removeOpenInView() } } // Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present. Otherwise // it could just be a visit to a regular page on maps.apple.com. fileprivate func isAppleMapsURL(_ url: URL) -> Bool { if url.scheme == "http" || url.scheme == "https" { if url.host == "maps.apple.com" && url.query != nil { return true } } return false } // Recognize a iTunes Store URL. These all trigger the native apps. Note that appstore.com and phobos.apple.com // used to be in this list. I have removed them because they now redirect to itunes.apple.com. If we special case // them then iOS will actually first open Safari, which then redirects to the app store. This works but it will // leave a 'Back to Safari' button in the status bar, which we do not want. fileprivate func isStoreURL(_ url: URL) -> Bool { if url.scheme == "http" || url.scheme == "https" { if url.host == "itunes.apple.com" { return true } } return false } // This is the place where we decide what to do with a new navigation action. There are a number of special schemes // and http(s) urls that need to be handled in a different way. All the logic for that is inside this delegate // method. func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(WKNavigationActionPolicy.cancel) return } if url.scheme == "about" { decisionHandler(WKNavigationActionPolicy.allow) return } if !navigationAction.isAllowed && navigationAction.navigationType != .backForward { log.warning("Denying unprivileged request: \(navigationAction.request)") decisionHandler(WKNavigationActionPolicy.cancel) return } // First special case are some schemes that are about Calling. We prompt the user to confirm this action. This // gives us the exact same behaviour as Safari. if url.scheme == "tel" || url.scheme == "facetime" || url.scheme == "facetime-audio" { UIApplication.shared.openURL(url) decisionHandler(WKNavigationActionPolicy.cancel) return } // Second special case are a set of URLs that look like regular http links, but should be handed over to iOS // instead of being loaded in the webview. Note that there is no point in calling canOpenURL() here, because // iOS will always say yes. TODO Is this the same as isWhitelisted? if isAppleMapsURL(url) { UIApplication.shared.openURL(url) decisionHandler(WKNavigationActionPolicy.cancel) return } if let tab = tabManager.selectedTab, isStoreURL(url) { decisionHandler(WKNavigationActionPolicy.cancel) let alreadyShowingSnackbarOnThisTab = tab.bars.count > 0 if !alreadyShowingSnackbarOnThisTab { TimerSnackBar.showAppStoreConfirmationBar(forTab: tab, appStoreURL: url) } return } // Handles custom mailto URL schemes. if url.scheme == "mailto" { if let mailToMetadata = url.mailToMetadata(), let mailScheme = self.profile.prefs.stringForKey(PrefsKeys.KeyMailToOption), mailScheme != "mailto" { self.mailtoLinkHandler.launchMailClientForScheme(mailScheme, metadata: mailToMetadata, defaultMailtoURL: url) } else { UIApplication.shared.openURL(url) } LeanPlumClient.shared.track(event: .openedMailtoLink) decisionHandler(WKNavigationActionPolicy.cancel) return } // This is the normal case, opening a http or https url, which we handle by loading them in this WKWebView. We // always allow this. Additionally, data URIs are also handled just like normal web pages. if url.scheme == "http" || url.scheme == "https" || url.scheme == "data" || url.scheme == "blob" { if navigationAction.navigationType == .linkActivated { resetSpoofedUserAgentIfRequired(webView, newURL: url) } else if navigationAction.navigationType == .backForward { restoreSpoofedUserAgentIfRequired(webView, newRequest: navigationAction.request) } decisionHandler(WKNavigationActionPolicy.allow) return } // Ignore JS navigated links, the intention is to match Safari and native WKWebView behaviour. if navigationAction.navigationType == .linkActivated { UIApplication.shared.open(url, options: [:]) { openedURL in if !openedURL { let alert = UIAlertController(title: Strings.UnableToOpenURLErrorTitle, message: Strings.UnableToOpenURLError, preferredStyle: .alert) alert.addAction(UIAlertAction(title: UIConstants.OKString, style: UIAlertActionStyle.default, handler: nil)) self.present(alert, animated: true, completion: nil) } } } decisionHandler(WKNavigationActionPolicy.cancel) } func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // If this is a certificate challenge, see if the certificate has previously been // accepted by the user. let origin = "\(challenge.protectionSpace.host):\(challenge.protectionSpace.port)" if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust, let cert = SecTrustGetCertificateAtIndex(trust, 0), profile.certStore.containsCertificate(cert, forOrigin: origin) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust)) return } guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM, let tab = tabManager[webView] else { completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) return } // If this is a request to our local web server, use our private credentials. if challenge.protectionSpace.host == "localhost" && challenge.protectionSpace.port == Int(WebServer.sharedInstance.server.port) { completionHandler(.useCredential, WebServer.sharedInstance.credentials) return } // The challenge may come from a background tab, so ensure it's the one visible. tabManager.selectTab(tab) let loginsHelper = tab.getContentScript(name: LoginsHelper.name()) as? LoginsHelper Authenticator.handleAuthRequest(self, challenge: challenge, loginsHelper: loginsHelper).uponQueue(DispatchQueue.main) { res in if let credentials = res.successValue { completionHandler(.useCredential, credentials.credentials) } else { completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil) } } } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { guard let tab = tabManager[webView] else { return } tab.url = webView.url self.scrollController.resetZoomState() if tabManager.selectedTab === tab { updateUIForReaderHomeStateForTab(tab) } } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if let tab = tabManager[webView] { navigateInTab(tab: tab, to: navigation) } } }