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

#include <stdlib.h>

#include "WebGL2Context.h"
#include "jsapi.h"
#include "mozIThirdPartyUtil.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_gfx.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StaticPrefs_webgl.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/ContentList.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/OffscreenCanvas.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/gfx/Matrix.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/webgpu/Instance.h"
#include "nsContentUtils.h"
#include "nsGfxCIID.h"
#include "nsICanvasRenderingContextInternal.h"
#include "nsIObserverService.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsIScriptError.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsPrintfCString.h"
#include "nsTArray.h"
#include "nsUnicharUtils.h"

#define TOPIC_CANVAS_PERMISSIONS_PROMPT "canvas-permissions-prompt"
#define TOPIC_CANVAS_PERMISSIONS_PROMPT_HIDE_DOORHANGER \
  "canvas-permissions-prompt-hide-doorhanger"
#define PERMISSION_CANVAS_EXTRACT_DATA "canvas"_ns

using namespace mozilla::gfx;

static bool IsUnrestrictedPrincipal(nsIPrincipal* aPrincipal) {
  if (!aPrincipal) {
    return false;
  }

  // The system principal can always extract canvas data.
  if (aPrincipal->IsSystemPrincipal()) {
    return true;
  }

  // Allow chrome: and resource: (this especially includes PDF.js)
  if (aPrincipal->SchemeIs("chrome") || aPrincipal->SchemeIs("resource")) {
    return true;
  }

  // Allow extension principals.
  return aPrincipal->GetIsAddonOrExpandedAddonPrincipal();
}

namespace mozilla::CanvasUtils {

class OffscreenCanvasPermissionRunnable final
    : public dom::WorkerMainThreadRunnable {
 public:
  OffscreenCanvasPermissionRunnable(dom::WorkerPrivate* aWorkerPrivate,
                                    nsIPrincipal* aPrincipal)
      : WorkerMainThreadRunnable(aWorkerPrivate,
                                 "OffscreenCanvasPermissionRunnable"_ns),
        mPrincipal(aPrincipal) {
    MOZ_ASSERT(aWorkerPrivate);
    aWorkerPrivate->AssertIsOnWorkerThread();
  }

  bool MainThreadRun() override {
    AssertIsOnMainThread();

    mResult = GetCanvasExtractDataPermission(mPrincipal);
    return true;
  }

  uint32_t GetResult() const { return mResult; }

 private:
  nsCOMPtr<nsIPrincipal> mPrincipal;
  uint32_t mResult = nsIPermissionManager::UNKNOWN_ACTION;
};

uint32_t GetCanvasExtractDataPermission(nsIPrincipal* aPrincipal) {
  if (!aPrincipal) {
    return nsIPermissionManager::UNKNOWN_ACTION;
  }

  if (IsUnrestrictedPrincipal(aPrincipal)) {
    return true;
  }

  if (NS_IsMainThread()) {
    nsresult rv;
    nsCOMPtr<nsIPermissionManager> permissionManager =
        do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, nsIPermissionManager::UNKNOWN_ACTION);

    uint32_t permission;
    rv = permissionManager->TestPermissionFromPrincipal(
        aPrincipal, PERMISSION_CANVAS_EXTRACT_DATA, &permission);
    NS_ENSURE_SUCCESS(rv, nsIPermissionManager::UNKNOWN_ACTION);

    return permission;
  }
  if (auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate()) {
    RefPtr<OffscreenCanvasPermissionRunnable> runnable =
        new OffscreenCanvasPermissionRunnable(workerPrivate, aPrincipal);
    ErrorResult rv;
    runnable->Dispatch(workerPrivate, dom::WorkerStatus::Canceling, rv);
    if (rv.Failed()) {
      return nsIPermissionManager::UNKNOWN_ACTION;
    }
    return runnable->GetResult();
  }
  return nsIPermissionManager::UNKNOWN_ACTION;
}

