/* 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 "nsClipboardProxy.h"

#if defined(ACCESSIBILITY) && defined(XP_WIN)
#  include "mozilla/a11y/Compatibility.h"
#endif
#include "mozilla/ClipboardContentAnalysisChild.h"
#include "mozilla/ClipboardReadRequestChild.h"
#include "mozilla/ClipboardWriteRequestChild.h"
#include "mozilla/Components.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/net/CookieJarSettings.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "nsArrayUtils.h"
#include "nsBaseClipboard.h"
#include "nsIContentAnalysis.h"
#include "nsITransferable.h"
#include "nsISupportsPrimitives.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsXULAppAPI.h"
#include "nsContentUtils.h"
#include "PermissionMessageUtils.h"

using mozilla::ipc::ResponseRejectReason;
using namespace mozilla;
using namespace mozilla::dom;

NS_IMPL_ISUPPORTS(nsClipboardProxy, nsIClipboard, nsIClipboardProxy)

nsClipboardProxy::nsClipboardProxy() : mClipboardCaps(false, false, false) {}

NS_IMETHODIMP
nsClipboardProxy::SetData(nsITransferable* aTransferable,
                          nsIClipboardOwner* anOwner,
                          nsIClipboard::ClipboardType aWhichClipboard,
                          mozilla::dom::WindowContext* aWindowContext) {
#if defined(ACCESSIBILITY) && defined(XP_WIN)
  a11y::Compatibility::SuppressA11yForClipboardCopy();
#endif

  ContentChild* child = ContentChild::GetSingleton();
  IPCTransferable ipcTransferable;
  nsContentUtils::TransferableToIPCTransferable(aTransferable, &ipcTransferable,
                                                false, nullptr);
  child->SendSetClipboard(std::move(ipcTransferable), aWhichClipboard,
                          aWindowContext);
  return NS_OK;
}

NS_IMETHODIMP nsClipboardProxy::AsyncSetData(
    nsIClipboard::ClipboardType aWhichClipboard,
    mozilla::dom::WindowContext* aSettingWindowContext,
    nsIAsyncClipboardRequestCallback* aCallback,
    nsIAsyncSetClipboardData** _retval) {
  RefPtr<ClipboardWriteRequestChild> request =
      MakeRefPtr<ClipboardWriteRequestChild>(aCallback);
  ContentChild::GetSingleton()->SendPClipboardWriteRequestConstructor(
      request, aWhichClipboard, aSettingWindowContext);
  request.forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
nsClipboardProxy::GetData(nsITransferable* aTransferable,
                          nsIClipboard::ClipboardType aWhichClipboard,
                          mozilla::dom::WindowContext* aWindowContext) {
  MOZ_DIAGNOSTIC_ASSERT(aWindowContext && aWindowContext->IsInProcess(),
                        "content clipboard reads must be associated with an "
                        "in-process WindowContext");
  if (aWindowContext->IsDiscarded()) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  nsTArray<nsCString> types;
  aTransferable->FlavorsTransferableCanImport(types);
  // The web custom format map is a synthetic flavor that aggregates the
  // per-essence "web foo/bar" slots; it must not be exposed to content
  // processes through the sync transferable API. Content code should
  // discover web custom formats via nsIClipboard::getDataSnapshot{,Sync}.
  for (const auto& type : types) {
    if (type.EqualsLiteral(kWebCustomFormatMapType)) {
      return NS_ERROR_FAILURE;
    }
  }

  IPCTransferableDataOrError transferableOrError;
  if (MOZ_UNLIKELY(nsIContentAnalysis::MightBeActive())) {
    RefPtr<ClipboardContentAnalysisChild> contentAnalysis =
        ClipboardContentAnalysisChild::GetOrCreate();
    if (!contentAnalysis) {
      return NS_ERROR_FAILURE;
    }
    if (!contentAnalysis->SendGetClipboard(types, aWhichClipboard,
                                           aWindowContext->InnerWindowId(),
                                           &transferableOrError)) {
      return NS_ERROR_FAILURE;
    }
  } else {
    if (!ContentChild::GetSingleton()->SendGetClipboard(
            types, aWhichClipboard, aWindowContext, &transferableOrError)) {
      return NS_ERROR_FAILURE;
    };
  }

  if (transferableOrError.type() == IPCTransferableDataOrError::Tnsresult) {
    MOZ_ASSERT(NS_FAILED(transferableOrError.get_nsresult()));
    return transferableOrError.get_nsresult();
  }

  return nsContentUtils::IPCTransferableDataToTransferable(
      transferableOrError.get_IPCTransferableData(), false /* aAddDataFlavor */,
      aTransferable, false /* aFilterUnknownFlavors */);
}

