// Copyright © 2020 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
using System;
using System.ComponentModel;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using CefSharp.Internals;
#if OFFSCREEN
namespace CefSharp.OffScreen
#elif WPF
namespace CefSharp.Wpf
#elif WINFORMS
namespace CefSharp.WinForms
#endif
{
//ChromiumWebBrowser Partial class implementation shared between the
//WPF, Winforms and Offscreen
public partial class ChromiumWebBrowser
{
public const string BrowserNotInitializedExceptionErrorMessage =
"The ChromiumWebBrowser instance creates the underlying Chromium Embedded Framework (CEF) browser instance in an async fashion. " +
"The undelying CefBrowser instance is not yet initialized. Use the IsBrowserInitializedChanged event and check " +
"the IsBrowserInitialized property to determine when the browser has been initialized.";
private const string CefInitializeFailedErrorMessage = "Cef.Initialize() failed.Check the log file see https://github.com/cefsharp/CefSharp/wiki/Trouble-Shooting#log-file for details.";
private const string CefIsInitializedFalseErrorMessage = "Cef.IsInitialized was false!.Check the log file for errors!. See https://github.com/cefsharp/CefSharp/wiki/Trouble-Shooting#log-file for details.";
///
/// Used as workaround for issue https://github.com/cefsharp/CefSharp/issues/3021
///
private int canExecuteJavascriptInMainFrameChildProcessId;
///
/// The browser initialized - boolean represented as 0 (false) and 1(true) as we use Interlocker to increment/reset
///
private int browserInitialized;
///
/// The value for disposal, if it's 1 (one) then this instance is either disposed
/// or in the process of getting disposed
///
private int disposeSignaled;
///
/// The browser
///
private IBrowser browser;
///
/// Initial browser load task complection source
///
private TaskCompletionSource initialLoadTaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
///
/// Initial browser load action
///
private Action initialLoadAction;
///
/// Get access to the core instance.
/// Maybe null if the underlying CEF Browser has not yet been
/// created or if this control has been disposed. Check
/// before accessing.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IBrowser BrowserCore { get; internal set; }
///
/// A flag that indicates if you can execute javascript in the main frame.
/// Flag is set to true in IRenderProcessMessageHandler.OnContextCreated.
/// and false in IRenderProcessMessageHandler.OnContextReleased
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool CanExecuteJavascriptInMainFrame { get; private set; }
///
/// Implement and assign to handle dialog events.
///
/// The dialog handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IDialogHandler DialogHandler { get; set; }
///
/// Implement and assign to handle events related to JavaScript Dialogs.
///
/// The js dialog handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IJsDialogHandler JsDialogHandler { get; set; }
///
/// Implement and assign to handle events related to key press.
///
/// The keyboard handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IKeyboardHandler KeyboardHandler { get; set; }
///
/// Implement and assign to handle events related to browser requests.
///
/// The request handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IRequestHandler RequestHandler { get; set; }
///
/// Implement and assign to handle events related to downloading files.
///
/// The download handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IDownloadHandler DownloadHandler { get; set; }
///
/// Implement and assign to handle events related to browser load status.
///
/// The load handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public ILoadHandler LoadHandler { get; set; }
///
/// Implement and assign to handle events related to popups.
///
/// The life span handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public ILifeSpanHandler LifeSpanHandler { get; set; }
///
/// Implement and assign to handle events related to browser display state.
///
/// The display handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IDisplayHandler DisplayHandler { get; set; }
///
/// Implement and assign to handle events related to the browser context menu
///
/// The menu handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IContextMenuHandler MenuHandler { get; set; }
///
/// Implement and assign to handle messages from the render process.
///
/// The render process message handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IRenderProcessMessageHandler RenderProcessMessageHandler { get; set; }
///
/// Implement to handle events related to find results.
///
/// The find handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IFindHandler FindHandler { get; set; }
///
/// Implement to handle audio events.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IAudioHandler AudioHandler { get; set; }
///
/// Implement to handle frame events.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IFrameHandler FrameHandler { get; set; }
///
/// Implement to handle events related to permission requests.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IPermissionHandler PermissionHandler { get; set; }
///
/// The for this ChromiumWebBrowser.
///
/// The focus handler.
/// If you need customized focus handling behavior for WinForms, the suggested
/// best practice would be to inherit from DefaultFocusHandler and try to avoid
/// needing to override the logic in OnGotFocus. The implementation in
/// DefaultFocusHandler relies on very detailed behavior of how WinForms and
/// Windows interact during window activation.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IFocusHandler FocusHandler { get; set; }
///
/// Implement and assign to handle events related to dragging.
///
/// The drag handler.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IDragHandler DragHandler { get; set; }
///
/// Implement and control the loading of resources
///
/// The resource handler factory.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IResourceRequestHandlerFactory ResourceRequestHandlerFactory { get; set; }
///
/// Event handler that will get called when the resource load for a navigation fails or is canceled.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler LoadError;
///
/// Event handler that will get called when the browser begins loading a frame. Multiple frames may be loading at the same
/// time. Sub-frames may start or continue loading after the main frame load has ended. This method may not be called for a
/// particular frame if the load request for that frame fails. For notification of overall browser load status use
/// OnLoadingStateChange instead.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
/// Whilst this may seem like a logical place to execute js, it's called before the DOM has been loaded, implement
/// as it's called when the underlying V8Context is created
///
public event EventHandler FrameLoadStart;
///
/// Event handler that will get called when the browser is done loading a frame. Multiple frames may be loading at the same
/// time. Sub-frames may start or continue loading after the main frame load has ended. This method will always be called
/// for all frames irrespective of whether the request completes successfully.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler FrameLoadEnd;
///
/// Event handler that will get called when the Loading state has changed.
/// This event will be fired twice. Once when loading is initiated either programmatically or
/// by user action, and once when loading is terminated due to completion, cancellation of failure.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler LoadingStateChanged;
///
/// Event handler for receiving Javascript console messages being sent from web pages.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
/// (The exception to this is when you're running with settings.MultiThreadedMessageLoop = false, then they'll be the same thread).
///
public event EventHandler ConsoleMessage;
///
/// Event handler for changes to the status message.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang.
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
/// (The exception to this is when you're running with settings.MultiThreadedMessageLoop = false, then they'll be the same thread).
///
public event EventHandler StatusMessage;
///
/// Event handler that will get called when the message that originates from CefSharp.PostMessage
///
public event EventHandler JavascriptMessageReceived;
///
/// A flag that indicates whether the WebBrowser is initialized (true) or not (false).
///
/// true if this instance is browser initialized; otherwise, false.
bool IChromiumWebBrowserBase.IsBrowserInitialized
{
get { return InternalIsBrowserInitialized(); }
}
void IWebBrowserInternal.SetCanExecuteJavascriptOnMainFrame(string frameId, bool canExecute)
{
//When loading pages of a different origin the frameId changes
//For the first loading of a new origin the messages from the render process
//Arrive in a different order than expected, the OnContextCreated message
//arrives before the OnContextReleased, then the message for OnContextReleased
//incorrectly overrides the value
//https://github.com/cefsharp/CefSharp/issues/3021
var chromiumChildProcessId = GetChromiumChildProcessId(frameId);
if (chromiumChildProcessId > canExecuteJavascriptInMainFrameChildProcessId && !canExecute)
{
return;
}
canExecuteJavascriptInMainFrameChildProcessId = chromiumChildProcessId;
CanExecuteJavascriptInMainFrame = canExecute;
}
void IWebBrowserInternal.SetJavascriptMessageReceived(JavascriptMessageReceivedEventArgs args)
{
//Run the event on the ThreadPool (rather than the CEF Thread we are currently on).
Task.Run(() => JavascriptMessageReceived?.Invoke(this, args));
}
///
/// Handles the event.
///
/// The instance containing the event data.
void IWebBrowserInternal.OnFrameLoadStart(FrameLoadStartEventArgs args)
{
FrameLoadStart?.Invoke(this, args);
}
///
/// Handles the event.
///
/// The instance containing the event data.
void IWebBrowserInternal.OnFrameLoadEnd(FrameLoadEndEventArgs args)
{
FrameLoadEnd?.Invoke(this, args);
}
///
/// Handles the event.
///
/// The instance containing the event data.
void IWebBrowserInternal.OnConsoleMessage(ConsoleMessageEventArgs args)
{
ConsoleMessage?.Invoke(this, args);
}
///
/// Handles the event.
///
/// The instance containing the event data.
void IWebBrowserInternal.OnStatusMessage(StatusMessageEventArgs args)
{
StatusMessage?.Invoke(this, args);
}
///
/// Handles the event.
///
/// The instance containing the event data.
void IWebBrowserInternal.OnLoadError(LoadErrorEventArgs args)
{
LoadError?.Invoke(this, args);
initialLoadAction?.Invoke(null, args.ErrorCode);
}
///
/// Gets or sets a value indicating whether this instance has parent.
///
/// true if this instance has parent; otherwise, false.
bool IWebBrowserInternal.HasParent { get; set; }
///
/// Used by CefSharp.Puppeteer to associate a single DevToolsContext with a ChromiumWebBrowser instance.
///
IDisposable IWebBrowserInternal.DevToolsContext { get; set; }
///
/// Gets the browser adapter.
///
/// The browser adapter.
IBrowserAdapter IWebBrowserInternal.BrowserAdapter
{
get { return managedCefBrowserAdapter; }
}
void IWebBrowserInternal.OnAfterBrowserCreated(IBrowser browser)
{
if (IsDisposed || browser.IsDisposed)
{
return;
}
this.browser = browser;
BrowserCore = browser;
initialLoadAction = InitialLoad;
Interlocked.Exchange(ref browserInitialized, 1);
OnAfterBrowserCreated(browser);
}
///
/// Sets the loading state change.
///
/// The instance containing the event data.
void IWebBrowserInternal.SetLoadingStateChange(LoadingStateChangedEventArgs args)
{
SetLoadingStateChange(args);
LoadingStateChanged?.Invoke(this, args);
initialLoadAction?.Invoke(args.IsLoading, null);
}
///
public void LoadUrl(string url)
{
Load(url);
}
///
public Task LoadUrlAsync(string url)
{
//LoadUrlAsync is actually a static method so that CefSharp.Wpf.HwndHost can reuse the code
return CefSharp.WebBrowserExtensions.LoadUrlAsync(this, url);
}
///
public Task WaitForNavigationAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
//WaitForNavigationAsync is actually a static method so that CefSharp.Wpf.HwndHost can reuse the code
return CefSharp.WebBrowserExtensions.WaitForNavigationAsync(this, timeout, cancellationToken);
}
///
public Task WaitForInitialLoadAsync()
{
return initialLoadTaskCompletionSource.Task;
}
///
public bool TryGetBrowserCoreById(int browserId, out IBrowser browser)
{
var browserAdapter = managedCefBrowserAdapter;
if (IsDisposed || browserAdapter == null || browserAdapter.IsDisposed)
{
browser = null;
return false;
}
browser = browserAdapter.GetBrowser(browserId);
return browser != null;
}
///
public async Task GetContentSizeAsync()
{
ThrowExceptionIfDisposed();
ThrowExceptionIfBrowserNotInitialized();
using (var devToolsClient = browser.GetDevToolsClient())
{
//Get the content size
var layoutMetricsResponse = await devToolsClient.Page.GetLayoutMetricsAsync().ConfigureAwait(continueOnCapturedContext: false);
var rect = layoutMetricsResponse.CssContentSize;
return new Structs.DomRect(rect.X, rect.Y, rect.Width, rect.Height);
}
}
private void InitialLoad(bool? isLoading, CefErrorCode? errorCode)
{
if (IsDisposed)
{
initialLoadAction = null;
initialLoadTaskCompletionSource.TrySetCanceled();
return;
}
if (isLoading.HasValue)
{
if (isLoading.Value)
{
return;
}
initialLoadAction = null;
var host = browser?.GetHost();
var navEntry = host?.GetVisibleNavigationEntry();
int statusCode = navEntry?.HttpStatusCode ?? -1;
//By default 0 is some sort of error, we map that to -1
//so that it's clearer that something failed.
if (statusCode == 0)
{
statusCode = -1;
}
initialLoadTaskCompletionSource.TrySetResult(new LoadUrlAsyncResponse(CefErrorCode.None, statusCode));
}
else if (errorCode.HasValue)
{
//Actions that trigger a download will raise an aborted error.
//Generally speaking Aborted is safe to ignore
if (errorCode == CefErrorCode.Aborted)
{
return;
}
initialLoadAction = null;
initialLoadTaskCompletionSource.TrySetResult(new LoadUrlAsyncResponse(errorCode.Value, -1));
}
}
partial void OnAfterBrowserCreated(IBrowser browser);
partial void SetLoadingStateChange(LoadingStateChangedEventArgs args);
///
/// Sets the handler references to null.
/// Where required also calls Dispose().
///
private void FreeHandlersExceptLifeSpanAndFocus()
{
AudioHandler?.Dispose();
AudioHandler = null;
DialogHandler = null;
FindHandler = null;
RequestHandler = null;
DisplayHandler = null;
LoadHandler = null;
KeyboardHandler = null;
JsDialogHandler = null;
DragHandler = null;
DownloadHandler = null;
MenuHandler = null;
ResourceRequestHandlerFactory = null;
RenderProcessMessageHandler = null;
this.FreeDevToolsContext();
}
private static void InitializeCefInternal()
{
if (Cef.IsInitialized == null)
{
if (!Cef.Initialize(new CefSettings()))
{
throw new InvalidOperationException(CefInitializeFailedErrorMessage);
}
}
if (Cef.IsInitialized == false)
{
throw new InvalidOperationException(CefIsInitializedFalseErrorMessage);
}
}
///
/// Check is browser is initialized
///
/// true if browser is initialized
private bool InternalIsBrowserInitialized()
{
// Use CompareExchange to read the current value - if disposeCount is 1, we set it to 1, effectively a no-op
// Volatile.Read would likely use a memory barrier which I believe is unnecessary in this scenario
return Interlocked.CompareExchange(ref browserInitialized, 0, 0) == 1;
}
///
/// Throw exception if browser not initialized.
///
/// Thrown when an exception error condition occurs.
private void ThrowExceptionIfBrowserNotInitialized()
{
if (!InternalIsBrowserInitialized())
{
throw new Exception(BrowserNotInitializedExceptionErrorMessage);
}
}
///
/// Throw exception if disposed.
///
/// Thrown when a supplied object has been disposed.
private void ThrowExceptionIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException("ChromiumWebBrowser");
}
}
private int GetChromiumChildProcessId(string frameIdentifier)
{
try
{
var parts = frameIdentifier.Split('-');
if (int.TryParse(parts[0], out var childProcessId))
return childProcessId;
}
catch
{
}
return -1;
}
}
}