Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  ContentAnalysis.cpp   Sprache: C

 

/* 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 "ContentAnalysis.h"
#include "ContentAnalysisIPCTypes.h"
#include "content_analysis/sdk/analysis_client.h"

#include "base/process_util.h"
#include "GMPUtils.h"  // ToHexString
#include "MainThreadUtils.h"
#include "mozilla/Components.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/DragEvent.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/GetFilesHelper.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPrefs_browser.h"
#include "nsAppRunner.h"
#include "nsBaseClipboard.h"
#include "nsComponentManagerUtils.h"
#include "nsIClassInfoImpl.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIPrintSettings.h"
#include "nsIStorageStream.h"
#include "nsISupportsPrimitives.h"
#include "nsITransferable.h"
#include "nsProxyRelease.h"
#include "nsThreadPool.h"
#include "ScopedNSSTypes.h"
#include "xpcpublic.h"

#include <algorithm>
#include <sstream>
#include <string>

#ifdef XP_WIN
#  include <windows.h>
#  define SECURITY_WIN32 1
#  include <security.h>
#  include "mozilla/NativeNt.h"
#  include "mozilla/WinDllServices.h"
#endif  // XP_WIN

namespace mozilla::contentanalysis {

LazyLogModule gContentAnalysisLog("contentanalysis");
#define LOGD(...)                                        \
  MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \
          mozilla::LogLevel::Debug, (__VA_ARGS__))

#define LOGE(...)                                        \
  MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \
          mozilla::LogLevel::Error, (__VA_ARGS__))

}  // namespace mozilla::contentanalysis

namespace {

const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name";
const char* kClientSignature = "browser.contentanalysis.client_signature";
const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list";
const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list";
const char* kAgentNamePref = "browser.contentanalysis.agent_name";
const char* kInterceptionPointPrefNames[] = {
    "browser.contentanalysis.interception_point.clipboard.enabled",
    "browser.contentanalysis.interception_point.download.enabled",
    "browser.contentanalysis.interception_point.drag_and_drop.enabled",
    "browser.contentanalysis.interception_point.file_upload.enabled",
    "browser.contentanalysis.interception_point.print.enabled",
};

// Allow up to this many threads to be concurrently engaged in synchronous
// communcations with the agent.  That limit is set by
// browser.contentanalysis.max_connections but is clamped to not exceed
// this value.
const unsigned long kMaxContentAnalysisAgentThreads = 256;
// Max number of threads that we keep even if they have no tasks to run.
const unsigned long kMaxIdleContentAnalysisAgentThreads = 2;
// Time (ms) we wait before declaring a thread idle.  100ms is the
// threadpool default.
const unsigned long kIdleContentAnalysisAgentTimeoutMs = 100;
// Time we wait before destroying the kMaxIdleContentAnalysisAgentThreads
// threads.  Content Analysis never does this, which is what UINT32_MAX
// means.
const unsigned long kMaxIdleContentAnalysisAgentTimeoutMs = UINT32_MAX;

// How long the threadpool will wait at shutdown for the agent to complete any
// in-progress operations before it abandons the threads (they will keep
// running).
const uint32_t kShutdownThreadpoolTimeoutMs = 2 * 1000;

// kTextMime must be the first entry.
auto kTextFormatsToAnalyze = {kTextMime, kHTMLMime};

const char* SafeGetStaticErrorName(nsresult aRv) {
  const auto* ret = mozilla::GetStaticErrorName(aRv);
  return ret ? ret : "<illegal value>";
}

nsresult MakePromise(JSContext* aCx, mozilla::dom::Promise** aPromise) {
  nsIGlobalObject* go = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!go)) {
    return NS_ERROR_UNEXPECTED;
  }
  mozilla::ErrorResult result;
  RefPtr promise = mozilla::dom::Promise::Create(go, result);
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }
  promise.forget(aPromise);
  return NS_OK;
}

static nsCString GenerateUUID() {
  nsID id = nsID::GenerateUUID();
  return nsCString(id.ToString().get());
}

static nsresult GetFileDisplayName(const nsString& aFilePath,
                                   nsString& aFileDisplayName) {
  nsCOMPtr<nsIFile> file;
  MOZ_TRY(NS_NewLocalFile(aFilePath, getter_AddRefs(file)));
  return file->GetDisplayName(aFileDisplayName);
}

nsIContentAnalysisAcknowledgement::FinalAction ConvertResult(
    nsIContentAnalysisResponse::Action aResponseResult) {
  switch (aResponseResult) {
    case nsIContentAnalysisResponse::Action::eReportOnly:
      return nsIContentAnalysisAcknowledgement::FinalAction::eReportOnly;
    case nsIContentAnalysisResponse::Action::eWarn:
      return nsIContentAnalysisAcknowledgement::FinalAction::eWarn;
    case nsIContentAnalysisResponse::Action::eBlock:
    case nsIContentAnalysisResponse::Action::eCanceled:
      return nsIContentAnalysisAcknowledgement::FinalAction::eBlock;
    case nsIContentAnalysisResponse::Action::eAllow:
      return nsIContentAnalysisAcknowledgement::FinalAction::eAllow;
    case nsIContentAnalysisResponse::Action::eUnspecified:
      return nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
    default:
      LOGE(
          "ConvertResult got unexpected responseResult "
          "%d",
          static_cast<uint32_t>(aResponseResult));
      return nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
  }
}

bool SourceIsSameTab(nsIContentAnalysisRequest* aRequest) {
  RefPtr<mozilla::dom::WindowGlobalParent> sourceWindowGlobal;
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetSourceWindowGlobal(getter_AddRefs(sourceWindowGlobal)));
  if (!sourceWindowGlobal) {
    return false;
  }

  RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)));
  return windowGlobal->GetBrowsingContext()->Top() ==
             sourceWindowGlobal->GetBrowsingContext()->Top() &&
         windowGlobal->DocumentPrincipal() &&
         windowGlobal->DocumentPrincipal()->Subsumes(
             sourceWindowGlobal->DocumentPrincipal());
}

}  // anonymous namespace

/* static */ bool nsIContentAnalysis::MightBeActive() {
  // A DLP connection is not permitted to be added/removed while the
  // browser is running, so we can cache this.
  // Furthermore, if this is set via enterprise policy the pref will be locked
  // so users won't be able to change it.
  // Ideally we would make this a mirror: once pref, but this interacts in
  // some weird ways with the enterprise policy for testing purposes.
  static bool sIsEnabled =
      mozilla::StaticPrefs::browser_contentanalysis_enabled();
  // Note that we can't check gAllowContentAnalysis here because it
  // only gets set in the parent process.
  return sIsEnabled;
}

namespace mozilla::contentanalysis {
ContentAnalysisRequest::~ContentAnalysisRequest() {
#ifdef XP_WIN
  CloseHandle(mPrintDataHandle);
#endif
}

NS_IMETHODIMP
ContentAnalysisRequest::GetAnalysisType(AnalysisType* aAnalysisType) {
  *aAnalysisType = mAnalysisType;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetReason(Reason* aReason) {
  *aReason = mReason;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetTextContent(nsAString& aTextContent) {
  aTextContent = mTextContent;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetFilePath(nsAString& aFilePath) {
  aFilePath = mFilePath;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetPrintDataHandle(uint64_t* aPrintDataHandle) {
#ifdef XP_WIN
  uintptr_t printDataHandle = reinterpret_cast<uintptr_t>(mPrintDataHandle);
  uint64_t printDataValue = static_cast<uint64_t>(printDataHandle);
  *aPrintDataHandle = printDataValue;
  return NS_OK;
#else
  return NS_ERROR_NOT_IMPLEMENTED;
#endif
}

NS_IMETHODIMP
ContentAnalysisRequest::GetPrinterName(nsAString& aPrinterName) {
  aPrinterName = mPrinterName;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetPrintDataSize(uint64_t* aPrintDataSize) {
#ifdef XP_WIN
  *aPrintDataSize = mPrintDataSize;
  return NS_OK;
#else
  return NS_ERROR_NOT_IMPLEMENTED;
#endif
}

NS_IMETHODIMP
ContentAnalysisRequest::GetUrl(nsIURI** aUrl) {
  NS_ENSURE_ARG_POINTER(aUrl);
  NS_IF_ADDREF(*aUrl = mUrl);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetEmail(nsAString& aEmail) {
  aEmail = mEmail;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetSha256Digest(nsACString& aSha256Digest) {
  aSha256Digest = mSha256Digest;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetResources(
    nsTArray<RefPtr<nsIClientDownloadResource>>& aResources) {
  aResources = mResources.Clone();
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetRequestToken(nsACString& aRequestToken) {
  aRequestToken = mRequestToken;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetRequestToken(const nsACString& aRequestToken) {
  mRequestToken = aRequestToken;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetUserActionId(nsACString& aUserActionId) {
  aUserActionId = mUserActionId;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetUserActionId(const nsACString& aUserActionId) {
  mUserActionId = aUserActionId;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetUserActionRequestsCount(
    int64_t* aUserActionRequestsCount) {
  NS_ENSURE_ARG_POINTER(aUserActionRequestsCount);
  *aUserActionRequestsCount = mUserActionRequestsCount;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetUserActionRequestsCount(
    int64_t aUserActionRequestsCount) {
  mUserActionRequestsCount = aUserActionRequestsCount;
  return NS_OK;
}
NS_IMETHODIMP
ContentAnalysisRequest::GetOperationTypeForDisplay(
    OperationType* aOperationType) {
  *aOperationType = mOperationTypeForDisplay;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetFileNameForDisplay(nsAString& aFileNameForDisplay) {
  aFileNameForDisplay = mFileNameForDisplay;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetWindowGlobalParent(
    dom::WindowGlobalParent** aWindowGlobalParent) {
  NS_IF_ADDREF(*aWindowGlobalParent = mWindowGlobalParent);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetSourceWindowGlobal(
    mozilla::dom::WindowGlobalParent** aSourceWindowGlobal) {
  NS_IF_ADDREF(*aSourceWindowGlobal = mSourceWindowGlobal);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetTransferable(nsITransferable** aTransferable) {
  NS_IF_ADDREF(*aTransferable = mTransferable);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetDataTransfer(
    mozilla::dom::DataTransfer** aDataTransfer) {
  NS_IF_ADDREF(*aDataTransfer = mDataTransfer);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetDataTransfer(
    mozilla::dom::DataTransfer* aDataTransfer) {
  mDataTransfer = aDataTransfer;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetTimeoutMultiplier(uint32_t* aTimeoutMultiplier) {
  NS_ENSURE_ARG_POINTER(aTimeoutMultiplier);
  *aTimeoutMultiplier = mTimeoutMultiplier;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetTimeoutMultiplier(uint32_t aTimeoutMultiplier) {
  mTimeoutMultiplier = aTimeoutMultiplier;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::GetTestOnlyIgnoreCanceledAndAlwaysSubmitToAgent(
    bool* aAlwaysSubmitToAgent) {
  *aAlwaysSubmitToAgent = mTestOnlyAlwaysSubmitToAgent;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisRequest::SetTestOnlyIgnoreCanceledAndAlwaysSubmitToAgent(
    bool aAlwaysSubmitToAgent) {
  mTestOnlyAlwaysSubmitToAgent = aAlwaysSubmitToAgent;
  return NS_OK;
}

nsresult ContentAnalysis::CreateContentAnalysisClient(
    nsCString&& aPipePathName, nsString&& aClientSignatureSetting,
    bool aIsPerUser) {
  MOZ_ASSERT(!NS_IsMainThread());

  std::shared_ptr<content_analysis::sdk::Client> client;
  bool isShutDown = IsShutDown();
  if (!isShutDown) {
    client.reset(
        content_analysis::sdk::Client::Create({aPipePathName.get(), aIsPerUser})
            .release());
    LOGD("Content analysis is %s", client ? "connected" : "not available");
  } else {
    LOGD("ContentAnalysis::IsShutDown is true");
  }

#ifdef XP_WIN
  if (client && !aClientSignatureSetting.IsEmpty()) {
    std::string agentPath = client->GetAgentInfo().binary_path;
    nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath);
    UniquePtr<wchar_t[]> orgName =
        mozilla::DllServices::Get()->GetBinaryOrgName(agentWidePath.get());
    bool signatureMatches = false;
    if (orgName) {
      auto dependentOrgName = nsDependentString(orgName.get());
      LOGD("Content analysis client signed with organization name \"%S\"",
           dependentOrgName.getW());
      signatureMatches = aClientSignatureSetting.Equals(dependentOrgName);
    } else {
      LOGD("Content analysis client has no signature");
    }
    if (!signatureMatches) {
      LOGE(
          "Got mismatched content analysis client signature! All content "
          "analysis operations will fail.");
      nsresult rv = NS_ERROR_INVALID_SIGNATURE;
      glean::content_analysis::connection_failure
          .Get(nsCString{SafeGetStaticErrorName(rv)})
          .Add();
      NS_DispatchToMainThread(
          NS_NewRunnableFunction(__func__, [self = RefPtr{this}, rv]() {
            AssertIsOnMainThread();
            self->mCaClientPromise->Reject(rv, __func__);
            self->mCreatingClient = false;
          }));

      return NS_OK;
    }
  }
#endif  // XP_WIN
  NS_DispatchToMainThread(NS_NewRunnableFunction(
      __func__,
      [self = RefPtr{this}, isShutDown, client = std::move(client)]() {
        AssertIsOnMainThread();
        // Note that if mCaClientPromise has been resolved or rejected
        // calling Resolve() or Reject() is a noop.
        if (client) {
          self->mHaveResolvedClientPromise = true;
          self->mCaClientPromise->Resolve(client, __func__);
        } else {
          nsresult promiseResult = isShutDown ? NS_ERROR_ILLEGAL_DURING_SHUTDOWN
                                              : NS_ERROR_CONNECTION_REFUSED;
          glean::content_analysis::connection_failure
              .Get(nsCString{SafeGetStaticErrorName(promiseResult)})
              .Add();
          self->mCaClientPromise->Reject(promiseResult, __func__);
        }
        self->mCreatingClient = false;
      }));

  return NS_OK;
}

ContentAnalysisRequest::ContentAnalysisRequest(
    AnalysisType aAnalysisType, Reason aReason, nsString aString,
    bool aStringIsFilePath, nsCString aSha256Digest, nsCOMPtr<nsIURI> aUrl,
    OperationType aOperationType, dom::WindowGlobalParent* aWindowGlobalParent,
    dom::WindowGlobalParent* aSourceWindowGlobal, nsCString&& aUserActionId)
    : mAnalysisType(aAnalysisType),
      mReason(aReason),
      mUrl(std::move(aUrl)),
      mSha256Digest(std::move(aSha256Digest)),
      mUserActionId(std::move(aUserActionId)),
      mOperationTypeForDisplay(aOperationType),
      mWindowGlobalParent(aWindowGlobalParent),
      mSourceWindowGlobal(aSourceWindowGlobal) {
  MOZ_ASSERT(aAnalysisType != AnalysisType::ePrint,
             "Print should use other ContentAnalysisRequest constructor!");
  MOZ_ASSERT(aReason != nsIContentAnalysisRequest::Reason::ePrintPreviewPrint &&
             aReason != nsIContentAnalysisRequest::Reason::eSystemDialogPrint);
  if (aStringIsFilePath) {
    mFilePath = std::move(aString);
  } else {
    mTextContent = std::move(aString);
  }
  if (mOperationTypeForDisplay == OperationType::eUpload ||
      mOperationTypeForDisplay == OperationType::eDownload) {
    MOZ_ASSERT(aStringIsFilePath);
    nsresult rv = GetFileDisplayName(mFilePath, mFileNameForDisplay);
    if (NS_FAILED(rv)) {
      mFileNameForDisplay = u"file";
    }
  }
}

ContentAnalysisRequest::ContentAnalysisRequest(
    AnalysisType aAnalysisType, Reason aReason, nsITransferable* aTransferable,
    dom::WindowGlobalParent* aWindowGlobalParent,
    dom::WindowGlobalParent* aSourceWindowGlobal)
    : mAnalysisType(aAnalysisType),
      mReason(aReason),
      mTransferable(aTransferable),
      mOperationTypeForDisplay(
          nsIContentAnalysisRequest::OperationType::eClipboard),
      mWindowGlobalParent(aWindowGlobalParent),
      mSourceWindowGlobal(aSourceWindowGlobal) {}

ContentAnalysisRequest::ContentAnalysisRequest(
    const nsTArray<uint8_t> aPrintData, nsCOMPtr<nsIURI> aUrl,
    nsString aPrinterName, Reason aReason,
    dom::WindowGlobalParent* aWindowGlobalParent)
    : mAnalysisType(AnalysisType::ePrint),
      mReason(aReason),
      mUrl(std::move(aUrl)),
      mPrinterName(std::move(aPrinterName)),
      mWindowGlobalParent(aWindowGlobalParent) {
#ifdef XP_WIN
  LARGE_INTEGER dataContentLength;
  dataContentLength.QuadPart = static_cast<LONGLONG>(aPrintData.Length());
  mPrintDataHandle = ::CreateFileMappingW(
      INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, dataContentLength.HighPart,
      dataContentLength.LowPart, nullptr);
  if (mPrintDataHandle) {
    mozilla::nt::AutoMappedView view(mPrintDataHandle, FILE_MAP_ALL_ACCESS);
    memcpy(view.as<uint8_t>(), aPrintData.Elements(), aPrintData.Length());
    mPrintDataSize = aPrintData.Length();
  }
#else
  MOZ_ASSERT_UNREACHABLE(
      "Content Analysis is not supported on non-Windows platforms");
#endif
  // We currently only use this constructor when printing.
  MOZ_ASSERT(aReason == nsIContentAnalysisRequest::Reason::ePrintPreviewPrint ||
             aReason == nsIContentAnalysisRequest::Reason::eSystemDialogPrint);
  mOperationTypeForDisplay = OperationType::eOperationPrint;
}

RefPtr<ContentAnalysisRequest> ContentAnalysisRequest::Clone(
    nsIContentAnalysisRequest* aRequest) {
  auto clone = MakeRefPtr<ContentAnalysisRequest>();
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetAnalysisType(&clone->mAnalysisType));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetReason(&clone->mReason));
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetTransferable(getter_AddRefs(clone->mTransferable)));
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetDataTransfer(getter_AddRefs(clone->mDataTransfer)));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetTextContent(clone->mTextContent));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetFilePath(clone->mFilePath));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetUrl(getter_AddRefs(clone->mUrl)));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetSha256Digest(clone->mSha256Digest));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetResources(clone->mResources));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetEmail(clone->mEmail));
  // Do not copy mRequestToken or mUserActionId or mUserActionIdCount
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetOperationTypeForDisplay(&clone->mOperationTypeForDisplay));
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetFileNameForDisplay(clone->mFileNameForDisplay));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetPrinterName(clone->mPrinterName));
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetWindowGlobalParent(
      getter_AddRefs(clone->mWindowGlobalParent)));
#ifdef XP_WIN
  uint64_t printDataValue;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetPrintDataHandle(&printDataValue));
  uintptr_t printDataHandle = static_cast<uint64_t>(printDataValue);
  clone->mPrintDataHandle = reinterpret_cast<HANDLE>(printDataHandle);
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetPrintDataSize(&clone->mPrintDataSize));
#endif
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetSourceWindowGlobal(
      getter_AddRefs(clone->mSourceWindowGlobal)));
  // Do not copy mTimeoutMultiplier
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetTestOnlyIgnoreCanceledAndAlwaysSubmitToAgent(
      &clone->mTestOnlyAlwaysSubmitToAgent));
  return clone;
}

nsresult ContentAnalysisRequest::GetFileDigest(const nsAString& aFilePath,
                                               nsCString& aDigestString) {
  MOZ_DIAGNOSTIC_ASSERT(
      !NS_IsMainThread(),
      "ContentAnalysisRequest::GetFileDigest does file IO and should "
      "not run on the main thread");
  nsresult rv;
  mozilla::Digest digest;
  digest.Begin(SEC_OID_SHA256);
  PRFileDesc* fd = nullptr;
  nsCOMPtr<nsIFile> file;
  MOZ_TRY(NS_NewLocalFile(aFilePath, getter_AddRefs(file)));
  rv = file->OpenNSPRFileDesc(PR_RDONLY | nsIFile::OS_READAHEAD, 0, &fd);
  NS_ENSURE_SUCCESS(rv, rv);
  auto closeFile = MakeScopeExit([fd]() { PR_Close(fd); });
  constexpr uint32_t kBufferSize = 1024 * 1024;
  auto buffer = mozilla::MakeUnique<uint8_t[]>(kBufferSize);
  if (!buffer) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  PRInt32 bytesRead = PR_Read(fd, buffer.get(), kBufferSize);
  while (bytesRead != 0) {
    if (bytesRead == -1) {
      return NS_ErrorAccordingToNSPR();
    }
    digest.Update(mozilla::Span<const uint8_t>(buffer.get(), bytesRead));
    bytesRead = PR_Read(fd, buffer.get(), kBufferSize);
  }
  nsTArray<uint8_t> digestResults;
  rv = digest.End(digestResults);
  NS_ENSURE_SUCCESS(rv, rv);
  aDigestString = mozilla::ToHexString(digestResults);
  return NS_OK;
}

static nsresult ConvertToProtobuf(
    nsIClientDownloadResource* aIn,
    content_analysis::sdk::ClientDownloadRequest_Resource* aOut) {
  nsString url;
  nsresult rv = aIn->GetUrl(url);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_url(NS_ConvertUTF16toUTF8(url).get());

  uint32_t resourceType;
  rv = aIn->GetType(&resourceType);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_type(
      static_cast<content_analysis::sdk::ClientDownloadRequest_ResourceType>(
          resourceType));

  return NS_OK;
}

#if defined(DEBUG)
static bool IsRequestReadyForAgent(nsIContentAnalysisRequest* aRequest) {
  NS_ENSURE_TRUE(aRequest, false);

  // The windowGlobal is allowed to be null at this point in gtests (only).
  // The URL must be set in that case.  We check that below.
  RefPtr<dom::WindowGlobalParent> windowGlobal;
  NS_ENSURE_SUCCESS(
      aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)), false);

  // Any DataTransfer should have been expanded into individual requests.
  nsCOMPtr<dom::DataTransfer> dataTransfer;
  NS_ENSURE_SUCCESS(aRequest->GetDataTransfer(getter_AddRefs(dataTransfer)),
                    false);
  NS_ENSURE_TRUE(!dataTransfer, false);

  // Any nsITransferable should have been expanded into individual requests.
  nsCOMPtr<nsITransferable> transferable;
  NS_ENSURE_SUCCESS(aRequest->GetTransferable(getter_AddRefs(transferable)),
                    false);
  NS_ENSURE_TRUE(!transferable, false);

  nsCString userActionId;
  NS_ENSURE_SUCCESS(aRequest->GetUserActionId(userActionId), false);
  NS_ENSURE_TRUE(!userActionId.IsEmpty(), false);

  int64_t userActionRequestsCount;
  NS_ENSURE_SUCCESS(
      aRequest->GetUserActionRequestsCount(&userActionRequestsCount), false);
  NS_ENSURE_TRUE(userActionRequestsCount, false);

  nsCOMPtr<nsIURI> url;
  NS_ENSURE_SUCCESS(aRequest->GetUrl(getter_AddRefs(url)), false);
  if (!url) {
    // If no URL is given then we use the one for the window.
    NS_ENSURE_TRUE(windowGlobal, false);
    url = ContentAnalysis::GetURIForBrowsingContext(
        windowGlobal->Canonical()->GetBrowsingContext());
    NS_ENSURE_TRUE(url, false);
  }

  return true;
}
#endif  // defined(DEBUG)

static nsresult ConvertToProtobuf(
    nsIContentAnalysisRequest* aIn,
    content_analysis::sdk::ContentAnalysisRequest* aOut) {
  MOZ_ASSERT(IsRequestReadyForAgent(aIn));

  nsIContentAnalysisRequest::AnalysisType analysisType;
  nsresult rv = aIn->GetAnalysisType(&analysisType);
  NS_ENSURE_SUCCESS(rv, rv);
  auto connector =
      static_cast<content_analysis::sdk::AnalysisConnector>(analysisType);
  aOut->set_analysis_connector(connector);

  nsIContentAnalysisRequest::Reason reason;
  rv = aIn->GetReason(&reason);
  NS_ENSURE_SUCCESS(rv, rv);
  auto sdkReason =
      static_cast<content_analysis::sdk::ContentAnalysisRequest::Reason>(
          reason);
  aOut->set_reason(sdkReason);

  nsCString requestToken;
  rv = aIn->GetRequestToken(requestToken);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_request_token(requestToken.get(), requestToken.Length());
  nsCString userActionId;
  rv = aIn->GetUserActionId(userActionId);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_user_action_id(userActionId.get(), userActionId.Length());
  int64_t userActionRequestsCount;
  rv = aIn->GetUserActionRequestsCount(&userActionRequestsCount);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_user_action_requests_count(userActionRequestsCount);

  int32_t timeout = StaticPrefs::browser_contentanalysis_agent_timeout();
  // Non-positive timeout values indicate testing, and the test agent does not
  // care about this value.
  timeout = std::max(timeout, 1);
  uint32_t timeoutMultiplier;
  rv = aIn->GetTimeoutMultiplier(&timeoutMultiplier);
  NS_ENSURE_SUCCESS(rv, rv);
  timeoutMultiplier = std::max(timeoutMultiplier, static_cast<uint32_t>(1));
  auto checkedTimeout = CheckedInt64(time(nullptr)) +
                        timeout * userActionRequestsCount * timeoutMultiplier;
  if (!checkedTimeout.isValid()) {
    return NS_ERROR_FAILURE;
  }
  aOut->set_expires_at(checkedTimeout.value());

  const std::string tag = "dlp";  // TODO:
  *aOut->add_tags() = tag;

  auto* requestData = aOut->mutable_request_data();

  RefPtr<dom::WindowGlobalParent> windowGlobal;
  rv = aIn->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIURI> url;
  rv = aIn->GetUrl(getter_AddRefs(url));
  NS_ENSURE_SUCCESS(rv, rv);
  if (!url) {
    // We already checked that this exists.
    MOZ_ASSERT(windowGlobal);
    // If no URL is given then we use the one for the window.
    url = ContentAnalysis::GetURIForBrowsingContext(
        windowGlobal->Canonical()->GetBrowsingContext());
    // We also already checked for this.
    MOZ_ASSERT(url);
  }
  nsCString urlString;
  rv = url->GetSpec(urlString);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!urlString.IsEmpty()) {
    requestData->set_url(urlString.get());
  }

  if (windowGlobal) {
    nsString title;
    windowGlobal->GetDocumentTitle(title);
    requestData->set_tab_title(NS_ConvertUTF16toUTF8(title).get());
  }

  nsString email;
  rv = aIn->GetEmail(email);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!email.IsEmpty()) {
    requestData->set_email(NS_ConvertUTF16toUTF8(email).get());
  }

  nsCString sha256Digest;
  rv = aIn->GetSha256Digest(sha256Digest);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!sha256Digest.IsEmpty()) {
    requestData->set_digest(sha256Digest.get());
  }

  if (analysisType == nsIContentAnalysisRequest::AnalysisType::ePrint) {
#if XP_WIN
    uint64_t printDataHandle;
    MOZ_TRY(aIn->GetPrintDataHandle(&printDataHandle));
    if (!printDataHandle) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
    aOut->mutable_print_data()->set_handle(printDataHandle);

    uint64_t printDataSize;
    MOZ_TRY(aIn->GetPrintDataSize(&printDataSize));
    aOut->mutable_print_data()->set_size(printDataSize);

    nsString printerName;
    MOZ_TRY(aIn->GetPrinterName(printerName));
    requestData->mutable_print_metadata()->set_printer_name(
        NS_ConvertUTF16toUTF8(printerName).get());
#else
    return NS_ERROR_NOT_IMPLEMENTED;
#endif
  } else {
    nsString filePath;
    rv = aIn->GetFilePath(filePath);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!filePath.IsEmpty()) {
      std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get();
      aOut->set_file_path(filePathStr);
      auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1);
      if (!filename.empty()) {
        requestData->set_filename(filename);
      }
    } else {
      nsString textContent;
      rv = aIn->GetTextContent(textContent);
      NS_ENSURE_SUCCESS(rv, rv);
      MOZ_ASSERT(!textContent.IsEmpty());
      aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get());
    }
  }