/*
┌──────────────────────────────────────────────────────────────────────────┐
│IsImageExtractionAllowed(dom::OffscreenCanvas*, JSContext*, nsIPrincipal*)│
└────────────────────────────────────┬─────────────────────────────────────┘
                                     │
                   ┌─────────────────▼────────────────────┐
 ┌─────No──────────│Any prompt RFP target enabled? See [1]│
 ▼                 └─────────────────┬────────────────────┘
 │                                   │Yes
 │                 ┌─────────────────▼────────┐
 ├─────Yes─────────┤Is unrestricted principal?│
 ▼                 └─────────────────┬────────┘
 │                                   │No
 │                 ┌─────────────────▼────────┐
 │          ┌──No──┤Are third parties blocked?│
 │          │      └─────────────────┬────────┘
 │          │                        │Yes
 │          │      ┌─────────────────▼─────────────┐
 │          │      │Are we in a third-party window?├───────Yes──────────┐
 │          │      └─────────────────┬─────────────┘                    ▼
 │          │                        │No                                │
 │          │      ┌─────────────────▼──┐                               │
 │          └──────►Do we show a prompt?├────────────Yes─┐              │
 │                 └─────────────────┬──┘                ▼              │
 │                                   │No                 │              │
 │                 ┌─────────────────▼─────────────┐     │              │
 │                 │Do we allow reading canvas data│     │              │
 │                 │in response to user input?     ├─No──┤              │
 │                 └─────────────────┬─────────────┘     ▼              │
 │                                   │Yes                │              │
 │                 ┌─────────────────▼─────────┐         │              │
 ├─────Yes─────────┼Are we handling user input?│         │              │
 ▼                 └─────────────────┬─────────┘         │              │
 │                                   │No                 │              │
 │                 ┌─────────────────▼─────────────┐     │              │
┌▼─────┐           │Show Permission Prompt (either ◄─────┘          ┌───▼──┐
│return│           │w/ doorhanger, or w/o depending│                │return│
│true  │           │on User Input)                 ├────────────────►false │
└──────┘           └───────────────────────────────┘                └──────┘
[1]: CanvasImageExtractionPrompt, CanvasExtractionBeforeUserInputIsBlocked,
     CanvasExtractionFromThirdPartiesIsBlocked are the RFP targets mentioned.
 */
