| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/remote_objects/remote_object.h" |
| |
| #include <tuple> |
| |
| #include "base/numerics/safe_conversions.h" |
| #include "gin/converter.h" |
| #include "third_party/blink/public/web/blink.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_binding.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_private_property.h" |
| |
| namespace blink { |
| |
| gin::WrapperInfo RemoteObject::kWrapperInfo = {gin::kEmbedderNativeGin}; |
| |
| namespace { |
| |
| const char kMethodInvocationAsConstructorDisallowed[] = |
| "Java bridge method can't be invoked as a constructor"; |
| const char kMethodInvocationNonexistentMethod[] = |
| "Java bridge method does not exist for this object"; |
| const char kMethodInvocationOnNonInjectedObjectDisallowed[] = |
| "Java bridge method can't be invoked on a non-injected object"; |
| const char kMethodInvocationErrorMessage[] = |
| "Java bridge method invocation error"; |
| |
| String RemoteInvocationErrorToString( |
| mojom::blink::RemoteInvocationError value) { |
| switch (value) { |
| case mojom::blink::RemoteInvocationError::METHOD_NOT_FOUND: |
| return "method not found"; |
| case mojom::blink::RemoteInvocationError::OBJECT_GET_CLASS_BLOCKED: |
| return "invoking Object.getClass() is not permitted"; |
| case mojom::blink::RemoteInvocationError::EXCEPTION_THROWN: |
| return "an exception was thrown"; |
| case mojom::blink::RemoteInvocationError::NON_ASSIGNABLE_TYPES: |
| return "an incompatible object type passed to method parameter"; |
| default: |
| return String::Format("unknown RemoteInvocationError value: %d", value); |
| } |
| } |
| |
| v8::Local<v8::Object> GetMethodCache(v8::Isolate* isolate, |
| v8::Local<v8::Object> object) { |
| static const V8PrivateProperty::SymbolKey kMethodCacheKey; |
| V8PrivateProperty::Symbol method_cache_symbol = |
| V8PrivateProperty::GetSymbol(isolate, kMethodCacheKey); |
| v8::Local<v8::Value> result; |
| if (!method_cache_symbol.GetOrUndefined(object).ToLocal(&result)) |
| return v8::Local<v8::Object>(); |
| |
| if (result->IsUndefined()) { |
| result = v8::Object::New(isolate, v8::Null(isolate), nullptr, nullptr, 0); |
| std::ignore = method_cache_symbol.Set(object, result); |
| } |
| |
| DCHECK(result->IsObject()); |
| return result.As<v8::Object>(); |
| } |
| |
| mojom::blink::RemoteInvocationArgumentPtr JSValueToMojom( |
| const v8::Local<v8::Value>& js_value, |
| v8::Isolate* isolate) { |
| if (js_value->IsNumber()) { |
| return mojom::blink::RemoteInvocationArgument::NewNumberValue( |
| js_value->NumberValue(isolate->GetCurrentContext()).ToChecked()); |
| } |
| |
| if (js_value->IsBoolean()) { |
| return mojom::blink::RemoteInvocationArgument::NewBooleanValue( |
| js_value->BooleanValue(isolate)); |
| } |
| |
| if (js_value->IsString()) { |
| return mojom::blink::RemoteInvocationArgument::NewStringValue( |
| ToCoreString(js_value.As<v8::String>())); |
| } |
| |
| if (js_value->IsNull()) { |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull); |
| } |
| |
| if (js_value->IsUndefined()) { |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kUndefined); |
| } |
| |
| if (js_value->IsArray()) { |
| auto array = js_value.As<v8::Array>(); |
| WTF::Vector<mojom::blink::RemoteInvocationArgumentPtr> nested_arguments; |
| for (uint32_t i = 0; i < array->Length(); ++i) { |
| v8::Local<v8::Value> element_v8; |
| |
| if (!array->Get(isolate->GetCurrentContext(), i).ToLocal(&element_v8)) |
| return nullptr; |
| |
| // The array length might change during iteration. Set the output array |
| // elements to null for nonexistent input array elements. |
| if (!array->HasRealIndexedProperty(isolate->GetCurrentContext(), i) |
| .FromMaybe(false)) { |
| nested_arguments.push_back( |
| mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull)); |
| } else { |
| mojom::blink::RemoteInvocationArgumentPtr nested_argument; |
| |
| // This code prevents infinite recursion on the sender side. |
| // Null value is sent according to the Java-side conversion rules for |
| // expected parameter types: |
| // - multi-dimensional and object arrays are not allowed and are |
| // converted to nulls; |
| // - for primitive arrays, the null value will be converted to primitive |
| // zero; |
| // - for string arrays, the null value will be converted to a null |
| // string. See RemoteObjectImpl.convertArgument() in |
| // content/public/android/java/src/org/chromium/content/browser/remoteobjects/RemoteObjectImpl.java |
| if (element_v8->IsObject()) { |
| nested_argument = |
| mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull); |
| } else { |
| nested_argument = JSValueToMojom(element_v8, isolate); |
| } |
| |
| if (!nested_argument) |
| return nullptr; |
| |
| nested_arguments.push_back(std::move(nested_argument)); |
| } |
| } |
| |
| return mojom::blink::RemoteInvocationArgument::NewArrayValue( |
| std::move(nested_arguments)); |
| } |
| |
| if (js_value->IsTypedArray()) { |
| auto typed_array = js_value.As<v8::TypedArray>(); |
| mojom::blink::RemoteArrayType array_type; |
| if (typed_array->IsInt8Array()) { |
| array_type = mojom::blink::RemoteArrayType::kInt8Array; |
| } else if (typed_array->IsUint8Array() || |
| typed_array->IsUint8ClampedArray()) { |
| array_type = mojom::blink::RemoteArrayType::kUint8Array; |
| } else if (typed_array->IsInt16Array()) { |
| array_type = mojom::blink::RemoteArrayType::kInt16Array; |
| } else if (typed_array->IsUint16Array()) { |
| array_type = mojom::blink::RemoteArrayType::kUint16Array; |
| } else if (typed_array->IsInt32Array()) { |
| array_type = mojom::blink::RemoteArrayType::kInt32Array; |
| } else if (typed_array->IsUint32Array()) { |
| array_type = mojom::blink::RemoteArrayType::kUint32Array; |
| } else if (typed_array->IsFloat32Array()) { |
| array_type = mojom::blink::RemoteArrayType::kFloat32Array; |
| } else if (typed_array->IsFloat64Array()) { |
| array_type = mojom::blink::RemoteArrayType::kFloat64Array; |
| } else { |
| return nullptr; |
| } |
| |
| auto remote_typed_array = mojom::blink::RemoteTypedArray::New(); |
| mojo_base::BigBuffer buffer(typed_array->ByteLength()); |
| typed_array->CopyContents(buffer.data(), buffer.size()); |
| |
| remote_typed_array->buffer = std::move(buffer); |
| remote_typed_array->type = array_type; |
| |
| return mojom::blink::RemoteInvocationArgument::NewTypedArrayValue( |
| std::move(remote_typed_array)); |
| } |
| |
| if (js_value->IsArrayBuffer() || js_value->IsArrayBufferView()) { |
| // If ArrayBuffer or ArrayBufferView is not a TypedArray, we should treat it |
| // as undefined. |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kUndefined); |
| } |
| |
| if (js_value->IsObject()) { |
| v8::Local<v8::Object> object_val = js_value.As<v8::Object>(); |
| |
| RemoteObject* remote_object = nullptr; |
| if (gin::ConvertFromV8(isolate, object_val, &remote_object)) { |
| return mojom::blink::RemoteInvocationArgument::NewObjectIdValue( |
| remote_object->object_id()); |
| } |
| |
| v8::Local<v8::Value> length_value; |
| v8::TryCatch try_catch(isolate); |
| v8::MaybeLocal<v8::Value> maybe_length_value = object_val->Get( |
| isolate->GetCurrentContext(), V8AtomicString(isolate, "length")); |
| if (try_catch.HasCaught() || !maybe_length_value.ToLocal(&length_value)) { |
| length_value = v8::Null(isolate); |
| try_catch.Reset(); |
| } |
| |
| if (!length_value->IsNumber()) { |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kUndefined); |
| } |
| |
| double length = length_value.As<v8::Number>()->Value(); |
| if (length < 0 || length > std::numeric_limits<int32_t>::max()) { |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull); |
| } |
| |
| v8::Local<v8::Array> property_names; |
| if (!object_val->GetOwnPropertyNames(isolate->GetCurrentContext()) |
| .ToLocal(&property_names)) { |
| return mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull); |
| } |
| |
| WTF::Vector<mojom::blink::RemoteInvocationArgumentPtr> nested_arguments( |
| base::checked_cast<wtf_size_t>(length)); |
| for (uint32_t i = 0; i < property_names->Length(); ++i) { |
| v8::Local<v8::Value> key; |
| if (!property_names->Get(isolate->GetCurrentContext(), i).ToLocal(&key) || |
| key->IsString()) { |
| try_catch.Reset(); |
| continue; |
| } |
| |
| if (!key->IsNumber()) { |
| NOTREACHED() << "Key \"" << *v8::String::Utf8Value(isolate, key) |
| << "\" is not a number"; |
| continue; |
| } |
| |
| uint32_t key_value; |
| if (!key->Uint32Value(isolate->GetCurrentContext()).To(&key_value)) |
| continue; |
| |
| v8::Local<v8::Value> value_v8; |
| v8::MaybeLocal<v8::Value> maybe_value = |
| object_val->Get(isolate->GetCurrentContext(), key); |
| if (try_catch.HasCaught() || !maybe_value.ToLocal(&value_v8)) { |
| value_v8 = v8::Null(isolate); |
| try_catch.Reset(); |
| } |
| |
| auto nested_argument = JSValueToMojom(value_v8, isolate); |
| if (!nested_argument) |
| continue; |
| nested_arguments[key_value] = std::move(nested_argument); |
| } |
| |
| // Ensure that the vector has a null value. |
| for (wtf_size_t i = 0; i < nested_arguments.size(); i++) { |
| if (!nested_arguments[i]) { |
| nested_arguments[i] = |
| mojom::blink::RemoteInvocationArgument::NewSingletonValue( |
| mojom::blink::SingletonJavaScriptValue::kNull); |
| } |
| } |
| |
| return mojom::blink::RemoteInvocationArgument::NewArrayValue( |
| std::move(nested_arguments)); |
| } |
| |
| return nullptr; |
| } |
| |
| v8::Local<v8::Value> MojomToJSValue( |
| const mojom::blink::RemoteInvocationResultValuePtr& result_value, |
| v8::Isolate* isolate) { |
| if (result_value->is_number_value()) { |
| return v8::Number::New(isolate, result_value->get_number_value()); |
| } |
| |
| if (result_value->is_boolean_value()) { |
| return v8::Boolean::New(isolate, result_value->get_boolean_value()); |
| } |
| |
| if (result_value->is_string_value()) { |
| return V8String(isolate, result_value->get_string_value()); |
| } |
| |
| switch (result_value->get_singleton_value()) { |
| case mojom::blink::SingletonJavaScriptValue::kNull: |
| return v8::Null(isolate); |
| case mojom::blink::SingletonJavaScriptValue::kUndefined: |
| return v8::Undefined(isolate); |
| } |
| |
| return v8::Local<v8::Value>(); |
| } |
| } // namespace |
| |
| RemoteObject::RemoteObject(v8::Isolate* isolate, |
| RemoteObjectGatewayImpl* gateway, |
| int32_t object_id) |
| : gin::NamedPropertyInterceptor(isolate, this), |
| gateway_(gateway), |
| object_id_(object_id) {} |
| |
| RemoteObject::~RemoteObject() { |
| if (gateway_) { |
| gateway_->ReleaseObject(object_id_, this); |
| |
| if (object_) |
| object_->NotifyReleasedObject(); |
| } |
| } |
| |
| gin::ObjectTemplateBuilder RemoteObject::GetObjectTemplateBuilder( |
| v8::Isolate* isolate) { |
| return gin::Wrappable<RemoteObject>::GetObjectTemplateBuilder(isolate) |
| .AddNamedPropertyInterceptor(); |
| } |
| |
| void RemoteObject::RemoteObjectInvokeCallback( |
| const v8::FunctionCallbackInfo<v8::Value>& info) { |
| v8::Isolate* isolate = info.GetIsolate(); |
| if (info.IsConstructCall()) { |
| // This is not a constructor. Throw and return. |
| isolate->ThrowException(v8::Exception::Error( |
| V8String(isolate, kMethodInvocationAsConstructorDisallowed))); |
| return; |
| } |
| |
| RemoteObject* remote_object; |
| if (!gin::ConvertFromV8(isolate, info.Holder(), &remote_object)) { |
| // Someone messed with the |this| pointer. Throw and return. |
| isolate->ThrowException(v8::Exception::Error( |
| V8String(isolate, kMethodInvocationOnNonInjectedObjectDisallowed))); |
| return; |
| } |
| |
| String method_name = ToCoreString(info.Data().As<v8::String>()); |
| |
| v8::Local<v8::Object> method_cache = GetMethodCache( |
| isolate, remote_object->GetWrapper(isolate).ToLocalChecked()); |
| if (method_cache.IsEmpty()) |
| return; |
| |
| v8::Local<v8::Value> cached_method = |
| method_cache |
| ->Get(isolate->GetCurrentContext(), info.Data().As<v8::String>()) |
| .ToLocalChecked(); |
| |
| if (cached_method->IsUndefined()) { |
| isolate->ThrowException(v8::Exception::Error( |
| V8String(isolate, kMethodInvocationNonexistentMethod))); |
| return; |
| } |
| |
| WTF::Vector<mojom::blink::RemoteInvocationArgumentPtr> arguments; |
| arguments.ReserveInitialCapacity(info.Length()); |
| |
| for (int i = 0; i < info.Length(); i++) { |
| auto argument = JSValueToMojom(info[i], isolate); |
| if (!argument) |
| return; |
| |
| arguments.push_back(std::move(argument)); |
| } |
| |
| remote_object->EnsureRemoteIsBound(); |
| mojom::blink::RemoteInvocationResultPtr result; |
| remote_object->object_->InvokeMethod(method_name, std::move(arguments), |
| &result); |
| |
| if (result->error != mojom::blink::RemoteInvocationError::OK) { |
| String message = String::Format("%s : ", kMethodInvocationErrorMessage) + |
| RemoteInvocationErrorToString(result->error); |
| isolate->ThrowException(v8::Exception::Error(V8String(isolate, message))); |
| return; |
| } |
| |
| if (!result->value) |
| return; |
| |
| if (result->value->is_object_id()) { |
| RemoteObject* object_result = remote_object->gateway_->GetRemoteObject( |
| info.GetIsolate(), result->value->get_object_id()); |
| gin::Handle<RemoteObject> controller = |
| gin::CreateHandle(isolate, object_result); |
| if (controller.IsEmpty()) |
| info.GetReturnValue().SetUndefined(); |
| else |
| info.GetReturnValue().Set(controller.ToV8()); |
| } else { |
| info.GetReturnValue().Set(MojomToJSValue(result->value, isolate)); |
| } |
| } |
| |
| void RemoteObject::EnsureRemoteIsBound() { |
| if (!object_.is_bound()) { |
| gateway_->BindRemoteObjectReceiver(object_id_, |
| object_.BindNewPipeAndPassReceiver()); |
| } |
| } |
| |
| v8::Local<v8::Value> RemoteObject::GetNamedProperty( |
| v8::Isolate* isolate, |
| const std::string& property) { |
| auto wtf_property = WTF::String::FromUTF8(property); |
| |
| v8::Local<v8::String> v8_property = V8AtomicString(isolate, wtf_property); |
| v8::Local<v8::Object> method_cache = |
| GetMethodCache(isolate, GetWrapper(isolate).ToLocalChecked()); |
| if (method_cache.IsEmpty()) |
| return v8::Local<v8::Value>(); |
| |
| v8::Local<v8::Value> cached_method = |
| method_cache->Get(isolate->GetCurrentContext(), v8_property) |
| .ToLocalChecked(); |
| |
| if (!cached_method->IsUndefined()) |
| return cached_method; |
| |
| // if not in the cache, ask the browser |
| EnsureRemoteIsBound(); |
| bool method_exists = false; |
| object_->HasMethod(wtf_property, &method_exists); |
| |
| if (!method_exists) { |
| return v8::Local<v8::Value>(); |
| } |
| |
| auto function = v8::Function::New(isolate->GetCurrentContext(), |
| RemoteObjectInvokeCallback, v8_property) |
| .ToLocalChecked(); |
| |
| std::ignore = method_cache->CreateDataProperty(isolate->GetCurrentContext(), |
| v8_property, function); |
| return function; |
| } |
| |
| std::vector<std::string> RemoteObject::EnumerateNamedProperties( |
| v8::Isolate* isolate) { |
| EnsureRemoteIsBound(); |
| WTF::Vector<WTF::String> methods; |
| object_->GetMethods(&methods); |
| std::vector<std::string> result; |
| for (const auto& method : methods) |
| result.push_back(method.Utf8()); |
| return result; |
| } |
| |
| } // namespace blink |