// Copyright © 2014 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CefSharp.Event;
using CefSharp.JavascriptBinding;
using CefSharp.ModelBinding;
namespace CefSharp.Internals
{
///
/// This class manages the registration of objects in the browser
/// process to be exposed to JavaScript in the renderer process.
/// Registration performs method, parameter, property type analysis
/// of the registered objects into meta-data tied to reflection data
/// for later use.
///
/// This class also is the adaptation layer between the BrowserProcessService
/// and the registered objects. This means when the renderer wants to call an
/// exposed method, get a property of an object, or
/// set a property of an object in the browser process, that this
/// class does deals with the previously created meta-data and invokes the correct
/// behavior via reflection APIs.
///
/// All of the registered objects are tracked via meta-data for the objects
/// expressed starting with the JavaScriptObject type.
///
public class JavascriptObjectRepository : FreezableBase, IJavascriptObjectRepositoryInternal
{
///
/// CefSharp.BindObjectAsync was called from Javascript without pasing in any params
/// the will be called with
/// set to this value.
///
public const string AllObjects = "All";
///
/// Legacy Javascript Binding is enabled, the event
/// will be called with
/// set to this value
///
public const string LegacyObjects = "Legacy";
private static long lastId;
///
public event EventHandler ResolveObject;
///
public event EventHandler ObjectBoundInJavascript;
///
public event EventHandler ObjectsBoundInJavascript;
///
/// A hash from assigned object ids to the objects,
/// this is done to speed up finding the object in O(1) time
/// instead of traversing the JavaScriptRootObject tree.
///
protected readonly ConcurrentDictionary objects = new ConcurrentDictionary();
///
/// Javascript Name converter
///
private IJavascriptNameConverter nameConverter;
///
/// Has the browser this repository is associated with been initilized (set in OnAfterCreated)
///
public bool IsBrowserInitialized { get; set; }
///
public void Dispose()
{
ResolveObject = null;
ObjectBoundInJavascript = null;
ObjectsBoundInJavascript = null;
}
///
public bool HasBoundObjects
{
get { return objects.Count > 0; }
}
///
/// Configurable settings for this repository, such as the property names CefSharp injects into the window.
///
public JavascriptBindingSettings Settings { get; private set; }
///
/// Converted .Net method/property/field names to the name that
/// will be used in Javasript. Used for when .Net naming conventions
/// differ from Javascript naming conventions.
///
public IJavascriptNameConverter NameConverter
{
get { return nameConverter; }
set
{
ThrowIfFrozen();
nameConverter = value;
}
}
///
/// JavascriptObjectRepository
///
public JavascriptObjectRepository()
{
Settings = new JavascriptBindingSettings();
nameConverter = new LegacyCamelCaseJavascriptNameConverter();
}
///
public bool IsBound(string name)
{
return objects.Values.Any(x => x.Name == name);
}
List IJavascriptObjectRepositoryInternal.GetLegacyBoundObjects()
{
RaiseResolveObjectEvent(LegacyObjects);
return objects.Values.Where(x => x.RootObject).ToList();
}
//Ideally this would internal, unfurtunately it's used in C++
//and it's hard to expose internals
List IJavascriptObjectRepositoryInternal.GetObjects(List names)
{
//If there are no objects names or the count is 0 then we will raise
//the resolve event then return all objects that are registered,
//we'll only perform checking if object(s) of specific name is requested.
var getAllObjects = names == null || names.Count == 0;
if (getAllObjects)
{
RaiseResolveObjectEvent(AllObjects);
return objects.Values.Where(x => x.RootObject).ToList();
}
foreach (var name in names)
{
if (!IsBound(name))
{
RaiseResolveObjectEvent(name);
}
}
var objectsByName = objects.Values.Where(x => names.Contains(x.JavascriptName) && x.RootObject).ToList();
//TODO: JSB Add another event that signals when no object matching a name
//in the list was provided.
return objectsByName;
}
void IJavascriptObjectRepositoryInternal.ObjectsBound(List> objs)
{
var boundObjectHandler = ObjectBoundInJavascript;
var boundObjectsHandler = ObjectsBoundInJavascript;
if (boundObjectHandler != null || boundObjectsHandler != null)
{
//Execute on Threadpool so we don't unnessicarily block the CEF IO thread
Task.Run(() =>
{
foreach (var obj in objs)
{
boundObjectHandler?.Invoke(this, new JavascriptBindingCompleteEventArgs(this, obj.Item1, obj.Item2, obj.Item3));
}
boundObjectsHandler?.Invoke(this, new JavascriptBindingMultipleCompleteEventArgs(this, objs.Select(x => x.Item1).ToList()));
});
}
}
private JavascriptObject CreateJavascriptObject(bool rootObject)
{
var id = Interlocked.Increment(ref lastId);
var result = new JavascriptObject
{
Id = id,
RootObject = rootObject
};
objects[id] = result;
return result;
}
#if NETCOREAPP
public void Register(string name, object value, BindingOptions options)
#else
public void Register(string name, object value, bool isAsync, BindingOptions options)
#endif
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (value == null)
{
throw new ArgumentNullException("value");
}
Freeze();
//Enable WCF if not already enabled - can only be done before the browser has been initliazed
//if done after the subprocess won't be WCF enabled it we'll have to throw an exception
#if NETCOREAPP
var isAsync = true;
#else
if (!IsBrowserInitialized && !isAsync)
{
CefSharpSettings.WcfEnabled = true;
}
if (!CefSharpSettings.WcfEnabled && !isAsync)
{
throw new InvalidOperationException(@"To enable synchronous JS bindings set WcfEnabled true in CefSharpSettings before you create
your ChromiumWebBrowser instances.");
}
#endif
//Validation name is unique
if (objects.Values.Count(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)) > 0)
{
throw new ArgumentException("Object already bound with name:" + name, name);
}
//Binding of System types is problematic, so we don't support it
var type = value.GetType();
if (type.IsPrimitive || type.BaseType.Namespace.StartsWith("System."))
{
throw new ArgumentException("Registering of .Net framework built in types is not supported, " +
"create your own Object and proxy the calls if you need to access a Window/Form/Control.", "value");
}
var jsObject = CreateJavascriptObject(rootObject: true);
jsObject.Value = value;
jsObject.Name = name;
jsObject.JavascriptName = name;
jsObject.IsAsync = isAsync;
jsObject.Binder = options?.Binder;
jsObject.MethodInterceptor = options?.MethodInterceptor;
#if !NETCOREAPP
jsObject.PropertyInterceptor = options?.PropertyInterceptor;
#endif
AnalyseObjectForBinding(jsObject, analyseMethods: true, analyseProperties: !isAsync, readPropertyValue: false);
}
///
public void UnRegisterAll()
{
objects.Clear();
}
///
public bool UnRegister(string name)
{
foreach (var kvp in objects)
{
if (string.Equals(kvp.Value.Name, name, StringComparison.OrdinalIgnoreCase))
{
JavascriptObject obj;
objects.TryRemove(kvp.Key, out obj);
return true;
}
}
return false;
}
TryCallMethodResult IJavascriptObjectRepositoryInternal.TryCallMethod(long objectId, string name, object[] parameters)
{
return TryCallMethod(objectId, name, parameters);
}
protected virtual TryCallMethodResult TryCallMethod(long objectId, string name, object[] parameters)
{
var exception = "";
object result = null;
JavascriptObject obj;
if (!objects.TryGetValue(objectId, out obj))
{
var paramCount = parameters == null ? 0 : parameters.Length;
return new TryCallMethodResult(false, result, $"Object Not Found Matching Id:{objectId}, MethodName:{name}, ParamCount:{paramCount}");
}
var method = obj.Methods.FirstOrDefault(p => p.JavascriptName == name);
if (method == null)
{
throw new InvalidOperationException(string.Format("Method {0} not found on Object of Type {1}", name, obj.Value.GetType()));
}
try
{
//Check if the bound object method contains a ParamArray as the last parameter on the method signature.
//NOTE: No additional parameters are permitted after the params keyword in a method declaration,
//and only one params keyword is permitted in a method declaration.
//https://msdn.microsoft.com/en-AU/library/w5zay9db.aspx
if (method.HasParamArray)
{
var paramList = new List