diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 279e70a65..b3ba13afe 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -49,6 +49,8 @@ "alerts.open_url_from_notification.title" = "Open URL?"; "alerts.prompt.cancel" = "Cancel"; "alerts.prompt.ok" = "OK"; +"alerts.navigation_error.message" = "This page cannot be displayed because it's outside your Home Assistant server or the page was not found."; +"alerts.navigation_error.title" = "Navigation Error"; "always_open_label" = "Always Open"; "announcement.drop_support.button" = "Continue"; "announcement.drop_support.subtitle" = "After careful consideration, we will be discontinuing support for iOS 12, 13 and 14 in our upcoming updates."; diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index 5a60c0ffd..a5e6ce2cc 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -1288,6 +1288,47 @@ extension WebViewController { } } + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + // Only check domain for user-initiated link clicks + guard navigationAction.navigationType == .linkActivated else { + // Allow all other navigation types (back/forward, reload, programmatic, etc.) + decisionHandler(.allow) + return + } + + guard let targetURL = navigationAction.request.url else { + decisionHandler(.allow) + return + } + + // Allow special schemes like about:blank + if let scheme = targetURL.scheme?.lowercased(), ["about", "file"].contains(scheme) { + decisionHandler(.allow) + return + } + + // Check if the target URL belongs to the active server domain + guard let activeURL = server.info.connection.activeURL() else { + // If there's no active URL, allow navigation (let other error handling deal with it) + decisionHandler(.allow) + return + } + + // Allow navigation within the same domain + if targetURL.baseIsEqual(to: activeURL) { + decisionHandler(.allow) + } else { + // URL is outside the active domain - cancel and show alert + Current.Log.warning("Navigation blocked: URL \(targetURL) is outside active domain \(activeURL)") + decisionHandler(.cancel) + showNavigationErrorAlert() + } + } + func webView( _ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, @@ -1311,8 +1352,13 @@ extension WebViewController { // error response, let's inspect if it's restoring a page or normal navigation if navigationResponse.response.url != initialURL { - // just a normal loading error - decisionHandler(.allow) + // Normal loading error (not initial URL restoration) + // Cancel the navigation, go back if possible, and show alert + decisionHandler(.cancel) + if webView.canGoBack { + webView.goBack() + } + showNavigationErrorAlert() } else { // first: clear that saved url, it's bad initialURL = nil @@ -1329,6 +1375,24 @@ extension WebViewController { } } + private func showNavigationErrorAlert() { + let alert = UIAlertController( + title: L10n.Alerts.NavigationError.title, + message: L10n.Alerts.NavigationError.message, + preferredStyle: .alert + ) + alert.addAction(.init(title: L10n.okLabel, style: .default)) + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + if presentedViewController != nil { + Current.Log.info("Navigation error alert not shown because another view is already presented") + return + } + present(alert, animated: true) + } + } + // WKUIDelegate func webView( _ webView: WKWebView, diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 03d9e7085..62c6cea15 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -236,6 +236,12 @@ public enum L10n { public static var title: String { return L10n.tr("Localizable", "alerts.deprecations.notification_category.title") } } } + public enum NavigationError { + /// This page cannot be displayed because it's outside your Home Assistant server or the page was not found. + public static var message: String { return L10n.tr("Localizable", "alerts.navigation_error.message") } + /// Navigation Error + public static var title: String { return L10n.tr("Localizable", "alerts.navigation_error.title") } + } public enum OpenUrlFromDeepLink { /// Open URL (%@) from deep link? public static func message(_ p1: Any) -> String {