chromium/third_party/blink/renderer/modules/remote_objects/remote_object.cc

// 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",
                            static_cast<int>(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(isolate, 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_IN_MIGRATION()
            << "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.This(), &remote_object)) {
    // Someone messed with the |this| pointer. Throw and return.
    isolate->ThrowException(v8::Exception::Error(
        V8String(isolate, kMethodInvocationOnNonInjectedObjectDisallowed)));
    return;
  }

  String method_name = ToCoreString(isolate, 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