File: WebView\WebView.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Devices;
 
namespace Microsoft.Maui.Controls
{
	/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="Type[@FullName='Microsoft.Maui.Controls.WebView']/Docs/*" />
	public partial class WebView : View, IWebViewController, IElementConfiguration<WebView>, IWebView
	{
		/// <summary>Bindable property for <see cref="Source"/>.</summary>
		public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(WebViewSource), typeof(WebView), default(WebViewSource),
			propertyChanging: (bindable, oldvalue, newvalue) =>
			{
				var source = oldvalue as WebViewSource;
				if (source != null)
					source.SourceChanged -= ((WebView)bindable).OnSourceChanged;
			}, propertyChanged: (bindable, oldvalue, newvalue) =>
			{
				var source = newvalue as WebViewSource;
				var webview = (WebView)bindable;
				if (source != null)
				{
					source.SourceChanged += webview.OnSourceChanged;
					SetInheritedBindingContext(source, webview.BindingContext);
				}
			});
 
		static readonly BindablePropertyKey CanGoBackPropertyKey = BindableProperty.CreateReadOnly(nameof(CanGoBack), typeof(bool), typeof(WebView), false);
 
		/// <summary>Bindable property for <see cref="CanGoBack"/>.</summary>
		public static readonly BindableProperty CanGoBackProperty = CanGoBackPropertyKey.BindableProperty;
 
		static readonly BindablePropertyKey CanGoForwardPropertyKey = BindableProperty.CreateReadOnly(nameof(CanGoForward), typeof(bool), typeof(WebView), false);
 
		/// <summary>Bindable property for <see cref="CanGoForward"/>.</summary>
		public static readonly BindableProperty CanGoForwardProperty = CanGoForwardPropertyKey.BindableProperty;
 
		/// <summary>Bindable property for <see cref="UserAgent"/>.</summary>
		public static readonly BindableProperty UserAgentProperty = BindableProperty.Create(nameof(UserAgent), typeof(string), typeof(WebView), null);
 
		/// <summary>Bindable property for <see cref="Cookies"/>.</summary>
		public static readonly BindableProperty CookiesProperty = BindableProperty.Create(nameof(Cookies), typeof(CookieContainer), typeof(WebView), null);
 
		readonly Lazy<PlatformConfigurationRegistry<WebView>> _platformConfigurationRegistry;
 