bool IsImageExtractionAllowed_impl(
    bool aCanvasImageExtractionPrompt,
    bool aCanvasExtractionBeforeUserInputIsBlocked,
    bool aCanvasExtractionFromThirdPartiesIsBlocked, JSContext* aCx,
    nsIPrincipal* aPrincipal,
    const std::function<bool()>& aGetIsThirdPartyWindow,
    const std::function<void(const nsAutoString&)>& aReportToConsole,
    const std::function<void(bool)>& aTryPrompt) {
  /*
   * There are three RFPTargets that change the behavior here, and they can be
   * in any combination
   * - CanvasImageExtractionPrompt - whether or not to prompt the user for
   * canvas extraction. If enabled, before canvas is extracted we will ensure
   * the user has granted permission.
   * - CanvasExtractionBeforeUserInputIsBlocked - if enabled, canvas extraction
   * before user input has occurred is always blocked, regardless of any other
   * Target behavior
   * - CanvasExtractionFromThirdPartiesIsBlocked - if enabled, canvas extraction
   * by third parties is always blocked, regardless of any other Target behavior
   *
   * There are two odd cases:
   * 1) When CanvasImageExtractionPrompt=false but
   *    CanvasExtractionBeforeUserInputIsBlocked=true Conceptually this is
   *    "Always allow canvas extraction in response to user input, and never
   *     allow it otherwise"
   *
   *    That's fine as a concept, but it might be a little confusing, so we
   *    still want to show the permission icon in the address bar, but never
   *    the permission doorhanger.
   * 2) When CanvasExtractionFromThirdPartiesIsBlocked=false - we will prompt
   *    the user for permission _for the frame_ (maybe with the doorhanger,
   *    maybe not).  The prompt shows the frame's origin, but it's easy to
   *    mistake that for the origin of the top-level page and grant it when you
   *    don't mean to.  This combination isn't likely to be used by anyone
   *    except those opting in, so that's alright.
   */

  if (!aCanvasImageExtractionPrompt &&
      !aCanvasExtractionBeforeUserInputIsBlocked &&
      !aCanvasExtractionFromThirdPartiesIsBlocked) {
    return true;
  }

  // Don't proceed if we don't have a document or JavaScript context.
  if (!aCx) {
    return false;
  }

  if (IsUnrestrictedPrincipal(aPrincipal)) {
    return true;
  }

  Maybe<nsAutoCString> origin = Nothing();
  auto getOrigin = [&]() {
    if (origin.isSome()) {
      return origin->IsEmpty();
    }

    nsAutoCString originResult;
    nsresult rv = NS_ERROR_FAILURE;
    if (aPrincipal) {
      rv = aPrincipal->GetOrigin(originResult);
    }
    origin = NS_SUCCEEDED(rv) ? Some(originResult) : Some(""_ns);

    return NS_SUCCEEDED(rv);
  };

  if (aCanvasExtractionFromThirdPartiesIsBlocked) {
    if (aGetIsThirdPartyWindow()) {
      nsAutoString message;
      message.AppendPrintf(
          "Blocked %s third party from extracting canvas data.",
          getOrigin() ? origin->get() : "unknown");
      aReportToConsole(message);
      return false;
    }
  }

  if (!aCanvasImageExtractionPrompt &&
      !aCanvasExtractionBeforeUserInputIsBlocked) {
    return true;
  }

  // -------------------------------------------------------------------
  // Check a site's permission

  // If the user has previously granted or not granted permission, we can return
  // immediately. Load Permission Manager service.
  uint64_t permission = GetCanvasExtractDataPermission(aPrincipal);
  switch (permission) {
    case nsIPermissionManager::ALLOW_ACTION:
      return true;
    case nsIPermissionManager::DENY_ACTION:
      return false;
    default:
      break;
  }

  // -------------------------------------------------------------------
  // At this point, there's only one way to return true: if we are always
  // allowing canvas in response to user input, and not prompting
  bool hidePermissionDoorhanger = false;
  if (!aCanvasImageExtractionPrompt &&
      aCanvasExtractionBeforeUserInputIsBlocked) {
    // If so, see if this is in response to user input.
    if (NS_IsMainThread() && dom::UserActivation::IsHandlingUserInput()) {
      return true;
    }

    hidePermissionDoorhanger = true;
  }

  // -------------------------------------------------------------------
  // Now we know we're going to block it, and log something to the console,
  // and show some sort of prompt maybe with the doorhanger, maybe not

  hidePermissionDoorhanger |=
      aCanvasExtractionBeforeUserInputIsBlocked &&
      (!NS_IsMainThread() || !dom::UserActivation::IsHandlingUserInput());

  nsAutoString message;
  message.AppendPrintf("Blocked %s from extracting canvas data",
                       getOrigin() ? origin->get() : "unknown");
  message.AppendPrintf(hidePermissionDoorhanger
                           ? " because no user input was detected"
                           : " but prompting the user.");
  aReportToConsole(message);

  aTryPrompt(hidePermissionDoorhanger);

  return false;
}