#ifdef XP_WIN
  ULONG userLen = 0;
  GetUserNameExW(NameSamCompatible, nullptr, &userLen);
  if (GetLastError() == ERROR_MORE_DATA && userLen > 0) {
    auto user = mozilla::MakeUnique<wchar_t[]>(userLen);
    if (GetUserNameExW(NameSamCompatible, user.get(), &userLen)) {
      auto* clientMetadata = aOut->mutable_client_metadata();
      auto* browser = clientMetadata->mutable_browser();
      browser->set_machine_user(NS_ConvertUTF16toUTF8(user.get()).get());
    }
  }
#endif

  nsTArray<RefPtr<nsIClientDownloadResource>> resources;
  rv = aIn->GetResources(resources);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!resources.IsEmpty()) {
    auto* pbClientDownloadRequest = requestData->mutable_csd();
    for (auto& nsResource : resources) {
      rv = ConvertToProtobuf(nsResource.get(),
                             pbClientDownloadRequest->add_resources());
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}

namespace {
// We don't want this overload to be called for string parameters, so
// use std::enable_if
template <typename T>
typename std::enable_if_t<!std::is_same<std::string, std::decay_t<T>>::value,
                          void>
LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) {
  ss << value;
}

// 0 indicates no max length
template <typename T>
typename std::enable_if_t<std::is_same<std::string, std::decay_t<T>>::value,
                          void>
LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) {
  if (!maxLength || value.length() < maxLength) {
    ss << value;
  } else {
    ss << value.substr(0, maxLength) << " (truncated)";
  }
}
}  // namespace

static void LogRequest(
    const content_analysis::sdk::ContentAnalysisRequest* aPbRequest) {
  // We cannot use Protocol Buffer's DebugString() because we optimize for
  // lite runtime.
  if (!static_cast<LogModule*>(gContentAnalysisLog)
           ->ShouldLog(LogLevel::Debug)) {
    return;
  }

  std::stringstream ss;
  ss << "ContentAnalysisRequest:"
     << "\n";

#define ADD_FIELD_WITH_VALFUNC(PBUF, NAME, FUNC, VALFUNC) \
  ss << "  " << (NAME) << ": ";                           \
  if ((PBUF)->has_##FUNC()) {                             \
    LogWithMaxLength(ss, VALFUNC(), 500);                 \
    ss << "\n";                                           \
  } else                                                  \
    ss << "<none>"                                        \
       << "\n";

#define ADD_FIELD(PBUF, NAME, FUNC) \
  ADD_FIELD_WITH_VALFUNC(PBUF, NAME, FUNC, (PBUF)->FUNC)

#define ADD_EXISTS(PBUF, NAME, FUNC) \
  ss << "  " << (NAME) << ": "       \
     << ((PBUF)->has_##FUNC() ? "<exists>" : "<none>") << "\n";

#define ADD_NONEMPTY(PBUF, NAME, FUNC)                                      \
  ss << "  " << (NAME) << ": "                                              \
     << (((PBUF)->has_##FUNC() && (!(PBUF)->FUNC().empty())) ? "<nonempty>" \
                                                             : "<none>")    \
     << "\n";

  ADD_FIELD(aPbRequest, "Expires", expires_at);
  ADD_FIELD(aPbRequest, "Analysis Type", analysis_connector);
  ADD_FIELD(aPbRequest, "Request Token", request_token);
  ADD_FIELD(aPbRequest, "User Action ID", user_action_id);
  ADD_FIELD(aPbRequest, "User Action Requests Count",
            user_action_requests_count);
  ADD_FIELD(aPbRequest, "File Path", file_path);
  ADD_NONEMPTY(aPbRequest, "Text Content", text_content);
  // TODO: Tags
  ADD_EXISTS(aPbRequest, "Request Data Struct", request_data);
  const auto* requestData =
      aPbRequest->has_request_data() ? &aPbRequest->request_data() : nullptr;
  if (requestData) {
    ADD_FIELD(requestData, "  Url", url);
    ADD_FIELD(requestData, "  Email", email);
    auto hexDigestFunc = [&requestData]() {
      return ToHexString(
          reinterpret_cast<const uint8_t*>(requestData->digest().c_str()),
          requestData->digest().length());
    };
    ADD_FIELD_WITH_VALFUNC(requestData, "  SHA-256 Digest", digest,
                           hexDigestFunc);
    ADD_FIELD(requestData, "  Filename", filename);
    ADD_EXISTS(requestData, "  Client Download Request struct", csd);
    const auto* csd = requestData->has_csd() ? &requestData->csd() : nullptr;
    if (csd) {
      uint32_t i = 0;
      for (const auto& resource : csd->resources()) {
        ss << "      Resource " << i << ":"
           << "\n";
        ADD_FIELD(&resource, "      Url", url);
        ADD_FIELD(&resource, "      Type", type);
        ++i;
      }
    }
  }
  ADD_EXISTS(aPbRequest, "Client Metadata Struct", client_metadata);
  const auto* clientMetadata = aPbRequest->has_client_metadata()
                                   ? &aPbRequest->client_metadata()
                                   : nullptr;
  if (clientMetadata) {
    ADD_EXISTS(clientMetadata, "  Browser Struct", browser);
    const auto* browser =
        clientMetadata->has_browser() ? &clientMetadata->browser() : nullptr;
    if (browser) {
      ADD_FIELD(browser, "    Machine User", machine_user);
    }
  }

#undef ADD_EXISTS
#undef ADD_FIELD

  LOGD("%s", ss.str().c_str());
}

ContentAnalysisResponse::ContentAnalysisResponse(
    content_analysis::sdk::ContentAnalysisResponse&& aResponse,
    const nsCString& aUserActionId)
    : mUserActionId(aUserActionId) {
  mAction = Action::eUnspecified;
  for (const auto& result : aResponse.results()) {
    if (!result.has_status() ||
        result.status() !=
            content_analysis::sdk::ContentAnalysisResponse::Result::SUCCESS) {
      mAction = Action::eUnspecified;
      return;
    }
    // The action values increase with severity, so the max is the most severe.
    for (const auto& rule : result.triggered_rules()) {
      mAction =
          static_cast<Action>(std::max(static_cast<uint32_t>(mAction),
                                       static_cast<uint32_t>(rule.action())));
    }
  }

  // If no rules blocked then we should allow.
  if (mAction == Action::eUnspecified) {
    mAction = Action::eAllow;
  }

  const auto& requestToken = aResponse.request_token();
  mRequestToken.Assign(requestToken.data(), requestToken.size());
}

ContentAnalysisResponse::ContentAnalysisResponse(
    Action aAction, const nsACString& aRequestToken,
    const nsACString& aUserActionId)
    : mAction(aAction),
      mRequestToken(aRequestToken),
      mUserActionId(aUserActionId),
      mIsSyntheticResponse(true) {
  MOZ_ASSERT(mAction != Action::eUnspecified);
}

/* static */
already_AddRefed<ContentAnalysisResponse> ContentAnalysisResponse::FromProtobuf(
    content_analysis::sdk::ContentAnalysisResponse&& aResponse,
    const nsCString& aUserActionId) {
  auto ret = RefPtr<ContentAnalysisResponse>(
      new ContentAnalysisResponse(std::move(aResponse), aUserActionId));

  if (ret->mAction == Action::eUnspecified) {
    return nullptr;
  }

  return ret.forget();
}

NS_IMETHODIMP
ContentAnalysisResponse::GetRequestToken(nsACString& aRequestToken) {
  aRequestToken = mRequestToken;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisResponse::GetUserActionId(nsACString& aUserActionId) {
  aUserActionId = mUserActionId;
  return NS_OK;
}

static void LogResponse(
    content_analysis::sdk::ContentAnalysisResponse* aPbResponse) {
  if (!static_cast<LogModule*>(gContentAnalysisLog)
           ->ShouldLog(LogLevel::Debug)) {
    return;
  }

  std::stringstream ss;
  ss << "ContentAnalysisResponse:"
     << "\n";

#define ADD_FIELD(PBUF, NAME, FUNC) \
  ss << "  " << (NAME) << ": ";     \
  if ((PBUF)->has_##FUNC())         \
    ss << (PBUF)->FUNC() << "\n";   \
  else                              \
    ss << "<none>"                  \
       << "\n";

  ADD_FIELD(aPbResponse, "Request Token", request_token);
  uint32_t i = 0;
  for (const auto& result : aPbResponse->results()) {
    ss << "  Result " << i << ":"
       << "\n";
    ADD_FIELD(&result, "    Status", status);
    uint32_t j = 0;
    for (const auto& rule : result.triggered_rules()) {
      ss << "    Rule " << j << ":"
         << "\n";
      ADD_FIELD(&rule, "    action", action);
      ++j;
    }
    ++i;
  }

#undef ADD_FIELD

  LOGD("%s", ss.str().c_str());
}

static nsresult ConvertToProtobuf(
    nsIContentAnalysisAcknowledgement* aIn, const nsACString& aRequestToken,
    content_analysis::sdk::ContentAnalysisAcknowledgement* aOut) {
  aOut->set_request_token(aRequestToken.Data(), aRequestToken.Length());

  nsIContentAnalysisAcknowledgement::Result result;
  nsresult rv = aIn->GetResult(&result);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_status(
      static_cast<content_analysis::sdk::ContentAnalysisAcknowledgement_Status>(
          result));

  nsIContentAnalysisAcknowledgement::FinalAction finalAction;
  rv = aIn->GetFinalAction(&finalAction);
  NS_ENSURE_SUCCESS(rv, rv);
  aOut->set_final_action(
      static_cast<
          content_analysis::sdk::ContentAnalysisAcknowledgement_FinalAction>(
          finalAction));

  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisResponse::GetAction(Action* aAction) {
  *aAction = mAction;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisResponse::GetCancelError(CancelError* aCancelError) {
  *aCancelError = mCancelError;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisResponse::GetIsCachedResponse(bool* aIsCachedResponse) {
  *aIsCachedResponse = mIsCachedResponse;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisResponse::GetIsSyntheticResponse(bool* aIsSyntheticResponse) {
  *aIsSyntheticResponse = mIsSyntheticResponse;
  return NS_OK;
}

static void LogAcknowledgement(
    content_analysis::sdk::ContentAnalysisAcknowledgement* aPbAck) {
  if (!static_cast<LogModule*>(gContentAnalysisLog)
           ->ShouldLog(LogLevel::Debug)) {
    return;
  }

  std::stringstream ss;
  ss << "ContentAnalysisAcknowledgement:"
     << "\n";

#define ADD_FIELD(PBUF, NAME, FUNC) \
  ss << "  " << (NAME) << ": ";     \
  if ((PBUF)->has_##FUNC())         \
    ss << (PBUF)->FUNC() << "\n";   \
  else                              \
    ss << "<none>"                  \
       << "\n";

  ADD_FIELD(aPbAck, "Request Token", request_token);
  ADD_FIELD(aPbAck, "Status", status);
  ADD_FIELD(aPbAck, "Final Action", final_action);

#undef ADD_FIELD

  LOGD("%s", ss.str().c_str());
}

void ContentAnalysisResponse::SetOwner(ContentAnalysis* aOwner) {
  mOwner = std::move(aOwner);
}

void ContentAnalysisResponse::SetCancelError(CancelError aCancelError) {
  mCancelError = aCancelError;
}

void ContentAnalysisResponse::ResolveWarnAction(bool aAllowContent) {
  MOZ_ASSERT(mAction == Action::eWarn);
  mAction = aAllowContent ? Action::eAllow : Action::eBlock;
}

ContentAnalysisAcknowledgement::ContentAnalysisAcknowledgement(
    Result aResult, FinalAction aFinalAction)
    : mResult(aResult), mFinalAction(aFinalAction) {}

NS_IMETHODIMP
ContentAnalysisAcknowledgement::GetResult(Result* aResult) {
  *aResult = mResult;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysisAcknowledgement::GetFinalAction(FinalAction* aFinalAction) {
  *aFinalAction = mFinalAction;
  return NS_OK;
}

namespace {
static bool ShouldAllowAction(
    nsIContentAnalysisResponse::Action aResponseCode) {
  return aResponseCode == nsIContentAnalysisResponse::Action::eAllow ||
         aResponseCode == nsIContentAnalysisResponse::Action::eReportOnly ||
         aResponseCode == nsIContentAnalysisResponse::Action::eWarn;
}

static DefaultResult GetDefaultResultFromPref(bool isTimeout) {
  uint32_t value = isTimeout
                       ? StaticPrefs::browser_contentanalysis_timeout_result()
                       : StaticPrefs::browser_contentanalysis_default_result();
  if (value > static_cast<uint32_t>(DefaultResult::eLastValue)) {
    LOGE(
        "Invalid value for browser.contentanalysis.%s pref "
        "value",
        isTimeout ? "default_timeout_result" : "default_result");
    return DefaultResult::eBlock;
  }
  return static_cast<DefaultResult>(value);
}
}  // namespace

NS_IMETHODIMP ContentAnalysisResponse::GetShouldAllowContent(
    bool* aShouldAllowContent) {
  *aShouldAllowContent = ShouldAllowAction(mAction);
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysisActionResult::GetShouldAllowContent(
    bool* aShouldAllowContent) {
  *aShouldAllowContent = ShouldAllowAction(mAction);
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysisNoResult::GetShouldAllowContent(
    bool* aShouldAllowContent) {
  // Make sure to use the non-timeout pref here, because timeouts won't
  // go through this code path.
  if (GetDefaultResultFromPref(/* isTimeout */ false) ==
      DefaultResult::eAllow) {
    *aShouldAllowContent =
        mValue != NoContentAnalysisResult::DENY_DUE_TO_CANCELED;
  } else {
    // Note that we allow content if we're unable to get it (for example, if
    // there's clipboard content that is not text or file)
    *aShouldAllowContent =
        mValue ==
            NoContentAnalysisResult::ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE ||
        mValue == NoContentAnalysisResult::
                      ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS ||
        mValue == NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE ||
        mValue == NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA;
  }
  return NS_OK;
}

void ContentAnalysis::EnsureParsedUrlFilters() {
  MOZ_ASSERT(NS_IsMainThread());
  if (mParsedUrlLists) {
    return;
  }

  mParsedUrlLists = true;
  nsAutoCString allowList;
  MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kAllowUrlPref, allowList));
  for (const nsACString& regexSubstr : allowList.Split(u' ')) {
    if (!regexSubstr.IsEmpty()) {
      auto flatStr = PromiseFlatCString(regexSubstr);
      const char* regex = flatStr.get();
      LOGD("CA will allow URLs that match %s", regex);
      mAllowUrlList.push_back(std::regex(regex));
    }
  }

  nsAutoCString denyList;
  MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kDenyUrlPref, denyList));
  for (const nsACString& regexSubstr : denyList.Split(u' ')) {
    if (!regexSubstr.IsEmpty()) {
      auto flatStr = PromiseFlatCString(regexSubstr);
      const char* regex = flatStr.get();
      LOGD("CA will block URLs that match %s", regex);
      mDenyUrlList.push_back(std::regex(regex));
    }
  }
}

ContentAnalysis::UrlFilterResult ContentAnalysis::FilterByUrlLists(
    nsIContentAnalysisRequest* aRequest, nsIURI* aUri) {
  EnsureParsedUrlFilters();

  nsCString urlString;
  nsresult rv = aUri->GetSpec(urlString);
  NS_ENSURE_SUCCESS(rv, UrlFilterResult::eDeny);
  MOZ_ASSERT(!urlString.IsEmpty());
  LOGD("Content Analysis checking URL against URL filter list | URL: %s",
       urlString.get());

  std::string url = urlString.get();
  size_t count = 0;
  for (const auto& denyFilter : mDenyUrlList) {
    if (std::regex_match(url, denyFilter)) {
      LOGD("Denying CA request : Deny filter %zu matched url %s", count,
           url.c_str());
      return UrlFilterResult::eDeny;
    }
    ++count;
  }

  count = 0;
  UrlFilterResult result = UrlFilterResult::eCheck;
  for (const auto& allowFilter : mAllowUrlList) {
    if (std::regex_match(url, allowFilter)) {
      LOGD("CA request : Allow filter %zu matched %s", count, url.c_str());
      result = UrlFilterResult::eAllow;
      break;
    }
    ++count;
  }

  // The rest only applies to download resources.
  nsIContentAnalysisRequest::AnalysisType analysisType;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetAnalysisType(&analysisType));
  if (analysisType != ContentAnalysisRequest::AnalysisType::eFileDownloaded) {
    MOZ_ASSERT(result == UrlFilterResult::eCheck ||
               result == UrlFilterResult::eAllow);
    LOGD("CA request filter result: %s",
         result == UrlFilterResult::eCheck ? "check" : "allow");
    return result;
  }

  nsTArray<RefPtr<nsIClientDownloadResource>> resources;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetResources(resources));
  for (size_t resourceIdx = 0; resourceIdx < resources.Length();
       /* noop */) {
    auto& resource = resources[resourceIdx];
    nsAutoString nsUrl;
    MOZ_ALWAYS_SUCCEEDS(resource->GetUrl(nsUrl));
    std::string url = NS_ConvertUTF16toUTF8(nsUrl).get();
    count = 0;
    for (auto& denyFilter : mDenyUrlList) {
      if (std::regex_match(url, denyFilter)) {
        LOGD(
            "Denying CA request : Deny filter %zu matched download resource "
            "at url %s",
            count, url.c_str());
        return UrlFilterResult::eDeny;
      }
      ++count;
    }

    count = 0;
    bool removed = false;
    for (auto& allowFilter : mAllowUrlList) {
      if (std::regex_match(url, allowFilter)) {
        LOGD(
            "CA request : Allow filter %zu matched download resource "
            "at url %s",
            count, url.c_str());
        resources.RemoveElementAt(resourceIdx);
        removed = true;
        break;
      }
      ++count;
    }
    if (!removed) {
      ++resourceIdx;
    }
  }

  // Check unless all were allowed.
  return resources.Length() ? UrlFilterResult::eCheck : UrlFilterResult::eAllow;
}

NS_IMPL_ISUPPORTS(ContentAnalysisRequest, nsIContentAnalysisRequest);
NS_IMPL_ISUPPORTS(ContentAnalysisResponse, nsIContentAnalysisResponse,
                  nsIContentAnalysisResult, nsIClassInfo);
NS_IMPL_CI_INTERFACE_GETTER(ContentAnalysisResponse, nsIContentAnalysisResponse,
                            nsIContentAnalysisResult);
NS_IMPL_THREADSAFE_CI(ContentAnalysisResponse);
NS_IMPL_ISUPPORTS(ContentAnalysisActionResult, nsIContentAnalysisResult);
NS_IMPL_ISUPPORTS(ContentAnalysisNoResult, nsIContentAnalysisResult);

NS_IMPL_ISUPPORTS(ContentAnalysisAcknowledgement,
                  nsIContentAnalysisAcknowledgement);
NS_IMPL_ISUPPORTS(ContentAnalysisCallback, nsIContentAnalysisCallback);
NS_IMPL_ISUPPORTS(ContentAnalysisDiagnosticInfo,
                  nsIContentAnalysisDiagnosticInfo);
NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis, nsIObserver,
                  ContentAnalysis);

ContentAnalysis::ContentAnalysis()
    : mRequestTokenToBasicRequestInfoMap(
          "ContentAnalysis::mRequestTokenToBasicRequestInfoMap"),
      mSetByEnterprise(false) {
  // Limit one per process
  [[maybe_unused]] static bool sCreated = false;
  MOZ_ASSERT(!sCreated);
  sCreated = true;

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (!obsServ) {
    // We must be shutting down so don't init anything.
    return;
  }
  obsServ->AddObserver(this, "xpcom-shutdown-threads", false);

  mCaClientPromise =
      new ClientPromise::Private("ContentAnalysis::ContentAnalysis");

  mThreadPool = new nsThreadPool();
  MOZ_ALWAYS_SUCCEEDS(
      mThreadPool->SetName(nsAutoCString("ContentAnalysisAgentIO")));

  unsigned long threadLimit =
      std::min(static_cast<unsigned long>(
                   StaticPrefs::browser_contentanalysis_max_connections()),
               kMaxContentAnalysisAgentThreads);
  MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetThreadLimit(threadLimit));

  // Update thread limit if the pref changes, for testing (otherwise it is
  // locked).  We cannot use RegisterCallbackAndCall since the callback needs
  // to get the service that we are currently constructing.
  Preferences::RegisterCallback(
      [](const char* aPref, void*) {
        auto self = GetContentAnalysisFromService();
        if (!self) {
          return;
        }
        unsigned long threadLimit = std::min(
            static_cast<unsigned long>(
                StaticPrefs::browser_contentanalysis_max_connections()),
            kMaxContentAnalysisAgentThreads);
        MOZ_ALWAYS_SUCCEEDS(self->mThreadPool->SetThreadLimit(threadLimit));
      },
      nsDependentCString(
          StaticPrefs::GetPrefName_browser_contentanalysis_max_connections()));

  MOZ_ALWAYS_SUCCEEDS(
      mThreadPool->SetIdleThreadLimit(kMaxIdleContentAnalysisAgentThreads));
  MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadGraceTimeout(
      kIdleContentAnalysisAgentTimeoutMs));
  MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadMaximumTimeout(
      kMaxIdleContentAnalysisAgentTimeoutMs));
}

ContentAnalysis::~ContentAnalysis() {
  LOGD("ContentAnalysis::~ContentAnalysis");
  AssertIsOnMainThread();
  MOZ_ASSERT(mUserActionMap.IsEmpty());
  MOZ_ASSERT(!mThreadPool);
  DebugOnly lock = mIsShutDown.Lock();
  MOZ_ASSERT(*lock.inspect());
}

NS_IMETHODIMP
ContentAnalysis::Observe(nsISupports* subject, const char* topic,
                         const char16_t* data) {
  AssertIsOnMainThread();
  MOZ_ASSERT(nsCString("xpcom-shutdown-threads") == topic);
  LOGD("Content Analysis received xpcom-shutdown-threads");
  Close();
  return NS_OK;
}

void ContentAnalysis::Close() {
  AssertIsOnMainThread();
  {
    // Make sure that we don't try to reconnect to the agent.
    auto lock = mIsShutDown.Lock();
    if (*lock) {
      // was previously called
      return;
    }
    *lock = true;
  }

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (obsServ) {
    obsServ->RemoveObserver(this, "xpcom-shutdown-threads");
  }

  // Reject the promise to avoid assertions when it gets destroyed
  // Note that if the promise has already been resolved or rejected this is a
  // noop
  mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);

  // In case the promise _was_ resolved before, create a new one and reject
  // that.
  mCaClientPromise =
      new ClientPromise::Private("ContentAnalysis:ShutdownReject");
  mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);

  // The userActionMap must be cleared before the object is destroyed.
  mUserActionMap.Clear();

  mThreadPool->ShutdownWithTimeout(kShutdownThreadpoolTimeoutMs);
  mThreadPool = nullptr;
  LOGD("Content Analysis service is closed");
}