		bool _canGoBack;
		bool _canGoForward;
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='.ctor']/Docs/*" />
		public WebView()
		{
			_platformConfigurationRegistry = new Lazy<PlatformConfigurationRegistry<WebView>>(() => new PlatformConfigurationRegistry<WebView>(this));
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		bool IWebViewController.CanGoBack
		{
			get { return CanGoBack; }
			set { SetValue(CanGoBackPropertyKey, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='CanGoBack']/Docs/*" />
		public bool CanGoBack
		{
			get { return (bool)GetValue(CanGoBackProperty); }
		}
 
		[EditorBrowsable(EditorBrowsableState.Never)]
		bool IWebViewController.CanGoForward
		{
			get { return CanGoForward; }
			set { SetValue(CanGoForwardPropertyKey, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='CanGoForward']/Docs/*" />
		public bool CanGoForward
		{
			get { return (bool)GetValue(CanGoForwardProperty); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='UserAgent']/Docs/*" />
		public string UserAgent
		{
			get { return (string)GetValue(UserAgentProperty); }
			set { SetValue(UserAgentProperty, value ?? UserAgent); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='Cookies']/Docs/*" />
		public CookieContainer Cookies
		{
			get { return (CookieContainer)GetValue(CookiesProperty); }
			set { SetValue(CookiesProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='Source']/Docs/*" />
		[System.ComponentModel.TypeConverter(typeof(WebViewSourceTypeConverter))]
		public WebViewSource Source
		{
			get { return (WebViewSource)GetValue(SourceProperty); }
			set { SetValue(SourceProperty, value); }
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='Eval']/Docs/*" />
		public void Eval(string script)
		{
			Handler?.Invoke(nameof(IWebView.Eval), script);
			_evalRequested?.Invoke(this, new EvalRequested(script));
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='EvaluateJavaScriptAsync']/Docs/*" />
		public async Task<string> EvaluateJavaScriptAsync(string script)
		{
			if (script == null)
				return null;
 
			// Make all the platforms mimic Android's implementation, which is by far the most complete.
			if (DeviceInfo.Platform != DevicePlatform.Android)
			{
				script = EscapeJsString(script);
 
				if (DeviceInfo.Platform != DevicePlatform.WinUI)
				{
					// Use JSON.stringify() method to converts a JavaScript value to a JSON string
					script = "try{JSON.stringify(eval('" + script + "'))}catch(e){'null'};";
				}
				else
					script = "try{eval('" + script + "')}catch(e){'null'};";
			}
 
			string result;
 
			if (_evaluateJavaScriptRequested != null) // With Handlers we don't use events, if is null we are using a renderer and a handler otherwise.
			{
				// This is the WebViewRenderer subscribing to these requests; the handler stuff
				// doesn't use them.
				result = await _evaluateJavaScriptRequested?.Invoke(script);
			}
			else
			{
				// Use the handler command to evaluate the JS
				result = await Handler.InvokeAsync(nameof(IWebView.EvaluateJavaScriptAsync),
					new EvaluateJavaScriptAsyncRequest(script));
			}
 
			//if the js function errored or returned null/undefined treat it as null
			if (result == "null")
				result = null;
 
			//JSON.stringify wraps the result in literal quotes, we just want the actual returned result
			//note that if the js function returns the string "null" we will get here and not above
			else if (result != null)
				result = result.Trim('"');
 
			return result;
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='GoBack']/Docs/*" />
		public void GoBack()
		{
			Handler?.Invoke(nameof(IWebView.GoBack));
			_goBackRequested?.Invoke(this, EventArgs.Empty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='GoForward']/Docs/*" />
		public void GoForward()
		{
			Handler?.Invoke(nameof(IWebView.GoForward));
			_goForwardRequested?.Invoke(this, EventArgs.Empty);
		}
 
		/// <include file="../../docs/Microsoft.Maui.Controls/WebView.xml" path="//Member[@MemberName='Reload']/Docs/*" />
		public void Reload()
		{
			Handler?.Invoke(nameof(IWebView.Reload));
			_reloadRequested?.Invoke(this, EventArgs.Empty);
		}
 
		/// <summary>
		/// Raised after web navigation completes.
		/// </summary>
		public event EventHandler<WebNavigatedEventArgs> Navigated;
 
		/// <summary>
		/// Raised after web navigation begins.
		/// </summary>
		public event EventHandler<WebNavigatingEventArgs> Navigating;
 
		/// <summary>
		///  Raised when a WebView process ends unexpectedly.
		/// </summary>
		public event EventHandler<WebViewProcessTerminatedEventArgs> ProcessTerminated;
 
		/// <inheritdoc/>
		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();
 
			WebViewSource source = Source;
			if (source != null)
			{
				SetInheritedBindingContext(source, BindingContext);
			}
		}
 
		/// <inheritdoc/>
		protected override void OnPropertyChanged(string propertyName)
		{
			if (propertyName == "BindingContext")
			{
				WebViewSource source = Source;
				if (source != null)
					SetInheritedBindingContext(source, BindingContext);
			}
 
			base.OnPropertyChanged(propertyName);
		}
 
		protected void OnSourceChanged(object sender, EventArgs e)
		{
			OnPropertyChanged(SourceProperty.PropertyName);
		}
 
		event EventHandler<EvalRequested> _evalRequested;
 
		/// <inheritdoc/>
		event EventHandler<EvalRequested> IWebViewController.EvalRequested
		{
			add { _evalRequested += value; }
			remove { _evalRequested -= value; }
		}
 
		event EvaluateJavaScriptDelegate _evaluateJavaScriptRequested;
 
		/// <inheritdoc/>
		event EvaluateJavaScriptDelegate IWebViewController.EvaluateJavaScriptRequested
		{
			add { _evaluateJavaScriptRequested += value; }
			remove { _evaluateJavaScriptRequested -= value; }
		}
 
		event EventHandler _goBackRequested;
 
		/// <inheritdoc/>
		event EventHandler IWebViewController.GoBackRequested
		{
			add { _goBackRequested += value; }
			remove { _goBackRequested -= value; }
		}
 
		event EventHandler _goForwardRequested;
 
		/// <inheritdoc/>
		event EventHandler IWebViewController.GoForwardRequested
		{
			add { _goForwardRequested += value; }
			remove { _goForwardRequested -= value; }
		}
 
		void IWebViewController.SendNavigated(WebNavigatedEventArgs args)
		{
			Navigated?.Invoke(this, args);
		}
 
		void IWebViewController.SendNavigating(WebNavigatingEventArgs args)
		{
			Navigating?.Invoke(this, args);
		}
 
		event EventHandler _reloadRequested;
 
		/// <inheritdoc/>
		event EventHandler IWebViewController.ReloadRequested
		{
			add { _reloadRequested += value; }
			remove { _reloadRequested -= value; }
		}
 
		/// <inheritdoc/>
		public IPlatformElementConfiguration<T, WebView> On<T>() where T : IConfigPlatform
		{
			return _platformConfigurationRegistry.Value.On<T>();
		}
 
		private static string EscapeJsString(string js)
		{
			if (js == null)
				return null;
 
			if (js.IndexOf("'", StringComparison.Ordinal) == -1)
				return js;
 
			//get every quote in the string along with all the backslashes preceding it
			var singleQuotes = Regex.Matches(js, @"(\\*?)'");
 
			var uniqueMatches = new List<string>();
 
			for (var i = 0; i < singleQuotes.Count; i++)
			{
				var matchedString = singleQuotes[i].Value;
				if (!uniqueMatches.Contains(matchedString))
				{
					uniqueMatches.Add(matchedString);
				}
			}
 
			uniqueMatches.Sort((x, y) => y.Length.CompareTo(x.Length));
 
			//escape all quotes from the script as well as add additional escaping to all quotes that were already escaped
			for (var i = 0; i < uniqueMatches.Count; i++)
			{
				var match = uniqueMatches[i];
				var numberOfBackslashes = match.Length - 1;
				var slashesToAdd = (numberOfBackslashes * 2) + 1;
				var replacementStr = "'".PadLeft(slashesToAdd + 1, '\\');
				js = Regex.Replace(js, @"(?<=[^\\])" + Regex.Escape(match), replacementStr);
			}
 
			return js;
		}
 
		/// <inheritdoc/>
		IWebViewSource IWebView.Source => Source;
 
		/// <inheritdoc/>
		bool IWebView.CanGoBack
		{
			get => _canGoBack;
			set
			{
				_canGoBack = value;
				((IWebViewController)this).CanGoBack = _canGoBack;
				Handler?.UpdateValue(nameof(IWebView.CanGoBack));
			}
		}
 
		/// <inheritdoc/>
		bool IWebView.CanGoForward
		{
			get => _canGoForward;
			set
			{
				_canGoForward = value;
				((IWebViewController)this).CanGoForward = _canGoForward;
				Handler?.UpdateValue(nameof(IWebView.CanGoForward));
			}
		}
 
		/// <inheritdoc/>
		bool IWebView.Navigating(WebNavigationEvent evnt, string url)
		{
			var args = new WebNavigatingEventArgs(evnt, new UrlWebViewSource { Url = url }, url);
			(this as IWebViewController)?.SendNavigating(args);
 
			return args.Cancel;
		}
 
		/// <inheritdoc/>
		void IWebView.Navigated(WebNavigationEvent evnt, string url, WebNavigationResult result)
		{
			var args = new WebNavigatedEventArgs(evnt, new UrlWebViewSource { Url = url }, url, result);
			(this as IWebViewController)?.SendNavigated(args);
		}
 
		void IWebView.ProcessTerminated(WebProcessTerminatedEventArgs args)
		{
#if ANDROID
			var platformArgs = new PlatformWebViewProcessTerminatedEventArgs(args.Sender, args.RenderProcessGoneDetail);
			var webViewProcessTerminatedEventArgs = new WebViewProcessTerminatedEventArgs(platformArgs);
#elif IOS || MACCATALYST
			var platformArgs = new PlatformWebViewProcessTerminatedEventArgs(args.Sender);
			var webViewProcessTerminatedEventArgs = new WebViewProcessTerminatedEventArgs(platformArgs);
#elif WINDOWS
			var platformArgs = new PlatformWebViewProcessTerminatedEventArgs(args.Sender, args.CoreWebView2ProcessFailedEventArgs);
			var webViewProcessTerminatedEventArgs = new WebViewProcessTerminatedEventArgs(platformArgs);
#else
			var webViewProcessTerminatedEventArgs = new WebViewProcessTerminatedEventArgs();
#endif
			ProcessTerminated?.Invoke(this, webViewProcessTerminatedEventArgs);
		}
	}
}