/** * Copyright (c) 2021 Vuplex Inc. All rights reserved. * * Licensed under the Vuplex Commercial Software Library License, you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * https://vuplex.com/commercial-library-license * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using UnityEngine; #if NET_4_6 || NET_STANDARD_2_0 using System.Threading.Tasks; #endif namespace Vuplex.WebView { /// /// The base `IWebView` implementation used by 3D WebView for Windows and macOS. /// This class also includes extra methods for Standalone-specific functionality. /// public abstract class StandaloneWebView : BaseWebView, IWithDownloads, IWithKeyDownAndUp, IWithKeyModifiers, IWithMovablePointer, IWithPointerDownAndUp, IWithPopups { /// /// Event raised if a server requests HTTP authentication. /// /// /// If no handler is attached to this event, then the host's authentication request will be ignored /// and the page will not be paused. If a handler is attached to this event, then the page will /// be paused until `AuthRequestedEventArgs.Continue()` or `Cancel()` is called. /// /// You can test basic HTTP auth using [this page](https://jigsaw.w3.org/HTTP/Basic/) /// with the username "guest" and the password "guest". /// public event EventHandler AuthRequested { add { if (_authRequestedHandler != null) { throw new InvalidOperationException("AuthRequested supports only one event handler. Please remove the existing handler before adding a new one."); } _authRequestedHandler = value; WebView_setAuthEnabled(_nativeWebViewPtr, true); } remove { if (_authRequestedHandler == value) { _authRequestedHandler = null; WebView_setAuthEnabled(_nativeWebViewPtr, false); } } } /// public event EventHandler DownloadProgressChanged; /// public event EventHandler PopupRequested; public static new void ClearAllData() { var pluginIsInitialized = WebView_pluginIsInitialized(); if (pluginIsInitialized) { _throwAlreadyInitializedException("clear the browser data", "ClearAllData"); } var cachePath = _getCachePath(); if (Directory.Exists(cachePath)) { Directory.Delete(cachePath, true); } } public override void Copy() { _assertValidState(); WebView_copy(_nativeWebViewPtr); } public override void Cut() { _assertValidState(); WebView_cut(_nativeWebViewPtr); } /// /// Enables remote debugging with Chrome DevTools on the given port. /// Note that this method can only be called prior to /// initializing any webviews. /// /// /// - For example, if you provide 8080 as the `portNumber`, you can navigate to /// `http://localhost:8080 `in Chrome to see a list of webviews to inspect. /// - For more information on debugging, please see /// [this support article](https://support.vuplex.com/articles/how-to-debug-web-content#standalone). /// /// Port number in the range 1024 - 65535. public static void EnableRemoteDebugging(int portNumber) { if (!(1024 <= portNumber && portNumber <= 65535)) { throw new ArgumentException(string.Format("The given port number ({0}) is not in the range from 1024 to 65535.", portNumber)); } var success = WebView_enableRemoteDebugging(portNumber); if (!success) { _throwAlreadyInitializedException("enable remote debugging", "EnableRemoteDebugging"); } } #if NET_4_6 || NET_STANDARD_2_0 /// /// Gets the cookie that matches the given URL and cookie name, or /// null if no cookie matches. /// Note that if a cookie's domain includes a leading dot /// to denote it matches any subdomain (e.g. `\"https://.vuplex.com\"`), /// then the URL parameter must include the leading dot in order to match. /// /// /// This method can only be called after /// one or more webviews have been initialized. /// public static Task GetCookie(string url, string cookieName) { var task = new TaskCompletionSource(); GetCookie(url, cookieName, task.SetResult); return task.Task; } #endif /// /// Like the other version of `GetCookie()`, except it uses a callback /// instead of a `Task` in order to be compatible with legacy .NET. /// public static void GetCookie(string url, string cookieName, Action callback) { var pluginIsInitialized = WebView_pluginIsInitialized(); if (!pluginIsInitialized) { throw new InvalidOperationException("On Windows and macOS, GetCookie() can only be called when the Chromium process is running (i.e. after a webview is initialized)."); } var resultCallbackId = Guid.NewGuid().ToString(); _pendingGetCookieResultCallbacks[resultCallbackId] = callback; WebView_getCookie(url, cookieName, resultCallbackId); } #if NET_4_6 || NET_STANDARD_2_0 /// /// Gets all of the cookies that match the given URL. /// Note that if a cookie's domain includes a leading dot /// to denote it matches any subdomain (e.g. `\"https://.vuplex.com\"`), /// then the URL parameter must include the leading dot in order to match. /// /// /// This method can only be called after /// one or more webviews have been initialized. /// public static Task GetCookies(string url) { var task = new TaskCompletionSource(); GetCookies(url, task.SetResult); return task.Task; } #endif /// /// Like the other version of `GetCookies()`, except it uses a callback /// instead of a `Task` in order to be compatible with legacy .NET. /// public static void GetCookies(string url, Action callback) { var pluginIsInitialized = WebView_pluginIsInitialized(); if (!pluginIsInitialized) { throw new InvalidOperationException("On Windows and macOS, GetCookies() can only be called when the Chromium process is running (i.e. after a webview is initialized)."); } var resultCallbackId = Guid.NewGuid().ToString(); _pendingGetCookiesResultCallbacks[resultCallbackId] = callback; WebView_getCookies(url, resultCallbackId); } [Obsolete("The IWithKeyModifiers interface is now deprecated. Please use the IWithKeyDownAndUp interface instead.")] public void HandleKeyboardInput(string key, KeyModifier modifiers) { KeyDown(key, modifiers); KeyUp(key, modifiers); } public override void Init(Texture2D viewportTexture, float width, float height, Texture2D videoTexture) { base.Init(viewportTexture, width, height, videoTexture); _nativeWebViewPtr = WebView_new(gameObject.name, _nativeWidth, _nativeHeight, null); if (_nativeWebViewPtr == IntPtr.Zero) { throw new WebViewUnavailableException("Failed to instantiate a new webview. This could indicate that you're using an expired trial version of 3D WebView."); } } /// public void KeyDown(string key, KeyModifier modifiers) { _assertValidState(); WebView_keyDown(_nativeWebViewPtr, key, (int)modifiers); } /// public void KeyUp(string key, KeyModifier modifiers) { _assertValidState(); WebView_keyUp(_nativeWebViewPtr, key, (int)modifiers); } /// public void MovePointer(Vector2 point) { _assertValidState(); int nativeX = (int) (point.x * _nativeWidth); int nativeY = (int) (point.y * _nativeHeight); WebView_movePointer(_nativeWebViewPtr, nativeX, nativeY); } public override void Paste() { _assertValidState(); WebView_paste(_nativeWebViewPtr); } /// public void PointerDown(Vector2 point) { _pointerDown(point, MouseButton.Left, 1); } /// public void PointerDown(Vector2 point, PointerOptions options) { if (options == null) { options = new PointerOptions(); } _pointerDown(point, options.Button, options.ClickCount); } /// public void PointerUp(Vector2 point) { _pointerUp(point, MouseButton.Left, 1); } /// public void PointerUp(Vector2 point, PointerOptions options) { if (options == null) { options = new PointerOptions(); } _pointerUp(point, options.Button, options.ClickCount); } public override void SelectAll() { _assertValidState(); WebView_selectAll(_nativeWebViewPtr); } /// /// By default, web pages cannot access the device's /// camera or microphone via JavaScript. /// Invoking `SetAudioAndVideoCaptureEnabled(true)` allows /// **all web pages** to access the camera and microphone. /// /// /// This is useful, for example, to enable WebRTC support. /// This method can only be called prior to initializing any webviews. /// public static void SetAudioAndVideoCaptureEnabled(bool enabled) { var success = WebView_setAudioAndVideoCaptureEnabled(enabled); if (!success) { _throwAlreadyInitializedException("enable audio and video capture", "SetAudioAndVideoCaptureEnabled"); } } /// /// Sets additional command line arguments to pass to Chromium. /// /// /// [Here's an unofficial list of Chromium command line arguments](https://peter.sh/experiments/chromium-command-line-switches/). /// This method can only be called prior to initializing any webviews. /// /// /// StandaloneWebView.SetCommandLineArguments("--ignore-certificate-errors --disable-web-security"); /// public static void SetCommandLineArguments(string args) { var success = WebView_setCommandLineArguments(args); if (!success) { _throwAlreadyInitializedException("set command line arguments", "SetCommandLineArguments"); } } #if NET_4_6 || NET_STANDARD_2_0 /// /// Sets the given cookie and returns a `Task>bool>` indicating /// whether the cookie was set successfully. /// /// /// If setting the cookie fails, it could be because the data in the provided Cookie /// was malformed. For more info regarding the failure, check the logs. /// This method can only be called after one or more webviews have been initialized. /// /// /// var success = await StandaloneWebView.SetCookie(new Cookie { /// Domain = "vuplex.com", /// Path = "/", /// Name = "example_name", /// Value = "example_value" /// }); /// public static Task SetCookie(Cookie cookie) { var task = new TaskCompletionSource(); SetCookie(cookie, task.SetResult); return task.Task; } #endif /// /// Like the other version of `SetCookie()`, except it uses a callback /// instead of a `Task` in order to be compatible with legacy .NET. /// public static void SetCookie(Cookie cookie, Action callback) { var pluginIsInitialized = WebView_pluginIsInitialized(); if (!pluginIsInitialized) { throw new InvalidOperationException("On Windows and macOS, SetCookie() can only be called when the Chromium process is running (i.e. after a webview is initialized)."); } if (cookie == null) { throw new ArgumentException("Cookie cannot be null."); } if (!cookie.IsValid) { throw new ArgumentException("Cannot set invalid cookie: " + cookie); } var resultCallbackId = Guid.NewGuid().ToString(); _pendingSetCookieResultCallbacks[resultCallbackId] = callback; WebView_setCookie(cookie.ToJson(), resultCallbackId); } /// public void SetDownloadsEnabled(bool enabled) { _assertValidState(); var downloadsDirectoryPath = enabled ? Path.Combine(Application.temporaryCachePath, Path.Combine("Vuplex.WebView", "downloads")) : ""; WebView_setDownloadsEnabled(_nativeWebViewPtr, downloadsDirectoryPath); } public static void SetIgnoreCertificateErrors(bool ignore) { var success = WebView_setIgnoreCertificateErrors(ignore); if (!success) { _throwAlreadyInitializedException("ignore certificate errors", "SetIgnoreCertificateErrors"); } } /// /// By default, the native file picker for file input elements is disabled, /// but it can be enabled with this method. /// public void SetNativeFileDialogEnabled(bool enabled) { _assertValidState(); WebView_setNativeFileDialogEnabled(_nativeWebViewPtr, enabled); } public void SetPopupMode(PopupMode popupMode) { WebView_setPopupMode(_nativeWebViewPtr, (int)popupMode); } /// /// By default, web pages cannot share the device's screen /// via JavaScript. Invoking `SetScreenSharingEnabled(true)` allows /// **all web pages** to share the screen. /// /// /// The screen that is shared is the default screen, and there isn't currently /// support for sharing a different screen or a specific application window. /// This is a limitation of Chromium Embedded Framework (CEF), which 3D WebView /// uses to embed Chromium. Also, this method can only be called prior to /// initializing any webviews. /// public static void SetScreenSharingEnabled(bool enabled) { var success = WebView_setScreenSharingEnabled(enabled); if (!success) { _throwAlreadyInitializedException("enable or disable screen sharing", "SetScreenSharingEnabled"); } } public static new void SetStorageEnabled(bool enabled) { var cachePath = enabled ? _getCachePath() : ""; var success = WebView_setCachePath(cachePath); if (!success) { _throwAlreadyInitializedException("enable or disable storage", "SetStorageEnabled"); } } /// /// Sets the target web frame rate. The default is `60`, which is also the maximum value. /// Specifying a target frame rate of `0` disables the frame rate limit. This method can only be called prior /// to initializing any webviews. /// public static void SetTargetFrameRate(uint targetFrameRate) { var success = WebView_setTargetFrameRate(targetFrameRate); if (!success) { _throwAlreadyInitializedException("set the target frame rate", "SetTargetFrameRate"); } } /// /// Sets the zoom level to the specified value. Specify `0.0` to reset the zoom level. /// public void SetZoomLevel(float zoomLevel) { _assertValidState(); WebView_setZoomLevel(_nativeWebViewPtr, zoomLevel); } public static void TerminatePlugin() { WebView_terminatePlugin(); } delegate void GetCookieCallback(string requestId, string serializedCookies); delegate void SetCookieCallback(string requestId, bool success); delegate void UnitySendMessageFunction(string gameObjectName, string methodName, string message); EventHandler _authRequestedHandler; static Dictionary> _pendingGetCookieResultCallbacks = new Dictionary>(); static Dictionary> _pendingGetCookiesResultCallbacks = new Dictionary>(); static Dictionary> _pendingSetCookieResultCallbacks = new Dictionary>(); protected static string _getCachePath() { // Only `Path.Combine(string, string)` is available in .NET 2.0. return Path.Combine(Application.persistentDataPath, Path.Combine("Vuplex.WebView", "chromium-cache")); } /// /// The native plugin invokes this method. /// void HandleAuthRequested(string host) { var handler = _authRequestedHandler; if (handler == null) { // This shouldn't happen. WebViewLogger.LogWarning("The native webview sent an auth request, but no event handler is attached to AuthRequested."); WebView_cancelAuth(_nativeWebViewPtr); return; } var eventArgs = new AuthRequestedEventArgs( host, (username, password) => WebView_continueAuth(_nativeWebViewPtr, username, password), () => WebView_cancelAuth(_nativeWebViewPtr) ); handler(this, eventArgs); } /// /// The native plugin invokes this method. /// void HandleDownloadProgressChanged(string serializedMessage) { var handler = DownloadProgressChanged; if (handler != null) { var message = DownloadMessage.FromJson(serializedMessage); handler(this, message.ToEventArgs()); } } [AOT.MonoPInvokeCallback(typeof(GetCookieCallback))] static void _handleGetCookieResult(string resultCallbackId, string serializedCookie) { var callback = _pendingGetCookieResultCallbacks[resultCallbackId]; _pendingGetCookieResultCallbacks.Remove(resultCallbackId); if (callback != null) { var cookie = Cookie.FromJson(serializedCookie); callback(cookie); } } [AOT.MonoPInvokeCallback(typeof(GetCookieCallback))] static void _handleGetCookiesResult(string resultCallbackId, string serializedCookies) { var callback = _pendingGetCookiesResultCallbacks[resultCallbackId]; _pendingGetCookiesResultCallbacks.Remove(resultCallbackId); var cookies = Cookie.ArrayFromJson(serializedCookies); callback(cookies); } /// /// The native plugin invokes this method. /// void HandlePopup(string message) { var handler = PopupRequested; if (handler == null) { return; } var components = message.Split(new char[] { ',' }, 2); var url = components[0]; var popupBrowserId = components[1]; if (popupBrowserId.Length == 0) { handler(this, new PopupRequestedEventArgs(url, null)); return; } var popupWebView = _instantiate(); Dispatcher.RunOnMainThread(() => { Web.CreateTexture(1, 1, texture => { // Use the same resolution and dimensions as the current webview. popupWebView.SetResolution(_numberOfPixelsPerUnityUnit); popupWebView._initPopup(texture, _width, _height, popupBrowserId); handler(this, new PopupRequestedEventArgs(url, popupWebView as IWebView)); }); }); } [AOT.MonoPInvokeCallback(typeof(SetCookieCallback))] static void _handleSetCookieResult(string resultCallbackId, bool success) { var callback = _pendingSetCookieResultCallbacks[resultCallbackId]; _pendingSetCookieResultCallbacks.Remove(resultCallbackId); if (callback != null) { callback(success); } } void _initPopup(Texture2D viewportTexture, float width, float height, string popupId) { base.Init(viewportTexture, width, height, null); _nativeWebViewPtr = WebView_new(gameObject.name, _nativeWidth, _nativeHeight, popupId); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void _initializePlugin() { // The generic `GetFunctionPointerForDelegate` is unavailable in .NET 2.0. var sendMessageFunction = Marshal.GetFunctionPointerForDelegate((UnitySendMessageFunction)_unitySendMessage); WebView_setSendMessageFunction(sendMessageFunction); WebView_setCookieCallbacks( Marshal.GetFunctionPointerForDelegate((GetCookieCallback)_handleGetCookieResult), Marshal.GetFunctionPointerForDelegate((GetCookieCallback)_handleGetCookiesResult), Marshal.GetFunctionPointerForDelegate((SetCookieCallback)_handleSetCookieResult) ); SetStorageEnabled(true); // cache, cookies, and storage are enabled by default } protected abstract StandaloneWebView _instantiate(); static void _throwAlreadyInitializedException(string action, string methodName) { var message = String.Format("Unable to {0} because a webview has already been created. On Windows and macOS, {1}() can only be called prior to initializing any webviews.", action, methodName); throw new InvalidOperationException(message); } void _pointerDown(Vector2 point, MouseButton mouseButton, int clickCount) { _assertValidState(); int nativeX = (int) (point.x * _nativeWidth); int nativeY = (int) (point.y * _nativeHeight); WebView_pointerDown(_nativeWebViewPtr, nativeX, nativeY, (int)mouseButton, clickCount); } void _pointerUp(Vector2 point, MouseButton mouseButton, int clickCount) { _assertValidState(); int nativeX = (int) (point.x * _nativeWidth); int nativeY = (int) (point.y * _nativeHeight); WebView_pointerUp(_nativeWebViewPtr, nativeX, nativeY, (int)mouseButton, clickCount); } [AOT.MonoPInvokeCallback(typeof(UnitySendMessageFunction))] static void _unitySendMessage(string gameObjectName, string methodName, string message) { Dispatcher.RunOnMainThread(() => { var gameObj = GameObject.Find(gameObjectName); if (gameObj == null) { WebViewLogger.LogErrorFormat("Unable to send the message, because there is no GameObject named '{0}'", gameObjectName); return; } gameObj.SendMessage(methodName, message); }); } [DllImport(_dllName)] static extern void WebView_cancelAuth(IntPtr webViewPtr); [DllImport(_dllName)] static extern void WebView_continueAuth(IntPtr webViewPtr, string username, string password); [DllImport(_dllName)] static extern void WebView_copy(IntPtr webViewPtr); [DllImport(_dllName)] static extern void WebView_cut(IntPtr webViewPtr); [DllImport(_dllName)] static extern bool WebView_enableRemoteDebugging(int portNumber); [DllImport(_dllName)] static extern void WebView_getCookie(string url, string name, string resultCallbackId); [DllImport(_dllName)] static extern void WebView_getCookies(string url, string resultCallbackId); [DllImport(_dllName)] static extern void WebView_keyDown(IntPtr webViewPtr, string key, int modifiers); [DllImport(_dllName)] static extern void WebView_keyUp(IntPtr webViewPtr, string key, int modifiers); [DllImport (_dllName)] static extern void WebView_movePointer(IntPtr webViewPtr, int x, int y); [DllImport(_dllName)] static extern IntPtr WebView_new(string gameObjectName, int width, int height, string popupBrowserId); [DllImport(_dllName)] static extern void WebView_paste(IntPtr webViewPtr); [DllImport(_dllName)] static extern bool WebView_pluginIsInitialized(); [DllImport (_dllName)] static extern void WebView_pointerDown(IntPtr webViewPtr, int x, int y, int mouseButton, int clickCount); [DllImport (_dllName)] static extern void WebView_pointerUp(IntPtr webViewPtr, int x, int y, int mouseButton, int clickCount); [DllImport(_dllName)] static extern void WebView_selectAll(IntPtr webViewPtr); [DllImport(_dllName)] static extern bool WebView_setAudioAndVideoCaptureEnabled(bool enabled); [DllImport (_dllName)] static extern void WebView_setAuthEnabled(IntPtr webViewPtr, bool enabled); [DllImport(_dllName)] static extern bool WebView_setCachePath(string cachePath); [DllImport(_dllName)] static extern bool WebView_setCommandLineArguments(string args); [DllImport(_dllName)] static extern void WebView_setCookie(string serializedCookie, string resultCallbackId); [DllImport(_dllName)] static extern int WebView_setCookieCallbacks(IntPtr getCookieCallback, IntPtr getCookiesCallback, IntPtr setCookieCallback); [DllImport (_dllName)] static extern void WebView_setDownloadsEnabled(IntPtr webViewPtr, string downloadsDirectoryPath); [DllImport(_dllName)] static extern bool WebView_setIgnoreCertificateErrors(bool ignore); [DllImport(_dllName)] static extern void WebView_setNativeFileDialogEnabled(IntPtr webViewPtr, bool enabled); [DllImport(_dllName)] static extern void WebView_setPopupMode(IntPtr webViewPtr, int popupMode); [DllImport(_dllName)] static extern bool WebView_setScreenSharingEnabled(bool enabled); [DllImport(_dllName)] static extern int WebView_setSendMessageFunction(IntPtr sendMessageFunction); [DllImport(_dllName)] static extern bool WebView_setTargetFrameRate(uint targetFrameRate); [DllImport(_dllName)] static extern void WebView_setZoomLevel(IntPtr webViewPtr, float zoomLevel); [DllImport(_dllName)] static extern void WebView_terminatePlugin(); } } #endif