bool ContentAnalysis::IsShutDown() {
  auto lock = mIsShutDown.ConstLock();
  return *lock;
}

NS_IMETHODIMP ContentAnalysis::ForceRecreateClientForTest() {
  return CreateClientIfNecessary(/* aForceCreate */ true);
}

nsresult ContentAnalysis::CreateClientIfNecessary(
    bool aForceCreate /* = false */) {
  AssertIsOnMainThread();

  if (IsShutDown()) {
    return NS_OK;
  }

  nsCString pipePathName;
  nsresult rv = Preferences::GetCString(kPipePathNamePref, pipePathName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mCaClientPromise->Reject(rv, __func__);
    return rv;
  }
  if (mHaveResolvedClientPromise && !aForceCreate) {
    return NS_OK;
  }
  // mCreatingClient is only accessed on the main thread
  if (mCreatingClient) {
    return NS_OK;
  }
  mCreatingClient = true;
  mHaveResolvedClientPromise = false;
  // Reject the promise to avoid assertions when it gets destroyed
  // Note that if the promise has already been resolved or rejected this is a
  // noop
  mCaClientPromise->Reject(NS_ERROR_FAILURE, __func__);
  mCaClientPromise =
      new ClientPromise::Private("ContentAnalysis::ContentAnalysis");

  bool isPerUser = StaticPrefs::browser_contentanalysis_is_per_user();
  nsString clientSignature;
  // It's OK if this fails, we will default to the empty string
  Preferences::GetString(kClientSignature, clientSignature);
  RecordConnectionSettingsTelemetry(clientSignature);
  LOGD("Dispatching background task to create Content Analysis client");
  glean::content_analysis::connection_attempt.Add();
  if (aForceCreate) {
    // indicates this is a retry attempt
    glean::content_analysis::connection_attempt_retry.Add();
  }
  rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction(
      "ContentAnalysis::CreateContentAnalysisClient",
      [owner = RefPtr{this}, pipePathName = std::move(pipePathName),
       clientSignature = std::move(clientSignature), isPerUser]() mutable {
        owner->CreateContentAnalysisClient(
            std::move(pipePathName), std::move(clientSignature), isPerUser);
      }));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    glean::content_analysis::connection_failure
        .Get(nsCString{SafeGetStaticErrorName(rv)})
        .Add();
    mCaClientPromise->Reject(rv, __func__);
    return rv;
  }
  return NS_OK;
}

void ContentAnalysis::RecordConnectionSettingsTelemetry(
    const nsString& clientSignature) {
  AssertIsOnMainThread();
  {
    nsCString agentName;
    Preferences::GetCString(kAgentNamePref, agentName);
    glean::content_analysis::agent_name.Set(agentName);
  }
  AutoTArray<nsCString, 1> interceptionPointsOff;
  for (const char* interceptionPointPrefName : kInterceptionPointPrefNames) {
    bool interceptionPointPrefValue = false;
    Preferences::GetBool(interceptionPointPrefName,
                         &interceptionPointPrefValue);
    if (!interceptionPointPrefValue) {
      interceptionPointsOff.AppendElement(interceptionPointPrefName);
    }
  }
  if (!interceptionPointsOff.IsEmpty()) {
    glean::content_analysis::interception_points_turned_off.Set(
        interceptionPointsOff);
  }
  glean::content_analysis::show_blocked_result.Set(
      StaticPrefs::browser_contentanalysis_show_blocked_result());
  glean::content_analysis::default_result.Set(
      StaticPrefs::browser_contentanalysis_default_result());
  glean::content_analysis::timeout_result.Set(
      StaticPrefs::browser_contentanalysis_timeout_result());
  if (!clientSignature.IsEmpty()) {
    glean::content_analysis::client_signature.Set(
        NS_ConvertUTF16toUTF8(clientSignature));
  }
  glean::content_analysis::bypass_for_same_tab_operations.Set(
      StaticPrefs::browser_contentanalysis_bypass_for_same_tab_operations());
  {
    nsCString allowUrlRegexList;
    Preferences::GetCString(kAllowUrlPref, allowUrlRegexList);
    // Unfortunately because of the way enterprise policies set and lock prefs,
    // we can't check if the value is different than the default in
    // StaticPrefList.yaml, and instead we have to duplicate that value here. At
    // least we have a test around this so we can update this value if the
    // default changes.
    const char* defaultAllowUrlRegexList = "^about:(?!blank|srcdoc).*";
    glean::content_analysis::allow_url_regex_list_set.Set(
        !allowUrlRegexList.Equals(defaultAllowUrlRegexList));
  }
  {
    nsCString denyUrlRegexList;
    Preferences::GetCString(kDenyUrlPref, denyUrlRegexList);
    glean::content_analysis::deny_url_regex_list_set.Set(
        !denyUrlRegexList.IsEmpty());
  }
}

NS_IMETHODIMP
ContentAnalysis::GetIsActive(bool* aIsActive) {
  *aIsActive = false;
  if (!StaticPrefs::browser_contentanalysis_enabled()) {
    LOGD("Local DLP Content Analysis is not enabled");
    return NS_OK;
  }
  // Accessing mSetByEnterprise and non-static prefs
  // so need to be on the main thread
  AssertIsOnMainThread();
  // gAllowContentAnalysisArgPresent is only set in the parent process
  MOZ_ASSERT(XRE_IsParentProcess());
  if (!gAllowContentAnalysisArgPresent && !mSetByEnterprise) {
    LOGE(
        "The content analysis pref is enabled but not by an enterprise "
        "policy and -allow-content-analysis was not present on the "
        "command-line.  Content Analysis will not be active.");
    return NS_OK;
  }

  *aIsActive = true;
  LOGD("Local DLP Content Analysis is enabled");
  return CreateClientIfNecessary();
}

