diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md index 59d8a0d36311..1776b7c61a1f 100644 --- a/docs/api/context-bridge.md +++ b/docs/api/context-bridge.md @@ -61,6 +61,20 @@ The `contextBridge` module has the following methods: * `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. * `api` any - Your API, more information on what this API can be and how it works is available below. +### `contextBridge.executeInMainWorld(executionScript)` _Experimental_ + + + +* `executionScript` Object + * `func` (...args: any[]) => any - A JavaScript function to execute. This function will be serialized which means + that any bound parameters and execution context will be lost. + * `args` any[] (optional) - An array of arguments to pass to the provided function. These + arguments will be copied between worlds in accordance with + [the table of supported types.](#parameter--error--return-type-support) + +Returns `any` - A copy of the resulting value from executing the function in the main world. +[Refer to the table](#parameter--error--return-type-support) on how values are copied between worlds. + ## Usage ### API diff --git a/lib/renderer/api/context-bridge.ts b/lib/renderer/api/context-bridge.ts index 99b133e6c3ac..7894ebab7cf2 100644 --- a/lib/renderer/api/context-bridge.ts +++ b/lib/renderer/api/context-bridge.ts @@ -5,13 +5,17 @@ const checkContextIsolationEnabled = () => { }; const contextBridge: Electron.ContextBridge = { - exposeInMainWorld: (key: string, api: any) => { + exposeInMainWorld: (key, api) => { checkContextIsolationEnabled(); return binding.exposeAPIInWorld(0, key, api); }, - exposeInIsolatedWorld: (worldId: number, key: string, api: any) => { + exposeInIsolatedWorld: (worldId, key, api) => { checkContextIsolationEnabled(); return binding.exposeAPIInWorld(worldId, key, api); + }, + executeInMainWorld: (script) => { + checkContextIsolationEnabled(); + return binding.executeInWorld(0, script); } }; @@ -27,8 +31,7 @@ export const internalContextBridge = { }, overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => { return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null); - }, - isInMainWorld: () => binding._isCalledFromMainWorld() as boolean + } }; if (binding._isDebug) { diff --git a/shell/renderer/api/electron_api_context_bridge.cc b/shell/renderer/api/electron_api_context_bridge.cc index dd35c5b7840e..f1dbffa32f1a 100644 --- a/shell/renderer/api/electron_api_context_bridge.cc +++ b/shell/renderer/api/electron_api_context_bridge.cc @@ -13,11 +13,14 @@ #include "base/containers/contains.h" #include "base/feature_list.h" +#include "base/json/json_writer.h" #include "base/trace_event/trace_event.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.h" +#include "gin/converter.h" #include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/promise.h" #include "shell/common/node_includes.h" @@ -25,6 +28,7 @@ #include "third_party/blink/public/web/web_blob.h" #include "third_party/blink/public/web/web_element.h" #include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck namespace features { BASE_FEATURE(kContextBridgeMutability, @@ -133,8 +137,21 @@ v8::MaybeLocal GetPrivate(v8::Local context, } // namespace -v8::MaybeLocal PassValueToOtherContext( +// Forward declare methods +void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info); +v8::MaybeLocal CreateProxyForAPI( + const v8::Local& api_object, + const v8::Local& source_context, + const blink::ExecutionContext* source_execution_context, + const v8::Local& destination_context, + context_bridge::ObjectCache* object_cache, + bool support_dynamic_properties, + int recursion_depth, + BridgeErrorTarget error_target); + +v8::MaybeLocal PassValueToOtherContextInner( v8::Local source_context, + const blink::ExecutionContext* source_execution_context, v8::Local destination_context, v8::Local value, v8::Local parent_value, @@ -142,7 +159,7 @@ v8::MaybeLocal PassValueToOtherContext( bool support_dynamic_properties, int recursion_depth, BridgeErrorTarget error_target) { - TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext"); + TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContextInner"); if (recursion_depth >= kMaxRecursion) { v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource ? source_context @@ -245,7 +262,6 @@ v8::MaybeLocal PassValueToOtherContext( if (global_source_context.IsEmpty() || global_destination_context.IsEmpty()) return; - context_bridge::ObjectCache object_cache; v8::MaybeLocal val; { v8::TryCatch try_catch(isolate); @@ -253,7 +269,7 @@ v8::MaybeLocal PassValueToOtherContext( global_source_context.Get(isolate); val = PassValueToOtherContext( source_context, global_destination_context.Get(isolate), result, - source_context->Global(), &object_cache, false, 0, + source_context->Global(), false, BridgeErrorTarget::kDestination); if (try_catch.HasCaught()) { if (try_catch.Message().IsEmpty()) { @@ -293,7 +309,6 @@ v8::MaybeLocal PassValueToOtherContext( if (global_source_context.IsEmpty() || global_destination_context.IsEmpty()) return; - context_bridge::ObjectCache object_cache; v8::MaybeLocal val; { v8::TryCatch try_catch(isolate); @@ -301,7 +316,7 @@ v8::MaybeLocal PassValueToOtherContext( global_source_context.Get(isolate); val = PassValueToOtherContext( source_context, global_destination_context.Get(isolate), result, - source_context->Global(), &object_cache, false, 0, + source_context->Global(), false, BridgeErrorTarget::kDestination); if (try_catch.HasCaught()) { if (try_catch.Message().IsEmpty()) { @@ -367,8 +382,8 @@ v8::MaybeLocal PassValueToOtherContext( v8::Local cloned_arr = v8::Array::New(destination_context->GetIsolate(), length); for (size_t i = 0; i < length; i++) { - auto value_for_array = PassValueToOtherContext( - source_context, destination_context, + auto value_for_array = PassValueToOtherContextInner( + source_context, source_execution_context, destination_context, arr->Get(source_context, i).ToLocalChecked(), value, object_cache, support_dynamic_properties, recursion_depth + 1, error_target); if (value_for_array.IsEmpty()) @@ -383,30 +398,34 @@ v8::MaybeLocal PassValueToOtherContext( return v8::MaybeLocal(cloned_arr); } - // Custom logic to "clone" Element references - blink::WebElement elem = - blink::WebElement::FromV8Value(destination_context->GetIsolate(), value); - if (!elem.IsNull()) { - v8::Context::Scope destination_context_scope(destination_context); - return v8::MaybeLocal( - elem.ToV8Value(destination_context->GetIsolate())); - } + // Clone certain DOM APIs only within Window contexts. + if (source_execution_context->IsWindow()) { + // Custom logic to "clone" Element references + blink::WebElement elem = blink::WebElement::FromV8Value( + destination_context->GetIsolate(), value); + if (!elem.IsNull()) { + v8::Context::Scope destination_context_scope(destination_context); + return v8::MaybeLocal( + elem.ToV8Value(destination_context->GetIsolate())); + } - // Custom logic to "clone" Blob references - blink::WebBlob blob = - blink::WebBlob::FromV8Value(destination_context->GetIsolate(), value); - if (!blob.IsNull()) { - v8::Context::Scope destination_context_scope(destination_context); - return v8::MaybeLocal( - blob.ToV8Value(destination_context->GetIsolate())); + // Custom logic to "clone" Blob references + blink::WebBlob blob = + blink::WebBlob::FromV8Value(destination_context->GetIsolate(), value); + if (!blob.IsNull()) { + v8::Context::Scope destination_context_scope(destination_context); + return v8::MaybeLocal( + blob.ToV8Value(destination_context->GetIsolate())); + } } // Proxy all objects if (IsPlainObject(value)) { auto object_value = value.As(); auto passed_value = CreateProxyForAPI( - object_value, source_context, destination_context, object_cache, - support_dynamic_properties, recursion_depth + 1, error_target); + object_value, source_context, source_execution_context, + destination_context, object_cache, support_dynamic_properties, + recursion_depth + 1, error_target); if (passed_value.IsEmpty()) return {}; return v8::MaybeLocal(passed_value.ToLocalChecked()); @@ -434,6 +453,28 @@ v8::MaybeLocal PassValueToOtherContext( } } +v8::MaybeLocal PassValueToOtherContext( + v8::Local source_context, + v8::Local destination_context, + v8::Local value, + v8::Local parent_value, + bool support_dynamic_properties, + BridgeErrorTarget error_target, + context_bridge::ObjectCache* existing_object_cache) { + TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext"); + + context_bridge::ObjectCache local_object_cache; + context_bridge::ObjectCache* object_cache = + existing_object_cache ? existing_object_cache : &local_object_cache; + + const blink::ExecutionContext* source_execution_context = + blink::ExecutionContext::From(source_context); + DCHECK(source_execution_context); + return PassValueToOtherContextInner( + source_context, source_execution_context, destination_context, value, + parent_value, object_cache, support_dynamic_properties, 0, error_target); +} + void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper"); CHECK(info.Data()->IsObject()); @@ -464,6 +505,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { { v8::Context::Scope func_owning_context_scope(func_owning_context); + + // Cache duplicate arguments as the same proxied value. context_bridge::ObjectCache object_cache; std::vector> original_args; @@ -473,8 +516,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { for (auto value : original_args) { auto arg = PassValueToOtherContext( calling_context, func_owning_context, value, - calling_context->Global(), &object_cache, support_dynamic_properties, - 0, BridgeErrorTarget::kSource); + calling_context->Global(), support_dynamic_properties, + BridgeErrorTarget::kSource, &object_cache); if (arg.IsEmpty()) return; proxied_args.push_back(arg.ToLocalChecked()); @@ -540,11 +583,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { v8::Local exception; { v8::TryCatch try_catch(args.isolate()); - ret = PassValueToOtherContext(func_owning_context, calling_context, - maybe_return_value.ToLocalChecked(), - func_owning_context->Global(), - &object_cache, support_dynamic_properties, - 0, BridgeErrorTarget::kDestination); + ret = PassValueToOtherContext( + func_owning_context, calling_context, + maybe_return_value.ToLocalChecked(), func_owning_context->Global(), + support_dynamic_properties, BridgeErrorTarget::kDestination); if (try_catch.HasCaught()) { did_error_converting_result = true; if (!try_catch.Message().IsEmpty()) { @@ -576,6 +618,7 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { v8::MaybeLocal CreateProxyForAPI( const v8::Local& api_object, const v8::Local& source_context, + const blink::ExecutionContext* source_execution_context, const v8::Local& destination_context, context_bridge::ObjectCache* object_cache, bool support_dynamic_properties, @@ -619,18 +662,20 @@ v8::MaybeLocal CreateProxyForAPI( v8::Local getter_proxy; v8::Local setter_proxy; if (!getter.IsEmpty()) { - if (!PassValueToOtherContext( - source_context, destination_context, getter, - api.GetHandle(), object_cache, - support_dynamic_properties, 1, error_target) + if (!PassValueToOtherContextInner( + source_context, source_execution_context, + destination_context, getter, api.GetHandle(), + object_cache, support_dynamic_properties, 1, + error_target) .ToLocal(&getter_proxy)) continue; } if (!setter.IsEmpty()) { - if (!PassValueToOtherContext( - source_context, destination_context, setter, - api.GetHandle(), object_cache, - support_dynamic_properties, 1, error_target) + if (!PassValueToOtherContextInner( + source_context, source_execution_context, + destination_context, setter, api.GetHandle(), + object_cache, support_dynamic_properties, 1, + error_target) .ToLocal(&setter_proxy)) continue; } @@ -646,10 +691,10 @@ v8::MaybeLocal CreateProxyForAPI( if (!api.Get(key, &value)) continue; - auto passed_value = PassValueToOtherContext( - source_context, destination_context, value, api.GetHandle(), - object_cache, support_dynamic_properties, recursion_depth + 1, - error_target); + auto passed_value = PassValueToOtherContextInner( + source_context, source_execution_context, destination_context, value, + api.GetHandle(), object_cache, support_dynamic_properties, + recursion_depth + 1, error_target); if (passed_value.IsEmpty()) return {}; proxy.Set(key, passed_value.ToLocalChecked()); @@ -661,24 +706,14 @@ v8::MaybeLocal CreateProxyForAPI( namespace { -void ExposeAPIInWorld(v8::Isolate* isolate, - const int world_id, - const std::string& key, - v8::Local api, - gin_helper::Arguments* args) { - TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key, - "worldId", world_id); - - auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); - CHECK(render_frame); - auto* frame = render_frame->GetWebFrame(); - CHECK(frame); - - v8::Local target_context = - world_id == WorldIDs::MAIN_WORLD_ID - ? frame->MainWorldScriptContext() - : frame->GetScriptContextFromWorldId(isolate, world_id); - +void ExposeAPI(v8::Isolate* isolate, + v8::Local source_context, + v8::Local target_context, + const std::string& key, + v8::Local api, + gin_helper::Arguments* args) { + DCHECK(!target_context.IsEmpty()); + v8::Context::Scope target_context_scope(target_context); gin_helper::Dictionary global(target_context->GetIsolate(), target_context->Global()); @@ -689,33 +724,70 @@ void ExposeAPIInWorld(v8::Isolate* isolate, return; } - v8::Local electron_isolated_context = - frame->GetScriptContextFromWorldId(args->isolate(), - WorldIDs::ISOLATED_WORLD_ID); + v8::MaybeLocal maybe_proxy = PassValueToOtherContext( + source_context, target_context, api, source_context->Global(), false, + BridgeErrorTarget::kSource); + if (maybe_proxy.IsEmpty()) + return; + auto proxy = maybe_proxy.ToLocalChecked(); - { - context_bridge::ObjectCache object_cache; - v8::Context::Scope target_context_scope(target_context); - - v8::MaybeLocal maybe_proxy = PassValueToOtherContext( - electron_isolated_context, target_context, api, - electron_isolated_context->Global(), &object_cache, false, 0, - BridgeErrorTarget::kSource); - if (maybe_proxy.IsEmpty()) - return; - auto proxy = maybe_proxy.ToLocalChecked(); - - if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) { - global.Set(key, proxy); - return; - } - - if (proxy->IsObject() && !proxy->IsTypedArray() && - !DeepFreeze(proxy.As(), target_context)) - return; - - global.SetReadOnlyNonConfigurable(key, proxy); + if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) { + global.Set(key, proxy); + return; } + + if (proxy->IsObject() && !proxy->IsTypedArray() && + !DeepFreeze(proxy.As(), target_context)) + return; + + global.SetReadOnlyNonConfigurable(key, proxy); +} + +// Attempt to get the target context based on the current context. +// +// For render frames, this is either the main world (0) or an arbitrary +// world ID. For service workers, Electron only supports one isolated +// context and the main worker context. Anything else is invalid. +v8::MaybeLocal GetTargetContext(v8::Isolate* isolate, + const int world_id) { + v8::Local source_context = isolate->GetCurrentContext(); + v8::MaybeLocal maybe_target_context; + + blink::ExecutionContext* execution_context = + blink::ExecutionContext::From(source_context); + if (execution_context->IsWindow()) { + auto* render_frame = GetRenderFrame(source_context->Global()); + CHECK(render_frame); + auto* frame = render_frame->GetWebFrame(); + CHECK(frame); + + maybe_target_context = + world_id == WorldIDs::MAIN_WORLD_ID + ? frame->MainWorldScriptContext() + : frame->GetScriptContextFromWorldId(isolate, world_id); + } else { + NOTREACHED(); + } + + CHECK(!maybe_target_context.IsEmpty()); + return maybe_target_context; +} + +void ExposeAPIInWorld(v8::Isolate* isolate, + const int world_id, + const std::string& key, + v8::Local api, + gin_helper::Arguments* args) { + TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key, + "worldId", world_id); + v8::Local source_context = isolate->GetCurrentContext(); + CHECK(!source_context.IsEmpty()); + v8::MaybeLocal maybe_target_context = + GetTargetContext(isolate, world_id); + if (maybe_target_context.IsEmpty()) + return; + v8::Local target_context = maybe_target_context.ToLocalChecked(); + ExposeAPI(isolate, source_context, target_context, key, api, args); } gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start, @@ -747,12 +819,10 @@ void OverrideGlobalValueFromIsolatedWorld( { v8::Context::Scope main_context_scope(main_context); - context_bridge::ObjectCache object_cache; v8::Local source_context = value->GetCreationContextChecked(); v8::MaybeLocal maybe_proxy = PassValueToOtherContext( source_context, main_context, value, source_context->Global(), - &object_cache, support_dynamic_properties, 1, - BridgeErrorTarget::kSource); + support_dynamic_properties, BridgeErrorTarget::kSource); DCHECK(!maybe_proxy.IsEmpty()); auto proxy = maybe_proxy.ToLocalChecked(); @@ -789,8 +859,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld( v8::Local source_context = getter->GetCreationContextChecked(); v8::MaybeLocal maybe_getter_proxy = PassValueToOtherContext( - source_context, main_context, getter, source_context->Global(), - &object_cache, false, 1, BridgeErrorTarget::kSource); + source_context, main_context, getter, source_context->Global(), false, + BridgeErrorTarget::kSource); DCHECK(!maybe_getter_proxy.IsEmpty()); getter_proxy = maybe_getter_proxy.ToLocalChecked(); } @@ -798,8 +868,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld( v8::Local source_context = getter->GetCreationContextChecked(); v8::MaybeLocal maybe_setter_proxy = PassValueToOtherContext( - source_context, main_context, setter, source_context->Global(), - &object_cache, false, 1, BridgeErrorTarget::kSource); + source_context, main_context, setter, source_context->Global(), false, + BridgeErrorTarget::kSource); DCHECK(!maybe_setter_proxy.IsEmpty()); setter_proxy = maybe_setter_proxy.ToLocalChecked(); } @@ -812,13 +882,205 @@ bool OverrideGlobalPropertyFromIsolatedWorld( } } -bool IsCalledFromMainWorld(v8::Isolate* isolate) { - auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); - CHECK(render_frame); - auto* frame = render_frame->GetWebFrame(); - CHECK(frame); - v8::Local main_context = frame->MainWorldScriptContext(); - return isolate->GetCurrentContext() == main_context; +// Serialize script to be executed in the given world. +v8::Local ExecuteInWorld(v8::Isolate* isolate, + const int world_id, + gin_helper::Arguments* args) { + // Get context of caller + v8::Local source_context = isolate->GetCurrentContext(); + + // Get execution script argument + gin_helper::Dictionary exec_script; + if (args->Length() >= 1 && !args->GetNext(&exec_script)) { + gin_helper::ErrorThrower(args->isolate()).ThrowError("Invalid script"); + return v8::Undefined(isolate); + } + + // Get "func" from execution script + v8::Local func; + if (!exec_script.Get("func", &func)) { + gin_helper::ErrorThrower(isolate).ThrowError( + "Function 'func' is required in script"); + return v8::Undefined(isolate); + } + + // Get optional "args" from execution script + v8::Local args_array; + v8::Local args_value; + if (exec_script.Get("args", &args_value)) { + if (!args_value->IsArray()) { + gin_helper::ErrorThrower(isolate).ThrowError("'args' must be an array"); + return v8::Undefined(isolate); + } + args_array = args_value.As(); + } + + // Serialize the function + std::string function_str; + { + v8::Local serialized_function; + if (!func->FunctionProtoToString(isolate->GetCurrentContext()) + .ToLocal(&serialized_function)) { + gin_helper::ErrorThrower(isolate).ThrowError( + "Failed to serialize function"); + return v8::Undefined(isolate); + } + // If ToLocal() succeeds, this should always be a string. + CHECK(gin::Converter::FromV8(isolate, serialized_function, + &function_str)); + } + + // Get the target context + v8::MaybeLocal maybe_target_context = + GetTargetContext(isolate, world_id); + v8::Local target_context; + if (!maybe_target_context.ToLocal(&target_context)) { + isolate->ThrowException(v8::Exception::Error(gin::StringToV8( + isolate, + base::StringPrintf("Failed to get context for world %d", world_id)))); + return v8::Undefined(isolate); + } + + // Compile the script + v8::Local compiled_script; + { + v8::Context::Scope target_scope(target_context); + std::string error_message; + v8::MaybeLocal maybe_compiled_script; + { + v8::TryCatch try_catch(isolate); + std::string return_func_code = + base::StringPrintf("(%s)", function_str.c_str()); + maybe_compiled_script = v8::Script::Compile( + target_context, gin::StringToV8(isolate, return_func_code)); + if (try_catch.HasCaught()) { + // Must throw outside of TryCatch scope + v8::String::Utf8Value error(isolate, try_catch.Exception()); + error_message = + *error ? *error : "Unknown error during script compilation"; + } + } + if (!maybe_compiled_script.ToLocal(&compiled_script)) { + isolate->ThrowException( + v8::Exception::Error(gin::StringToV8(isolate, error_message))); + return v8::Undefined(isolate); + } + } + + // Run the script + v8::Local copied_func; + { + v8::Context::Scope target_scope(target_context); + std::string error_message; + v8::MaybeLocal maybe_script_result; + { + v8::TryCatch try_catch(isolate); + maybe_script_result = compiled_script->Run(target_context); + if (try_catch.HasCaught()) { + // Must throw outside of TryCatch scope + v8::String::Utf8Value error(isolate, try_catch.Exception()); + error_message = + *error ? *error : "Unknown error during script execution"; + } + } + v8::Local script_result; + if (!maybe_script_result.ToLocal(&script_result)) { + isolate->ThrowException( + v8::Exception::Error(gin::StringToV8(isolate, error_message))); + return v8::Undefined(isolate); + } + if (!script_result->IsFunction()) { + isolate->ThrowException(v8::Exception::Error( + gin::StringToV8(isolate, + "Expected script to result in a function but a " + "non-function type was found"))); + return v8::Undefined(isolate); + } + // Get copied function from the script result + copied_func = script_result.As(); + } + + // Proxy args to be passed into copied function + std::vector> proxied_args; + { + v8::Context::Scope target_scope(target_context); + bool support_dynamic_properties = false; + uint32_t args_length = args_array.IsEmpty() ? 0 : args_array->Length(); + + // Cache duplicate arguments as the same proxied value. + context_bridge::ObjectCache object_cache; + + for (uint32_t i = 0; i < args_length; ++i) { + v8::Local arg; + if (!args_array->Get(source_context, i).ToLocal(&arg)) { + gin_helper::ErrorThrower(isolate).ThrowError( + base::StringPrintf("Failed to get argument at index %d", i)); + return v8::Undefined(isolate); + } + + auto proxied_arg = PassValueToOtherContext( + source_context, target_context, arg, source_context->Global(), + support_dynamic_properties, BridgeErrorTarget::kSource, + &object_cache); + if (proxied_arg.IsEmpty()) { + gin_helper::ErrorThrower(isolate).ThrowError( + base::StringPrintf("Failed to proxy argument at index %d", i)); + return v8::Undefined(isolate); + } + proxied_args.push_back(proxied_arg.ToLocalChecked()); + } + } + + // Call the function and get the result + v8::Local result; + { + v8::Context::Scope target_scope(target_context); + std::string error_message; + v8::MaybeLocal maybe_result; + { + v8::TryCatch try_catch(isolate); + maybe_result = + copied_func->Call(isolate, target_context, v8::Null(isolate), + proxied_args.size(), proxied_args.data()); + if (try_catch.HasCaught()) { + v8::String::Utf8Value error(isolate, try_catch.Exception()); + error_message = + *error ? *error : "Unknown error during function execution"; + } + } + if (!maybe_result.ToLocal(&result)) { + // Must throw outside of TryCatch scope + isolate->ThrowException( + v8::Exception::Error(gin::StringToV8(isolate, error_message))); + return v8::Undefined(isolate); + } + } + + // Clone the result into the source/caller context + v8::Local cloned_result; + { + v8::Context::Scope source_scope(source_context); + std::string error_message; + v8::MaybeLocal maybe_cloned_result; + { + v8::TryCatch try_catch(isolate); + // Pass value from target context back to source context + maybe_cloned_result = PassValueToOtherContext( + target_context, source_context, result, target_context->Global(), + false, BridgeErrorTarget::kSource); + if (try_catch.HasCaught()) { + v8::String::Utf8Value utf8(isolate, try_catch.Exception()); + error_message = *utf8 ? *utf8 : "Unknown error cloning result"; + } + } + if (!maybe_cloned_result.ToLocal(&cloned_result)) { + // Must throw outside of TryCatch scope + isolate->ThrowException( + v8::Exception::Error(gin::StringToV8(isolate, error_message))); + return v8::Undefined(isolate); + } + } + return cloned_result; } } // namespace @@ -835,13 +1097,12 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); + dict.SetMethod("executeInWorld", &electron::api::ExecuteInWorld); dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld); dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", &electron::api::OverrideGlobalValueFromIsolatedWorld); dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", &electron::api::OverrideGlobalPropertyFromIsolatedWorld); - dict.SetMethod("_isCalledFromMainWorld", - &electron::api::IsCalledFromMainWorld); #if DCHECK_IS_ON() dict.Set("_isDebug", true); #endif diff --git a/shell/renderer/api/electron_api_context_bridge.h b/shell/renderer/api/electron_api_context_bridge.h index 4f17f0b06e5f..f8498ae658ae 100644 --- a/shell/renderer/api/electron_api_context_bridge.h +++ b/shell/renderer/api/electron_api_context_bridge.h @@ -14,8 +14,6 @@ class Arguments; namespace electron::api { -void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info); - // Where the context bridge should create the exception it is about to throw enum class BridgeErrorTarget { // The source / calling context. This is default and correct 99% of the time, @@ -44,19 +42,9 @@ v8::MaybeLocal PassValueToOtherContext( * the bridge set this to the "context" of the value. */ v8::Local parent_value, - context_bridge::ObjectCache* object_cache, bool support_dynamic_properties, - int recursion_depth, - BridgeErrorTarget error_target); - -v8::MaybeLocal CreateProxyForAPI( - const v8::Local& api_object, - const v8::Local& source_context, - const v8::Local& destination_context, - context_bridge::ObjectCache* object_cache, - bool support_dynamic_properties, - int recursion_depth, - BridgeErrorTarget error_target); + BridgeErrorTarget error_target, + context_bridge::ObjectCache* existing_object_cache = nullptr); } // namespace electron::api diff --git a/shell/renderer/api/electron_api_web_frame.cc b/shell/renderer/api/electron_api_web_frame.cc index 2424eeec6507..0f5211c5b2b0 100644 --- a/shell/renderer/api/electron_api_web_frame.cc +++ b/shell/renderer/api/electron_api_web_frame.cc @@ -150,13 +150,11 @@ class ScriptExecutionCallback { "An unknown exception occurred while getting the result of the script"; { v8::TryCatch try_catch(isolate); - context_bridge::ObjectCache object_cache; v8::Local source_context = result->GetCreationContextChecked(); - maybe_result = - PassValueToOtherContext(source_context, promise_.GetContext(), result, - source_context->Global(), &object_cache, - false, 0, BridgeErrorTarget::kSource); + maybe_result = PassValueToOtherContext( + source_context, promise_.GetContext(), result, + source_context->Global(), false, BridgeErrorTarget::kSource); if (maybe_result.IsEmpty() || try_catch.HasCaught()) { success = false; } diff --git a/shell/renderer/renderer_client_base.cc b/shell/renderer/renderer_client_base.cc index ffac675b0f14..1db92a9a9520 100644 --- a/shell/renderer/renderer_client_base.cc +++ b/shell/renderer/renderer_client_base.cc @@ -611,10 +611,9 @@ void RendererClientBase::SetupMainWorldOverrides( v8::Local guest_view_internal; if (global.GetHidden("guestViewInternal", &guest_view_internal)) { - api::context_bridge::ObjectCache object_cache; auto result = api::PassValueToOtherContext( source_context, context, guest_view_internal, source_context->Global(), - &object_cache, false, 0, api::BridgeErrorTarget::kSource); + false, api::BridgeErrorTarget::kSource); if (!result.IsEmpty()) { isolated_api.Set("guestViewInternal", result.ToLocalChecked()); } diff --git a/spec/api-context-bridge-spec.ts b/spec/api-context-bridge-spec.ts index d82470ab8bf6..b7fa47104264 100644 --- a/spec/api-context-bridge-spec.ts +++ b/spec/api-context-bridge-spec.ts @@ -108,7 +108,7 @@ describe('contextBridge', () => { }; const callWithBindings = (fn: Function, worldId: number = 0) => - worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ; + worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); const getGCInfo = async (): Promise<{ trackedValues: number; @@ -408,6 +408,17 @@ describe('contextBridge', () => { expect(result).equal(true); }); + it('should proxy function arguments only once', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', (a: any, b: any) => a === b); + }); + const result = await callWithBindings(async (root: any) => { + const obj = { foo: 1 }; + return root.example(obj, obj); + }); + expect(result).to.be.true(); + }); + it('should properly handle errors thrown in proxied functions', async () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); }); @@ -1290,6 +1301,131 @@ describe('contextBridge', () => { }); }); }); + + describe('executeInMainWorld', () => { + it('serializes function and proxies args', async () => { + await makeBindingWindow(async () => { + const values = [ + undefined, + null, + 123, + 'string', + true, + [123, 'string', true, ['foo']], + () => 'string', + Symbol('foo') + ]; + function appendArg (arg: any) { + // @ts-ignore + globalThis.args = globalThis.args || []; + // @ts-ignore + globalThis.args.push(arg); + } + for (const value of values) { + try { + await contextBridge.executeInMainWorld({ + func: appendArg, + args: [value] + }); + } catch { + contextBridge.executeInMainWorld({ + func: appendArg, + args: ['FAIL'] + }); + } + } + }); + const result = await callWithBindings(() => { + // @ts-ignore + return globalThis.args.map(arg => { + // Map unserializable IPC types to their type string + if (['function', 'symbol'].includes(typeof arg)) { + return typeof arg; + } else { + return arg; + } + }); + }); + expect(result).to.deep.equal([ + undefined, + null, + 123, + 'string', + true, + [123, 'string', true, ['foo']], + 'function', + 'symbol' + ]); + }); + + it('allows function args to be invoked', async () => { + const donePromise = once(ipcMain, 'done'); + makeBindingWindow(() => { + const uuid = crypto.randomUUID(); + const done = (receivedUuid: string) => { + if (receivedUuid === uuid) { + require('electron').ipcRenderer.send('done'); + } + }; + contextBridge.executeInMainWorld({ + func: (callback, innerUuid) => { + callback(innerUuid); + }, + args: [done, uuid] + }); + }); + await donePromise; + }); + + it('proxies arguments only once', async () => { + await makeBindingWindow(() => { + const obj = {}; + // @ts-ignore + globalThis.result = contextBridge.executeInMainWorld({ + func: (a, b) => a === b, + args: [obj, obj] + }); + }); + const result = await callWithBindings(() => { + // @ts-ignore + return globalThis.result; + }, 999); + expect(result).to.be.true(); + }); + + it('safely clones returned objects', async () => { + await makeBindingWindow(() => { + const obj = contextBridge.executeInMainWorld({ + func: () => ({}) + }); + // @ts-ignore + globalThis.safe = obj.constructor === Object; + }); + const result = await callWithBindings(() => { + // @ts-ignore + return globalThis.safe; + }, 999); + expect(result).to.be.true(); + }); + + it('uses internal Function.prototype.toString', async () => { + await makeBindingWindow(() => { + const funcHack = () => { + // @ts-ignore + globalThis.hacked = 'nope'; + }; + funcHack.toString = () => '() => { globalThis.hacked = \'gotem\'; }'; + contextBridge.executeInMainWorld({ + func: funcHack + }); + }); + const result = await callWithBindings(() => { + // @ts-ignore + return globalThis.hacked; + }); + expect(result).to.equal('nope'); + }); + }); }); }; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 41da29713f3d..1ab4507af12d 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -63,7 +63,6 @@ declare namespace Electron { overrideGlobalValueFromIsolatedWorld(keys: string[], value: any): void; overrideGlobalValueWithDynamicPropsFromIsolatedWorld(keys: string[], value: any): void; overrideGlobalPropertyFromIsolatedWorld(keys: string[], getter: Function, setter?: Function): void; - isInMainWorld(): boolean; } }