diff --git a/mac/Config/Config/PackageContentWebView.swift b/mac/Config/Config/PackageContentWebView.swift index ecc12ed1a35..305d2d736fc 100644 --- a/mac/Config/Config/PackageContentWebView.swift +++ b/mac/Config/Config/PackageContentWebView.swift @@ -13,6 +13,7 @@ import WebKit import KeymanSettings public struct PackageContentWebView: NSViewRepresentable { + let packageFileUrl: URL // create the AppKit view instance @@ -21,12 +22,18 @@ public struct PackageContentWebView: NSViewRepresentable { // Connect the delegate to catch link clicks webView.navigationDelegate = context.coordinator - + return webView } - // update the view when SwiftUI state changes + /** + * update the view when SwiftUI state changes + */ public func updateNSView(_ nsView: WKWebView, context: Context) { + // first update the parent in the coordinator so we know which + // package we are displaying content for + context.coordinator.parent = self + let request = URLRequest(url: packageFileUrl) // only load the request if it's not already loading/loaded to prevent infinite loops @@ -41,19 +48,27 @@ public struct PackageContentWebView: NSViewRepresentable { * Coordinator acts as the WKNavigationDelegate */ public func makeCoordinator() -> Coordinator { - Coordinator() + Coordinator(self) } - /** - * If a url links to the web rather than locally, open it in the default browser - */ @MainActor public class Coordinator: NSObject, WKNavigationDelegate { + var parent: PackageContentWebView + + init(_ parent: PackageContentWebView) { + self.parent = parent + } + + /** + * If a url links to the web rather than locally, open it in the default browser + * Must force load links in the package content that change the main frame + * because WKWebView's security sandbox will block loading of new pages + */ public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) { - - // if not url, cancel + + // if no url, cancel guard let url = navigationAction.request.url else { decisionHandler(.cancel) return @@ -61,30 +76,45 @@ public struct PackageContentWebView: NSViewRepresentable { // if not user-activated, pass through, e.g. for redirects guard navigationAction.navigationType == .linkActivated else { - decisionHandler(.allow) - return + print("url not user activated \(url.path())") + decisionHandler(.allow) + return } - - // local files load in webview + + // load local files in webview, force-loading files that are in the same directory if url.isFileURL { - decisionHandler(.allow) + let standardizedIncomingUrl = url.standardizedFileURL + let packageDirectoryUrl = parent.packageFileUrl.deletingLastPathComponent() + let standardizedPackageDirectoryUrl = packageDirectoryUrl.standardizedFileURL + + // check whether the file is from the current package + if standardizedIncomingUrl.path.hasPrefix(standardizedPackageDirectoryUrl.path) { + decisionHandler(.cancel) // Cancel regular navigation + + // Force load with read permissions of package directory + webView.loadFileURL(standardizedIncomingUrl, allowingReadAccessTo: standardizedPackageDirectoryUrl) return + } else { + // block local files outside of your target directory, not sure how we would receive one + decisionHandler(.cancel) + return + } } - - // handle external links by opening in web browser + + // not a file URL: handle external links by opening in web browser decisionHandler(.cancel) - + var externalUrl = url - + // strip "link:" prefix if present let urlString = url.absoluteString if urlString.hasPrefix("link:https://") || urlString.hasPrefix("link:http://") { - let cleanString = urlString.replacingOccurrences(of: "link:", with: "") - if let cleanUrl = URL(string: cleanString) { - externalUrl = cleanUrl - } + let cleanString = urlString.replacingOccurrences(of: "link:", with: "") + if let cleanUrl = URL(string: cleanString) { + externalUrl = cleanUrl + } } - + // Open the external link in the default browser NSWorkspace.shared.open(externalUrl) }