NS_IMETHODIMP
ContentAnalysis::GetMightBeActive(bool* aMightBeActive) {
  *aMightBeActive = nsIContentAnalysis::MightBeActive();
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::GetIsSetByEnterprisePolicy(bool* aSetByEnterprise) {
  *aSetByEnterprise = mSetByEnterprise;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::SetIsSetByEnterprisePolicy(bool aSetByEnterprise) {
  mSetByEnterprise = aSetByEnterprise;
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::TestOnlySetCACmdLineArg(bool aVal) {
#ifdef ENABLE_TESTS
  gAllowContentAnalysisArgPresent = aVal;
  return NS_OK;
#else
  LOGE("ContentAnalysis::TestOnlySetCACmdLineArg is test-only");
  return NS_ERROR_UNEXPECTED;
#endif
}

Maybe<nsIContentAnalysisResponse::Action>
ContentAnalysis::CachedClipboardResponse::GetCachedResponse(
    nsIURI* aURI, int32_t aClipboardSequenceNumber) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Expecting main thread access only to avoid synchronization");
  if (Some(aClipboardSequenceNumber) != mClipboardSequenceNumber) {
    LOGD("CachedClipboardResponse seqno does not match cached value");
    return Nothing();
  }
  for (const auto& entry : mData) {
    bool uriEquals = false;
    // URI will not be set for some chrome contexts
    if ((!aURI && !entry.first) ||
        (aURI && NS_SUCCEEDED(aURI->Equals(entry.first, &uriEquals)) &&
         uriEquals)) {
      LOGD("CachedClipboardResponse match");
      return Some(entry.second);
    }
  }
  LOGD("CachedClipboardResponse did not match any cached URI");
  return Nothing();
}

void ContentAnalysis::CachedClipboardResponse::SetCachedResponse(
    const nsCOMPtr<nsIURI>& aURI, int32_t aClipboardSequenceNumber,
    nsIContentAnalysisResponse::Action aAction) {
  MOZ_ASSERT(NS_IsMainThread(),
             "Expecting main thread access only to avoid synchronization");
  if (mClipboardSequenceNumber != Some(aClipboardSequenceNumber)) {
    LOGD("CachedClipboardResponse caching new clipboard seqno");
    mData.Clear();
    mClipboardSequenceNumber = Some(aClipboardSequenceNumber);
  } else {
    LOGD(
        "CachedClipboardResponse caching new URI for existing cached clipboard "
        "seqno");
  }

  // Update the cached action for this URI if it already exists in the cache,
  // otherwise add a new cache entry for this URI.
  for (auto& entry : mData) {
    bool uriEquals = false;
    // URI will not be set for some chrome contexts
    if ((!aURI && !entry.first) ||
        (aURI && NS_SUCCEEDED(aURI->Equals(entry.first, &uriEquals)) &&
         uriEquals)) {
      entry.second = aAction;
      return;
    }
  }

  mData.AppendElement(std::make_pair(aURI, aAction));
}

NS_IMETHODIMP ContentAnalysis::SetCachedResponse(
    nsIURI* aURI, int32_t aClipboardSequenceNumber,
    nsIContentAnalysisResponse::Action aAction) {
  mCachedClipboardResponse.SetCachedResponse(aURI, aClipboardSequenceNumber,
                                             aAction);
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysis::GetCachedResponse(
    nsIURI* aURI, int32_t aClipboardSequenceNumber,
    nsIContentAnalysisResponse::Action* aAction, bool* aIsValid) {
  auto action = mCachedClipboardResponse.GetCachedResponse(
      aURI, aClipboardSequenceNumber);
  *aIsValid = action.isSome();
  if (action.isSome()) {
    *aAction = *action;
  }
  return NS_OK;
}

void ContentAnalysis::CancelWithError(nsCString&& aUserActionId,
                                      nsresult aResult) {
  MOZ_ASSERT(!aUserActionId.IsEmpty());
  if (!NS_IsMainThread()) {
    NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
        "CancelWithError",
        [aUserActionId = std::move(aUserActionId), aResult]() mutable {
          auto self = GetContentAnalysisFromService();
          if (!self) {
            // May be shutting down
            return;
          }
          self->CancelWithError(std::move(aUserActionId), aResult);
        }));
    return;
  }
  AssertIsOnMainThread();
  LOGD("CancelWithError | aUserActionId: %s | aResult: %s\n",
       aUserActionId.get(), SafeGetStaticErrorName(aResult));

  AutoTArray<nsCString, 1> tokens;
  RefPtr<nsIContentAnalysisCallback> callback;
  bool autoAcknowledge;
  if (auto maybeUserActionData = mUserActionMap.Lookup(aUserActionId)) {
    // We are cancelling all existing requests for this user action.
    tokens =
        ToTArray<AutoTArray<nsCString, 1>>(maybeUserActionData->mRequestTokens);
    callback = maybeUserActionData->mCallback;
    autoAcknowledge = maybeUserActionData->mAutoAcknowledge;
  } else {
    LOGD(
        "ContentAnalysis::CancelWithError user action not found -- already "
        "responded | userActionId: %s",
        aUserActionId.get());
    auto userActionIdToCanceledResponseMap =
        mUserActionIdToCanceledResponseMap.Lock();
    if (auto entry = userActionIdToCanceledResponseMap->Lookup(aUserActionId)) {
      entry->mNumExpectedResponses--;
      if (!entry->mNumExpectedResponses) {
        entry.Remove();
      }
    }
    return;
  }

  if (tokens.IsEmpty()) {
    // There are two cases where this happens.
    // (1) This Cancel was for the last request in the user action.  We don't
    // have any other tokens to cancel and we have nothing to tell the agent to
    // cancel.  Note that this case is only possible if this cancel call is
    // due to a negative verdict from the agent, and that handler will remove
    // our userActionId from mUserActionMap, so there is nothing left to do.
    // (2) We canceled before the final request list was formed.  We still
    // need to call the callback -- we do this when the final request list
    // is complete.
    MOZ_ASSERT(
        aResult == NS_ERROR_ABORT,
        "Token list can only be empty when canceling all remaining requests");
    LOGD(
        "ContentAnalysis::CancelWithError user action not found -- either was "
        "after last response or before first request was submitted | "
        "userActionId: %s",
        aUserActionId.get());
    RemoveFromUserActionMap(std::move(aUserActionId));
    return;
  }

  LOGD(
      "ContentAnalysis::CancelWithError cancelling user action: %s with error: "
      "%s",
      aUserActionId.get(), SafeGetStaticErrorName(aResult));

  bool isShutdown = aResult == NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
  bool isCancel = aResult == NS_ERROR_ABORT;
  bool isTimeout = aResult == NS_ERROR_DOM_TIMEOUT_ERR;

  // Propagate shutdown error to the callback as that same error.  All other
  // cases use the default response, except user cancel, which always uses
  // cancel response.
  // Note that, for shutdown errors, if we returned a default warn response
  // (as opposed to some other value -- we currently return the error),
  // the result would be a shutdown hang while the dialog waited for a user
  // response (bug 1912245).
  nsIContentAnalysisResponse::Action action =
      nsIContentAnalysisResponse::Action::eCanceled;
  if (!isShutdown && !isCancel) {
    DefaultResult defaultResponse = GetDefaultResultFromPref(isTimeout);
    switch (defaultResponse) {
      case DefaultResult::eAllow:
        action = nsIContentAnalysisResponse::Action::eAllow;
        break;
      case DefaultResult::eWarn:
        action = nsIContentAnalysisResponse::Action::eWarn;
        break;
      case DefaultResult::eBlock:
        // eBlock would show a block dialog but eCanceled will not.
        action = nsIContentAnalysisResponse::Action::eCanceled;
        break;
      default:
        MOZ_ASSERT(false);
        action = nsIContentAnalysisResponse::Action::eCanceled;
    }
  }

  nsIContentAnalysisResponse::CancelError cancelError;
  switch (aResult) {
    case NS_ERROR_NOT_AVAILABLE:
    case NS_ERROR_CONNECTION_REFUSED:
      cancelError = nsIContentAnalysisResponse::CancelError::eNoAgent;
      break;
    case NS_ERROR_INVALID_SIGNATURE:
      cancelError =
          nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature;
      break;
    case NS_ERROR_WONT_HANDLE_CONTENT:
    case NS_ERROR_ABORT:
      cancelError = nsIContentAnalysisResponse::CancelError::
          eOtherRequestInGroupCancelled;
      break;
    case NS_ERROR_ILLEGAL_DURING_SHUTDOWN:
      cancelError = nsIContentAnalysisResponse::CancelError::eShutdown;
      break;
    case NS_ERROR_DOM_TIMEOUT_ERR:
      cancelError = nsIContentAnalysisResponse::CancelError::eTimeout;
      break;
    default:
      cancelError = nsIContentAnalysisResponse::CancelError::eErrorOther;
      break;
  }

  bool calledError = false;
  for (const auto& token : tokens) {
    auto response =
        MakeRefPtr<ContentAnalysisResponse>(action, token, aUserActionId);
    response->SetCancelError(cancelError);
    // Alert the UI and (if action is not warn) the callback.  We aren't
    // handling an actual response so we have nothing to acknowledge.
    NotifyResponseObservers(response, nsCString(aUserActionId), autoAcknowledge,
                            isTimeout);
    if (action != nsIContentAnalysisResponse::Action::eWarn) {
      if (callback) {
        if (isShutdown) {
          // One Error response call is sufficient to complete the
          // MultipartRequestCallback.
          if (!calledError) {
            callback->Error(aResult);
            calledError = true;
          }
        } else {
          callback->ContentResult(response);
        }
      }
    }
  }

  if (action == nsIContentAnalysisResponse::Action::eWarn) {
    // A default warn response will handle the rest after the user chooses
    // a result.
    return;
  }

  RemoveFromUserActionMap(nsCString(aUserActionId));

  // NS_ERROR_WONT_HANDLE_CONTENT and NS_ERROR_CONNECTION_REFUSED mean the
  // request was never sent to the agent, so we don't cancel it.
  if (aResult != NS_ERROR_WONT_HANDLE_CONTENT &&
      aResult != NS_ERROR_CONNECTION_REFUSED) {
    auto userActionIdToCanceledResponseMap =
        mUserActionIdToCanceledResponseMap.Lock();
    userActionIdToCanceledResponseMap->InsertOrUpdate(
        aUserActionId,
        CanceledResponse{ConvertResult(action), tokens.Length()});
  } else {
    LOGD("CancelWithError cancelling unsubmitted request with error %s.",
         SafeGetStaticErrorName(aResult));
    return;
  }

  // Re-get service in case the registered service is mocked for testing.
  nsCOMPtr<nsIContentAnalysis> contentAnalysis =
      mozilla::components::nsIContentAnalysis::Service();
  if (contentAnalysis) {
    contentAnalysis->SendCancelToAgent(aUserActionId);
  } else {
    LOGD(
        "Content Analysis Service has been shut down.  Cancel will not be "
        "sent to agent.");
  }
}

NS_IMETHODIMP ContentAnalysis::SendCancelToAgent(
    const nsACString& aUserActionId) {
  CallClientWithRetry<std::nullptr_t>(
      __func__,
      [userActionId = nsCString(aUserActionId)](
          std::shared_ptr<content_analysis::sdk::Client> client) mutable
          -> Result<std::nullptr_t, nsresult> {
        MOZ_ASSERT(!NS_IsMainThread());
        auto owner = GetContentAnalysisFromService();
        if (!owner) {
          // May be shutting down
          return nullptr;
        }
        content_analysis::sdk::ContentAnalysisCancelRequests cancelRequest;
        cancelRequest.set_user_action_id(userActionId.get(),
                                         userActionId.Length());
        int err = client->CancelRequests(cancelRequest);
        if (err != 0) {
          LOGE(
              "SendCancelToAgent got error %d for "
              "user_action_id: %s",
              err, userActionId.get());
          return Err(NS_ERROR_FAILURE);
        }
        LOGD(
            "SendCancelToAgent successfully sent CancelRequests to "
            "agent for user_action_id: %s",
            userActionId.get());
        return nullptr;
      })
      ->Then(
          GetCurrentSerialEventTarget(), __func__, []() { /* nothing to do */ },
          [](nsresult rv) {
            LOGE("SendCancelToAgent failed to get the client with error %s",
                 SafeGetStaticErrorName(rv));
          });
  return NS_OK;
}

RefPtr<ContentAnalysis> ContentAnalysis::GetContentAnalysisFromService() {
  RefPtr<ContentAnalysis> contentAnalysisService =
      mozilla::components::nsIContentAnalysis::Service();
  return contentAnalysisService;
}

static bool ShouldCheckReason(nsIContentAnalysisRequest::Reason aReason) {
  switch (aReason) {
    case nsIContentAnalysisRequest::Reason::eFilePickerDialog:
      return mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_file_upload_enabled();
    case nsIContentAnalysisRequest::Reason::eClipboardPaste:
      return mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_clipboard_enabled();
    case nsIContentAnalysisRequest::Reason::ePrintPreviewPrint:
    case nsIContentAnalysisRequest::Reason::eSystemDialogPrint:
      return mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_print_enabled();
    case nsIContentAnalysisRequest::Reason::eDragAndDrop:
      return mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_drag_and_drop_enabled();
    case nsIContentAnalysisRequest::Reason::eNormalDownload:
    case nsIContentAnalysisRequest::Reason::eSaveAsDownload:
      return mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_download_enabled();
    default:
      MOZ_ASSERT_UNREACHABLE("Unrecognized content analysis request reason");
      return false;  // don't try to check it
  }
}

template <typename T, typename U>
RefPtr<MozPromise<T, nsresult, true>> ContentAnalysis::CallClientWithRetry(
    StaticString aMethodName, U&& aClientCallFunc) {
  AssertIsOnMainThread();
  auto promise =
      MakeRefPtr<typename MozPromise<T, nsresult, true>::Private>(aMethodName);

  // Make a copy of aClientCallFunc using copy constructor
  auto reconnectAndRetry = [clientCallFunc = aClientCallFunc, aMethodName,
                            promise](nsresult rv) {
    AssertIsOnMainThread();
    LOGD("Failed to get client - trying to reconnect: %s",
         SafeGetStaticErrorName(rv));
    RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
    if (!owner) {
      // May be shutting down
      promise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, aMethodName);
      return;
    }
    // try to reconnect
    rv = owner->CreateClientIfNecessary(/* aForceCreate */ true);
    if (NS_FAILED(rv)) {
      LOGD("Failed to reconnect to client: %s", SafeGetStaticErrorName(rv));
      owner->mCaClientPromise->Reject(rv, aMethodName);
      promise->Reject(rv, aMethodName);
      return;
    }
    owner->mCaClientPromise->Then(
        GetCurrentSerialEventTarget(), aMethodName,
        [aMethodName, promise, clientCallFunc = std::move(clientCallFunc)](
            std::shared_ptr<content_analysis::sdk::Client> client) mutable {
          auto contentAnalysis = GetContentAnalysisFromService();
          if (!contentAnalysis) {
            promise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, aMethodName);
            return;
          }
          nsresult rv = contentAnalysis->mThreadPool->Dispatch(
              NS_NewCancelableRunnableFunction(
                  aMethodName, [aMethodName, promise,
                                clientCallFunc = std::move(clientCallFunc),
                                client = std::move(client)]() mutable {
                    auto result = clientCallFunc(client);
                    if (result.isOk()) {
                      promise->Resolve(result.unwrap(), aMethodName);
                    } else {
                      promise->Reject(result.unwrapErr(), aMethodName);
                    }
                  }));
          if (NS_FAILED(rv)) {
            LOGE(
                "Failed to launch background task in second call for %s, "
                "error=%s",
                aMethodName.get(), SafeGetStaticErrorName(rv));
            promise->Reject(rv, aMethodName);
          }
        },
        [aMethodName, promise](nsresult rv) {
          LOGE("Failed to get client again for %s, error=%s", aMethodName.get(),
               SafeGetStaticErrorName(rv));
          promise->Reject(rv, aMethodName);
        });
  };

  mCaClientPromise->Then(
      GetCurrentSerialEventTarget(), aMethodName,
      // Make a copy of aClientCallFunc using copy or move constructor
      [aMethodName, promise, clientCallFunc = std::forward<U>(aClientCallFunc),
       reconnectAndRetry](
          std::shared_ptr<content_analysis::sdk::Client> client) mutable {
        auto contentAnalysis = GetContentAnalysisFromService();
        if (!contentAnalysis) {
          promise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, aMethodName);
          return;
        }
        nsresult rv = contentAnalysis->mThreadPool->Dispatch(
            NS_NewCancelableRunnableFunction(
                aMethodName, [aMethodName, promise,
                              clientCallFunc = std::move(clientCallFunc),
                              reconnectAndRetry = std::move(reconnectAndRetry),
                              client = std::move(client)]() mutable {
                  auto result = clientCallFunc(client);
                  if (result.isOk()) {
                    promise->Resolve(result.unwrap(), aMethodName);
                    return;
                  }
                  nsresult rv = result.unwrapErr();
                  NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
                      "reconnect to Content Analysis client",
                      [rv, reconnectAndRetry = std::move(reconnectAndRetry)]() {
                        reconnectAndRetry(rv);
                      }));
                }));
        if (NS_FAILED(rv)) {
          LOGE(
              "Failed to launch background task in first call for %s, error=%s",
              aMethodName.get(), SafeGetStaticErrorName(rv));
          promise->Reject(rv, aMethodName);
        }
      },
      [reconnectAndRetry](nsresult rv) mutable { reconnectAndRetry(rv); });
  return promise.forget();
}

nsresult ContentAnalysis::RunAnalyzeRequestTask(
    const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge,
    const RefPtr<nsIContentAnalysisCallback>& aCallback) {
  AssertIsOnMainThread();

  nsresult rv = NS_ERROR_FAILURE;
  // Set up the scope exit before checking the return
  // value so we will call Error() if this call failed.
  auto callbackCopy = aCallback;
  auto se = MakeScopeExit([&]() {
    if (!NS_SUCCEEDED(rv)) {
      LOGE("RunAnalyzeRequestTask failed");
      callbackCopy->Error(rv);
    }
  });

  nsCString requestToken;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetRequestToken(requestToken));
  nsCString userActionId;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetUserActionId(userActionId));

  // We will need to submit the request to the agent.
  content_analysis::sdk::ContentAnalysisRequest pbRequest;
  rv = ConvertToProtobuf(aRequest, &pbRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get());
  LogRequest(&pbRequest);
  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  // Avoid serializing the string here if no one is observing this message
  if (obsServ && obsServ->HasObservers("dlp-request-sent-raw")) {
    std::string requestString = pbRequest.SerializeAsString();
    nsTArray<char16_t> requestArray;
    requestArray.SetLength(requestString.size() + 1);
    for (size_t i = 0; i < requestString.size(); ++i) {
      // Since NotifyObservers() expects a null-terminated string,
      // make sure none of these values are 0.
      requestArray[i] = requestString[i] + 0xFF00;
    }
    requestArray[requestString.size()] = 0;
    obsServ->NotifyObservers(static_cast<nsIContentAnalysis*>(this),
                             "dlp-request-sent-raw", requestArray.Elements());
  }

  bool ignoreCanceled;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetTestOnlyIgnoreCanceledAndAlwaysSubmitToAgent(
      &ignoreCanceled));

  {
    nsDependentCString analysisTypeStr(
        content_analysis::sdk::AnalysisConnector_Name(
            pbRequest.analysis_connector())
            .c_str());
    glean::content_analysis::request_sent_by_analysis_type.Get(analysisTypeStr)
        .Add();
  }
  {
    nsDependentCString reasonStr(
        content_analysis::sdk::ContentAnalysisRequest_Reason_Name(
            pbRequest.reason())
            .c_str());
    glean::content_analysis::request_sent_by_reason.Get(reasonStr).Add();
  }

  CallClientWithRetry<std::nullptr_t>(
      __func__,
      [userActionId = userActionId, pbRequest = std::move(pbRequest),
       aAutoAcknowledge, ignoreCanceled](
          std::shared_ptr<content_analysis::sdk::Client> client) mutable {
        MOZ_ASSERT(!NS_IsMainThread());
        return DoAnalyzeRequest(std::move(userActionId), std::move(pbRequest),
                                aAutoAcknowledge, client, ignoreCanceled);
      })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ },
          [userActionId = std::move(userActionId),
           requestToken = std::move(requestToken)](nsresult rv) mutable {
            LOGD(
                "RunAnalyzeRequestTask failed to get client a second time for "
                "requestToken=%s, userActionId=%s",
                requestToken.get(), userActionId.get());
            RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
            if (!owner) {
              // May be shutting down
              return;
            }
            owner->CancelWithError(std::move(userActionId), rv);
          });

  return NS_OK;
}

Result<std::nullptr_t, nsresult> ContentAnalysis::DoAnalyzeRequest(
    nsCString&& aUserActionId,
    content_analysis::sdk::ContentAnalysisRequest&& aRequest,
    bool aAutoAcknowledge,
    const std::shared_ptr<content_analysis::sdk::Client>& aClient,
    bool aTestOnlyIgnoreCanceled) {
  MOZ_ASSERT(!NS_IsMainThread());
  RefPtr<ContentAnalysis> owner =
      ContentAnalysis::GetContentAnalysisFromService();
  if (!owner) {
    // May be shutting down
    // Don't return an error because we don't want to retry
    return nullptr;
  }

  if (aRequest.has_file_path() && !aRequest.file_path().empty() &&
      (!aRequest.request_data().has_digest() ||
       aRequest.request_data().digest().empty())) {
    // Calculate the digest
    nsCString digest;
    nsCString fileCPath(aRequest.file_path().data(),
                        aRequest.file_path().length());
    nsString filePath = NS_ConvertUTF8toUTF16(fileCPath);
    nsresult rv = ContentAnalysisRequest::GetFileDigest(filePath, digest);
    if (NS_FAILED(rv)) {
      owner->CancelWithError(std::move(aUserActionId), rv);
      // Don't return an error because we don't want to retry
      return nullptr;
    }
    if (!digest.IsEmpty()) {
      aRequest.mutable_request_data()->set_digest(digest.get());
    }
  }

  bool actionWasCanceled = false;
  if (!aTestOnlyIgnoreCanceled) {
    auto userActionIdToCanceledResponseMap =
        owner->mUserActionIdToCanceledResponseMap.Lock();
    actionWasCanceled =
        userActionIdToCanceledResponseMap->Contains(aUserActionId);
  }
  if (actionWasCanceled) {
    LOGD(
        "DoAnalyzeRequest | userAction: %s | requestToken: %s | was already "
        "canceled",
        aUserActionId.get(), aRequest.request_token().c_str());
    return Err(NS_ERROR_WONT_HANDLE_CONTENT);
  }

  // Run request, then dispatch back to main thread to resolve
  // aCallback
  content_analysis::sdk::ContentAnalysisResponse pbResponse;
  nsDependentCString analysisConnectorName(
      content_analysis::sdk::AnalysisConnector_Name(
          aRequest.analysis_connector())
          .c_str());
  auto timerId = glean::content_analysis::response_duration_by_analysis_type
                     .Get(analysisConnectorName)
                     .Start();
  {
    // Insert this into the map before calling Send() because another thread
    // calling Send() may get a response before our Send() call finishes.
    auto map = owner->mRequestTokenToBasicRequestInfoMap.Lock();
    map->InsertOrUpdate(
        nsCString(aRequest.request_token()),
        BasicRequestInfo{aUserActionId, timerId,
                         std::move(analysisConnectorName), aAutoAcknowledge});
  }

  LOGD(
      "DoAnalyzeRequest | userAction: %s | requestToken: %s | sending request "
      "to agent",
      aUserActionId.get(), aRequest.request_token().c_str());
  int err = aClient->Send(aRequest, &pbResponse);
  if (err != 0) {
    LOGE("DoAnalyzeRequest got err=%d for request_token=%s, user_action_id=%s",
         err, aRequest.request_token().c_str(), aUserActionId.get());
    Maybe<BasicRequestInfo> entry;
    {
      auto map = owner->mRequestTokenToBasicRequestInfoMap.Lock();
      entry = map->Extract(nsCString(aRequest.request_token()));
    }
    if (entry.isSome()) {
      glean::content_analysis::response_duration_by_analysis_type
          .Get(entry->mAnalysisTypeStr)
          .Cancel(std::move(entry->mTimerId));
    }

    return Err(NS_ERROR_FAILURE);
  }
  HandleResponseFromAgent(std::move(pbResponse));
  return nullptr;
}

void ContentAnalysis::HandleResponseFromAgent(
    content_analysis::sdk::ContentAnalysisResponse&& aResponse) {
  MOZ_ASSERT(!NS_IsMainThread());
  NS_DispatchToMainThread(NS_NewRunnableFunction(
      __func__, [aResponse = std::move(aResponse)]() mutable {
        LOGD("RunAnalyzeRequestTask on main thread about to send response");
        LogResponse(&aResponse);
        RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
        if (!owner) {
          // May be shutting down
          return;
        }

        nsCOMPtr<nsIObserverService> obsServ =
            mozilla::services::GetObserverService();
        // This message is only used for testing purposes, so avoid
        // serializing the string here if no one is observing this message.
        // This message is only really useful if we're in a timeout
        // situation, otherwise dlp-response is fine.
        if (obsServ && obsServ->HasObservers("dlp-response-received-raw")) {
          std::string responseString = aResponse.SerializeAsString();
          nsTArray<char16_t> responseArray;
          responseArray.SetLength(responseString.size() + 1);
          for (size_t i = 0; i < responseString.size(); ++i) {
            // Since NotifyObservers() expects a null-terminated string,
            // make sure none of these values are 0.
            responseArray[i] = responseString[i] + 0xFF00;
          }
          responseArray[responseString.size()] = 0;
          obsServ->NotifyObservers(static_cast<nsIContentAnalysis*>(owner),
                                   "dlp-response-received-raw",
                                   responseArray.Elements());
        }

        Maybe<BasicRequestInfo> maybeBasicRequestInfo;
        {
          auto map = owner->mRequestTokenToBasicRequestInfoMap.Lock();
          maybeBasicRequestInfo =
              map->Extract(nsCString(aResponse.request_token()));
        }
        if (maybeBasicRequestInfo.isNothing()) {
          LOGE(
              "RunAnalyzeRequestTask could not find userActionId for "
              "request token %s",
              aResponse.request_token().c_str());
          // We have no hope of doing anything useful, so just early return.
          return;
        }
        glean::content_analysis::response_duration_by_analysis_type
            .Get(maybeBasicRequestInfo->mAnalysisTypeStr)
            .StopAndAccumulate(std::move(maybeBasicRequestInfo->mTimerId));
        nsCString userActionId = maybeBasicRequestInfo->mUserActionId;

        RefPtr<ContentAnalysisResponse> response =
            ContentAnalysisResponse::FromProtobuf(std::move(aResponse),
                                                  userActionId);
        if (!response) {
          LOGE("Content analysis got invalid response!");
          return;
        }
        // We add our own values for action here (eAllow and eCancel)
        // so just use the numeric value for glean.
        nsAutoCString actionStr;
        actionStr.AppendInt(static_cast<int>(response->GetAction()));
        glean::content_analysis::response_action.Get(actionStr).Add();
        // Normally, if we timeout/user-cancel a request, we remove the
        // adjacent entry in mUserActionMap.  However, we don't do that if
        // the chosen default behavior is to warn.  We don't want to issue
        // a response in that case.
        nsCString requestToken;
        MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(requestToken));
        if (owner->mWarnResponseDataMap.Contains(requestToken)) {
          return;
        }

        owner->NotifyObserversAndMaybeIssueResponseFromAgent(
            response, std::move(userActionId),
            maybeBasicRequestInfo->mAutoAcknowledge);
      }));
}

void ContentAnalysis::NotifyResponseObservers(
    ContentAnalysisResponse* aResponse, nsCString&& aUserActionId,
    bool aAutoAcknowledge, bool aIsTimeout) {
  MOZ_ASSERT(NS_IsMainThread());
  aResponse->SetOwner(this);

  if (aResponse->GetAction() == nsIContentAnalysisResponse::Action::eWarn) {
    // Store data so we can asynchronously run the warn dialog, then call
    // IssueResponse with the result.
    nsCString requestToken;
    MOZ_ALWAYS_SUCCEEDS(aResponse->GetRequestToken(requestToken));

    mWarnResponseDataMap.InsertOrUpdate(
        requestToken, WarnResponseData{aResponse, std::move(aUserActionId),
                                       aAutoAcknowledge, aIsTimeout});
  }

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (obsServ) {
    obsServ->NotifyObservers(
        static_cast<nsIContentAnalysisResponse*>(aResponse), "dlp-response",
        nullptr);
  }
}

