NavigationDelegate.swift (7039B)
1 // This Source Code Form is subject to the terms of the Mozilla Public 2 // License, v. 2.0. If a copy of the MPL was not distributed with this 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 public enum LoadRequestTarget { 6 /// The load is targeted to complete within the current `GeckoSession`. 7 case current 8 /// The load is targeted to complete within a new `GeckoSession`. 9 case new 10 } 11 12 public struct LoadRequest { 13 /// The URI to be loaded. 14 public let uri: String 15 16 /// The URI of the origin page that triggered the load request. 17 /// 18 /// `nil` for initial loads, and loads originating from `data:` URIs. 19 public let triggerUri: String? 20 21 /// The target where the window has requested to open. 22 public let target: LoadRequestTarget 23 24 /// True if and only if the request was triggered by an HTTP redirect. 25 /// 26 /// If the user loads URI "a", which redirects to URI "b", then 27 /// `onLoadRequest` will be called twice, first with uri "a" and `isRedirect 28 /// = false`, then with uri "b" and `isRedirect = true`. 29 public let isRedirect: Bool 30 31 /// True if there was an active user gesture when the load was requested. 32 public let hasUserGesture: Bool 33 34 /// This load request was initiated by a direct navigation from the 35 /// application. E.g. when calling `GeckoSession.load`. 36 public let isDirectNavigation: Bool 37 } 38 39 public protocol NavigationDelegate { 40 /// A view has started loading content from the network. 41 func onLocationChange(session: GeckoSession, url: String?, permissions: [ContentPermission]) 42 43 /// The view's ability to go back has changed. 44 func onCanGoBack(session: GeckoSession, canGoBack: Bool) 45 46 /// The view's ability to go forward has changed. 47 func onCanGoForward(session: GeckoSession, canGoForward: Bool) 48 49 /// A request to open an URI. This is called before each top-level page load 50 /// to allow custom behavior. For example, this can be used to override the 51 /// behavior of TAGET_WINDOW_NEW requests, which defaults to requesting a 52 /// new GeckoSession via onNewSession. 53 /// 54 /// Returns an `AllowOrDeny` which indicates whether or not the load was 55 /// handled. If unhandled, Gecko will continue the load as normal. If 56 /// handled (a `deny` value), Gecko will abandon the load. 57 func onLoadRequest(session: GeckoSession, request: LoadRequest) async -> AllowOrDeny 58 59 /// A request to load a URI in a non-top-level context. 60 /// 61 /// Returns an `AllowOrDeny` which indicates whether or not the load was 62 /// handled. If unhandled, Gecko will continue the load as normal. If 63 /// handled (a `deny` value), Gecko will abandon the load. 64 func onSubframeLoadRequest(session: GeckoSession, request: LoadRequest) async -> AllowOrDeny 65 66 /// A request has been made to open a new session. The URI is provided only for informational 67 /// purposes. Do not call GeckoSession.load here. Additionally, the returned GeckoSession must be 68 /// a newly-created one. 69 /// 70 /// If nil is returned, the request for the request for a new window by web 71 /// content will fail. e.g., `window.open()` will return null. The 72 /// implementation of onNewSession is responsible for maintaining a 73 /// reference to the returned object, to prevent it from being destroyed. 74 func onNewSession(session: GeckoSession, uri: String) async -> GeckoSession? 75 76 /// A load error has occurred. 77 /// 78 /// The returned string is a URI to display as an error. Returning `nil` 79 /// will halt the load entirely. 80 /// 81 /// The following special methods are made available to the URI: 82 /// 83 /// - document.addCertException(isTemporary), returns Promise 84 /// - document.getFailedCertSecurityInfo(), returns FailedCertSecurityInfo 85 /// - document.getNetErrorInfo(), returns NetErrorInfo 86 /// - document.reloadWithHttpsOnlyException() 87 // FIXME: Implement onLoadError & WebRequestError 88 // func onLoadError(session: GeckoSession, uri: String?, error: WebRequestError) -> String? 89 } 90 91 enum NavigationEvents: String, CaseIterable { 92 case locationChange = "GeckoView:LocationChange" 93 case onNewSession = "GeckoView:OnNewSession" 94 case onLoadError = "GeckoView:OnLoadError" 95 case onLoadRequest = "GeckoView:OnLoadRequest" 96 } 97 98 func newNavigationHandler(_ session: GeckoSession) -> GeckoSessionHandler< 99 NavigationDelegate, NavigationEvents 100 > { 101 GeckoSessionHandler(moduleName: "GeckoViewNavigation", session: session) { 102 @MainActor session, delegate, event, message in 103 switch event { 104 case .locationChange: 105 if message!["isTopLevel"] as! Bool { 106 let permissions = message!["permissions"] as? [[String: Any?]] 107 delegate?.onLocationChange( 108 session: session, 109 url: message!["uri"] as? String, 110 permissions: permissions?.map(ContentPermission.fromDictionary) ?? []) 111 } 112 delegate?.onCanGoBack(session: session, canGoBack: message!["canGoBack"] as! Bool) 113 delegate?.onCanGoForward( 114 session: session, canGoForward: message!["canGoForward"] as! Bool) 115 return nil 116 117 case .onNewSession: 118 let newSessionId = message!["newSessionId"] as! String 119 if let result = await delegate?.onNewSession( 120 session: session, uri: message!["uri"] as! String) 121 { 122 assert(result.isOpen()) 123 result.open(windowId: newSessionId) 124 return true 125 } else { 126 return false 127 } 128 129 case .onLoadError: 130 let uri = message!["uri"] as! String 131 let errorCode = message!["error"] as! Int64 132 let errorModule = message!["errorModule"] as! Int32 133 let errorClass = message!["errorClass"] as! Int32 134 return nil 135 136 case .onLoadRequest: 137 func convertTarget(_ target: Int32) -> LoadRequestTarget { 138 switch target { 139 case 0: // OPEN_DEFAULTWINDOW 140 return .current 141 case 1: // OPEN_CURRENTWINDOW 142 return .current 143 default: // OPEN_NEWWINDOW, OPEN_NEWTAB 144 return .new 145 } 146 } 147 148 // Match with nsIWebNavigation.idl. 149 let LOAD_REQUEST_IS_REDIRECT = 0x800000 150 151 let loadRequest = LoadRequest( 152 uri: message!["uri"] as! String, 153 triggerUri: message!["triggerUri"] as? String, 154 target: convertTarget(message!["where"] as! Int32), 155 isRedirect: ((message!["flags"] as! Int) & LOAD_REQUEST_IS_REDIRECT) != 0, 156 hasUserGesture: message!["hasUserGesture"] as! Bool, 157 isDirectNavigation: true) 158 159 let result = await delegate?.onLoadRequest(session: session, request: loadRequest) 160 return result == .allow 161 } 162 } 163 }