bool IsImageExtractionAllowed(dom::Document* aDocument, JSContext* aCx,
                              nsIPrincipal* aPrincipal) {
  if (NS_WARN_IF(!aDocument)) {
    return false;
  }

  bool canvasImageExtractionPrompt = aDocument->ShouldResistFingerprinting(
      RFPTarget::CanvasImageExtractionPrompt);
  bool canvasExtractionBeforeUserInputIsBlocked =
      aDocument->ShouldResistFingerprinting(
          RFPTarget::CanvasExtractionBeforeUserInputIsBlocked);
  bool canvasExtractionFromThirdPartiesIsBlocked =
      aDocument->ShouldResistFingerprinting(
          RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked);

  // This part is duplicate but it helps us return faster
  // before we create bunch of lambdas
  if (!canvasImageExtractionPrompt &&
      !canvasExtractionBeforeUserInputIsBlocked &&
      !canvasExtractionFromThirdPartiesIsBlocked) {
    return true;
  }

  auto getIsThirdPartyWindow = [&]() {
    return aDocument->GetWindowContext()
               ? aDocument->GetWindowContext()->GetIsThirdPartyWindow()
               : false;
  };

  auto reportToConsole = [&](const nsAutoString& message) {
    nsContentUtils::ReportToConsoleNonLocalized(
        message, nsIScriptError::warningFlag, "Security"_ns, aDocument);
  };

  auto prompt = [&](bool hidePermissionDoorhanger) {
    if (!aPrincipal) {
      return;
    }

    nsAutoCString origin;
    nsresult rv = aPrincipal->GetOrigin(origin);
    if (NS_FAILED(rv)) {
      return;
    }

    if (!XRE_IsContentProcess()) {
      MOZ_ASSERT_UNREACHABLE(
          "Who's calling this from the parent process without a chrome window "
          "(it would have been exempt from the RFP targets)?");
      return;
    }

    nsPIDOMWindowOuter* win = aDocument->GetWindow();
    if (RefPtr<dom::BrowserChild> browserChild =
            dom::BrowserChild::GetFrom(win)) {
      browserChild->SendShowCanvasPermissionPrompt(origin,
                                                   hidePermissionDoorhanger);
    }
  };

  return IsImageExtractionAllowed_impl(
      canvasImageExtractionPrompt, canvasExtractionBeforeUserInputIsBlocked,
      canvasExtractionFromThirdPartiesIsBlocked, aCx, aPrincipal,
      getIsThirdPartyWindow, reportToConsole, prompt);
}

ImageExtraction ImageExtractionResult(dom::HTMLCanvasElement* aCanvasElement,
                                      JSContext* aCx,
                                      nsIPrincipal* aPrincipal) {
  if (IsUnrestrictedPrincipal(aPrincipal)) {
    return ImageExtraction::Unrestricted;
  }

  nsCOMPtr<dom::Document> ownerDoc = aCanvasElement->OwnerDoc();
  if (!IsImageExtractionAllowed(ownerDoc, aCx, aPrincipal)) {
    return ImageExtraction::Placeholder;
  }

  if (ownerDoc->ShouldResistFingerprinting(
          RFPTarget::EfficientCanvasRandomization) &&
      GetCanvasExtractDataPermission(aPrincipal) !=
          nsIPermissionManager::ALLOW_ACTION) {
    return ImageExtraction::EfficientRandomize;
  }

  if ((ownerDoc->ShouldResistFingerprinting(RFPTarget::CanvasRandomization) ||
       ownerDoc->ShouldResistFingerprinting(RFPTarget::WebGLRandomization)) &&
      GetCanvasExtractDataPermission(aPrincipal) !=
          nsIPermissionManager::ALLOW_ACTION) {
    return ImageExtraction::Randomize;
  }

  return ImageExtraction::Unrestricted;
}