void ContentAnalysis::IssueResponse(ContentAnalysisResponse* aResponse,
                                    nsCString&& aUserActionId,
                                    bool aAcknowledge, bool aIsTimeout) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aResponse->GetAction() !=
             nsIContentAnalysisResponse::Action::eWarn);

  // Call the callback and maybe send an auto acknowledge.
  nsCString token;
  MOZ_ALWAYS_SUCCEEDS(aResponse->GetRequestToken(token));
  RefPtr<nsIContentAnalysisCallback> callback;
  if (auto maybeUserActionData = mUserActionMap.Lookup(aUserActionId)) {
    callback = maybeUserActionData->mCallback;
  } else {
    LOGD(
        "ContentAnalysis::IssueResponse user action not found -- already "
        "responded | userActionId: %s",
        aUserActionId.get());

    if (aAcknowledge) {
      // Respond to the agent with TOO_LATE because the response arrived
      // after the request was cancelled (for any reason).
      nsIContentAnalysisAcknowledgement::FinalAction action;
      auto userActionIdToCanceledResponseMap =
          mUserActionIdToCanceledResponseMap.Lock();
      userActionIdToCanceledResponseMap->WithEntryHandle(
          aUserActionId, [&](auto&& canceledResponseEntry) {
            if (canceledResponseEntry) {
              action = canceledResponseEntry->mAction;
              --canceledResponseEntry->mNumExpectedResponses;
              if (!canceledResponseEntry->mNumExpectedResponses) {
                // We've handled all responses for canceled requests for this
                // user action.
                canceledResponseEntry.Remove();
              }
            } else {
              if (mWarnResponseDataMap.Contains(token)) {
                // We got a response from the agent but we're still waiting
                // for a warn response from the user. This can basically only
                // happen if the request timed out but TimeoutResult=1 (i.e.
                // warn) is set.
                LOGD(
                    "Got response from agent for token %s but user hasn't "
                    "replied to warn dialog yet",
                    token.get());
                return;
              }
              MOZ_ASSERT_UNREACHABLE("missing canceled response action");
              action =
                  nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
            }
            RefPtr<ContentAnalysisAcknowledgement> acknowledgement =
                MakeRefPtr<ContentAnalysisAcknowledgement>(
                    nsIContentAnalysisAcknowledgement::Result::eTooLate,
                    action);
            aResponse->Acknowledge(acknowledgement);
          });
    }
    return;
  }

  if (aAcknowledge) {
    // Acknowledge every response we receive.
    auto acknowledgement = MakeRefPtr<ContentAnalysisAcknowledgement>(
        aIsTimeout ? nsIContentAnalysisAcknowledgement::Result::eTooLate
                   : nsIContentAnalysisAcknowledgement::Result::eSuccess,
        ConvertResult(aResponse->GetAction()));
    aResponse->Acknowledge(acknowledgement);
  }

  LOGD("Content analysis notifying observers and calling callback for token %s",
       token.get());
  callback->ContentResult(aResponse);

  // A negative verdict should have removed our user action.  (This method
  // is not called for warn verdicts.)
  MOZ_ASSERT(aResponse->GetShouldAllowContent() ||
             !mUserActionMap.Contains(aUserActionId));
}

void ContentAnalysis::NotifyObserversAndMaybeIssueResponseFromAgent(
    ContentAnalysisResponse* aResponse, nsCString&& aUserActionId,
    bool aAutoAcknowledge) {
  NotifyResponseObservers(aResponse, nsCString(aUserActionId), aAutoAcknowledge,
                          false /* isTimeout */);

  // For warn responses, IssueResponse will be called later by
  // RespondToWarnDialog, with the action replaced with the user's selection.
  if (aResponse->GetAction() != nsIContentAnalysisResponse::Action::eWarn) {
    // This is a response from the agent, so not a timeout.
    IssueResponse(aResponse, std::move(aUserActionId), aAutoAcknowledge,
                  false /* aIsTimeout */);
  }
}

static void AddCARForText(
    nsString&& text, nsIContentAnalysisRequest::Reason aReason,
    nsIContentAnalysisRequest::OperationType aOperationType, nsIURI* aURI,
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsCString&& aUserActionId,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
  if (text.IsEmpty()) {
    // Content Analysis doesn't expect to analyze an empty string.
    // Just skip it.
    return;
  }

  LOGD("Adding CA request for text: '%s'", NS_ConvertUTF16toUTF8(text).get());
  auto contentAnalysisRequest = MakeRefPtr<ContentAnalysisRequest>(
      nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, aReason,
      std::move(text), false, EmptyCString(), aURI, aOperationType,
      aWindowGlobal, aSourceWindowGlobal, std::move(aUserActionId));
  aRequests->AppendElement(contentAnalysisRequest);
}

void AddCARForUpload(nsString&& filePath,
                     nsIContentAnalysisRequest::Reason aReason, nsIURI* aURI,
                     mozilla::dom::WindowGlobalParent* aWindowGlobal,
                     mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
                     nsCString&& aUserActionId,
                     nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
  if (filePath.IsEmpty()) {
    return;
  }

  // Let the content analysis code calculate the digest
  LOGD("Adding CA request for file: '%s'",
       NS_ConvertUTF16toUTF8(filePath).get());
  auto contentAnalysisRequest = MakeRefPtr<ContentAnalysisRequest>(
      nsIContentAnalysisRequest::AnalysisType::eFileAttached, aReason,
      std::move(filePath), true, EmptyCString(), aURI,
      nsIContentAnalysisRequest::OperationType::eUpload, aWindowGlobal,
      aSourceWindowGlobal, std::move(aUserActionId));
  aRequests->AppendElement(contentAnalysisRequest);
}

static nsresult AddClipboardCARForCustomData(
    mozilla::dom::WindowGlobalParent* aWindowGlobal, nsITransferable* aTrans,
    nsIURI* aURI, mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsCString&& aUserActionId,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
  nsCOMPtr<nsISupports> transferData;
  if (StaticPrefs::
          browser_contentanalysis_interception_point_clipboard_plain_text_only()) {
    return NS_OK;
  }

  if (NS_FAILED(aTrans->GetTransferData(kCustomTypesMime,
                                        getter_AddRefs(transferData)))) {
    return NS_OK;  // nothing to check and not an error
  }
  nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData);
  if (!cStringData) {
    return NS_OK;  // nothing to check and not an error
  }
  nsCString str;
  nsresult rv = cStringData->GetData(str);
  if (NS_FAILED(rv)) {
    return NS_OK;  // nothing to check and not an error
  }
  nsTArray<nsString> texts;
  dom::DataTransfer::ParseExternalCustomTypesString(
      mozilla::Span(str.Data(), str.Length()),
      [&](dom::DataTransfer::ParseExternalCustomTypesStringData&& aData) {
        texts.AppendElement(std::move(std::move(aData).second));
      });
  for (auto& text : texts) {
    AddCARForText(std::move(text),
                  nsIContentAnalysisRequest::Reason::eClipboardPaste,
                  nsIContentAnalysisRequest::OperationType::eClipboard, aURI,
                  aWindowGlobal, aSourceWindowGlobal, nsCString(aUserActionId),
                  aRequests);
  }
  return NS_OK;
}

static nsresult AddClipboardCARForText(
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    nsITransferable* aTextTrans, const char* aFlavor, nsIURI* aURI,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsCString&& aUserActionId,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
  nsCOMPtr<nsISupports> transferData;
  if (NS_FAILED(
          aTextTrans->GetTransferData(aFlavor, getter_AddRefs(transferData)))) {
    return NS_OK;  // nothing to check and not an error
  }
  nsString text;
  nsCOMPtr<nsISupportsString> textData = do_QueryInterface(transferData);
  if (MOZ_LIKELY(textData)) {
    if (NS_FAILED(textData->GetData(text))) {
      return NS_ERROR_FAILURE;
    }
  }
  if (text.IsEmpty()) {
    nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData);
    if (cStringData) {
      nsCString cText;
      if (NS_FAILED(cStringData->GetData(cText))) {
        return NS_ERROR_FAILURE;
      }
      text = NS_ConvertUTF8toUTF16(cText);
    }
  }

  AddCARForText(
      std::move(text), nsIContentAnalysisRequest::Reason::eClipboardPaste,
      nsIContentAnalysisRequest::OperationType::eClipboard, aURI, aWindowGlobal,
      aSourceWindowGlobal, std::move(aUserActionId), aRequests);
  return NS_OK;
}

static nsresult AddClipboardCARForFile(
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    nsITransferable* aFileTrans, nsIURI* aURI,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsCString&& aUserActionId,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aRequests) {
  nsCOMPtr<nsISupports> transferData;
  nsresult rv =
      aFileTrans->GetTransferData(kFileMime, getter_AddRefs(transferData));
  if (NS_SUCCEEDED(rv)) {
    if (nsCOMPtr<nsIFile> file = do_QueryInterface(transferData)) {
      nsString filePath;
      NS_ENSURE_SUCCESS(file->GetPath(filePath), NS_ERROR_FAILURE);
      AddCARForUpload(std::move(filePath),
                      nsIContentAnalysisRequest::Reason::eClipboardPaste, aURI,
                      aWindowGlobal, aSourceWindowGlobal,
                      std::move(aUserActionId), aRequests);
    } else {
      MOZ_ASSERT_UNREACHABLE("clipboard data had kFileMime but no nsIFile!");
      return NS_ERROR_FAILURE;
    }
  }
  return NS_OK;
}

static Result<bool, nsresult> AddRequestsFromTransferableIfAny(
    nsIContentAnalysisRequest* aOriginalRequest, nsIURI* aUri,
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aNewRequests) {
  NS_ENSURE_TRUE(aNewRequests, Err(NS_ERROR_INVALID_ARG));

  nsCOMPtr<nsITransferable> transferable;
  NS_ENSURE_SUCCESS(
      aOriginalRequest->GetTransferable(getter_AddRefs(transferable)),
      Err(NS_ERROR_FAILURE));
  if (!transferable) {
    return false;
  }

  nsAutoCString userActionId;
  MOZ_ALWAYS_SUCCEEDS(aOriginalRequest->GetUserActionId(userActionId));

  nsresult rv = AddClipboardCARForCustomData(
      aWindowGlobal, transferable, aUri, aSourceWindowGlobal,
      nsCString(userActionId), aNewRequests);
  NS_ENSURE_SUCCESS(rv, Err(rv));

  for (const auto& textFormat : kTextFormatsToAnalyze) {
    rv = AddClipboardCARForText(aWindowGlobal, transferable, textFormat, aUri,
                                aSourceWindowGlobal, nsCString(userActionId),
                                aNewRequests);
    NS_ENSURE_SUCCESS(rv, Err(rv));
    if (StaticPrefs::
            browser_contentanalysis_interception_point_clipboard_plain_text_only()) {
      // kTextMime is the first entry in kTextFormatsToAnalyze
      break;
    }
  }

  rv = AddClipboardCARForFile(aWindowGlobal, transferable, aUri,
                              aSourceWindowGlobal, std::move(userActionId),
                              aNewRequests);
  NS_ENSURE_SUCCESS(rv, Err(rv));
  return true;
}

static Result<bool, nsresult> AddRequestsFromDataTransferIfAny(
    nsIContentAnalysisRequest* aOriginalRequest, nsIURI* aUri,
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal,
    nsTArray<RefPtr<nsIContentAnalysisRequest>>* aNewRequests) {
  NS_ENSURE_TRUE(aNewRequests, Err(NS_ERROR_INVALID_ARG));

  nsCOMPtr<dom::DataTransfer> dataTransfer;
  NS_ENSURE_SUCCESS(
      aOriginalRequest->GetDataTransfer(getter_AddRefs(dataTransfer)),
      Err(NS_ERROR_FAILURE));
  if (!dataTransfer) {
    return false;
  }

  nsAutoCString userActionId;
  MOZ_ALWAYS_SUCCEEDS(aOriginalRequest->GetUserActionId(userActionId));

  auto& principal = *nsContentUtils::GetSystemPrincipal();
  for (const auto& textFormat : kTextFormatsToAnalyze) {
    nsAutoString text;
    ErrorResult error;
    // If format is not found then 'text' will be empty.
    dataTransfer->GetData(nsString(NS_ConvertUTF8toUTF16(textFormat)), text,
                          principal, error);
    NS_ENSURE_TRUE(!error.Failed(), Err(error.StealNSResult()));

    AddCARForText(std::move(text),
                  nsIContentAnalysisRequest::Reason::eDragAndDrop,
                  nsIContentAnalysisRequest::OperationType::eDroppedText, aUri,
                  aWindowGlobal, aSourceWindowGlobal, nsCString(userActionId),
                  aNewRequests);
    if (StaticPrefs::
            browser_contentanalysis_interception_point_drag_and_drop_plain_text_only()) {
      // kTextMime is the first entry in kTextFormatsToAnalyze
      break;
    }
  }

  if (dataTransfer->HasFile()) {
    RefPtr fileList = dataTransfer->GetFiles(principal);
    for (uint32_t i = 0; i < fileList->Length(); ++i) {
      auto* file = fileList->Item(i);
      if (!file) {
        continue;
      }
      nsString filePath;
      ErrorResult error;
      file->GetMozFullPathInternal(filePath, error);
      NS_ENSURE_TRUE(!error.Failed(), Err(error.StealNSResult()));

      AddCARForUpload(std::move(filePath),
                      nsIContentAnalysisRequest::Reason::eDragAndDrop, aUri,
                      aWindowGlobal, aSourceWindowGlobal,
                      nsCString(userActionId), aNewRequests);
    }
  }
  return true;
}

Result<already_AddRefed<nsIContentAnalysisRequest>, nsresult>
MakeRequestForFileInFolder(dom::File* aFile,
                           nsIContentAnalysisRequest* aFolderRequest) {
  nsCOMPtr<nsIURI> url;
  nsresult rv = aFolderRequest->GetUrl(getter_AddRefs(url));
  NS_ENSURE_SUCCESS(rv, Err(rv));
  nsIContentAnalysisRequest::AnalysisType analysisType;
  rv = aFolderRequest->GetAnalysisType(&analysisType);
  NS_ENSURE_SUCCESS(rv, Err(rv));
  nsIContentAnalysisRequest::Reason reason;
  rv = aFolderRequest->GetReason(&reason);
  NS_ENSURE_SUCCESS(rv, Err(rv));
  nsIContentAnalysisRequest::OperationType operationType;
  rv = aFolderRequest->GetOperationTypeForDisplay(&operationType);
  NS_ENSURE_SUCCESS(rv, Err(rv));
  RefPtr<dom::WindowGlobalParent> windowGlobal;
  rv = aFolderRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
  NS_ENSURE_SUCCESS(rv, Err(rv));
  RefPtr<mozilla::dom::WindowGlobalParent> sourceWindowGlobal;
  rv =
      aFolderRequest->GetSourceWindowGlobal(getter_AddRefs(sourceWindowGlobal));
  NS_ENSURE_SUCCESS(rv, Err(rv));
  nsCString userActionId;
  rv = aFolderRequest->GetUserActionId(userActionId);
  NS_ENSURE_SUCCESS(rv, Err(rv));

  nsAutoString pathString;
  mozilla::ErrorResult error;
  aFile->GetMozFullPathInternal(pathString, error);
  rv = error.StealNSResult();
  NS_ENSURE_SUCCESS(rv, Err(rv));

  return MakeRefPtr<ContentAnalysisRequest>(
             analysisType, reason, pathString, true, EmptyCString(), url,
             operationType, windowGlobal, sourceWindowGlobal,
             std::move(userActionId))
      .forget()
      .downcast<nsIContentAnalysisRequest>();
}

RefPtr<ContentAnalysis::MultipartRequestCallback>
ContentAnalysis::MultipartRequestCallback::Create(
    ContentAnalysis* aContentAnalysis,
    const nsTArray<ContentAnalysis::ContentAnalysisRequestArray>& aRequests,
    nsIContentAnalysisCallback* aCallback, bool aAutoAcknowledge) {
  auto mpcb = MakeRefPtr<MultipartRequestCallback>();
  mpcb->Initialize(aContentAnalysis, aRequests, aCallback, aAutoAcknowledge);
  return mpcb;
}

void ContentAnalysis::MultipartRequestCallback::Initialize(
    ContentAnalysis* aContentAnalysis,
    const nsTArray<ContentAnalysis::ContentAnalysisRequestArray>& aRequests,
    nsIContentAnalysisCallback* aCallback, bool aAutoAcknowledge) {
  MOZ_ASSERT(aContentAnalysis);
  MOZ_ASSERT(aCallback);
  MOZ_ASSERT(NS_IsMainThread());

  mWeakContentAnalysis = aContentAnalysis;
  mCallback = aCallback;

  mNumCARequestsRemaining = 0;
  nsTHashSet<nsCString> requestTokens;
  if (!aRequests.IsEmpty()) {
    for (const auto& requests : aRequests) {
      mNumCARequestsRemaining += requests.Length();
    }

    for (const auto& requests : aRequests) {
      for (const auto& request : requests) {
        // Pull the user action ID from the first entry we find.  They will
        // all have the same ID.  If that ID isn't in the user action map
        // then we were canceled while we were building the request list.
        // In that case, we haven't called the callback, so do that here.
        if (mUserActionId.IsEmpty()) {
          MOZ_ALWAYS_SUCCEEDS(request->GetUserActionId(mUserActionId));
          MOZ_ASSERT(!mUserActionId.IsEmpty());
          if (!mWeakContentAnalysis->mUserActionMap.Contains(mUserActionId)) {
            LOGD(
                "ContentAnalysis::MultipartRequestCallback created after "
                "request was canceled.  Calling callback.");
            RefPtr result = MakeRefPtr<ContentAnalysisActionResult>(
                nsIContentAnalysisResponse::Action::eCanceled);
            mCallback->ContentResult(result);
            mResponded = true;
            return;
          }
        }
        MOZ_ALWAYS_SUCCEEDS(
            request->SetUserActionRequestsCount(mNumCARequestsRemaining));
        nsCString requestToken;
        MOZ_ALWAYS_SUCCEEDS(request->GetRequestToken(requestToken));
        if (requestToken.IsEmpty()) {
          requestToken = GenerateUUID();
          MOZ_ALWAYS_SUCCEEDS(request->SetRequestToken(requestToken));
        }
        requestTokens.Insert(requestToken);
      }
    }
  }

  if (mNumCARequestsRemaining == 0) {
    // No requests will be submitted so no response will be sent by agent.
    // Respond now instead.
    LOGD(
        "Content analysis requested but nothing needs to be checked. "
        "Request is approved.");
    RefPtr result = MakeRefPtr<ContentAnalysisActionResult>(
        nsIContentAnalysisResponse::Action::eAllow);
    aCallback->ContentResult(result);
    return;
  }

  LOGD("ContentAnalysis processing %zu given and synthesized requests",
       mNumCARequestsRemaining);

  MOZ_ASSERT(!mUserActionId.IsEmpty());
  MOZ_ASSERT(!requestTokens.IsEmpty());

  auto checkedTimeoutMs =
      CheckedInt32(StaticPrefs::browser_contentanalysis_agent_timeout()) *
      1000 * mNumCARequestsRemaining;
  auto timeoutMs = checkedTimeoutMs.isValid()
                       ? checkedTimeoutMs.value()
                       : std::numeric_limits<int32_t>::max();
  // Non-positive timeout values indicate testing, and the test agent does not
  // care about this value.  Use 25ms (unscaled) in that case.
  timeoutMs = std::max(timeoutMs, 25);
  RefPtr timeoutRunnable = NS_NewCancelableRunnableFunction(
      "ContentAnalysis timeout",
      [userActionId = mUserActionId,
       weakContentAnalysis = mWeakContentAnalysis]() mutable {
        if (!weakContentAnalysis) {
          return;
        }
        // Entries awaiting a warn-dialog-selection should not be
        // considered as part of timeout.  Ignore timeout if all remaining
        // requests are awaiting a warn respones.  Otherwise cancel all of
        // them (including any awaiting a warn response) as timed out.
        bool found = false;
        if (auto remainingEntry =
                weakContentAnalysis->mUserActionMap.Lookup(userActionId)) {
          MOZ_ASSERT(!remainingEntry->mIsHandlingTimeout);
          for (const auto& remainingToken : remainingEntry->mRequestTokens) {
            if (!weakContentAnalysis->mWarnResponseDataMap.Contains(
                    remainingToken)) {
              // This request is not awaiting warn so cancel the entire user
              // action.
              found = true;
              // We do not allow calling Cancel() on runnables while they are
              // running, so this makes sure that CA does not do that.
              remainingEntry->mIsHandlingTimeout = true;
              break;
            }
          }
        }
        if (found) {
          weakContentAnalysis->CancelWithError(std::move(userActionId),
                                               NS_ERROR_DOM_TIMEOUT_ERR);
        }
      });
  NS_DelayedDispatchToCurrentThread((RefPtr{timeoutRunnable}).forget(),
                                    timeoutMs);

  // Update our entry in the user action map with the request tokens and a
  // timeout event.
  auto uaData = UserActionData{this, std::move(requestTokens), timeoutRunnable,
                               aAutoAcknowledge};
  MOZ_ASSERT(mWeakContentAnalysis->mUserActionMap.Lookup(mUserActionId));
  mWeakContentAnalysis->mUserActionMap.InsertOrUpdate(mUserActionId,
                                                      std::move(uaData));
}

NS_IMETHODIMP
ContentAnalysis::MultipartRequestCallback::ContentResult(
    nsIContentAnalysisResult* aResult) {
  MOZ_ASSERT(NS_IsMainThread());
  if (mWeakContentAnalysis) {
    // Remove aResult's request token from the remaining requests list.
    if (auto maybeUserActionData =
            mWeakContentAnalysis->mUserActionMap.Lookup(mUserActionId)) {
      nsCOMPtr<nsIContentAnalysisResponse> response =
          do_QueryInterface(aResult);
      MOZ_ASSERT(response);
      nsAutoCString token;
      MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(token));
      DebugOnly<bool> removed =
          maybeUserActionData->mRequestTokens.EnsureRemoved(token);
      // Either we removed the token or it was previously removed, along with
      // all others, as part of a cancellation.
      MOZ_ASSERT(removed || maybeUserActionData->mRequestTokens.IsEmpty(),
                 "Request token was not found");
    }
  }

  if (mResponded) {
    return NS_OK;
  }

  bool allow = aResult->GetShouldAllowContent();
  --mNumCARequestsRemaining;
  if (allow && mNumCARequestsRemaining > 0) {
    LOGD(
        "MultipartRequestCallback received allow response.  Awaiting "
        "%zu remaining responses",
        mNumCARequestsRemaining);
    return NS_OK;
  }

  LOGD("MultipartRequestCallback issuing response.  Permitted? %s",
       allow ? "yes" : "no");

  mResponded = true;
  mCallback->ContentResult(aResult);
  if (!allow) {
    CancelRequests();
  } else {
    RemoveFromUserActionMap();
  }
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::MultipartRequestCallback::Error(nsresult aRv) {
  MOZ_ASSERT(NS_IsMainThread());
  if (mResponded) {
    return NS_OK;
  }
  LOGD(
      "MultipartRequestCallback received %s while awaiting "
      "%zu remaining responses",
      SafeGetStaticErrorName(aRv), mNumCARequestsRemaining);

  mResponded = true;
  mCallback->Error(aRv);
  CancelRequests();
  return NS_OK;
}