NS_IMETHODIMP
nsClipboardProxy::GetDataIfSmallerThan(
    nsITransferable* aTransferable, uint64_t aThreshold,
    nsIClipboard::ClipboardType aWhichClipboard, WindowContext* aWindowContext,
    JSContext* aJSContext, Promise** aPromise) {
  MOZ_DIAGNOSTIC_ASSERT(aWindowContext && aWindowContext->IsInProcess());

  if (aWindowContext->IsDiscarded()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aJSContext);
  if (!global) {
    return NS_ERROR_UNEXPECTED;
  }

  RefPtr<Promise> promise = Promise::Create(global, IgnoreErrors());
  if (!promise) {
    return NS_ERROR_UNEXPECTED;
  }

  auto guard = MakeScopeExit([&]() { promise.forget(aPromise); });

  nsCOMPtr<nsITransferable> transferable = aTransferable;
  auto resolver = [promise,
                   transferable](const IPCTransferableDataOrError& result) {
    if (result.type() == IPCTransferableDataOrError::Tnsresult) {
      if (result.get_nsresult() == NS_ERROR_CLIPBOARD_TOO_BIG) {
        promise->MaybeResolve(false);
      } else {
        promise->MaybeReject(result.get_nsresult());
      }
      return;
    }

    const auto& ipcData = result.get_IPCTransferableData();
    nsresult rv = nsContentUtils::IPCTransferableDataToTransferable(
        ipcData, false, transferable, false);

    if (NS_FAILED(rv)) {
      promise->MaybeReject(rv);
      return;
    }

    promise->MaybeResolve(true);
  };
  auto rejecter = [promise](mozilla::ipc::ResponseRejectReason) {
    promise->MaybeReject(NS_ERROR_FAILURE);
  };

  nsTArray<nsCString> types;
  aTransferable->FlavorsTransferableCanImport(types);

  if (MOZ_UNLIKELY(nsIContentAnalysis::MightBeActive())) {
    RefPtr<ClipboardContentAnalysisChild> child =
        ClipboardContentAnalysisChild::GetOrCreate();
    if (!child) {
      promise->MaybeReject(NS_ERROR_UNEXPECTED);
      return NS_OK;
    }
    child->SendGetClipboardDataIfSmallerThan(
        std::move(types), aThreshold, aWhichClipboard,
        aWindowContext->InnerWindowId(), std::move(resolver),
        std::move(rejecter));
  } else {
    auto* child = ContentChild::GetSingleton();
    child->SendGetClipboardDataIfSmallerThan(
        std::move(types), aThreshold, aWhichClipboard, aWindowContext,
        std::move(resolver), std::move(rejecter));
  }

  return NS_OK;
}

NS_IMETHODIMP
nsClipboardProxy::GetDataIfSmallerThanNative(
    nsITransferable* aTransferable, uint64_t aThreshold,
    ClipboardType aWhichClipboard,
    mozilla::dom::WindowContext* aWindowContext) {
  MOZ_ASSERT_UNREACHABLE(
      "getDataIfSmallerThanNative should only be called in the parent process");
  return NS_ERROR_NOT_AVAILABLE;
}

