// Copyright © 2019 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. #pragma once #include "include/cef_v8.h" #include "RegisterBoundObjectRegistry.h" #include "..\CefSharp.Core\Internals\Messaging\Messages.h" #include "..\CefSharp.Core\Internals\Serialization\Primitives.h" using namespace System; using namespace CefSharp::Internals::Messaging; using namespace CefSharp::Internals::Serialization; namespace CefSharp { const CefString kBindObjectAsync = CefString("BindObjectAsync"); const CefString kBindObjectAsyncCamelCase = CefString("bindObjectAsync"); private class BindObjectAsyncHandler : public CefV8Handler { private: gcroot _callbackRegistry; gcroot^> _javascriptObjects; gcroot _browserWrapper; public: BindObjectAsyncHandler(RegisterBoundObjectRegistry^ callbackRegistery, Dictionary^ javascriptObjects, CefBrowserWrapper^ browserWrapper) { _callbackRegistry = callbackRegistery; _javascriptObjects = javascriptObjects; _browserWrapper = browserWrapper; } ~BindObjectAsyncHandler() { _callbackRegistry = nullptr; _javascriptObjects = nullptr; _browserWrapper = nullptr; } bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) OVERRIDE { auto context = CefV8Context::GetCurrentContext(); if (context.get() && context->Enter()) { try { auto params = CefListValue::Create(); auto boundObjectRequired = false; auto notifyIfAlreadyBound = false; auto ignoreCache = false; auto cachedObjects = gcnew List(); //TODO: Create object to represent this information auto objectNamesWithBoundStatus = gcnew List^>(); auto objectCount = 0; if (arguments.size() > 0) { objectCount = (int)arguments.size(); //If first argument is an object, we'll see if it contains config values if (arguments[0]->IsObject()) { //Upper and camelcase options are supported notifyIfAlreadyBound = GetV8BoolValue(arguments[0], "NotifyIfAlreadyBound", "notifyIfAlreadyBound"); ignoreCache = GetV8BoolValue(arguments[0], "IgnoreCache", "ignoreCache"); //If we have a config object then we remove that from the count objectCount = objectCount - 1; } auto global = context->GetGlobal(); //Loop through all arguments and ignore anything that's not a string for (auto i = 0; i < arguments.size(); i++) { //Validate arg as being a string if (arguments[i]->IsString()) { auto objectName = arguments[i]->GetStringValue(); auto managedObjectName = StringUtils::ToClr(objectName); auto alreadyBound = global->HasValue(objectName); auto cached = false; //Check if the object has already been bound if (alreadyBound) { cached = _javascriptObjects->ContainsKey(managedObjectName); } else { //If no matching object found then we'll add the object name to the list boundObjectRequired = true; params->SetString(i, objectName); JavascriptObject^ obj; if (_javascriptObjects->TryGetValue(managedObjectName, obj)) { cachedObjects->Add(obj); cached = true; } } objectNamesWithBoundStatus->Add(Tuple::Create(managedObjectName, alreadyBound, cached)); } } } else { //No objects names were specified so we default to makeing the request boundObjectRequired = true; } auto frame = context->GetFrame(); if (frame.get() && frame->IsValid()) { if (boundObjectRequired || ignoreCache) { //If the number of cached objects matches the number of args //(we have a cached copy of all requested objects) //then we'll immediately bind the cached objects if (cachedObjects->Count == objectCount && ignoreCache == false) { if (Object::ReferenceEquals(_browserWrapper, nullptr)) { exception = "BindObjectAsyncHandler::Execute - Browser wrapper null, unable to bind objects"; return true; } auto browser = context->GetBrowser(); auto rootObjectWrappers = _browserWrapper->JavascriptRootObjectWrappers; JavascriptRootObjectWrapper^ rootObject; if (!rootObjectWrappers->TryGetValue(frame->GetIdentifier(), rootObject)) { rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), _browserWrapper->BrowserProcess); rootObjectWrappers->TryAdd(frame->GetIdentifier(), rootObject); } //Cached objects only contains a list of objects not already bound rootObject->Bind(cachedObjects, context->GetGlobal()); //Objects already bound or ignore cache CefRefPtr promiseResolve; CefRefPtr promiseException; auto promiseResolveScript = StringUtils::ToNative("Promise.resolve({Success:true, Count:" + cachedObjects->Count + ", Message:'OK'});"); if (context->Eval(promiseResolveScript, CefString(), 0, promiseResolve, promiseException)) { retval = promiseResolve; } else { exception = promiseException->GetMessage(); return true; } NotifyObjectBound(frame, objectNamesWithBoundStatus); } else { CefRefPtr promiseData; CefRefPtr promiseException; //this will create a promise and give us the reject/resolve functions {p: Promise, res: resolve(), rej: reject()} if (!context->Eval(CefAppUnmanagedWrapper::kPromiseCreatorScript, CefString(), 0, promiseData, promiseException)) { exception = promiseException->GetMessage(); return true; } //when refreshing the browser this is sometimes null, in this case return true and log message //https://github.com/cefsharp/CefSharp/pull/2446 if (promiseData == NULL) { LOG(WARNING) << "BindObjectAsyncHandler::Execute promiseData returned NULL"; return true; } //return the promose retval = promiseData->GetValue("p"); //References to the promise resolve and reject methods auto resolve = promiseData->GetValue("res"); auto reject = promiseData->GetValue("rej"); auto callback = gcnew JavascriptAsyncMethodCallback(context, resolve, reject); auto request = CefProcessMessage::Create(kJavascriptRootObjectRequest); auto argList = request->GetArgumentList(); //Obtain a callbackId then send off the Request for objects auto callbackId = _callbackRegistry->SaveMethodCallback(callback); SetInt64(argList, 0, callbackId); argList->SetList(1, params); frame->SendProcessMessage(CefProcessId::PID_BROWSER, request); } } else { //Objects already bound or ignore cache CefRefPtr promiseResolve; CefRefPtr promiseException; auto promiseResolveScript = CefString("Promise.resolve({Success:false, Count:0, Message:'Object(s) already bound'});"); if (context->Eval(promiseResolveScript, CefString(), 0, promiseResolve, promiseException)) { retval = promiseResolve; if (notifyIfAlreadyBound) { NotifyObjectBound(frame, objectNamesWithBoundStatus); } } else { exception = promiseException->GetMessage(); } } } else { exception = "BindObjectAsyncHandler::Execute - Frame is invalid."; } } finally { context->Exit(); } } else { exception = "BindObjectAsyncHandler::Execute - Unable to Get or Enter Context"; } return true; } private: void NotifyObjectBound(const CefRefPtr frame, List^>^ objectNamesWithBoundStatus) { //Send message notifying Browser Process of which objects were bound //We do this after the objects have been created in the V8Context to gurantee //they are accessible. auto msg = CefProcessMessage::Create(kJavascriptObjectsBoundInJavascript); auto args = msg->GetArgumentList(); auto boundObjects = CefListValue::Create(); auto index = 0; for each(auto obj in objectNamesWithBoundStatus) { auto dict = CefDictionaryValue::Create(); auto name = obj->Item1; auto alreadyBound = obj->Item2; auto isCached = obj->Item3; dict->SetString("Name", StringUtils::ToNative(name)); dict->SetBool("IsCached", isCached); dict->SetBool("AlreadyBound", alreadyBound); boundObjects->SetDictionary(index++, dict); } args->SetList(0, boundObjects); frame->SendProcessMessage(CefProcessId::PID_BROWSER, msg); } bool GetV8BoolValue(const CefRefPtr val, const CefString key, const CefString camelCaseKey) { if (val->HasValue(key)) { auto obj = val->GetValue(key); if (obj->IsBool()) { return obj->GetBoolValue(); } } if (val->HasValue(camelCaseKey)) { auto obj = val->GetValue(camelCaseKey); if (obj->IsBool()) { return obj->GetBoolValue(); } } return false; } IMPLEMENT_REFCOUNTING(BindObjectAsyncHandler); }; }