ContentAnalysis::MultipartRequestCallback::~MultipartRequestCallback() {
  MOZ_ASSERT(NS_IsMainThread());

  // Either we have called our callback and removed our userActionId or we are
  // shutting down.
  MOZ_ASSERT(!mWeakContentAnalysis || mWeakContentAnalysis->IsShutDown() ||
             !mWeakContentAnalysis->mUserActionMap.Contains(mUserActionId));
}

void ContentAnalysis::MultipartRequestCallback::CancelRequests() {
  MOZ_ASSERT(mResponded);
  // If any request fails to be submitted or is rejected then we need to
  // cancel all of the other outstanding requests.  Note that we may be
  // getting here as part of being cancelled already, in which case we
  // have nothing to cancel but our caller may still be cancelling requests
  // from our user action, which is fine.
  if (mWeakContentAnalysis) {
    mWeakContentAnalysis->CancelRequestsByUserAction(mUserActionId);
  }
}

void ContentAnalysis::MultipartRequestCallback::RemoveFromUserActionMap() {
  if (mWeakContentAnalysis) {
    mWeakContentAnalysis->RemoveFromUserActionMap(nsCString(mUserActionId));
  }
}

void ContentAnalysis::RemoveFromUserActionMap(nsCString&& aUserActionId) {
  if (auto entry = mUserActionMap.Lookup(aUserActionId)) {
    // Implementation note: we need mIsHandlingTimeout because this is called
    // during mTimeoutRunnable and CancelableRunnable is not robust to having
    // Cancel called at that time.
    if (entry->mTimeoutRunnable && !entry->mIsHandlingTimeout) {
      // Timeout may or may not have been called.
      entry->mTimeoutRunnable->Cancel();
    }
    entry.Remove();
  }
}

NS_IMPL_QUERY_INTERFACE(ContentAnalysis::MultipartRequestCallback,
                        nsIContentAnalysisCallback)

Result<RefPtr<ContentAnalysis::RequestsPromise>, nsresult>
ContentAnalysis::ExpandFolderRequest(nsIContentAnalysisRequest* aRequest,
                                     nsIFile* file) {
  // We just need to iterate over the directory, so use the junk scope
  RefPtr<mozilla::dom::Directory> directory = mozilla::dom::Directory::Create(
      xpc::NativeGlobal(xpc::PrivilegedJunkScope()), file);
  NS_ENSURE_TRUE(directory, Err(NS_ERROR_FAILURE));

  mozilla::dom::OwningFileOrDirectory owningDirectory;
  owningDirectory.SetAsDirectory() = directory;
  nsTArray<mozilla::dom::OwningFileOrDirectory> directoryArray{
      std::move(owningDirectory)};

  using mozilla::dom::GetFilesHelper;
  mozilla::ErrorResult error;
  RefPtr<GetFilesHelper> helper =
      GetFilesHelper::Create(directoryArray, true /* aRecursiveFlag */, error);
  nsresult rv = error.StealNSResult();
  NS_ENSURE_SUCCESS(rv, Err(rv));

  auto gfhPromise = MakeRefPtr<GetFilesHelper::MozPromiseType>(__func__);
  helper->AddMozPromise(gfhPromise,
                        xpc::NativeGlobal(xpc::PrivilegedJunkScope()));

  // Use MozPromise chaining (the undocumented feature where returning a
  // MozPromise from handlers chains to that new promise).  The chained
  // promise is the RequestsPromise that will resolve to requests for each
  // file in the folder.
  RefPtr<RequestsPromise> requestPromise = gfhPromise->Then(
      GetMainThreadSerialEventTarget(), "make ca file requests",
      [request = RefPtr{aRequest}](
          const nsTArray<RefPtr<mozilla::dom::File>>& aFiles) {
        ContentAnalysisRequestArray requests(aFiles.Length());
        for (const auto& file : aFiles) {
          auto requestOrError = MakeRequestForFileInFolder(file, request);
          if (requestOrError.isErr()) {
            return RequestsPromise::CreateAndReject(requestOrError.unwrapErr(),
                                                    __func__);
          }
          requests.AppendElement(requestOrError.unwrap());
        }
        return RequestsPromise::CreateAndResolve(requests, __func__);
      },
      [](nsresult rv) {
        return RequestsPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
      });

  return requestPromise;
}

// Asynchronously expand/filter requests based on policies that bypass
// the agent.  This includes replacing folder requests with requests to scan
// their contents (files), etc.  Returns either promises for all remaining
// requests (provided and synthetic) or a ContentAnalysisResult if no
// requests need to be run.
Result<RefPtr<ContentAnalysis::RequestsPromise::AllPromiseType>,
       RefPtr<nsIContentAnalysisResult>>
ContentAnalysis::GetFinalRequestList(
    const ContentAnalysisRequestArray& aRequests) {
  Maybe<NoContentAnalysisResult> allowResult;

  // We keep allowResult just in case all requests end up getting filtered.
  // It gives us an explanation for that.  If any requests survive this
  // function then allowResult isn't returned.  Negative results should
  // be returned early.  They should not set allowResult.
  auto setAllowResult = [&allowResult](NoContentAnalysisResult aVal) {
    DebugOnly checkResult = [aVal]() {
      return MakeRefPtr<ContentAnalysisNoResult>(aVal)->GetShouldAllowContent();
    };
    // shouldAllowContent must be true.
    MOZ_ASSERT(checkResult.value());

    if (!allowResult) {
      allowResult = Some(aVal);
      return;
    }
    if (*allowResult == NoContentAnalysisResult::
                            ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS) {
      // Allow aVal to override the prior allow result.
      allowResult = Some(aVal);
    }
  };

  // Expand the DataTransfer and Transferable requests into requests for
  // their individual contents.  Also filter out the requests that don't
  // need to be run.
  ContentAnalysisRequestArray expandedTransferRequests(aRequests.Length());
  for (const auto& request : aRequests) {
    // Check request's reason to see if prefs always permit this operation.
    nsIContentAnalysisRequest::Reason reason;
    MOZ_ALWAYS_SUCCEEDS(request->GetReason(&reason));
    if (!ShouldCheckReason(reason)) {
      LOGD("Allowing request -- operations of this type are always permitted.");
      setAllowResult(NoContentAnalysisResult::
                         ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS);
      continue;
    }

    // Content analysis is only needed if an outside webpage has access to
    // the data. So, skip content analysis if there is:
    //  - the window is a chrome docshell
    //  - the window is being rendered in the parent process (for example,
    //  about:support and the like)
    RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
    request->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
    nsCOMPtr<nsIURI> uri;
    request->GetUrl(getter_AddRefs(uri));
    // NOTE: We only consider uri here (when windowGlobal isn't specified)
    // for current tests to work.  gtests specify URI but no window.
    // We should never "really" hit that condition.
    if ((!windowGlobal && !uri) ||
        (windowGlobal && (windowGlobal->GetBrowsingContext()->IsChrome() ||
                          windowGlobal->IsInProcess()))) {
      LOGD("Allowing request -- window was null or chrome or in-process.");
      setAllowResult(NoContentAnalysisResult::
                         ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS);
      continue;
    }

    // Maybe skip check if source of operation is same tab.
    if (mozilla::StaticPrefs::
            browser_contentanalysis_bypass_for_same_tab_operations() &&
        SourceIsSameTab(request)) {
      // ALLOW_DUE_TO_SAME_TAB_SOURCE may replace a result of
      // ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS from an earlier
      // request.
      LOGD(
          "Allowing request -- same tab operations are always permitted by "
          "pref.");
      setAllowResult(NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE);
      continue;
    }

    // Check if the context is privileged.
    if (!uri) {
      // If no URL is given then use the one for the window.
      uri = ContentAnalysis::GetURIForBrowsingContext(
          windowGlobal->Canonical()->GetBrowsingContext());
      if (!uri) {
        // if we still have no URL then the request is from a privileged window
        LOGD("Allowing request -- priviledged window.");
        setAllowResult(NoContentAnalysisResult::
                           ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS);
        continue;
      }
    }

    // Check URLs of requested info against
    // browser.contentanalysis.allow_url_regex_list/deny_url_regex_list.
    // Build the list once since creating regexs is slow.
    // Requests with URLs that match the allow list are removed from the check.
    // There is only one URL in all cases except downloads.  If all contents
    // are removed or the page URL is allowed (for downloads) then the
    // operation is allowed.
    // Requests with URLs that match the deny list block the entire operation.
    auto filterResult = FilterByUrlLists(request, uri);
    if (filterResult == ContentAnalysis::UrlFilterResult::eDeny) {
      LOGD("Blocking request due to deny URL filter.");
      glean::content_analysis::request_blocked_by_deny_url.Add();
      return Err(MakeRefPtr<ContentAnalysisActionResult>(
          nsIContentAnalysisResponse::Action::eBlock));
    }
    if (filterResult == ContentAnalysis::UrlFilterResult::eAllow) {
      LOGD("Allowing request -- all operations match allow URL filter.");
      glean::content_analysis::request_allowed_by_allow_url.Add();
      setAllowResult(NoContentAnalysisResult::
                         ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS);
      continue;
    }

    RefPtr<dom::WindowGlobalParent> sourceWindowGlobal;
    request->GetSourceWindowGlobal(getter_AddRefs(sourceWindowGlobal));

    Result<bool, nsresult> hadTransferOrError =
        AddRequestsFromTransferableIfAny(request, uri, windowGlobal,
                                         sourceWindowGlobal,
                                         &expandedTransferRequests);
    if (hadTransferOrError.isOk() && !hadTransferOrError.unwrap()) {
      // Request didn't have a Transferable with contents.  Check for a
      // DataTransfer.
      hadTransferOrError = AddRequestsFromDataTransferIfAny(
          request, uri, windowGlobal, sourceWindowGlobal,
          &expandedTransferRequests);
      if (hadTransferOrError.isOk() && !hadTransferOrError.unwrap()) {
        // Request didn't have a Transferable or DataTransfer with contents.
        // Copy it as-is.
        expandedTransferRequests.AppendElement(request);
      }
    }
    if (hadTransferOrError.isErr()) {
      LOGD(
          "Denying request -- error expanding nsITransferable or "
          "DataTransfer.");
      return RequestsPromise::AllPromiseType::CreateAndReject(
          hadTransferOrError.unwrapErr(), __func__);
    }
  }

  // We have expanded all Transferable and DataTransfer requests.  We now
  // look for folder requests to expand.
  ContentAnalysisRequestArray nonFolderRequests;
  nsTArray<RefPtr<RequestsPromise>> promises;
  for (auto& request : expandedTransferRequests) {
    // Always add request to nonFolderRequests unless we process a folder for
    // it. Note that the scope for this MakeScopeExit is the for loop, not the
    // function.
    auto copyRequest =
        MakeScopeExit([&]() { nonFolderRequests.AppendElement(request); });
    nsAutoString filename;
    nsresult rv = request->GetFilePath(filename);
    NS_ENSURE_SUCCESS(
        rv, RequestsPromise::AllPromiseType::CreateAndReject(rv, __func__));
    if (filename.IsEmpty()) {
      // Not a file so just copy the request to nonFolderRequests.
      continue;
    }

#ifdef DEBUG
    // Confirm that there is no text content to analyze.  See comment on
    // mFilePath.
    nsAutoString textContent;
    rv = request->GetTextContent(textContent);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    MOZ_ASSERT(textContent.IsEmpty());
#endif

    RefPtr<nsIFile> file;
    rv = NS_NewLocalFile(filename, getter_AddRefs(file));
    NS_ENSURE_SUCCESS(
        rv, RequestsPromise::AllPromiseType::CreateAndReject(rv, __func__));

    bool exists;
    rv = file->Exists(&exists);
    NS_ENSURE_SUCCESS(
        rv, RequestsPromise::AllPromiseType::CreateAndReject(rv, __func__));
    if (!exists) {
      continue;
    }

    bool isDir;
    rv = file->IsDirectory(&isDir);
    NS_ENSURE_SUCCESS(
        rv, RequestsPromise::AllPromiseType::CreateAndReject(rv, __func__));
    if (!isDir) {
      continue;
    }

    // Don't copy the folder request.
    copyRequest.release();

    LOGD("GetFinalRequestList expanding folder: %s",
         NS_ConvertUTF16toUTF8(filename.get()).get());
    Result<RefPtr<RequestsPromise>, nsresult> requestPromiseOrError =
        ExpandFolderRequest(request, file);
    if (requestPromiseOrError.isErr()) {
      LOGD("Denying request -- error expanding folder.");
      return RequestsPromise::AllPromiseType::CreateAndReject(
          requestPromiseOrError.unwrapErr(), __func__);
    }
    promises.AppendElement(requestPromiseOrError.unwrap());
  }

  // We have expanded all requests to check folders, Transferables and
  // DataTransfers.
  if (!nonFolderRequests.IsEmpty()) {
    promises.AppendElement(RequestsPromise::CreateAndResolve(
        std::move(nonFolderRequests), "non folder requests"));
  }

  if (promises.IsEmpty()) {
    if (allowResult) {
      LOGD(
          "Allowing request -- all requests were permitted early.  "
          "NoContentAnalysisResult = %d",
          (int)*allowResult);
      return Err(MakeRefPtr<ContentAnalysisNoResult>(*allowResult));
    }

    // This can happen e.g. if the requests were for empty folders, etc.
    LOGD("Allowing request -- no requests need to be checked.");
    return Err(MakeRefPtr<ContentAnalysisNoResult>(
        NoContentAnalysisResult::
            ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS));
  }

  // If there were any requests then ignore any allowResult because we still
  // have to do the remaining checks.
  return RequestsPromise::All(GetMainThreadSerialEventTarget(), promises);
}

NS_IMETHODIMP
ContentAnalysis::AnalyzeContentRequests(
    const nsTArray<RefPtr<nsIContentAnalysisRequest>>& aRequests,
    bool aAutoAcknowledge, JSContext* aCx, mozilla::dom::Promise** aPromise) {
  RefPtr<mozilla::dom::Promise> promise;
  nsresult rv = MakePromise(aCx, getter_AddRefs(promise));
  NS_ENSURE_SUCCESS(rv, rv);
  RefPtr<ContentAnalysisCallback> callback =
      new ContentAnalysisCallback(promise);
  promise.forget(aPromise);
  return AnalyzeContentRequestsCallback(aRequests, aAutoAcknowledge, callback);
}

NS_IMETHODIMP
ContentAnalysis::AnalyzeContentRequestsCallback(
    const nsTArray<RefPtr<nsIContentAnalysisRequest>>& aRequests,
    bool aAutoAcknowledge, nsIContentAnalysisCallback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_ARG(aCallback);
  LOGD("ContentAnalysis::AnalyzeContentRequestsCallback received %zu requests",
       aRequests.Length());

  // Wrap callback in a ContentAnalysisCallback, which will assert if the
  // callback is not called exactly once.
  auto safeCallback = MakeRefPtr<ContentAnalysisCallback>(aCallback);

  // If any member of aRequests has a different user action ID than another,
  // throw an error.  If the user action IDs are empty, generate one and set
  // it for the requests.
  nsAutoCString userActionId;
  bool isSettingId = false;
  if (!aRequests.IsEmpty()) {
    MOZ_ALWAYS_SUCCEEDS(aRequests[0]->GetUserActionId(userActionId));
    if (userActionId.IsEmpty()) {
      userActionId = GenerateUUID();
      isSettingId = true;
    }
  }

  for (const auto& request : aRequests) {
    if (isSettingId) {
      MOZ_ALWAYS_SUCCEEDS(request->SetUserActionId(userActionId));
    } else {
      nsAutoCString givenUserActionId;
      MOZ_ALWAYS_SUCCEEDS(request->GetUserActionId(givenUserActionId));
      if (givenUserActionId != userActionId) {
        safeCallback->Error(NS_ERROR_INVALID_ARG);
        return NS_ERROR_INVALID_ARG;
      }
    }
  }
  mUserActionMap.InsertOrUpdate(
      userActionId, UserActionData{aCallback, {}, nullptr, aAutoAcknowledge});

  Result<RefPtr<RequestsPromise::AllPromiseType>,
         RefPtr<nsIContentAnalysisResult>>
      requestListResult = GetFinalRequestList(aRequests);
  if (requestListResult.isErr()) {
    auto result = requestListResult.unwrapErr();
    LOGD(
        "ContentAnalysis::AnalyzeContentRequestsCallback received early result "
        "before creating the final request list | shouldAllow = %s",
        result->GetShouldAllowContent() ? "yes" : "no");
    // On a negative result, create only one failure dialog.  For a positive
    // result, we don't bother since there is no visual indication needed.
    if (!result->GetShouldAllowContent()) {
      if (!aRequests.IsEmpty()) {
        ShowBlockedRequestDialog(aRequests[0]);
      } else {
        // No dialog could be shown since we have no window.
        LOGD("Got a negative response for an empty request?");
      }
    }
    safeCallback->ContentResult(result);
    mUserActionMap.Remove(userActionId);
    return NS_OK;
  }

  // We need to pass this object to the lambda below because we need to
  // guarantee that we can get this "real" object, not a mock, for
  // MultipartRequestCallback.
  WeakPtr<ContentAnalysis> weakThis = this;
  RefPtr<RequestsPromise::AllPromiseType> finalRequests =
      requestListResult.unwrap();
  finalRequests->Then(
      GetMainThreadSerialEventTarget(), "issue ca requests",
      [aAutoAcknowledge, safeCallback, weakThis,
       userActionId](nsTArray<ContentAnalysisRequestArray>&& aRequests) {
        // We already have weakThis but we also get the nsIContentAnalysis
        // object from the service, since we do want the mock service (if
        // any) for the call to AnalyzeContentRequestPrivate.
        // In non-test runs, they will always be the same object.
        nsCOMPtr<nsIContentAnalysis> contentAnalysis =
            mozilla::components::nsIContentAnalysis::Service();
        if (!contentAnalysis || !weakThis) {
          LOGD(
              "ContentAnalysis::AnalyzeContentRequestsCallback received "
              "response during shutdown | userActionId = %s",
              userActionId.get());
          safeCallback->Error(NS_ERROR_NOT_AVAILABLE);
          return;
        }
        RefPtr<MultipartRequestCallback> mpcb =
            MultipartRequestCallback::Create(weakThis, aRequests, safeCallback,
                                             aAutoAcknowledge);
        if (mpcb->HasResponded()) {
          // Already responded because the request has been canceled already
          // (or some other error)
          return;
        }

        for (const auto& requests : aRequests) {
          for (const auto& request : requests) {
            contentAnalysis->AnalyzeContentRequestPrivate(
                request, aAutoAcknowledge, mpcb);
          }
        }
      },
      [safeCallback, weakThis, userActionId](nsresult rv) {
        LOGD(
            "ContentAnalysis::AnalyzeContentRequestsCallback received error "
            "response: %s | userActionId = %s",
            SafeGetStaticErrorName(rv), userActionId.get());
        safeCallback->Error(rv);
        if (weakThis) {
          weakThis->mUserActionMap.Remove(userActionId);
        }
      });
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysis::AnalyzeContentRequestPrivate(
    nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
    nsIContentAnalysisCallback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());

  // We check this here so that async calls to this method (e.g. via a promise
  // resolve) don't send requests after being told not to.
  if (mForbidFutureRequests) {
    nsCString requestToken;
    nsresult rv = aRequest->GetRequestToken(requestToken);
    NS_ENSURE_SUCCESS(rv, rv);
    LOGD(
        "ContentAnalysis received request [%p](%s) "
        "after forbidding future requests.  Request is rejected.",
        aRequest, requestToken.get());
    aCallback->Error(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
    return NS_OK;
  }

  LOGD(
      "ContentAnalysis::AnalyzeContentRequestPrivate analyzing request [%p] "
      "with callback [%p]",
      aRequest, aCallback);
  auto se = MakeScopeExit([&]() {
    LOGE("AnalyzeContentRequestPrivate failed");
    aCallback->Error(NS_ERROR_FAILURE);
  });

  // Make sure we send the notification first, so if we later return
  // an error the JS will handle it correctly.
  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (obsServ) {
    obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
  }

  bool isActive;
  nsresult rv = GetIsActive(&isActive);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isActive) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  ++mRequestCount;
  se.release();

  // since we're on the main thread, don't need to synchronize this
  return RunAnalyzeRequestTask(aRequest, aAutoAcknowledge, aCallback);
}

NS_IMETHODIMP
ContentAnalysis::CancelAllRequestsAssociatedWithUserAction(
    const nsACString& aUserActionId) {
  MOZ_ASSERT(NS_IsMainThread());
  // Find the compound action containing aUserActionId, if any.
  RefPtr<const UserActionSet> compoundUserAction;
  for (auto iter = mCompoundUserActions.iter(); !iter.done(); iter.next()) {
    auto& entry = iter.get();
    if (entry->has(nsCString(aUserActionId))) {
      compoundUserAction = entry;
      break;
    }
  }

  if (!compoundUserAction) {
    // It was not a compound request, just a single one.
    return CancelRequestsByUserAction(aUserActionId);
  }
  MOZ_ASSERT(!compoundUserAction->empty());

  // NB: We don't filter out completed user actions from the compound list
  // since we may need to look them up for this function later.  So we may
  // end up canceling requests that are already completed here -- that is a
  // no-op.
  LOGD("Cancelling %u requests associated with user action ID: %s",
       compoundUserAction->count(), PromiseFlatCString(aUserActionId).get());
  nsresult rv = NS_OK;
  for (auto iter = compoundUserAction->iter(); !iter.done(); iter.next()) {
    nsresult rv2 = CancelRequestsByUserAction(iter.get());
    if (NS_FAILED(rv2)) {
      rv = rv2;
    }
    // If we find a user action ID for a request that is not yet complete then
    // canceling it will cancel and remove the entire compound action.  In that
    // case, we are done.
    if (!mCompoundUserActions.has(compoundUserAction)) {
      break;
    }
  }

  LOGD(
      "Cancelling compound request associated with user action ID: %s %s | "
      "Error code: %s",
      PromiseFlatCString(aUserActionId).get(),
      (!mCompoundUserActions.has(compoundUserAction)) ? "succeeded" : "failed",
      SafeGetStaticErrorName(rv));
  return rv;
}