bool IsImageExtractionAllowed(dom::OffscreenCanvas* aOffscreenCanvas,
                              JSContext* aCx, nsIPrincipal* aPrincipal) {
  if (!aOffscreenCanvas) {
    return false;
  }

  bool canvasImageExtractionPrompt =
      aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::CanvasImageExtractionPrompt);
  bool canvasExtractionBeforeUserInputIsBlocked =
      aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::CanvasExtractionBeforeUserInputIsBlocked);
  bool canvasExtractionFromThirdPartiesIsBlocked =
      aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::CanvasExtractionFromThirdPartiesIsBlocked);

  // This part is duplicate but it helps us return faster
  // before we create bunch of lambdas
  if (!canvasImageExtractionPrompt &&
      !canvasExtractionBeforeUserInputIsBlocked &&
      !canvasExtractionFromThirdPartiesIsBlocked) {
    return true;
  }

  Maybe<uint64_t> winId = aOffscreenCanvas->GetWindowID();
  if (winId.isSome() && *winId == UINT64_MAX) {
    // Workers with no window return UINT64_MAX as their window ID.
    winId = Nothing();
  }

  auto getIsThirdPartyWindow = [&]() {
    if (winId.isNothing()) {
      return false;
    }

    if (NS_IsMainThread()) {
      if (RefPtr<dom::WindowContext> win =
              dom::WindowGlobalParent::GetById(*winId)) {
        return win->GetIsThirdPartyWindow();
      }
    } else if (auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate()) {
      return workerPrivate->IsThirdPartyContext();
    }

    return false;
  };

  auto reportToConsole = [&](const nsAutoString& message) {
    if (winId.isNothing()) {
      return;
    }

    nsContentUtils::ReportToConsoleByWindowID(
        message, nsIScriptError::warningFlag, "Security"_ns, *winId);
  };

  nsAutoCString origin;
  if (!aPrincipal || NS_FAILED(aPrincipal->GetOrigin(origin))) {
    origin = ""_ns;
  }

  RefPtr<dom::OffscreenCanvas> canvasRef = aOffscreenCanvas;
  auto prompt = [=](bool hidePermissionDoorhanger) {
    if (origin.IsEmpty()) {
      return;
    }

    if (!XRE_IsContentProcess()) {
      MOZ_ASSERT_UNREACHABLE(
          "Who's calling this from the parent process without a chrome "
          "window "
          "(it would have been exempt from the RFP targets)?");
      return;
    }

    if (NS_IsMainThread()) {
      nsCOMPtr<nsIGlobalObject> global = canvasRef->GetRelevantGlobal();
      NS_ENSURE_TRUE_VOID(global);

      RefPtr<nsPIDOMWindowInner> window = global->GetAsInnerWindow();
      NS_ENSURE_TRUE_VOID(window);

      RefPtr<dom::BrowserChild> browserChild =
          dom::BrowserChild::GetFrom(window);
      NS_ENSURE_TRUE_VOID(browserChild);

      browserChild->SendShowCanvasPermissionPrompt(origin,
                                                   hidePermissionDoorhanger);
      return;
    }

    class OffscreenCanvasPromptRunnable
        : public dom::WorkerProxyToMainThreadRunnable {
     public:
      explicit OffscreenCanvasPromptRunnable(const nsCString& aOrigin,
                                             bool aHidePermissionDoorhanger)
          : mOrigin(aOrigin),
            mHidePermissionDoorhanger(aHidePermissionDoorhanger) {}

      // Runnables don't support MOZ_CAN_RUN_SCRIPT, bug 1535398
      MOZ_CAN_RUN_SCRIPT_BOUNDARY void RunOnMainThread(
          dom::WorkerPrivate* aWorkerPrivate) override {
        MOZ_ASSERT(aWorkerPrivate);
        AssertIsOnMainThread();

        RefPtr<nsPIDOMWindowInner> inner = aWorkerPrivate->GetAncestorWindow();
        RefPtr<dom::BrowserChild> win = dom::BrowserChild::GetFrom(inner);
        NS_ENSURE_TRUE_VOID(win);

        win->SendShowCanvasPermissionPrompt(mOrigin, mHidePermissionDoorhanger);
      }

      void RunBackOnWorkerThreadForCleanup(
          dom::WorkerPrivate* aWorkerPrivate) override {
        MOZ_ASSERT(aWorkerPrivate);
        aWorkerPrivate->AssertIsOnWorkerThread();
      }

      nsCString mOrigin;
      bool mHidePermissionDoorhanger;
    };

    if (auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate()) {
      RefPtr<OffscreenCanvasPromptRunnable> runnable =
          new OffscreenCanvasPromptRunnable(origin, hidePermissionDoorhanger);
      runnable->Dispatch(workerPrivate);
      return;
    }
  };

  return IsImageExtractionAllowed_impl(
      canvasImageExtractionPrompt, canvasExtractionBeforeUserInputIsBlocked,
      canvasExtractionFromThirdPartiesIsBlocked, aCx, aPrincipal,
      getIsThirdPartyWindow, reportToConsole, prompt);
}

