/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "SerialPermissionRequest.h"

#include "SerialLogging.h"
#include "js/PropertyAndElement.h"
#include "js/Wrapper.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/JSONWriter.h"
#include "mozilla/RandomNum.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "nsContentPermissionHelper.h"
#include "nsContentUtils.h"
#include "nsIArray.h"
#include "nsThreadUtils.h"
#include "xpcpublic.h"

namespace mozilla::dom {

constexpr uint32_t kPermissionDeniedBaseDelayMs = 3000;
constexpr uint32_t kPermissionDeniedMaxRandomDelayMs = 10000;

NS_IMPL_ISUPPORTS(SerialPermissionRequest, nsIContentPermissionRequest)

SerialPermissionRequest::SerialPermissionRequest(
    WindowGlobalParent* aWindowGlobalParent, bool aAutoselect,
    nsTArray<IPCSerialPortInfo>&& aPorts)
    : mWindowGlobalParent(aWindowGlobalParent),
      mAutoselect(aAutoselect),
      mPorts(std::move(aPorts)) {
  MOZ_ASSERT(mWindowGlobalParent);
  MOZ_ASSERT(mWindowGlobalParent->DocumentPrincipal());
}

nsIPrincipal* SerialPermissionRequest::Principal() const {
  return mWindowGlobalParent->DocumentPrincipal();
}

SerialPermissionRequest::~SerialPermissionRequest() {
  // Defensive: if no path settled the promise, fall back to InternalError so
  // the consumer always sees a decision.
  mPromiseHolder.RejectIfExists(RequestPortReason::InternalError, __func__);
}

bool SerialPermissionRequest::IsSitePermAllow() const {
  return nsContentUtils::IsSitePermAllow(Principal(), "serial"_ns);
}

bool SerialPermissionRequest::IsSitePermDeny() const {
  return nsContentUtils::IsSitePermDeny(Principal(), "serial"_ns);
}

bool SerialPermissionRequest::ShouldShowAddonGate() const {
  nsIPrincipal* principal = Principal();
  return StaticPrefs::dom_webserial_gated() &&
         StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
         !IsSitePermAllow() && !principal->GetIsLoopbackHost() &&
         !principal->SchemeIs("file");
}

RefPtr<SerialChooserPromise> SerialPermissionRequest::Run() {
  AssertIsOnMainThread();

  RefPtr<SerialChooserPromise> promise = mPromiseHolder.Ensure(__func__);

  if (!IsSitePermAllow()) {
    if (IsSitePermDeny()) {
      CancelWithRandomizedDelay(RequestPortReason::AddonDenied);
      return promise;
    }
    if (nsContentUtils::IsSitePermDeny(Principal(), "install"_ns) &&
        StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
        !Principal()->GetIsLoopbackHost()) {
      CancelWithRandomizedDelay(RequestPortReason::AddonDenied);
      return promise;
    }
  }

  // Testing-only autoselect bypass: when the testing pref is on and gated
  // mode is off, we skip the prompt entirely and pick the first port. The
  // bypass is only honored when testing is enabled, so a malicious content
  // process cannot use the autoselect flag to skip the chooser in release.
  if (mAutoselect && StaticPrefs::dom_webserial_testing_enabled() &&
      !StaticPrefs::dom_webserial_gated()) {
    if (mPorts.IsEmpty()) {
      ResolveCancelled(RequestPortReason::UserCancelled);
      return promise;
    }
    ResolveWithPort(mPorts[0]);
    return promise;
  }

  if (NS_FAILED(DoPrompt())) {
    ResolveCancelled(RequestPortReason::InternalError);
  }
  return promise;
}

void SerialPermissionRequest::CancelWithRandomizedDelay(
    RequestPortReason aReason) {
  AssertIsOnMainThread();

  uint32_t randomDelayMS =
      xpc::IsInAutomation()
          ? 0
          : RandomUint64OrDie() % kPermissionDeniedMaxRandomDelayMs;
  auto delay = TimeDuration::FromMilliseconds(kPermissionDeniedBaseDelayMs +
                                              randomDelayMS);
  NS_NewTimerWithCallback(
      getter_AddRefs(mCancelTimer),
      [self = RefPtr{this}, aReason](auto) { self->ResolveCancelled(aReason); },
      delay, nsITimer::TYPE_ONE_SHOT,
      "SerialPermissionRequest::CancelWithRandomizedDelay"_ns);
}

nsresult SerialPermissionRequest::DoPrompt() {
  return nsContentPermissionUtils::AskPermission(this, /* aWindow */ nullptr);
}

void SerialPermissionRequest::ResolveWithPort(const IPCSerialPortInfo& aPort) {
  mCancelTimer = nullptr;
  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("    friendlyName: %s",
           NS_ConvertUTF16toUTF8(aPort.friendlyName()).get()));
  if (aPort.usbVendorId().isSome()) {
    MOZ_LOG(gWebSerialLog, LogLevel::Info,
            ("    usbVendorId: 0x%04x", aPort.usbVendorId().value()));
  }
  if (aPort.usbProductId().isSome()) {
    MOZ_LOG(gWebSerialLog, LogLevel::Info,
            ("    usbProductId: 0x%04x", aPort.usbProductId().value()));
  }

  mPromiseHolder.ResolveIfExists(aPort, __func__);
}

void SerialPermissionRequest::ResolveCancelled(RequestPortReason aReason) {
  mCancelTimer = nullptr;
  mPromiseHolder.RejectIfExists(aReason, __func__);
}

