// Copyright 2020 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/font_access/font_access.h"

#include <algorithm>

#include "base/feature_list.h"
#include "base/numerics/safe_conversions.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/font_access/font_enumeration_table.pb.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_query_options.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/modules/font_access/font_metadata.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"

namespace blink {

using mojom::blink::FontEnumerationStatus;

namespace {

const char kFeaturePolicyBlocked[] =
    "Access to the feature \"local-fonts\" is disallowed by Permissions Policy";
}

// static
const char FontAccess::kSupplementName[] = "FontAccess";

FontAccess::FontAccess(LocalDOMWindow* window)
    : Supplement<LocalDOMWindow>(*window), remote_(window) {}

void FontAccess::Trace(blink::Visitor* visitor) const {
  visitor->Trace(remote_);
  Supplement<LocalDOMWindow>::Trace(visitor);
}

// static
ScriptPromise<IDLSequence<FontMetadata>> FontAccess::queryLocalFonts(
    ScriptState* script_state,
    LocalDOMWindow& window,
    const QueryOptions* options,
    ExceptionState& exception_state) {
  DCHECK(ExecutionContext::From(script_state)->IsContextThread());
  return From(&window)->QueryLocalFontsImpl(script_state, options,
                                            exception_state);
}

// static
FontAccess* FontAccess::From(LocalDOMWindow* window) {
  auto* supplement = Supplement<LocalDOMWindow>::From<FontAccess>(window);
  if (!supplement) {
    supplement = MakeGarbageCollected<FontAccess>(window);
    Supplement<LocalDOMWindow>::ProvideTo(*window, supplement);
  }
  return supplement;
}

ScriptPromise<IDLSequence<FontMetadata>> FontAccess::QueryLocalFontsImpl(
    ScriptState* script_state,
    const QueryOptions* options,
    ExceptionState& exception_state) {
  if (!base::FeatureList::IsEnabled(blink::features::kFontAccess)) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Font Access feature is not supported.");
    return ScriptPromise<IDLSequence<FontMetadata>>();
  }
  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      "The execution context is not valid.");
    return ScriptPromise<IDLSequence<FontMetadata>>();
  }
  ExecutionContext* context = ExecutionContext::From(script_state);
  if (!context->IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kLocalFonts,
          ReportOptions::kReportOnFailure)) {
    exception_state.ThrowSecurityError(kFeaturePolicyBlocked);
    return ScriptPromise<IDLSequence<FontMetadata>>();
  }

  // Connect to font access manager remote if not bound already.
  if (!remote_.is_bound()) {
    context->GetBrowserInterfaceBroker().GetInterface(
        remote_.BindNewPipeAndPassReceiver(
            context->GetTaskRunner(TaskType::kFontLoading)));
    remote_.set_disconnect_handler(
        BindOnce(&FontAccess::OnDisconnect, WrapWeakPersistent(this)));
  }
  DCHECK(remote_.is_bound());

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<IDLSequence<FontMetadata>>>(
          script_state, exception_state.GetContext());
  auto promise = resolver->Promise();
  remote_->EnumerateLocalFonts(resolver->WrapCallbackInScriptScope(
      blink::BindOnce(&FontAccess::DidGetEnumerationResponse,
                      WrapWeakPersistent(this), WrapPersistent(options))));

  return promise;
}

void FontAccess::DidGetEnumerationResponse(
    const QueryOptions* options,
    ScriptPromiseResolver<IDLSequence<FontMetadata>>* resolver,
    FontEnumerationStatus status,
    base::ReadOnlySharedMemoryRegion region) {
  if (!resolver->GetScriptState()->ContextIsValid())
    return;

  if (RejectPromiseIfNecessary(status, resolver))
    return;

  // Return an empty font list if user has denied the permission request.
  if (status == FontEnumerationStatus::kPermissionDenied) {
    HeapVector<Member<FontMetadata>> entries;
    resolver->Resolve(std::move(entries));
    return;
  }

  // Font data exists; process and fill in the data.
  base::ReadOnlySharedMemoryMapping mapping = region.Map();
  if (mapping.size() > INT_MAX) {
    // Cannot deserialize without overflow.
    resolver->Reject(V8ThrowDOMException::CreateOrDie(
        resolver->GetScriptState()->GetIsolate(), DOMExceptionCode::kDataError,
        "Font data exceeds memory limit."));
    return;
  }

  // Used to compare with data coming from the browser to avoid conversions.
  const bool has_postscript_name_filter = options->hasPostscriptNames();
  std::set<std::string> selection_utf8;
  if (has_postscript_name_filter) {
    for (const String& postscript_name : options->postscriptNames()) {
      // While postscript names are encoded in a subset of ASCII, we convert the
      // input into UTF8. This will still allow exact matches to occur.
      selection_utf8.insert(postscript_name.Utf8());
    }
  }

  base::span<const uint8_t> mapped_mem(mapping);
  FontEnumerationTable table;
  table.ParseFromString(base::as_string_view(mapped_mem));

  HeapVector<Member<FontMetadata>> entries;
  for (const auto& element : table.fonts()) {
    // If the optional postscript name filter is set in QueryOptions,
    // only allow items that match.
    if (has_postscript_name_filter &&
        !selection_utf8.contains(element.postscript_name())) {
      continue;
    }

    auto entry = FontEnumerationEntry{
        .postscript_name = String::FromUtf8(element.postscript_name()),
        .full_name = String::FromUtf8(element.full_name()),
        .family = String::FromUtf8(element.family()),
        .style = String::FromUtf8(element.style()),
    };
    entries.push_back(MakeGarbageCollected<FontMetadata>(std::move(entry)));
  }

  resolver->Resolve(std::move(entries));
}

bool FontAccess::RejectPromiseIfNecessary(const FontEnumerationStatus& status,
                                          ScriptPromiseResolverBase* resolver) {
  switch (status) {
    case FontEnumerationStatus::kOk:
    case FontEnumerationStatus::kPermissionDenied:
      break;
    case FontEnumerationStatus::kUnimplemented:
      resolver->Reject(V8ThrowDOMException::CreateOrDie(
          resolver->GetScriptState()->GetIsolate(),
          DOMExceptionCode::kNotSupportedError,
          "Not yet supported on this platform."));
      return true;
    case FontEnumerationStatus::kNeedsUserActivation:
      resolver->Reject(V8ThrowDOMException::CreateOrDie(
          resolver->GetScriptState()->GetIsolate(),
          DOMExceptionCode::kSecurityError, "User activation is required."));
      return true;
    case FontEnumerationStatus::kNotVisible:
      resolver->Reject(V8ThrowDOMException::CreateOrDie(
          resolver->GetScriptState()->GetIsolate(),
          DOMExceptionCode::kSecurityError, "Page needs to be visible."));
      return true;
    case FontEnumerationStatus::kUnexpectedError:
    default:
      resolver->Reject(V8ThrowDOMException::CreateOrDie(
          resolver->GetScriptState()->GetIsolate(),
          DOMExceptionCode::kUnknownError, "An unexpected error occured."));
      return true;
  }
  return false;
}

void FontAccess::OnDisconnect() {
  remote_.reset();
}

}  // namespace blink
