|
| 1 | +# Background |
| 2 | + |
| 3 | +We are exposing an event that will be raised when an attempt to launch a protocol that is registered with the OS (external protocol) is made. |
| 4 | +When navigating to a URI, the URI scheme determines how to handle the URI. |
| 5 | +Some schemes like http, and https, are resolved by WebView2 and the navigation is handled in the WebView2. |
| 6 | +Other URI schemes may be registered externally to the WebView2 with the OS by other applications. |
| 7 | +Such schemes are external protocols and are handled by launching the registered application with the URI. |
| 8 | + |
| 9 | +The host will be given the option to cancel the external protocol launch with the `LaunchingExternalProtocol` event. |
| 10 | +Cancelling the launch gives the host the opportunity to hide the default dialog, display a custom dialog, and then launch the external protocol themselves. |
| 11 | + |
| 12 | +# Description |
| 13 | + |
| 14 | +This event will be raised before the external protocol launch occurs. |
| 15 | +When an attempt to launch an external protocol is made, the default dialog is displayed in which the user can select `Open` or `Cancel` if the host does not cancel the event. |
| 16 | +The `NavigationStarting` event will be raised before the `LaunchingExternalProtocol` event, followed by the `NavigationCompleted` event. |
| 17 | +The `SourceChanged`, `ContentLoading`, and `HistoryChanged` events will not be raised when a request is made to launch an external protocol. |
| 18 | + |
| 19 | +The `LaunchingExternalProtocol` event will be raised on the `CoreWebView2` interface. |
| 20 | + |
| 21 | +# Examples |
| 22 | + |
| 23 | +## Win32 C++ |
| 24 | + |
| 25 | +```cpp |
| 26 | + |
| 27 | +AppWindow* m_appWindow; |
| 28 | +wil::com_ptr<ICoreWebView2> m_webView; |
| 29 | +EventRegistrationToken m_launchingExternalProtocolToken = {}; |
| 30 | + |
| 31 | +void RegisterLaunchingExternalProtocolHandler() |
| 32 | +{ |
| 33 | + auto webView16 = m_webView.try_query<ICoreWebView2_16>(); |
| 34 | + if (webView16) |
| 35 | + { |
| 36 | + CHECK_FAILURE(webView16->add_LaunchingExternalProtocol( |
| 37 | + Callback<ICoreWebView2LaunchingExternalProtocolEventHandler>( |
| 38 | + [this]( |
| 39 | + ICoreWebView2* sender, |
| 40 | + ICoreWebView2LaunchingExternalProtocolEventArgs* args) |
| 41 | + { |
| 42 | + auto showDialog = [this, args] |
| 43 | + { |
| 44 | + // Set the `Cancel` property to `TRUE`, as we will either silently launch the |
| 45 | + // trusted app or display a custom dialog. |
| 46 | + args->put_Cancel(true); |
| 47 | + wil::unique_cotaskmem_string uri; |
| 48 | + CHECK_FAILURE(args->get_Uri(&uri)); |
| 49 | + if (wcsicmp(uri.get(), L"calculator://") == 0) |
| 50 | + { |
| 51 | + // If this matches our desired protocol, launch the |
| 52 | + // calculator app. |
| 53 | + std::wstring protocol_url = L"calculator://"; |
| 54 | + SHELLEXECUTEINFO info = {sizeof(info)}; |
| 55 | + info.fMask = SEE_MASK_NOASYNC; |
| 56 | + info.lpVerb = L"open"; |
| 57 | + info.lpFile = protocol_url.c_str(); |
| 58 | + info.nShow = SW_SHOWNORMAL; |
| 59 | + ::ShellExecuteEx(&info); |
| 60 | + } |
| 61 | + else |
| 62 | + { |
| 63 | + // To display a custom dialog we cancel the launch, display |
| 64 | + // a custom dialog, and then manually launch the external protocol. |
| 65 | + wil::unique_cotaskmem_string initiating_origin; |
| 66 | + CHECK_FAILURE(args->get_InitiatingOrigin(&initiating_origin)); |
| 67 | + std::wstring message = L"Launching External Protocol request"; |
| 68 | + if (initiating_origin.get() == L"") |
| 69 | + { |
| 70 | + message += L"from "; |
| 71 | + message += initiating_origin.get(); |
| 72 | + } |
| 73 | + message += L" to "; |
| 74 | + message += uri.get(); |
| 75 | + message += L"?\n\n"; |
| 76 | + message += L"Do you want to grant permission?\n"; |
| 77 | + int response = MessageBox( |
| 78 | + nullptr, message.c_str(), L"Launching External Protocol", |
| 79 | + MB_YESNOCANCEL | MB_ICONWARNING); |
| 80 | + if (response == IDYES) |
| 81 | + { |
| 82 | + std::wstring protocol_url = uri.get(); |
| 83 | + SHELLEXECUTEINFO info = {sizeof(info)}; |
| 84 | + info.fMask = SEE_MASK_NOASYNC; |
| 85 | + info.lpVerb = L"open"; |
| 86 | + info.lpFile = protocol_url.c_str(); |
| 87 | + info.nShow = SW_SHOWNORMAL; |
| 88 | + ::ShellExecuteEx(&info); |
| 89 | + } |
| 90 | + } |
| 91 | + return S_OK; |
| 92 | + }; |
| 93 | + wil::com_ptr<ICoreWebView2Deferral> deferral; |
| 94 | + CHECK_FAILURE(args->GetDeferral(&deferral)); |
| 95 | + |
| 96 | + m_appWindow->RunAsync( |
| 97 | + [deferral, showDialog]() |
| 98 | + { |
| 99 | + showDialog(); |
| 100 | + CHECK_FAILURE(deferral->Complete()); |
| 101 | + }); |
| 102 | + return S_OK; |
| 103 | + }) |
| 104 | + .Get(), |
| 105 | + &m_launchingExternalProtocolToken)); |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +``` |
| 110 | + |
| 111 | +## .NET and WinRT |
| 112 | + |
| 113 | +```c# |
| 114 | +private WebView2 webView; |
| 115 | +void RegisterLaunchingExternalProtocolHandler() |
| 116 | +{ |
| 117 | + webView.CoreWebView2.LaunchingExternalProtocol += (sender, args) { |
| 118 | + { |
| 119 | + CoreWebView2Deferral deferral = args.GetDeferral(); |
| 120 | + System.Threading.SynchronizationContext.Current.Post((_) => |
| 121 | + { |
| 122 | + using (deferral) |
| 123 | + { |
| 124 | + // Set the `Cancel` property to `TRUE`, as we will either silently launch the |
| 125 | + // trusted app or display a custom dialog. |
| 126 | + args.Cancel = true; |
| 127 | + if (String.Equals(args.Uri, "calculator:///", StringComparison.OrdinalIgnoreCase)) |
| 128 | + { |
| 129 | + // If this matches our desired protocol, then set the |
| 130 | + // event args to cancel the event and launch the |
| 131 | + // calculator app. |
| 132 | + ProcessStartInfo info = new ProcessStartInfo |
| 133 | + { |
| 134 | + FileName = args.Uri, |
| 135 | + UseShellExecute = true |
| 136 | + }; |
| 137 | + Process.Start(info); |
| 138 | + } |
| 139 | + else |
| 140 | + { |
| 141 | + // To display a custom dialog we cancel the launch, display |
| 142 | + // a custom dialog, and then manually launch the external protocol. |
| 143 | + string text = "Launching External Protocol"; |
| 144 | + if (args.InitiatingOrigin != "") |
| 145 | + { |
| 146 | + text += "from "; |
| 147 | + text += args.InitiatingOrigin; |
| 148 | + } |
| 149 | + text += " to "; |
| 150 | + text += args.Uri; |
| 151 | + text += "\n"; |
| 152 | + text += "Do you want to grant permission?"; |
| 153 | + string caption = "Launching External Protocol request"; |
| 154 | + MessageBoxButton btnMessageBox = MessageBoxButton.YesNoCancel; |
| 155 | + MessageBoxImage icnMessageBox = MessageBoxImage.None; |
| 156 | + MessageBoxResult resultbox = MessageBox.Show(text, caption, btnMessageBox, icnMessageBox); |
| 157 | + switch (resultbox) |
| 158 | + { |
| 159 | + case MessageBoxResult.Yes: |
| 160 | + ProcessStartInfo info = new ProcessStartInfo |
| 161 | + { |
| 162 | + FileName = args.Uri, |
| 163 | + UseShellExecute = true |
| 164 | + }; |
| 165 | + Process.Start(info); |
| 166 | + break; |
| 167 | + |
| 168 | + case MessageBoxResult.No: |
| 169 | + break; |
| 170 | + |
| 171 | + case MessageBoxResult.Cancel: |
| 172 | + break; |
| 173 | + } |
| 174 | + |
| 175 | + } |
| 176 | + |
| 177 | + } |
| 178 | + }, null); |
| 179 | + } |
| 180 | + }; |
| 181 | +} |
| 182 | + |
| 183 | +``` |
| 184 | + |
| 185 | +# API Notes |
| 186 | + |
| 187 | +See [API Details](#api-details) section below for API reference. |
| 188 | + |
| 189 | +# API Details |
| 190 | + |
| 191 | +## Win32 C++ |
| 192 | + |
| 193 | +```IDL |
| 194 | +// This is the ICoreWebView2_16 interface. |
| 195 | +[uuid(cc39bea3-d6d8-471b-919f-da253e2fbf03), object, pointer_default(unique)] |
| 196 | +interface ICoreWebView2_16 : ICoreWebView2_15 { |
| 197 | + /// Add an event handler for the `LaunchingExternalProtocol` event. |
| 198 | + /// The `LaunchingExternalProtocol` event is raised when a launch request is made to |
| 199 | + /// a protocol that is registered with the OS. |
| 200 | + /// The `LaunchingExternalProtocol` event may suppress the default dialog |
| 201 | + /// or replace the default dialog with a custom dialog. |
| 202 | + /// |
| 203 | + /// If a deferral is not taken on the event args, the external protocol launch is |
| 204 | + /// blocked until the event handler returns. If a deferral is taken, the |
| 205 | + /// external protocol launch is blocked until the deferral is completed. |
| 206 | + /// The host also has the option to cancel the protocol launch. |
| 207 | + /// |
| 208 | + /// The `NavigationStarting` and `NavigationCompleted` events will be raised, |
| 209 | + /// regardless of whether the `Cancel` property is set to `TRUE` or |
| 210 | + /// `FALSE`. The `SourceChanged`, `ContentLoading`, and `HistoryChanged` events |
| 211 | + /// will not be raised regardless of this property. |
| 212 | + /// The `LaunchingExternalProtocol` event will be raised after the |
| 213 | + /// `NavigationStarting` event and before the `NavigationCompleted` event. |
| 214 | + /// The default settings will also be updated upon navigation to an external |
| 215 | + /// protocol. |
| 216 | + /// |
| 217 | + /// If the request is made from a trustworthy origin |
| 218 | + /// (https://w3c.github.io/webappsec-secure-contexts#potentially-trustworthy-origin) |
| 219 | + /// a checkmark box will be displayed on the default browser UI that gives the user |
| 220 | + /// the option to always allow the external protocol to launch from this origin. |
| 221 | + /// If the user checks this box, upon the next request from that origin to the |
| 222 | + /// protocol, the event will still be raised, but there will be no default dialog shown |
| 223 | + /// when `Cancel` is `FALSE`. |
| 224 | + /// |
| 225 | + /// If the request is initiated by a cross-origin iframe without a user gesture, |
| 226 | + /// the request will be blocked and the `LaunchingExternalProtocol` event will not |
| 227 | + /// be raised. |
| 228 | + /// \snippet SettingsComponent.cpp LaunchingExternalProtocol |
| 229 | + HRESULT add_LaunchingExternalProtocol( |
| 230 | + [in] ICoreWebView2LaunchingExternalProtocolEventHandler* eventHandler, |
| 231 | + [out] EventRegistrationToken* token); |
| 232 | +
|
| 233 | + /// Remove an event handler previously added with |
| 234 | + /// `add_LaunchingExternalProtocol`. |
| 235 | + HRESULT remove_LaunchingExternalProtocol( |
| 236 | + [in] EventRegistrationToken token); |
| 237 | + } |
| 238 | +
|
| 239 | +/// Receives the `LaunchingExternalProtocol` event. |
| 240 | +[uuid(e5fea648-79c9-47aa-8314-f471fe627649), object, pointer_default(unique)] |
| 241 | +interface ICoreWebView2LaunchingExternalProtocolEventHandler: IUnknown { |
| 242 | + /// Provides the event args for the corresponding event. |
| 243 | + HRESULT Invoke( |
| 244 | + [in] ICoreWebView2* sender, |
| 245 | + [in] ICoreWebView2LaunchingExternalProtocolEventArgs* args); |
| 246 | +} |
| 247 | +
|
| 248 | +/// Event args for `LaunchingExternalProtocol` event. |
| 249 | +[uuid(fc43b557-9713-4a67-af8d-a76ef3a206e8), object, pointer_default(unique)] |
| 250 | +interface ICoreWebView2LaunchingExternalProtocolEventArgs: IUnknown { |
| 251 | + /// The URI with the external protocol to be launched. |
| 252 | +
|
| 253 | + [propget] HRESULT Uri([out, retval] LPWSTR* value); |
| 254 | +
|
| 255 | + /// The origin initiating the external protocol launch. |
| 256 | + /// The origin will be empty if the WebView2 navigated to the external protocol. |
| 257 | +
|
| 258 | + [propget] HRESULT InitiatingOrigin([out, retval] LPWSTR* value); |
| 259 | +
|
| 260 | + /// `TRUE` when the external protocol request was initiated through a user gesture. |
| 261 | + /// |
| 262 | + /// \> [!NOTE]\n\> Being initiated through a user gesture does not mean that user intended |
| 263 | + /// to access the associated resource. |
| 264 | +
|
| 265 | + [propget] HRESULT IsUserInitiated([out, retval] BOOL* value); |
| 266 | +
|
| 267 | + /// `TRUE` when the external protocol request was initiated via a non-main frame that |
| 268 | + /// has a different origin then the owning top-level page. When this property is `TRUE` |
| 269 | + /// this may indicate a potential security issue and it is advised to block this request. |
| 270 | +
|
| 271 | + [propget] HRESULT IsCrossOriginIframe([out, retval] BOOL* value); |
| 272 | +
|
| 273 | + /// The host may set this flag to cancel the external protocol launch. If set to |
| 274 | + /// `TRUE`, the external protocol will not be launched, and the default |
| 275 | + /// dialog is not displayed. This property can be used to replace the normal |
| 276 | + /// handling of launching external protocols. |
| 277 | +
|
| 278 | + [propget] HRESULT Cancel([out, retval] BOOL* value); |
| 279 | +
|
| 280 | + /// Sets the `Cancel` property. The default value is `FALSE`. |
| 281 | +
|
| 282 | + [propput] HRESULT Cancel([in] BOOL value); |
| 283 | +
|
| 284 | + /// Returns an `ICoreWebView2Deferral` object. Use this operation to |
| 285 | + /// complete the event at a later time. |
| 286 | +
|
| 287 | + HRESULT GetDeferral([out, retval] ICoreWebView2Deferral** value); |
| 288 | +} |
| 289 | +
|
| 290 | +``` |
| 291 | +## .NET and WinRT |
| 292 | + |
| 293 | +```c# |
| 294 | +namespace Microsoft.Web.WebView2.Core |
| 295 | +{ |
| 296 | + runtimeclass CoreWebView2LaunchingExternalProtocolEventArgs; |
| 297 | + |
| 298 | + runtimeclass CoreWebView2LaunchingExternalProtocolEventArgs |
| 299 | + { |
| 300 | + // CoreWebView2LaunchingExternalProtocolEventArgs members |
| 301 | + String Uri { get; }; |
| 302 | + String InitiatingOrigin { get; }; |
| 303 | + Boolean IsUserInitiated { get; }; |
| 304 | + Boolean IsCrossOriginIframe { get; }; |
| 305 | + Boolean Cancel { get; set; }; |
| 306 | + Windows.Foundation.Deferral GetDeferral(); |
| 307 | + } |
| 308 | + |
| 309 | + runtimeclass CoreWebView2 |
| 310 | + { |
| 311 | + // CoreWebView2 |
| 312 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2_16")] |
| 313 | + { |
| 314 | + event Windows.Foundation.TypedEventHandler<CoreWebView2, CoreWebView2LaunchingExternalProtocolEventArgs> LaunchingExternalProtocol; |
| 315 | + } |
| 316 | + } |
| 317 | +} |
| 318 | +``` |
0 commit comments