namespace {

class ClipboardDataSnapshotProxy final : public nsIClipboardDataSnapshot {
 public:
  explicit ClipboardDataSnapshotProxy(ClipboardReadRequestChild* aActor)
      : mActor(aActor) {
    MOZ_ASSERT(mActor);
  }

  NS_DECL_ISUPPORTS
  NS_DECL_NSICLIPBOARDDATASNAPSHOT

 private:
  virtual ~ClipboardDataSnapshotProxy() {
    MOZ_ASSERT(mActor);
    if (mActor->CanSend()) {
      PClipboardReadRequestChild::Send__delete__(mActor);
    }
  };

  RefPtr<ClipboardReadRequestChild> mActor;
};

NS_IMPL_ISUPPORTS(ClipboardDataSnapshotProxy, nsIClipboardDataSnapshot)

NS_IMETHODIMP ClipboardDataSnapshotProxy::GetValid(bool* aOutResult) {
  MOZ_ASSERT(mActor);
  *aOutResult = mActor->CanSend();
  return NS_OK;
}

NS_IMETHODIMP ClipboardDataSnapshotProxy::GetFlavorList(
    nsTArray<nsCString>& aFlavorList) {
  MOZ_ASSERT(mActor);
  aFlavorList.AppendElements(mActor->FlavorList());
  return NS_OK;
}

NS_IMETHODIMP ClipboardDataSnapshotProxy::GetData(
    nsITransferable* aTransferable,
    nsIAsyncClipboardRequestCallback* aCallback) {
  if (!aTransferable || !aCallback) {
    return NS_ERROR_INVALID_ARG;
  }

  // Get a list of flavors this transferable can import
  nsTArray<nsCString> flavors;
  nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
  if (NS_FAILED(rv)) {
    return rv;
  }

  MOZ_ASSERT(mActor);
  // If the requested flavor is not in the list, throw an error.
  for (const auto& flavor : flavors) {
    if (!mActor->FlavorList().Contains(flavor)) {
      return NS_ERROR_FAILURE;
    }
  }

  if (!mActor->CanSend()) {
    return aCallback->OnComplete(NS_ERROR_NOT_AVAILABLE);
  }

  mActor->SendGetData(flavors)->Then(
      GetMainThreadSerialEventTarget(), __func__,
      /* resolve */
      [self = RefPtr{this}, callback = nsCOMPtr{aCallback},
       transferable = nsCOMPtr{aTransferable}](
          const IPCTransferableDataOrError& aIpcTransferableDataOrError) {
        if (aIpcTransferableDataOrError.type() ==
            IPCTransferableDataOrError::Tnsresult) {
          MOZ_ASSERT(NS_FAILED(aIpcTransferableDataOrError.get_nsresult()));
          callback->OnComplete(aIpcTransferableDataOrError.get_nsresult());
          return;
        }

        nsresult rv = nsContentUtils::IPCTransferableDataToTransferable(
            aIpcTransferableDataOrError.get_IPCTransferableData(),
            false /* aAddDataFlavor */, transferable,
            false /* aFilterUnknownFlavors */);
        if (NS_FAILED(rv)) {
          callback->OnComplete(rv);
          return;
        }

        callback->OnComplete(NS_OK);
      },
      /* reject */
      [callback = nsCOMPtr{aCallback}](ResponseRejectReason aReason) {
        callback->OnComplete(ResponseRejectReason::ActorDestroyed == aReason
                                 ? NS_ERROR_NOT_AVAILABLE
                                 : NS_ERROR_FAILURE);
      });

  return NS_OK;
}

NS_IMETHODIMP ClipboardDataSnapshotProxy::GetDataSync(
    nsITransferable* aTransferable) {
  if (!aTransferable) {
    return NS_ERROR_INVALID_ARG;
  }

  // Get a list of flavors this transferable can import
  nsTArray<nsCString> flavors;
  nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
  if (NS_FAILED(rv)) {
    return rv;
  }

  MOZ_ASSERT(mActor);
  // If the requested flavor is not in the list, throw an error.
  for (const auto& flavor : flavors) {
    if (!mActor->FlavorList().Contains(flavor)) {
      return NS_ERROR_FAILURE;
    }
  }

  if (!mActor->CanSend()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  IPCTransferableDataOrError ipcTransferableDataOrError;
  bool success = mActor->SendGetDataSync(flavors, &ipcTransferableDataOrError);
  if (!success) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  if (ipcTransferableDataOrError.type() ==
      IPCTransferableDataOrError::Tnsresult) {
    MOZ_ASSERT(NS_FAILED(ipcTransferableDataOrError.get_nsresult()));
    return ipcTransferableDataOrError.get_nsresult();
  }
  rv = nsContentUtils::IPCTransferableDataToTransferable(
      ipcTransferableDataOrError.get_IPCTransferableData(),
      false /* aAddDataFlavor */, aTransferable,
      false /* aFilterUnknownFlavors */);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

static Result<RefPtr<ClipboardDataSnapshotProxy>, nsresult>
CreateClipboardDataSnapshotProxy(
    ClipboardReadRequestOrError&& aClipboardReadRequestOrError) {
  if (aClipboardReadRequestOrError.type() ==
      ClipboardReadRequestOrError::Tnsresult) {
    MOZ_ASSERT(NS_FAILED(aClipboardReadRequestOrError.get_nsresult()));
    return Err(aClipboardReadRequestOrError.get_nsresult());
  }

  ClipboardReadRequest& request =
      aClipboardReadRequestOrError.get_ClipboardReadRequest();
  auto requestChild = MakeRefPtr<ClipboardReadRequestChild>(
      std::move(request.availableTypes()));
  if (NS_WARN_IF(
          !ContentChild::GetSingleton()->BindPClipboardReadRequestEndpoint(
              std::move(request.childEndpoint()), requestChild))) {
    return Err(NS_ERROR_FAILURE);
  }

  return MakeRefPtr<ClipboardDataSnapshotProxy>(requestChild);
}

}  // namespace

NS_IMETHODIMP nsClipboardProxy::GetDataSnapshot(
    const nsTArray<nsCString>& aFlavorList,
    nsIClipboard::ClipboardType aWhichClipboard,
    mozilla::dom::WindowContext* aRequestingWindowContext,
    nsIPrincipal* aRequestingPrincipal,
    nsIClipboardGetDataSnapshotCallback* aCallback) {
  if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) {
    return NS_ERROR_INVALID_ARG;
  }

  if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
    MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
                      aWhichClipboard);
    return NS_ERROR_FAILURE;
  }

  ContentChild::GetSingleton()
      ->SendGetClipboardDataSnapshot(aFlavorList, aWhichClipboard,
                                     aRequestingWindowContext,
                                     WrapNotNull(aRequestingPrincipal))
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          /* resolve */
          [callback = nsCOMPtr{aCallback}](
              ClipboardReadRequestOrError&& aClipboardReadRequestOrError) {
            auto result = CreateClipboardDataSnapshotProxy(
                std::move(aClipboardReadRequestOrError));
            if (result.isErr()) {
              callback->OnError(result.unwrapErr());
              return;
            }

            callback->OnSuccess(result.inspect());
          },
          /* reject */
          [callback = nsCOMPtr{aCallback}](ResponseRejectReason aReason) {
            callback->OnError(NS_ERROR_FAILURE);
          });
  return NS_OK;
}