NS_IMETHODIMP
ContentAnalysis::CancelRequestsByUserAction(const nsACString& aUserActionId) {
  MOZ_ASSERT(NS_IsMainThread());
  CancelWithError(nsCString(aUserActionId), NS_ERROR_ABORT);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::CancelAllRequests(bool aForbidFutureRequests) {
  MOZ_ASSERT(NS_IsMainThread());
  LOGD(
      "CancelAllRequests running | aForbidFutureRequests: %s | number of "
      "outstanding UserActions: %u",
      aForbidFutureRequests ? "yes" : "no", mUserActionMap.Count());
  MOZ_ASSERT(!mForbidFutureRequests);
  mForbidFutureRequests = mForbidFutureRequests | aForbidFutureRequests;

  // Keys() iterates in-place and we will change the map so we need a copy.
  for (const auto& userActionId :
       mozilla::ToTArray<nsTArray<nsCString>>(mUserActionMap.Keys())) {
    CancelRequestsByUserAction(userActionId);
  }

  // Again, Keys() iterates in-place and we change the map so we need a copy.
  for (const auto& requestToken :
       mozilla::ToTArray<nsTArray<nsCString>>(mWarnResponseDataMap.Keys())) {
    LOGD(
        "Responding to warn dialog (from CancelAllRequests) for "
        "request %s",
        requestToken.get());
    RespondToWarnDialog(requestToken, false);
  }
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken,
                                     bool aAllowContent) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCString token(aRequestToken);
  LOGD("Content analysis getting warn response %d for request %s",
       aAllowContent ? 1 : 0, token.get());
  auto entry = mWarnResponseDataMap.Extract(token);
  if (!entry) {
    LOGD(
        "Content analysis request not found when trying to send warn "
        "response for request %s",
        token.get());
    return NS_OK;
  }

  entry->mResponse->ResolveWarnAction(aAllowContent);
  if (entry->mWasTimeout) {
    LOGD(
        "Warn response was for a previous timeout, inserting into "
        "mUserActionIdToCanceledResponseMap for "
        "userActionId %s",
        entry->mUserActionId.get());
    size_t count = 1;
    auto userActionIdToCanceledResponseMap =
        mUserActionIdToCanceledResponseMap.Lock();
    if (auto maybeData =
            userActionIdToCanceledResponseMap->Lookup(entry->mUserActionId)) {
      count += maybeData->mNumExpectedResponses;
    }

    userActionIdToCanceledResponseMap->InsertOrUpdate(
        entry->mUserActionId,
        CanceledResponse{ConvertResult(entry->mResponse->GetAction()), count});
  }
  bool haveGottenResponse;
  {
    auto map = mRequestTokenToBasicRequestInfoMap.Lock();
    haveGottenResponse = !map->Contains(aRequestToken);
  }

  // Don't acknowledge if we haven't gotten a response from the agent yet
  IssueResponse(entry->mResponse, nsCString(entry->mUserActionId),
                entry->mAutoAcknowledge && haveGottenResponse,
                entry->mWasTimeout);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::ShowBlockedRequestDialog(nsIContentAnalysisRequest* aRequest) {
  RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
  MOZ_ALWAYS_SUCCEEDS(
      aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)));
  if (!windowGlobal) {
    // Privileged context or gtest.  Either way we show no dialog.
    return NS_OK;
  }

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  if (!obsServ) {
    // We must be shutting down, so we can't show a blocked request dialog.
    return NS_OK;
  }

  nsCString token;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetRequestToken(token));
  if (token.IsEmpty()) {
    token = GenerateUUID();
    aRequest->SetRequestToken(token);
  }

  nsCString userActionId;
  MOZ_ALWAYS_SUCCEEDS(aRequest->GetUserActionId(userActionId));
  if (userActionId.IsEmpty()) {
    userActionId = GenerateUUID();
    aRequest->SetUserActionId(userActionId);
  }

  obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
  auto response = MakeRefPtr<ContentAnalysisResponse>(
      nsIContentAnalysisResponse::Action::eBlock, std::move(token),
      std::move(userActionId));
  response->SetOwner(this);
  obsServ->NotifyObservers(static_cast<nsIContentAnalysisResponse*>(response),
                           "dlp-response", nullptr);
  return NS_OK;
}

#if defined(XP_WIN)
RefPtr<ContentAnalysis::PrintAllowedPromise>
ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed(
    dom::CanonicalBrowsingContext* aBrowsingContext,
    nsIPrintSettings* aPrintSettings) {
  if (!mozilla::StaticPrefs::
          browser_contentanalysis_interception_point_print_enabled()) {
    return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true),
                                                 __func__);
  }
  // Note that the IsChrome() check here excludes a few
  // common about pages like about:config, about:preferences,
  // and about:support, but other about: pages may still
  // go through content analysis.
  if (aBrowsingContext->IsChrome()) {
    return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true),
                                                 __func__);
  }
  nsCOMPtr<nsIPrintSettings> contentAnalysisPrintSettings;
  if (NS_WARN_IF(NS_FAILED(aPrintSettings->Clone(
          getter_AddRefs(contentAnalysisPrintSettings)))) ||
      NS_WARN_IF(!aBrowsingContext->GetCurrentWindowGlobal())) {
    return PrintAllowedPromise::CreateAndReject(
        PrintAllowedError(NS_ERROR_FAILURE), __func__);
  }
  contentAnalysisPrintSettings->SetOutputDestination(
      nsIPrintSettings::OutputDestinationType::kOutputDestinationStream);
  contentAnalysisPrintSettings->SetOutputFormat(
      nsIPrintSettings::kOutputFormatPDF);
  nsCOMPtr<nsIStorageStream> storageStream =
      do_CreateInstance("@mozilla.org/storagestream;1");
  if (!storageStream) {
    return PrintAllowedPromise::CreateAndReject(
        PrintAllowedError(NS_ERROR_FAILURE), __func__);
  }
  // Use segment size of 512K
  nsresult rv = storageStream->Init(0x80000, UINT32_MAX);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return PrintAllowedPromise::CreateAndReject(PrintAllowedError(rv),
                                                __func__);
  }

  nsCOMPtr<nsIOutputStream> outputStream;
  storageStream->QueryInterface(NS_GET_IID(nsIOutputStream),
                                getter_AddRefs(outputStream));
  MOZ_ASSERT(outputStream);

  contentAnalysisPrintSettings->SetOutputStream(outputStream.get());
  RefPtr<dom::CanonicalBrowsingContext> browsingContext = aBrowsingContext;
  auto promise = MakeRefPtr<PrintAllowedPromise::Private>(__func__);
  nsCOMPtr<nsIPrintSettings> finalPrintSettings(aPrintSettings);
  aBrowsingContext
      ->PrintWithNoContentAnalysis(contentAnalysisPrintSettings, true, nullptr)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [browsingContext, contentAnalysisPrintSettings, finalPrintSettings,
           promise](
              dom::MaybeDiscardedBrowsingContext cachedStaticBrowsingContext)
              MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
                nsCOMPtr<nsIOutputStream> outputStream;
                contentAnalysisPrintSettings->GetOutputStream(
                    getter_AddRefs(outputStream));
                nsCOMPtr<nsIStorageStream> storageStream =
                    do_QueryInterface(outputStream);
                MOZ_ASSERT(storageStream);
                nsTArray<uint8_t> printData;
                uint32_t length = 0;
                storageStream->GetLength(&length);
                if (!printData.SetLength(length, fallible)) {
                  promise->Reject(
                      PrintAllowedError(NS_ERROR_OUT_OF_MEMORY,
                                        cachedStaticBrowsingContext),
                      __func__);
                  return;
                }
                nsCOMPtr<nsIInputStream> inputStream;
                nsresult rv = storageStream->NewInputStream(
                    0, getter_AddRefs(inputStream));
                if (NS_FAILED(rv)) {
                  promise->Reject(
                      PrintAllowedError(rv, cachedStaticBrowsingContext),
                      __func__);
                  return;
                }
                uint32_t currentPosition = 0;
                while (currentPosition < length) {
                  uint32_t elementsRead = 0;
                  // Make sure the reinterpret_cast<> below is safe
                  static_assert(std::is_trivially_assignable_v<
                                decltype(*printData.Elements()), char>);
                  rv = inputStream->Read(
                      reinterpret_cast<char*>(printData.Elements()) +
                          currentPosition,
                      length - currentPosition, &elementsRead);
                  if (NS_WARN_IF(NS_FAILED(rv) || !elementsRead)) {
                    promise->Reject(
                        PrintAllowedError(NS_FAILED(rv) ? rv : NS_ERROR_FAILURE,
                                          cachedStaticBrowsingContext),
                        __func__);
                    return;
                  }
                  currentPosition += elementsRead;
                }

                nsString printerName;
                rv = contentAnalysisPrintSettings->GetPrinterName(printerName);
                if (NS_WARN_IF(NS_FAILED(rv))) {
                  promise->Reject(
                      PrintAllowedError(rv, cachedStaticBrowsingContext),
                      __func__);
                  return;
                }

                auto* windowParent = browsingContext->GetCurrentWindowGlobal();
                if (!windowParent) {
                  // The print window may have been closed by the user by now.
                  // Cancel the print.
                  promise->Reject(
                      PrintAllowedError(NS_ERROR_ABORT,
                                        cachedStaticBrowsingContext),
                      __func__);
                  return;
                }
                nsCOMPtr<nsIURI> uri = GetURIForBrowsingContext(
                    windowParent->Canonical()->GetBrowsingContext());
                if (!uri) {
                  promise->Reject(
                      PrintAllowedError(NS_ERROR_FAILURE,
                                        cachedStaticBrowsingContext),
                      __func__);
                  return;
                }
                // It's a little unclear what we should pass to the agent if
                // print.always_print_silent is true, because in that case we
                // don't show the print preview dialog or the system print
                // dialog.
                //
                // I'm thinking of the print preview dialog case as the "normal"
                // one, so to me printing without a dialog is closer to the
                // system print dialog case.
                bool isFromPrintPreviewDialog =
                    !Preferences::GetBool("print.prefer_system_dialog") &&
                    !Preferences::GetBool("print.always_print_silent");
                RefPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
                    new contentanalysis::ContentAnalysisRequest(
                        std::move(printData), std::move(uri),
                        std::move(printerName),
                        isFromPrintPreviewDialog
                            ? nsIContentAnalysisRequest::Reason::
                                  ePrintPreviewPrint
                            : nsIContentAnalysisRequest::Reason::
                                  eSystemDialogPrint,
                        windowParent);
                auto callback =
                    MakeRefPtr<contentanalysis::ContentAnalysisCallback>(
                        [browsingContext, cachedStaticBrowsingContext, promise,
                         finalPrintSettings = std::move(finalPrintSettings)](
                            nsIContentAnalysisResult* aResult)
                            MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
                              promise->Resolve(
                                  PrintAllowedResult(
                                      aResult->GetShouldAllowContent(),
                                      cachedStaticBrowsingContext),
                                  __func__);
                            },
                        [promise,
                         cachedStaticBrowsingContext](nsresult aError) {
                          promise->Reject(
                              PrintAllowedError(aError,
                                                cachedStaticBrowsingContext),
                              __func__);
                        });
                nsCOMPtr<nsIContentAnalysis> contentAnalysis =
                    mozilla::components::nsIContentAnalysis::Service();
                if (NS_WARN_IF(!contentAnalysis)) {
                  promise->Reject(
                      PrintAllowedError(rv, cachedStaticBrowsingContext),
                      __func__);
                } else {
                  bool isActive = false;
                  nsresult rv = contentAnalysis->GetIsActive(&isActive);
                  // Should not be called if content analysis is not active
                  MOZ_ASSERT(isActive);
                  (void)NS_WARN_IF(NS_FAILED(rv));
                  AutoTArray<RefPtr<nsIContentAnalysisRequest>, 1> requests{
                      contentAnalysisRequest};
                  rv = contentAnalysis->AnalyzeContentRequestsCallback(
                      requests, /* aAutoAcknowledge */ true, callback);
                  if (NS_WARN_IF(NS_FAILED(rv))) {
                    promise->Reject(
                        PrintAllowedError(rv, cachedStaticBrowsingContext),
                        __func__);
                  }
                }
              },
          [promise](nsresult aError) {
            promise->Reject(PrintAllowedError(aError), __func__);
          });
  return promise;
}
#endif

static nsresult CheckClipboard(
    ContentAnalysisCallback* aCallback, Maybe<int32_t> aClipboardSequenceNumber,
    bool aStoreInCache, nsITransferable* aTransferable,
    mozilla::dom::WindowGlobalParent* aWindowGlobal,
    mozilla::dom::WindowGlobalParent* aSourceWindowGlobal) {
  NoContentAnalysisResult caResult =
      NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR;
  auto respondOnFailure = MakeScopeExit([&]() {
    LOGD("CheckClipboard skipping CA.  Response = %d", (int)caResult);
    RefPtr result = MakeRefPtr<ContentAnalysisNoResult>(caResult);
    aCallback->ContentResult(result);
  });

  nsCOMPtr<nsIContentAnalysis> contentAnalysis =
      mozilla::components::nsIContentAnalysis::Service();
  if (!contentAnalysis) {
    caResult = NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR;
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCOMPtr<nsIURI> uri =
      aWindowGlobal ? ContentAnalysis::GetURIForBrowsingContext(
                          aWindowGlobal->Canonical()->GetBrowsingContext())
                    : nullptr;

  auto request = MakeRefPtr<ContentAnalysisRequest>(
      nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
      nsIContentAnalysisRequest::Reason::eClipboardPaste, aTransferable,
      aWindowGlobal, aSourceWindowGlobal);

  // Don't use the cache if the request can store to the cache -- that
  // is an indication that this is a separate operation from the previous
  // one.
  if (!aStoreInCache && aClipboardSequenceNumber.isSome()) {
    bool isValid = false;
    nsIContentAnalysisResponse::Action action =
        nsIContentAnalysisResponse::Action::eUnspecified;
    contentAnalysis->GetCachedResponse(uri, *aClipboardSequenceNumber, &action,
                                       &isValid);
    if (isValid) {
      LOGD("Content analysis returning cached clipboard response %d", action);
      respondOnFailure.release();
      RefPtr actionResult = MakeRefPtr<ContentAnalysisActionResult>(action);
      if (!actionResult->GetShouldAllowContent()) {
        contentAnalysis->ShowBlockedRequestDialog(request);
      }
      aCallback->ContentResult(actionResult);
      return NS_OK;
    }
  }

  RefPtr wrapperCallback = aCallback;
  if (aStoreInCache && aClipboardSequenceNumber.isSome()) {
    // Add the result to the result cache before we call the caller's callback.
    wrapperCallback = MakeRefPtr<ContentAnalysisCallback>(
        [aClipboardSequenceNumber, uri,
         callback = RefPtr(aCallback)](nsIContentAnalysisResult* aResult) {
          bool allow = aResult->GetShouldAllowContent();
          nsCOMPtr<nsIContentAnalysis> contentAnalysis =
              mozilla::components::nsIContentAnalysis::Service();
          if (contentAnalysis) {
            LOGD("Content analysis setting cached clipboard response: %s",
                 allow ? "allow" : "block");
            contentAnalysis->SetCachedResponse(
                uri, *aClipboardSequenceNumber,
                allow ? nsIContentAnalysisResponse::Action::eAllow
                      : nsIContentAnalysisResponse::Action::eBlock);
          }

          callback->ContentResult(aResult);
        },
        [callback = RefPtr(aCallback)](nsresult rv) { callback->Error(rv); });
  }

  respondOnFailure.release();

  AutoTArray<RefPtr<nsIContentAnalysisRequest>, 1> requests{request};
  return contentAnalysis->AnalyzeContentRequestsCallback(
      requests, true /* autoAcknowledge */, wrapperCallback);
}

// This method must stay in sync with ContentAnalysis::kKnownClipboardTypes. All
// of those types must be analyzed here, and if we start analyzing more types
// here we should add it to ContentAnalysis::kKnownClipboardTypes.
void ContentAnalysis::CheckClipboardContentAnalysis(
    nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
    nsITransferable* aTransferable, nsIClipboard::ClipboardType aClipboardType,
    ContentAnalysisCallback* aResolver, bool aForFullClipboard) {
  // Make sure we call aResolver on error.  Use the current value of
  // noCAResult.
  NoContentAnalysisResult noCAResult =
      NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR;
  auto issueNoAnalysisResponse = MakeScopeExit([&]() {
    LOGD("CheckClipboardContentAnalysis skipping CA.  Response = %d",
         (int)noCAResult);
    auto result = MakeRefPtr<ContentAnalysisNoResult>(noCAResult);
    aResolver->ContentResult(result);
  });

  nsCOMPtr<nsIContentAnalysis> contentAnalysis =
      mozilla::components::nsIContentAnalysis::Service();
  if (!contentAnalysis) {
    noCAResult = NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR;
    return;
  }

  bool contentAnalysisIsActive;
  nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
  if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) {
    noCAResult =
        NoContentAnalysisResult::ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE;
    return;
  }

  mozilla::Maybe<uint64_t> cacheInnerWindowId =
      aClipboard->GetClipboardCacheInnerWindowId(aClipboardType);
  RefPtr<mozilla::dom::WindowGlobalParent> sourceWindowGlobal;
  if (cacheInnerWindowId.isSome()) {
    sourceWindowGlobal = mozilla::dom::WindowGlobalParent::GetByInnerWindowId(
        *cacheInnerWindowId);
  }

  Maybe<int32_t> maybeSequenceNumber =
      aClipboard->GetNativeClipboardSequenceNumber(aClipboardType)
          .map<decltype(Some<int>)>(Some)
          .unwrapOr(Nothing());

  CheckClipboard(aResolver, maybeSequenceNumber, aForFullClipboard,
                 aTransferable, aWindow, sourceWindowGlobal);

  issueNoAnalysisResponse.release();
}

bool ContentAnalysis::CheckClipboardContentAnalysisSync(
    nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
    const nsCOMPtr<nsITransferable>& trans,
    nsIClipboard::ClipboardType aClipboardType) {
  bool requestDone = false;
  bool result;
  auto callback = MakeRefPtr<ContentAnalysisCallback>(
      [&requestDone, &result](nsIContentAnalysisResult* aResult) {
        result = aResult->GetShouldAllowContent();
        requestDone = true;
      });
  CheckClipboardContentAnalysis(aClipboard, aWindow, trans, aClipboardType,
                                callback);
  mozilla::SpinEventLoopUntil("CheckClipboardContentAnalysisSync"_ns,
                              [&requestDone]() -> bool { return requestDone; });
  return result;
}

