Dactyloidae/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift
2026-06-26 21:04:09 -07:00

212 lines
9.4 KiB
Swift

/* 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)
}
}
}