NS_IMETHODIMP nsClipboardProxy::GetDataSnapshotSync(
    const nsTArray<nsCString>& aFlavorList,
    nsIClipboard::ClipboardType aWhichClipboard,
    mozilla::dom::WindowContext* aRequestingWindowContext,
    nsIClipboardDataSnapshot** _retval) {
  *_retval = nullptr;

  if (aFlavorList.IsEmpty()) {
    return NS_ERROR_INVALID_ARG;
  }

  if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
    MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
                      aWhichClipboard);
    return NS_ERROR_FAILURE;
  }
  if (MOZ_UNLIKELY(nsIContentAnalysis::MightBeActive())) {
    // If Content Analysis is active we want to fetch all the clipboard data
    // up front since we need to analyze it anyway.
    RefPtr<ClipboardContentAnalysisChild> contentAnalysis =
        ClipboardContentAnalysisChild::GetOrCreate();
    IPCTransferableDataOrError ipcTransferableDataOrError;
    bool result = contentAnalysis->SendGetAllClipboardDataSync(
        aFlavorList, aWhichClipboard, aRequestingWindowContext->InnerWindowId(),
        &ipcTransferableDataOrError);
    if (!result) {
      return NS_ERROR_FAILURE;
    }
    if (ipcTransferableDataOrError.type() ==
        IPCTransferableDataOrError::Tnsresult) {
      return ipcTransferableDataOrError.get_nsresult();
    }
    nsresult rv;
    nsCOMPtr<nsITransferable> trans =
        do_CreateInstance("@mozilla.org/widget/transferable;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);
    trans->Init(nullptr);
    rv = nsContentUtils::IPCTransferableDataToTransferable(
        ipcTransferableDataOrError.get_IPCTransferableData(),
        true /* aAddDataFlavor */, trans, false /* aFilterUnknownFlavors */);
    NS_ENSURE_SUCCESS(rv, rv);
    auto snapshot =
        mozilla::MakeRefPtr<nsBaseClipboard::ClipboardPopulatedDataSnapshot>(
            trans);
    snapshot.forget(_retval);
    return NS_OK;
  }
  ClipboardReadRequestOrError requestOrError;
  ContentChild* contentChild = ContentChild::GetSingleton();
  contentChild->SendGetClipboardDataSnapshotSync(
      aFlavorList, aWhichClipboard, aRequestingWindowContext, &requestOrError);
  auto result = CreateClipboardDataSnapshotProxy(std::move(requestOrError));
  if (result.isErr()) {
    return result.unwrapErr();
  }

  result.unwrap().forget(_retval);
  return NS_OK;
}