ImageExtraction ImageExtractionResult(dom::OffscreenCanvas* aOffscreenCanvas,
                                      JSContext* aCx,
                                      nsIPrincipal* aPrincipal) {
  if (IsUnrestrictedPrincipal(aPrincipal)) {
    return ImageExtraction::Unrestricted;
  }

  if (!IsImageExtractionAllowed(aOffscreenCanvas, aCx, aPrincipal)) {
    return ImageExtraction::Placeholder;
  }

  if (GetCanvasExtractDataPermission(aPrincipal) ==
      nsIPermissionManager::ALLOW_ACTION) {
    return ImageExtraction::Unrestricted;
  }

  if (aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::EfficientCanvasRandomization)) {
    return ImageExtraction::EfficientRandomize;
  }

  if (aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::CanvasRandomization) ||
      aOffscreenCanvas->ShouldResistFingerprinting(
          RFPTarget::WebGLRandomization)) {
    return ImageExtraction::Randomize;
  }

  return ImageExtraction::Unrestricted;
}

bool GetCanvasContextType(const nsAString& str,
                          dom::CanvasContextType* const out_type) {
  if (str.EqualsLiteral("2d")) {
    *out_type = dom::CanvasContextType::Canvas2D;
    return true;
  }

  if (str.EqualsLiteral("webgl") || str.EqualsLiteral("experimental-webgl")) {
    *out_type = dom::CanvasContextType::WebGL1;
    return true;
  }

  if (StaticPrefs::webgl_enable_webgl2()) {
    if (str.EqualsLiteral("webgl2")) {
      *out_type = dom::CanvasContextType::WebGL2;
      return true;
    }
  }

  if (webgpu::Instance::PrefEnabled() && gfxVars::AllowWebGPU()) {
    if (str.EqualsLiteral("webgpu")) {
      *out_type = dom::CanvasContextType::WebGPU;
      return true;
    }
  }

  if (str.EqualsLiteral("bitmaprenderer")) {
    *out_type = dom::CanvasContextType::ImageBitmap;
    return true;
  }

  return false;
}

/**
 * This security check utility might be called from an source that never
 * taints others. For example, while painting a CanvasPattern, which is
 * created from an ImageBitmap, onto a canvas. In this case, the caller could
 * set the CORSUsed true in order to pass this check and leave the aPrincipal
 * to be a nullptr since the aPrincipal is not going to be used.
 */
void DoDrawImageSecurityCheck(dom::HTMLCanvasElement* aCanvasElement,
                              nsIPrincipal* aPrincipal, bool forceWriteOnly,
                              bool CORSUsed) {
  // Callers should ensure that mCanvasElement is non-null before calling this
  if (!aCanvasElement) {
    NS_WARNING("DoDrawImageSecurityCheck called without canvas element!");
    return;
  }

  if (aCanvasElement->IsWriteOnly() && !aCanvasElement->mExpandedReader) {
    return;
  }

  // If we explicitly set WriteOnly just do it and get out
  if (forceWriteOnly) {
    aCanvasElement->SetWriteOnly();
    return;
  }

  // No need to do a security check if the image used CORS for the load
  if (CORSUsed) return;

  if (NS_WARN_IF(!aPrincipal)) {
    MOZ_ASSERT_UNREACHABLE("Must have a principal here");
    aCanvasElement->SetWriteOnly();
    return;
  }

  if (aCanvasElement->NodePrincipal()->Subsumes(aPrincipal)) {
    // This canvas has access to that image anyway
    return;
  }

  if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
    // This is a resource from an extension content script principal.

    if (aCanvasElement->mExpandedReader &&
        aCanvasElement->mExpandedReader->Subsumes(aPrincipal)) {
      // This canvas already allows reading from this principal.
      return;
    }

    if (!aCanvasElement->mExpandedReader) {
      // Allow future reads from this same princial only.
      aCanvasElement->SetWriteOnly(aPrincipal);
      return;
    }

    // If we got here, this must be the *second* extension tainting
    // the canvas.  Fall through to mark it WriteOnly for everyone.
  }

  aCanvasElement->SetWriteOnly();
}

