// 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;
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 : IJavascriptObjectRepository
{
public const string AllObjects = "All";
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.
///
private readonly ConcurrentDictionary objects = new ConcurrentDictionary();
///
/// 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; }
}
public bool IsBound(string name)
{
return objects.Values.Any(x => x.Name == name);
}
//Ideally this would internal, unfurtunately it's used in C++
//and it's hard to expose internals
public List GetObjects(List names = null)
{
//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;
}
public void 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 camelCaseJavascriptNames, bool rootObject)
{
var id = Interlocked.Increment(ref lastId);
var result = new JavascriptObject
{
Id = id,
CamelCaseJavascriptNames = camelCaseJavascriptNames,
RootObject = rootObject
};
objects[id] = result;
return result;
}
public void Register(string name, object value, bool isAsync, BindingOptions options)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (value == null)
{
throw new ArgumentNullException("value");
}
//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 (!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.");
}
//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 camelCaseJavascriptNames = options == null ? true : options.CamelCaseJavascriptNames;
var jsObject = CreateJavascriptObject(camelCaseJavascriptNames, rootObject: true);
jsObject.Value = value;
jsObject.Name = name;
jsObject.JavascriptName = name;
jsObject.IsAsync = isAsync;
jsObject.Binder = options?.Binder;
jsObject.MethodInterceptor = options?.MethodInterceptor;
AnalyseObjectForBinding(jsObject, analyseMethods: true, analyseProperties: !isAsync, readPropertyValue: false, camelCaseJavascriptNames: camelCaseJavascriptNames);
}
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;
}
internal bool TryCallMethod(long objectId, string name, object[] parameters, out object result, out string exception)
{
exception = "";
result = null;
JavascriptObject obj;
if (!objects.TryGetValue(objectId, out obj))
{
return false;
}
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