NS_IMETHODIMP
SerialPermissionRequest::GetTypes(nsIArray** aTypes) {
  NS_ENSURE_ARG_POINTER(aTypes);

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("SerialPermissionRequest::GetTypes called with %zu ports",
           mPorts.Length()));

  nsTArray<nsString> options;

  // Pass the autoselect flag to the JS UI so that it can auto-confirm the
  // chooser when testing+gated is in use.
  if (mAutoselect && StaticPrefs::dom_webserial_testing_enabled()) {
    options.AppendElement(u"{\"__autoselect__\":true}"_ns);
  }

  size_t index = 0;
  for (const auto& port : mPorts) {
    nsCString utf8Json;
    JSONStringRefWriteFunc writeFunc(utf8Json);
    JSONWriter writer(writeFunc, JSONWriter::SingleLineStyle);

    NS_ConvertUTF16toUTF8 utf8Path(port.path());
    NS_ConvertUTF16toUTF8 utf8FriendlyName(port.friendlyName());

    MOZ_LOG(gWebSerialLog, LogLevel::Debug,
            ("  Port[%zu]: path=%s, friendlyName=%s, vid=0x%04x, pid=0x%04x",
             index, utf8Path.get(), utf8FriendlyName.get(),
             port.usbVendorId().valueOr(0), port.usbProductId().valueOr(0)));

    writer.StartObjectElement();
    writer.StringProperty("path", MakeStringSpan(utf8Path.get()));
    writer.StringProperty("friendlyName",
                          MakeStringSpan(utf8FriendlyName.get()));
    writer.IntProperty("usbVendorId", port.usbVendorId().valueOr(0));
    writer.IntProperty("usbProductId", port.usbProductId().valueOr(0));
    writer.EndObject();

    nsString utf16Json;
    CopyUTF8toUTF16(utf8Json, utf16Json);
    options.AppendElement(std::move(utf16Json));
    ++index;
  }

  return nsContentPermissionUtils::CreatePermissionArray("serial"_ns, options,
                                                         aTypes);
}

NS_IMETHODIMP
SerialPermissionRequest::GetPrincipal(nsIPrincipal** aPrincipal) {
  NS_IF_ADDREF(*aPrincipal = Principal());
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetTopLevelPrincipal(nsIPrincipal** aPrincipal) {
  NS_IF_ADDREF(*aPrincipal = mWindowGlobalParent->TopWindowContext()
                                 ->DocumentPrincipal());
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetDelegatePrincipal(const nsACString& aType,
                                              nsIPrincipal** aPrincipal) {
  return PermissionDelegateHandler::GetDelegatePrincipal(aType, this,
                                                         aPrincipal);
}

NS_IMETHODIMP
SerialPermissionRequest::GetWindow(mozIDOMWindow** aWindow) {
  *aWindow = nullptr;
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetElement(Element** aElement) {
  NS_ENSURE_ARG_POINTER(aElement);
  RefPtr<Element> element = mWindowGlobalParent->GetRootOwnerElement();
  element.forget(aElement);
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetHasValidTransientUserGestureActivation(bool* aOut) {
  // The content process already rejects requestPort() without transient
  // user activation (Serial::RequestPort step 3), so the parent-side request
  // can report true without re-querying WindowContext (whose transient-
  // activation state is only tracked in-process and would assert here).
  *aOut = true;
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetIsRequestDelegatedToUnsafeThirdParty(bool* aOut) {
  // The parent process does not have a permission delegate handler available
  // (that lives on the document) so we conservatively report false.
  *aOut = false;
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::GetIgnoreAllowSitePermission(bool* aOut) {
  *aOut = false;
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::NotifyShown() { return NS_OK; }

NS_IMETHODIMP
SerialPermissionRequest::Cancel() {
  AssertIsOnMainThread();
  mCancelTimer = nullptr;

  RequestPortReason reason = RequestPortReason::UserCancelled;
  if (ShouldShowAddonGate()) {
    reason = RequestPortReason::AddonDenied;
  }

  ResolveCancelled(reason);
  return NS_OK;
}

NS_IMETHODIMP
SerialPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
  AssertIsOnMainThread();
  mCancelTimer = nullptr;

  if (!aChoices.isObject()) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  // The choices object can come in any realm — chrome calls Allow() from JS.
  // Unwrap and enter its realm before reading properties.
  JS::Rooted<JSObject*> obj(RootingCx(), &aChoices.toObject());
  obj = js::CheckedUnwrapStatic(obj);
  if (!obj) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  nsIGlobalObject* global = xpc::NativeGlobal(obj);
  if (!global) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  AutoJSAPI jsapi;
  if (!jsapi.Init(global)) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }
  JSContext* cx = jsapi.cx();

  JS::Rooted<JS::Value> serialVal(cx);
  if (!JS_GetProperty(cx, obj, "serial", &serialVal) || !serialVal.isString()) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  nsAutoJSString choice;
  if (!choice.init(cx, serialVal)) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  nsresult rv;
  int32_t selectedIndex = choice.ToInteger(&rv);
  if (NS_FAILED(rv) || selectedIndex < 0 ||
      static_cast<size_t>(selectedIndex) >= mPorts.Length()) {
    ResolveCancelled(RequestPortReason::InternalError);
    return NS_OK;
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Info,
          ("  Selected port at index %d:", selectedIndex));
  ResolveWithPort(mPorts[selectedIndex]);
  return NS_OK;
}

}  // namespace mozilla::dom