RefPtr<ContentAnalysis::FilesAllowedPromise>
ContentAnalysis::CheckUploadsInBatchMode(
    nsCOMArray<nsIFile>&& aFiles, bool aAutoAcknowledge,
    mozilla::dom::WindowGlobalParent* aWindow,
    nsIContentAnalysisRequest::Reason aReason, nsIURI* aURI /* = nullptr */) {
  nsresult rv;
  auto contentAnalysis = GetContentAnalysisFromService();
  // Ideally the caller would check all of this before going through the work
  // of building up aFiles, but we'll double-check here.
  if (NS_WARN_IF(!contentAnalysis)) {
    return FilesAllowedPromise::CreateAndReject(rv, __func__);
  }
  bool contentAnalysisIsActive = false;
  rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return FilesAllowedPromise::CreateAndReject(rv, __func__);
  }
  if (!contentAnalysisIsActive) {
    return FilesAllowedPromise::CreateAndResolve(std::move(aFiles), __func__);
  }

  auto numberOfRequestsLeft = std::make_shared<size_t>(aFiles.Length());
  auto allowedFiles = MakeRefPtr<media::Refcountable<nsCOMArray<nsIFile>>>();
  auto userActionIds =
      MakeRefPtr<media::Refcountable<mozilla::HashSet<nsCString>>>();
  auto promise = MakeRefPtr<FilesAllowedPromise::Private>(__func__);
  nsCOMPtr<nsIURI> uri;
  if (aWindow) {
    uri = aWindow->GetDocumentURI();
    // Clients should only pass aURI if they're not passing aWindow.
    MOZ_ASSERT(!aURI);
  } else {
    // Should only be used in tests
    uri = aURI;
  }

  if (!contentAnalysis->mCompoundUserActions.put(userActionIds)) {
    return FilesAllowedPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY,
                                                __func__);
  }

  auto cancelOnError = MakeScopeExit([&]() {
    // Cancel one request to cancel the compound request.
    if (!userActionIds->empty()) {
      contentAnalysis->CancelRequestsByUserAction(userActionIds->iter().get());
    }
  });

  for (auto* file : aFiles) {
#ifdef XP_WIN
    nsString pathString(file->NativePath());
#else
    nsString pathString = NS_ConvertUTF8toUTF16(file->NativePath());
#endif
    RefPtr<nsIContentAnalysisRequest> request =
        new mozilla::contentanalysis::ContentAnalysisRequest(
            nsIContentAnalysisRequest::AnalysisType::eFileAttached, aReason,
            pathString, true /* aStringIsFilePath */, EmptyCString(), uri,
            nsIContentAnalysisRequest::OperationType::eUpload, aWindow);
    nsCString userActionId = GenerateUUID();
    MOZ_ALWAYS_SUCCEEDS(request->SetUserActionId(userActionId));
    if (!userActionIds->put(userActionId)) {
      return FilesAllowedPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY,
                                                  __func__);
    }

    // For requests with the same userActionId, we multiply the timeout by the
    // number of requests to make sure the agent has enough time to handle all
    // of them. However, in this case we're using separate userActionIds for
    // each of these files to get the batch mode behavior, so set a timeout
    // multiplier to get the correct timeout.
    //
    // Note that this could theoretically be wrong, because if one of these
    // files is actually a folder this could expand into many more requests, and
    // using aFiles.Count() will undercount the total number of requests. But in
    // practice, from the Windows file dialog users can only select multiple
    // individual files that are not folders, or one single folder.
    request->SetTimeoutMultiplier(static_cast<uint32_t>(aFiles.Count()));
    nsTArray<RefPtr<nsIContentAnalysisRequest>> singleRequest{
        std::move(request)};
    auto callback =
        mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>(
            // Note that this gets coerced to a std::function<>, which means it
            // has to be copyable, so everything captured here must be copyable,
            // which is why allowedFiles needs to be wrapped in a RefPtr and not
            // simply std::move()d.
            [promise, allowedFiles, numberOfRequestsLeft, file = RefPtr{file},
             userActionIds](nsIContentAnalysisResult* aResult) {
              // Since we're on the main thread, don't need to synchronize
              // access to allowedFiles or numberOfRequestsLeft
              AssertIsOnMainThread();
              nsCOMPtr<nsIContentAnalysisResponse> response =
                  do_QueryInterface(aResult);
              LOGD(
                  "Processing callback for batched file request, "
                  "numberOfRequestsLeft=%zu",
                  *(numberOfRequestsLeft.get()));
              RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
              if (response && response->GetAction() ==
                                  nsIContentAnalysisResponse::eCanceled) {
                // This was cancelled, so even if some other files have been
                // allowed we want to return an empty result.
                LOGD("Batched file request got cancel response");
                // Some of these may have finished already, but that's OK.
                // Remove the userActionIds array, then cancel its entries, so
                // that we only cancel them once.
                if (owner) {
                  if (auto entry =
                          owner->mCompoundUserActions.lookup(userActionIds)) {
                    owner->mCompoundUserActions.remove(entry);
                    for (auto iter = userActionIds->iter(); !iter.done();
                         iter.next()) {
                      owner->CancelRequestsByUserAction(iter.get());
                    }
                  }
                }
                nsCOMArray<nsIFile> emptyFiles;
                // Note that Resolve() will do nothing if the promise has
                // already been resolved.
                promise->Resolve(std::move(emptyFiles), __func__);
                return;
              }
              if (aResult->GetShouldAllowContent()) {
                allowedFiles->AppendElement(file);
              }
              (*numberOfRequestsLeft)--;
              if (*numberOfRequestsLeft == 0) {
                promise->Resolve(std::move(*allowedFiles), __func__);
                if (owner) {
                  owner->mCompoundUserActions.remove(userActionIds);
                }
              }
            },
            [promise, userActionIds](nsresult aError) {
              // cancel all requests
              AssertIsOnMainThread();
              LOGE("Batched file request got error %s",
                   SafeGetStaticErrorName(aError));
              RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
              // Some of these may have finished already, but that's OK.
              // Remove the userActionIds array, then cancel its entries, so
              // that we only cancel these once.
              if (owner) {
                if (auto entry =
                        owner->mCompoundUserActions.lookup(userActionIds)) {
                  owner->mCompoundUserActions.remove(entry);
                  for (auto iter = userActionIds->iter(); !iter.done();
                       iter.next()) {
                    owner->CancelRequestsByUserAction(iter.get());
                  }
                }
              }
              nsCOMArray<nsIFile> emptyFiles;
              // Note that Resolve() will do nothing if the promise has already
              // been resolved.
              promise->Resolve(std::move(emptyFiles), __func__);
            });
    contentAnalysis->AnalyzeContentRequestsCallback(singleRequest,
                                                    aAutoAcknowledge, callback);
  }

  cancelOnError.release();
  return promise;
}

NS_IMETHODIMP
ContentAnalysis::AnalyzeBatchContentRequest(nsIContentAnalysisRequest* aRequest,
                                            bool aAutoAcknowledge,
                                            JSContext* aCx,
                                            mozilla::dom::Promise** aPromise) {
  AssertIsOnMainThread();
  // Get the ContentAnalysis service again to make this work with
  // the mock service
  nsCOMPtr<nsIContentAnalysis> contentAnalysis =
      mozilla::components::nsIContentAnalysis::Service();
  if (!contentAnalysis) {
    return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
  }
  // Ideally the caller would check all of this before going through the work
  // of building up aFiles, but we'll double-check here.
  bool contentAnalysisIsActive = false;
  nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  // Should not be called if content analysis is not active
  MOZ_ASSERT(contentAnalysisIsActive);
  if (!contentAnalysisIsActive) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  nsCOMPtr<dom::DataTransfer> dataTransfer;
  rv = aRequest->GetDataTransfer(getter_AddRefs(dataTransfer));
  NS_ENSURE_SUCCESS(rv, rv);
  // This method expects dataTransfer to be present
  MOZ_ASSERT(dataTransfer);
  if (!dataTransfer) {
    return NS_ERROR_FAILURE;
  }
  nsCOMArray<nsIFile> files;
  auto& systemPrincipal = *nsContentUtils::GetSystemPrincipal();
  if (dataTransfer->HasFile()) {
    // Get any files in the DataTransfer and pass them to
    // CheckUploadsInBatchMode() so they will be analyzed individually.
    RefPtr fileList = dataTransfer->GetFiles(systemPrincipal);
    files.SetCapacity(fileList->Length());
    for (uint32_t i = 0; i < fileList->Length(); ++i) {
      dom::File* file = fileList->Item(i);
      if (!file) {
        continue;
      }
      nsString filePath;
      mozilla::ErrorResult result;
      file->GetMozFullPathInternal(filePath, result);
      if (NS_WARN_IF(result.Failed())) {
        rv = result.StealNSResult();
        return rv;
      }
#ifdef XP_WIN
      const nsString& nativePathString = filePath;
#else
      nsCString nativePathString(NS_ConvertUTF16toUTF8(std::move(filePath)));
#endif
      nsCOMPtr<nsIFile> nsFile;
      rv = NS_NewPathStringLocalFile(nativePathString, getter_AddRefs(nsFile));
      NS_ENSURE_SUCCESS(rv, rv);
      files.AppendElement(nsFile);
    }
  }
  RefPtr<mozilla::dom::Promise> filesPromise;
  rv = MakePromise(aCx, getter_AddRefs(filesPromise));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!files.IsEmpty()) {
    RefPtr<mozilla::dom::WindowGlobalParent> windowGlobal;
    MOZ_ALWAYS_SUCCEEDS(
        aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)));
    CheckUploadsInBatchMode(std::move(files), aAutoAcknowledge, windowGlobal,
                            nsIContentAnalysisRequest::Reason::eDragAndDrop)
        ->Then(
            mozilla::GetMainThreadSerialEventTarget(), __func__,
            [filesPromise,
             request = RefPtr{aRequest}](nsCOMArray<nsIFile> aAllowedFiles) {
              nsTArray<RefPtr<nsIFile>> allowedFiles;
              allowedFiles.AppendElements(mozilla::Span(
                  aAllowedFiles.Elements(), aAllowedFiles.Length()));
              filesPromise->MaybeResolve(std::move(allowedFiles));
            },
            [filesPromise](nsresult aError) {
              filesPromise->MaybeReject(aError);
            });
  } else {
    // Handle the case where there are files in fileList but
    // all of them are null.
    filesPromise->MaybeResolve(nsTArray<RefPtr<nsIFile>>());
  }

  RefPtr<dom::DataTransfer> transferWithoutFiles;
  if (dataTransfer->HasFile()) {
    rv = dataTransfer->Clone(
        dataTransfer->GetParentObject(), dataTransfer->GetEventMessage(),
        false /* aUserCancelled */, dataTransfer->IsCrossDomainSubFrameDrop(),
        getter_AddRefs(transferWithoutFiles));
    NS_ENSURE_SUCCESS(rv, rv);
    transferWithoutFiles->SetMode(dom::DataTransfer::Mode::ReadWrite);
    auto* items = transferWithoutFiles->Items();
    if (items->Length() > 0) {
      auto idx = items->Length();
      do {
        --idx;
        bool found;
        auto* item = items->IndexedGetter(idx, found);
        MOZ_ASSERT(found);
        if (item->Kind() == dom::DataTransferItem::KIND_FILE) {
          items->Remove(idx, systemPrincipal, IgnoreErrors());
        }
      } while (idx);
    }
  } else {
    // There were no files to begin with, so avoid cloning dataTransfer.
    transferWithoutFiles = dataTransfer;
  }
  AutoTArray<RefPtr<dom::Promise>, 2> promises{filesPromise};
  if (transferWithoutFiles->Items()->Length() > 0) {
    RefPtr<ContentAnalysisRequest> requestWithoutFiles =
        ContentAnalysisRequest::Clone(aRequest);
    MOZ_ALWAYS_SUCCEEDS(
        requestWithoutFiles->SetDataTransfer(transferWithoutFiles.get()));
    AutoTArray<RefPtr<nsIContentAnalysisRequest>, 1> singleRequestWithoutFiles{
        std::move(requestWithoutFiles)};

    RefPtr<mozilla::dom::Promise> nonFilesPromise;
    rv = contentAnalysis->AnalyzeContentRequests(
        singleRequestWithoutFiles, aAutoAcknowledge, aCx,
        getter_AddRefs(nonFilesPromise));
    NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
    promises.AppendElement(nonFilesPromise);
  }
  ErrorResult errorResult;
  RefPtr<dom::Promise> allPromise =
      dom::Promise::All(aCx, promises, errorResult);
  allPromise.forget(aPromise);
  return errorResult.StealNSResult();
}

NS_IMETHODIMP
ContentAnalysisResponse::Acknowledge(
    nsIContentAnalysisAcknowledgement* aAcknowledgement) {
  MOZ_ASSERT(mOwner);
  if (mHasAcknowledged) {
    MOZ_ASSERT(false, "Already acknowledged this ContentAnalysisResponse!");
    return NS_ERROR_FAILURE;
  }
  mHasAcknowledged = true;

  if (mDoNotAcknowledge) {
    return NS_OK;
  }
  return mOwner->RunAcknowledgeTask(aAcknowledgement, mRequestToken);
};

nsresult ContentAnalysis::RunAcknowledgeTask(
    nsIContentAnalysisAcknowledgement* aAcknowledgement,
    const nsACString& aRequestToken) {
  bool isActive;
  nsresult rv = GetIsActive(&isActive);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isActive) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  AssertIsOnMainThread();

  content_analysis::sdk::ContentAnalysisAcknowledgement pbAck;
  rv = ConvertToProtobuf(aAcknowledgement, aRequestToken, &pbAck);
  NS_ENSURE_SUCCESS(rv, rv);

  LOGD("Issuing ContentAnalysisAcknowledgement");
  LogAcknowledgement(&pbAck);

  nsCOMPtr<nsIObserverService> obsServ =
      mozilla::services::GetObserverService();
  // Do an early check here to avoid an extra dispatch to the main
  // thread if no one is observing the message
  bool rawMessageHasObserver = false;
  if (obsServ) {
    rawMessageHasObserver =
        obsServ->HasObservers("dlp-acknowledgement-sent-raw");
  }

  // The content analysis connection is synchronous so run in the background.
  LOGD("RunAcknowledgeTask dispatching acknowledge task");
  CallClientWithRetry<std::nullptr_t>(
      __func__,
      [pbAck = std::move(pbAck), rawMessageHasObserver](
          std::shared_ptr<content_analysis::sdk::Client> client) mutable
          -> Result<std::nullptr_t, nsresult> {
        MOZ_ASSERT(!NS_IsMainThread());
        RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
        if (!owner) {
          // May be shutting down
          return nullptr;
        }

        int err = client->Acknowledge(pbAck);
        LOGD(
            "RunAcknowledgeTask sent transaction acknowledgement, "
            "err=%d",
            err);
        // Wait until the acknowledgement is sent before sending
        // the dlp-acknowledgement-sent-raw notification to make tests
        // more reliable.
        if (rawMessageHasObserver) {
          NS_DispatchToMainThread(NS_NewRunnableFunction(
              __func__, [owner, pbAck = std::move(pbAck)]() {
                nsCOMPtr<nsIObserverService> obsServ =
                    mozilla::services::GetObserverService();
                if (!obsServ) {
                  // Shutting down.  We don't have a connection to the agent
                  // anymore so sending acknowledgement would fail anyway.
                  return;
                }
                std::string acknowledgementString = pbAck.SerializeAsString();
                nsTArray<char16_t> acknowledgementArray;
                acknowledgementArray.SetLength(acknowledgementString.size() +
                                               1);
                for (size_t i = 0; i < acknowledgementString.size(); ++i) {
                  // Since NotifyObservers() expects a null-terminated string,
                  // make sure none of these values are 0.
                  acknowledgementArray[i] = acknowledgementString[i] + 0xFF00;
                }
                acknowledgementArray[acknowledgementString.size()] = 0;
                obsServ->NotifyObservers(
                    static_cast<nsIContentAnalysis*>(owner.get()),
                    "dlp-acknowledgement-sent-raw",
                    acknowledgementArray.Elements());
              }));
        }
        if (err != 0) {
          return Err(NS_ERROR_FAILURE);
        }
        return nullptr;
      })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ },
          [](nsresult rv) {
            LOGE("RunAcknowledgeTask failed to get the client");
          });
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::GetDiagnosticInfo(JSContext* aCx, dom::Promise** aPromise) {
  RefPtr<dom::Promise> promise;
  nsresult rv = MakePromise(aCx, getter_AddRefs(promise));
  nsMainThreadPtrHandle<dom::Promise> promiseHolder(
      new nsMainThreadPtrHolder<dom::Promise>(
          "ContentAnalysis::GetDiagnosticInfo promise", promise));
  NS_ENSURE_SUCCESS(rv, rv);
  AssertIsOnMainThread();
  CallClientWithRetry<std::nullptr_t>(
      __func__,
      [promiseHolder](
          std::shared_ptr<content_analysis::sdk::Client> client) mutable
          -> Result<std::nullptr_t, nsresult> {
        MOZ_ASSERT(!NS_IsMainThread());
        // I don't think this will be slow, but do it on the background thread
        // just to be safe
        std::string agentPath = client->GetAgentInfo().binary_path;
        // Need to switch back to main thread to create the
        // ContentAnalysisDiagnosticInfo and resolve the promise
        NS_DispatchToMainThread(NS_NewRunnableFunction(
            __func__, [promiseHolder = std::move(promiseHolder),
                       agentPath = std::move(agentPath)]() {
              RefPtr<ContentAnalysis> self = GetContentAnalysisFromService();
              if (!self) {
                // may be quitting
                promiseHolder->MaybeReject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
                return;
              }
              nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath);
              // Note that if we made it here, we have successfully connected to
              // the agent.
              auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>(
                  /* mConnectedToAgent */ true, std::move(agentWidePath), false,
                  self ? self->mRequestCount : 0);
              promiseHolder->MaybeResolve(info);
            }));
        return nullptr;
      })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__, []() {},
          [promiseHolder](nsresult rv) {
            RefPtr<ContentAnalysis> self = GetContentAnalysisFromService();
            auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>(
                false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE,
                self ? self->mRequestCount : 0);
            promiseHolder->MaybeResolve(info);
          });
  promise.forget(aPromise);
  return NS_OK;
}

/* static */ nsCOMPtr<nsIURI> ContentAnalysis::GetURIForBrowsingContext(
    dom::CanonicalBrowsingContext* aBrowsingContext) {
  dom::WindowGlobalParent* windowGlobal =
      aBrowsingContext->GetCurrentWindowGlobal();
  if (!windowGlobal) {
    return nullptr;
  }
  dom::CanonicalBrowsingContext* oldBrowsingContext = aBrowsingContext;
  nsIPrincipal* principal = windowGlobal->DocumentPrincipal();
  dom::CanonicalBrowsingContext* curBrowsingContext =
      aBrowsingContext->GetParent();
  while (curBrowsingContext) {
    dom::WindowGlobalParent* newWindowGlobal =
        curBrowsingContext->GetCurrentWindowGlobal();
    if (!newWindowGlobal) {
      break;
    }
    nsIPrincipal* newPrincipal = newWindowGlobal->DocumentPrincipal();
    if (!(newPrincipal->Subsumes(principal))) {
      break;
    }
    principal = newPrincipal;
    oldBrowsingContext = curBrowsingContext;
    curBrowsingContext = curBrowsingContext->GetParent();
  }
  if (nsContentUtils::IsPDFJS(principal)) {
    // the principal's URI is the URI of the pdf.js reader
    // so get the document's URI
    dom::WindowContext* windowContext =
        oldBrowsingContext->GetCurrentWindowContext();
    if (!windowContext) {
      return nullptr;
    }
    return windowContext->Canonical()->GetDocumentURI();
  }
  return principal->GetURI();
}

// IDL implementation
NS_IMETHODIMP ContentAnalysis::GetURIForBrowsingContext(
    dom::BrowsingContext* aBrowsingContext, nsIURI** aURI) {
  NS_ENSURE_ARG_POINTER(aBrowsingContext);
  NS_ENSURE_ARG_POINTER(aURI);
  nsCOMPtr<nsIURI> uri =
      GetURIForBrowsingContext(aBrowsingContext->Canonical());
  if (!uri) {
    return NS_ERROR_FAILURE;
  }
  uri.forget(aURI);
  return NS_OK;
}

NS_IMETHODIMP
ContentAnalysis::GetURIForDropEvent(dom::DragEvent* aEvent, nsIURI** aURI) {
  MOZ_ASSERT(XRE_IsParentProcess());
  *aURI = nullptr;
  auto* widgetEvent = aEvent->WidgetEventPtr();
  MOZ_ASSERT(widgetEvent);
  MOZ_ASSERT(widgetEvent->mClass == eDragEventClass &&
             widgetEvent->mMessage == eDrop);
  auto* bp =
      dom::BrowserParent::GetBrowserParentFromLayersId(widgetEvent->mLayersId);
  NS_ENSURE_TRUE(bp, NS_ERROR_NOT_AVAILABLE);
  auto* bc = bp->GetBrowsingContext();
  NS_ENSURE_TRUE(bc, NS_ERROR_NO_CONTENT);
  return GetURIForBrowsingContext(bc, aURI);
}

NS_IMETHODIMP ContentAnalysis::MakeResponseForTest(
    nsIContentAnalysisResponse::Action aAction, const nsACString& aToken,
    const nsACString& aUserActionId,
    nsIContentAnalysisResponse** aNewResponse) {
  auto response =
      MakeRefPtr<ContentAnalysisResponse>(aAction, aToken, aUserActionId);
  // Pretend this is not synthetic so dialogs will show in tests
  response->SetIsSyntheticResponse(false);
  response.forget(aNewResponse);
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysisCallback::ContentResult(
    nsIContentAnalysisResult* aResult) {
  LOGD("[%p] Called ContentAnalysisCallback::ContentResult", this);
  // Grab a reference to the parameter.
  RefPtr result = aResult;
  if (mPromise) {
    mPromise->MaybeResolve(aResult);
  } else if (mContentResponseCallback) {
    mContentResponseCallback(aResult);
  } else {
    MOZ_ASSERT_UNREACHABLE("ContentAnalysisCallback called multiple times");
  }

  ClearCallbacks();
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysisCallback::Error(nsresult aError) {
  LOGD("[%p] Called ContentAnalysisCallback::Error", this);
  if (mPromise) {
    mPromise->MaybeReject(aError);
  } else if (mErrorCallback) {
    mErrorCallback(aError);
  } else {
    MOZ_ASSERT_UNREACHABLE("ContentAnalysisCallback called multiple times");
  }

  ClearCallbacks();
  return NS_OK;
}

ContentAnalysisCallback::ContentAnalysisCallback(dom::Promise* aPromise)
    : mPromise(aPromise) {}

ContentAnalysisCallback::ContentAnalysisCallback(
    std::function<void(nsIContentAnalysisResult*)>&& aContentResponseCallback) {
  mErrorCallback = [aContentResponseCallback](nsresult) {
    RefPtr noResult = MakeRefPtr<ContentAnalysisNoResult>(
        NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
    aContentResponseCallback(noResult);
  };
  mContentResponseCallback = std::move(aContentResponseCallback);
}

NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetConnectedToAgent(
    bool* aConnectedToAgent) {
  *aConnectedToAgent = mConnectedToAgent;
  return NS_OK;
}
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetAgentPath(
    nsAString& aAgentPath) {
  aAgentPath = mAgentPath;
  return NS_OK;
}
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetFailedSignatureVerification(
    bool* aFailedSignatureVerification) {
  *aFailedSignatureVerification = mFailedSignatureVerification;
  return NS_OK;
}

NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetRequestCount(
    int64_t* aRequestCount) {
  *aRequestCount = mRequestCount;
  return NS_OK;
}

#undef LOGD
#undef LOGE
}  // namespace mozilla::contentanalysis

Messung V0.5 in Prozent
C=91 H=93 G=91

¤ Dauer der Verarbeitung: 0.106 Sekunden  (vorverarbeitet am  2026-09-28) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1126864
#Domains=1897691