NS_IMETHODIMP
nsClipboardProxy::EmptyClipboard(nsIClipboard::ClipboardType aWhichClipboard) {
  ContentChild::GetSingleton()->SendEmptyClipboard(aWhichClipboard);
  return NS_OK;
}

NS_IMETHODIMP
nsClipboardProxy::HasDataMatchingFlavors(
    const nsTArray<nsCString>& aFlavorList,
    nsIClipboard::ClipboardType aWhichClipboard, bool* aHasType) {
  *aHasType = false;

  ContentChild::GetSingleton()->SendClipboardHasType(aFlavorList,
                                                     aWhichClipboard, aHasType);

  return NS_OK;
}

NS_IMETHODIMP
nsClipboardProxy::IsClipboardTypeSupported(
    nsIClipboard::ClipboardType aWhichClipboard, bool* aIsSupported) {
  switch (aWhichClipboard) {
    case kGlobalClipboard:
      // We always support the global clipboard.
      *aIsSupported = true;
      return NS_OK;
    case kSelectionClipboard:
      *aIsSupported = mClipboardCaps.supportsSelectionClipboard();
      return NS_OK;
    case kFindClipboard:
      *aIsSupported = mClipboardCaps.supportsFindClipboard();
      return NS_OK;
    case kSelectionCache:
      *aIsSupported = mClipboardCaps.supportsSelectionCache();
      return NS_OK;
  }

  *aIsSupported = false;
  return NS_OK;
}

void nsClipboardProxy::SetCapabilities(
    const ClipboardCapabilities& aClipboardCaps) {
  mClipboardCaps = aClipboardCaps;
}
