// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/renderer/sync_encryption_keys_extension.h"

#include <utility>
#include <vector>

#include "base/bind.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/renderer/chrome_object_extensions_utils.h"
#include "content/public/renderer/render_frame.h"
#include "gin/arguments.h"
#include "gin/function_template.h"
#include "google_apis/gaia/gaia_urls.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/web/blink.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "url/origin.h"

namespace {

const url::Origin& GetAllowedOrigin() {
  static const base::NoDestructor<url::Origin> origin(
      url::Origin::Create(GaiaUrls::GetInstance()->gaia_url()));
  CHECK(!origin->opaque());
  return *origin;
}

// This function is intended to convert a binary blob representing an encryption
// key and provided by the web via a Javascript ArrayBuffer.
std::vector<uint8_t> ArrayBufferAsBytes(
    const v8::Local<v8::ArrayBuffer>& array_buffer) {
  auto backing_store = array_buffer->GetBackingStore();
  const uint8_t* start =
      reinterpret_cast<const uint8_t*>(backing_store->Data());
  const size_t length = backing_store->ByteLength();
  return std::vector<uint8_t>(start, start + length);
}

std::vector<std::vector<uint8_t>> EncryptionKeysAsBytes(
    const std::vector<v8::Local<v8::ArrayBuffer>>& encryption_keys) {
  std::vector<std::vector<uint8_t>> encryption_keys_as_bytes;
  for (const v8::Local<v8::ArrayBuffer>& encryption_key : encryption_keys) {
    encryption_keys_as_bytes.push_back(ArrayBufferAsBytes(encryption_key));
  }
  return encryption_keys_as_bytes;
}

}  // namespace

// static
void SyncEncryptionKeysExtension::Create(content::RenderFrame* frame) {
  new SyncEncryptionKeysExtension(frame);
}

SyncEncryptionKeysExtension::SyncEncryptionKeysExtension(
    content::RenderFrame* frame)
    : content::RenderFrameObserver(frame) {}

SyncEncryptionKeysExtension::~SyncEncryptionKeysExtension() {}

void SyncEncryptionKeysExtension::OnDestruct() {
  delete this;
}

void SyncEncryptionKeysExtension::DidCreateScriptContext(
    v8::Local<v8::Context> v8_context,
    int32_t world_id) {
  if (!render_frame()) {
    return;
  }

  url::Origin origin = render_frame()->GetWebFrame()->GetSecurityOrigin();
  if (render_frame()->IsMainFrame() &&
      world_id == content::ISOLATED_WORLD_ID_GLOBAL &&
      origin == GetAllowedOrigin()) {
    Install();
  }
}

void SyncEncryptionKeysExtension::Install() {
  DCHECK(render_frame());

  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context =
      render_frame()->GetWebFrame()->MainWorldScriptContext();
  if (context.IsEmpty()) {
    return;
  }

  v8::Context::Scope context_scope(context);

  v8::Local<v8::Object> chrome =
      content::GetOrCreateChromeObject(isolate, context);
  v8::Local<v8::Function> function =
      gin::CreateFunctionTemplate(
          isolate, base::BindRepeating(
                       &SyncEncryptionKeysExtension::SetSyncEncryptionKeys,
                       weak_ptr_factory_.GetWeakPtr()))
          ->GetFunction(context)
          .ToLocalChecked();
  chrome
      ->Set(context, gin::StringToSymbol(isolate, "setSyncEncryptionKeys"),
            function)
      .Check();
}

void SyncEncryptionKeysExtension::SetSyncEncryptionKeys(gin::Arguments* args) {
  DCHECK(render_frame());

  // This function as exposed to the web has the following signature:
  //   setSyncEncryptionKeys(callback, gaia_id, encryption_keys,
  //                         last_key_version)
  //
  // Where:
  //   callback: Allows caller to get notified upon completion.
  //   gaia_id: String representing the user's server-provided ID.
  //   encryption_keys: Array where each element is an ArrayBuffer representing
  //                    an encryption key (binary blob).
  //   last_key_version: Key version corresponding to the last key in
  //                     |encryption_keys|.

  v8::HandleScope handle_scope(args->isolate());

  v8::Local<v8::Function> callback;
  if (!args->GetNext(&callback)) {
    DLOG(ERROR) << "No callback";
    args->ThrowError();
    return;
  }

  std::string gaia_id;
  if (!args->GetNext(&gaia_id)) {
    DLOG(ERROR) << "No account ID";
    args->ThrowError();
    return;
  }

  std::vector<v8::Local<v8::ArrayBuffer>> encryption_keys;
  if (!args->GetNext(&encryption_keys)) {
    DLOG(ERROR) << "Not array of strings";
    args->ThrowError();
    return;
  }

  if (encryption_keys.empty()) {
    DLOG(ERROR) << "Array of strings empty";
    args->ThrowError();
    return;
  }

  int last_key_version = 0;
  if (!args->GetNext(&last_key_version)) {
    DLOG(ERROR) << "No version provided";
    args->ThrowError();
    return;
  }

  auto global_callback =
      std::make_unique<v8::Global<v8::Function>>(args->isolate(), callback);

  if (!remote_.is_bound()) {
    render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(&remote_);
  }

  remote_->SetEncryptionKeys(
      gaia_id, EncryptionKeysAsBytes(encryption_keys), last_key_version,
      base::BindOnce(&SyncEncryptionKeysExtension::RunCompletionCallback,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(global_callback)));
}

void SyncEncryptionKeysExtension::RunCompletionCallback(
    std::unique_ptr<v8::Global<v8::Function>> callback) {
  if (!render_frame()) {
    return;
  }

  v8::Isolate* isolate = blink::MainThreadIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context =
      render_frame()->GetWebFrame()->MainWorldScriptContext();
  v8::Context::Scope context_scope(context);
  v8::Local<v8::Function> callback_local =
      v8::Local<v8::Function>::New(isolate, *callback);

  render_frame()->GetWebFrame()->CallFunctionEvenIfScriptDisabled(
      callback_local, v8::Undefined(isolate), /*argc=*/0, /*argv=*/{});
}