/**
 * This security check utility might be called from an source that never
 * taints others. For example, while painting a CanvasPattern, which is
 * created from an ImageBitmap, onto a canvas. In this case, the caller could
 * set the aCORSUsed true in order to pass this check and leave the aPrincipal
 * to be a nullptr since the aPrincipal is not going to be used.
 */
void DoDrawImageSecurityCheck(dom::OffscreenCanvas* aOffscreenCanvas,
                              nsIPrincipal* aPrincipal, bool aForceWriteOnly,
                              bool aCORSUsed) {
  // Callers should ensure that mCanvasElement is non-null before calling this
  if (NS_WARN_IF(!aOffscreenCanvas)) {
    return;
  }

  nsIPrincipal* expandedReader = aOffscreenCanvas->GetExpandedReader();
  if (aOffscreenCanvas->IsWriteOnly() && !expandedReader) {
    return;
  }

  // If we explicitly set WriteOnly just do it and get out
  if (aForceWriteOnly) {
    aOffscreenCanvas->SetWriteOnly();
    return;
  }

  // No need to do a security check if the image used CORS for the load
  if (aCORSUsed) {
    return;
  }

  // If we are on a worker thread, we might not have any principals at all.
  nsIGlobalObject* global = aOffscreenCanvas->GetRelevantGlobal();
  nsIPrincipal* canvasPrincipal = global ? global->PrincipalOrNull() : nullptr;
  if (!aPrincipal || !canvasPrincipal) {
    aOffscreenCanvas->SetWriteOnly();
    return;
  }

  if (canvasPrincipal->Subsumes(aPrincipal)) {
    // This canvas has access to that image anyway
    return;
  }

  if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
    // This is a resource from an extension content script principal.

    if (expandedReader && expandedReader->Subsumes(aPrincipal)) {
      // This canvas already allows reading from this principal.
      return;
    }

    if (!expandedReader) {
      // Allow future reads from this same princial only.
      aOffscreenCanvas->SetWriteOnly(aPrincipal);
      return;
    }

    // If we got here, this must be the *second* extension tainting
    // the canvas.  Fall through to mark it WriteOnly for everyone.
  }

  aOffscreenCanvas->SetWriteOnly();
}

bool CoerceDouble(const JS::Value& v, double* d) {
  if (v.isDouble()) {
    *d = v.toDouble();
  } else if (v.isInt32()) {
    *d = double(v.toInt32());
  } else if (v.isUndefined()) {
    *d = 0.0;
  } else {
    return false;
  }
  return true;
}

bool HasDrawWindowPrivilege(JSContext* aCx, JSObject* /* unused */) {
  return nsContentUtils::CallerHasPermission(aCx,
                                             nsGkAtoms::all_urlsPermission);
}

bool CheckWriteOnlySecurity(bool aCORSUsed, nsIPrincipal* aPrincipal,
                            bool aHadCrossOriginRedirects) {
  if (!aPrincipal) {
    return true;
  }

  if (!aCORSUsed) {
    if (aHadCrossOriginRedirects) {
      return true;
    }

    nsIGlobalObject* incumbentSettingsObject = dom::GetIncumbentGlobal();
    if (!incumbentSettingsObject) {
      return true;
    }

    nsIPrincipal* principal = incumbentSettingsObject->PrincipalOrNull();
    if (NS_WARN_IF(!principal) || !(principal->Subsumes(aPrincipal))) {
      return true;
    }
  }

  return false;
}

}  // namespace mozilla::CanvasUtils
