/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* 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 "mozilla/ExtensionPolicyService.h"
#include "mozilla/extensions/DocumentObserver.h"
#include "mozilla/extensions/WebExtensionContentScript.h"
#include "mozilla/extensions/WebExtensionPolicy.h"

#include "mozilla/BasePrincipal.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/Services.h"
#include "mozilla/SimpleEnumerator.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/BrowsingContextGroup.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozIExtensionProcessScript.h"
#include "nsEscape.h"
#include "nsGkAtoms.h"
#include "nsHashKeys.h"
#include "nsIChannel.h"
#include "nsIContentPolicy.h"
#include "mozilla/dom/Document.h"
#include "nsGlobalWindowOuter.h"
#include "nsILoadInfo.h"
#include "nsIXULRuntime.h"
#include "nsImportModule.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsPIDOMWindow.h"
#include "nsXULAppAPI.h"
#include "nsQueryObject.h"

namespace mozilla {

using namespace extensions;

using dom::AutoJSAPI;
using dom::Document;
using dom::Promise;

#define DEFAULT_CSP_PREF \
  "extensions.webextensions.default-content-security-policy"
#define DEFAULT_DEFAULT_CSP "script-src 'self' 'wasm-unsafe-eval';"

#define DEFAULT_CSP_PREF_V3 \
  "extensions.webextensions.default-content-security-policy.v3"
#define DEFAULT_DEFAULT_CSP_V3 "script-src 'self'; upgrade-insecure-requests;"

#define RESTRICTED_DOMAINS_PREF "extensions.webextensions.restrictedDomains"

#define QUARANTINED_DOMAINS_PREF "extensions.quarantinedDomains.list"
#define QUARANTINED_DOMAINS_ENABLED "extensions.quarantinedDomains.enabled"

#define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script"
#define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script"

static const char kDocElementInserted[] = "initial-document-element-inserted";

/*****************************************************************************
 * ExtensionPolicyService
 *****************************************************************************/

using CoreByHostMap = nsTHashMap<nsCStringASCIICaseInsensitiveHashKey,
                                 RefPtr<extensions::WebExtensionPolicyCore>>;

static StaticRWLock sEPSLock;
static StaticAutoPtr<CoreByHostMap> sCoreByHost MOZ_GUARDED_BY(sEPSLock);
static StaticRefPtr<AtomSet> sRestrictedDomains MOZ_GUARDED_BY(sEPSLock);
static StaticRefPtr<AtomSet> sQuarantinedDomains MOZ_GUARDED_BY(sEPSLock);

/* static */
mozIExtensionProcessScript& ExtensionPolicyService::ProcessScript() {
  static nsCOMPtr<mozIExtensionProcessScript> sProcessScript;

  MOZ_ASSERT(NS_IsMainThread());

  if (MOZ_UNLIKELY(!sProcessScript)) {
    sProcessScript =
        do_ImportModule("resource://gre/modules/ExtensionProcessScript.jsm",
                        "ExtensionProcessScript");
    ClearOnShutdown(&sProcessScript);
  }
  return *sProcessScript;
}

/* static */ ExtensionPolicyService& ExtensionPolicyService::GetSingleton() {
  MOZ_ASSERT(NS_IsMainThread());

  static RefPtr<ExtensionPolicyService> sExtensionPolicyService;

  if (MOZ_UNLIKELY(!sExtensionPolicyService)) {
    sExtensionPolicyService = new ExtensionPolicyService();
    RegisterWeakMemoryReporter(sExtensionPolicyService);
    ClearOnShutdown(&sExtensionPolicyService);
  }
  return *sExtensionPolicyService.get();
}

/* static */
RefPtr<extensions::WebExtensionPolicyCore>
ExtensionPolicyService::GetCoreByHost(const nsACString& aHost) {
  StaticAutoReadLock lock(sEPSLock);
  return sCoreByHost ? sCoreByHost->Get(aHost) : nullptr;
}

ExtensionPolicyService::ExtensionPolicyService() {
  mObs = services::GetObserverService();
  MOZ_RELEASE_ASSERT(mObs);

  mDefaultCSP.SetIsVoid(true);
  mDefaultCSPV3.SetIsVoid(true);

  RegisterObservers();

  {
    StaticAutoWriteLock lock(sEPSLock);
    MOZ_DIAGNOSTIC_ASSERT(!sCoreByHost,
                          "ExtensionPolicyService created twice?");
    sCoreByHost = new CoreByHostMap();
  }

  UpdateRestrictedDomains();
  UpdateQuarantinedDomains();
}

ExtensionPolicyService::~ExtensionPolicyService() {
  UnregisterWeakMemoryReporter(this);

  {
    StaticAutoWriteLock lock(sEPSLock);
    sCoreByHost = nullptr;
    sRestrictedDomains = nullptr;
    sQuarantinedDomains = nullptr;
  }
}

bool ExtensionPolicyService::UseRemoteExtensions() const {
  static Maybe<bool> sRemoteExtensions;
  if (MOZ_UNLIKELY(sRemoteExtensions.isNothing())) {
    sRemoteExtensions = Some(StaticPrefs::extensions_webextensions_remote());
  }
  return sRemoteExtensions.value() && BrowserTabsRemoteAutostart();
}

bool ExtensionPolicyService::IsExtensionProcess() const {
  bool isRemote = UseRemoteExtensions();

  if (isRemote && XRE_IsContentProcess()) {
    auto& remoteType = dom::ContentChild::GetSingleton()->GetRemoteType();
    return remoteType == EXTENSION_REMOTE_TYPE;
  }
  return !isRemote && XRE_IsParentProcess();
}

bool ExtensionPolicyService::GetQuarantinedDomainsEnabled() const {
  StaticAutoReadLock lock(sEPSLock);
  return sQuarantinedDomains != nullptr;
}

WebExtensionPolicy* ExtensionPolicyService::GetByURL(const URLInfo& aURL) {
  if (aURL.Scheme() == nsGkAtoms::moz_extension) {
    return GetByHost(aURL.Host());
  }
  return nullptr;
}

WebExtensionPolicy* ExtensionPolicyService::GetByHost(
    const nsACString& aHost) const {
  AssertIsOnMainThread();
  RefPtr<WebExtensionPolicyCore> core = GetCoreByHost(aHost);
  return core ? core->GetMainThreadPolicy() : nullptr;
}

void ExtensionPolicyService::GetAll(
    nsTArray<RefPtr<WebExtensionPolicy>>& aResult) {
  AppendToArray(aResult, mExtensions.Values());
}

bool ExtensionPolicyService::RegisterExtension(WebExtensionPolicy& aPolicy) {
  bool ok =
      (!GetByID(aPolicy.Id()) && !GetByHost(aPolicy.MozExtensionHostname()));
  MOZ_ASSERT(ok);

  if (!ok) {
    return false;
  }

  mExtensions.InsertOrUpdate(aPolicy.Id(), RefPtr{&aPolicy});

  {
    StaticAutoWriteLock lock(sEPSLock);
    sCoreByHost->InsertOrUpdate(aPolicy.MozExtensionHostname(), aPolicy.Core());
  }
  return true;
}

bool ExtensionPolicyService::UnregisterExtension(WebExtensionPolicy& aPolicy) {
  bool ok = (GetByID(aPolicy.Id()) == &aPolicy &&
             GetByHost(aPolicy.MozExtensionHostname()) == &aPolicy);
  MOZ_ASSERT(ok);

  if (!ok) {
    return false;
  }

  mExtensions.Remove(aPolicy.Id());

  {
    StaticAutoWriteLock lock(sEPSLock);
    sCoreByHost->Remove(aPolicy.MozExtensionHostname());
  }
  return true;
}

bool ExtensionPolicyService::RegisterObserver(DocumentObserver& aObserver) {
  bool inserted = false;
  mObservers.LookupOrInsertWith(&aObserver, [&] {
    inserted = true;
    return RefPtr{&aObserver};
  });
  return inserted;
}

bool ExtensionPolicyService::UnregisterObserver(DocumentObserver& aObserver) {
  return mObservers.Remove(&aObserver);
}

/*****************************************************************************
 * nsIMemoryReporter
 *****************************************************************************/

NS_IMETHODIMP
ExtensionPolicyService::CollectReports(nsIHandleReportCallback* aHandleReport,
                                       nsISupports* aData, bool aAnonymize) {
  for (const auto& ext : mExtensions.Values()) {
    nsAtomCString id(ext->Id());

    NS_ConvertUTF16toUTF8 name(ext->Name());
    name.ReplaceSubstring("\"", "");
    name.ReplaceSubstring("\\", "");

    nsString url;
    MOZ_TRY_VAR(url, ext->GetURL(u""_ns));

    nsPrintfCString desc("Extension(id=%s, name=\"%s\", baseURL=%s)", id.get(),
                         name.get(), NS_ConvertUTF16toUTF8(url).get());
    desc.ReplaceChar('/', '\\');

    nsCString path("extensions/");
    path.Append(desc);

    aHandleReport->Callback(""_ns, path, KIND_NONHEAP, UNITS_COUNT, 1,
                            "WebExtensions that are active in this session"_ns,
                            aData);
  }

  return NS_OK;
}

/*****************************************************************************
 * Content script management
 *****************************************************************************/

void ExtensionPolicyService::RegisterObservers() {
  mObs->AddObserver(this, kDocElementInserted, false);
  if (XRE_IsContentProcess()) {
    mObs->AddObserver(this, "http-on-opening-request", false);
    mObs->AddObserver(this, "document-on-opening-request", false);
  }

  Preferences::AddStrongObserver(this, DEFAULT_CSP_PREF);
  Preferences::AddStrongObserver(this, DEFAULT_CSP_PREF_V3);
  Preferences::AddStrongObserver(this, RESTRICTED_DOMAINS_PREF);
  Preferences::AddStrongObserver(this, QUARANTINED_DOMAINS_PREF);
  Preferences::AddStrongObserver(this, QUARANTINED_DOMAINS_ENABLED);
}

void ExtensionPolicyService::UnregisterObservers() {
  mObs->RemoveObserver(this, kDocElementInserted);
  if (XRE_IsContentProcess()) {
    mObs->RemoveObserver(this, "http-on-opening-request");
    mObs->RemoveObserver(this, "document-on-opening-request");
  }

  Preferences::RemoveObserver(this, DEFAULT_CSP_PREF);
  Preferences::RemoveObserver(this, DEFAULT_CSP_PREF_V3);
  Preferences::RemoveObserver(this, RESTRICTED_DOMAINS_PREF);
  Preferences::RemoveObserver(this, QUARANTINED_DOMAINS_PREF);
  Preferences::RemoveObserver(this, QUARANTINED_DOMAINS_ENABLED);
}

nsresult ExtensionPolicyService::Observe(nsISupports* aSubject,
                                         const char* aTopic,
                                         const char16_t* aData) {
  if (!strcmp(aTopic, kDocElementInserted)) {
    nsCOMPtr<Document> doc = do_QueryInterface(aSubject);
    if (doc) {
      CheckDocument(doc);
    }
  } else if (!strcmp(aTopic, "http-on-opening-request") ||
             !strcmp(aTopic, "document-on-opening-request")) {
    nsCOMPtr<nsIChannel> chan = do_QueryInterface(aSubject);
    if (chan) {
      CheckRequest(chan);
    }
  } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
    const nsCString converted = NS_ConvertUTF16toUTF8(aData);
    const char* pref = converted.get();
    if (!strcmp(pref, DEFAULT_CSP_PREF)) {
      mDefaultCSP.SetIsVoid(true);
    } else if (!strcmp(pref, DEFAULT_CSP_PREF_V3)) {
      mDefaultCSPV3.SetIsVoid(true);
    } else if (!strcmp(pref, RESTRICTED_DOMAINS_PREF)) {
      UpdateRestrictedDomains();
    } else if (!strcmp(pref, QUARANTINED_DOMAINS_PREF) ||
               !strcmp(pref, QUARANTINED_DOMAINS_ENABLED)) {
      UpdateQuarantinedDomains();
    }
  }
  return NS_OK;
}

already_AddRefed<Promise> ExtensionPolicyService::ExecuteContentScript(
    nsPIDOMWindowInner* aWindow, WebExtensionContentScript& aScript) {
  if (!aWindow->IsCurrentInnerWindow()) {
    return nullptr;
  }

  RefPtr<Promise> promise;
  ProcessScript().LoadContentScript(&aScript, aWindow, getter_AddRefs(promise));
  return promise.forget();
}

RefPtr<Promise> ExtensionPolicyService::ExecuteContentScripts(
    JSContext* aCx, nsPIDOMWindowInner* aWindow,
    const nsTArray<RefPtr<WebExtensionContentScript>>& aScripts) {
  AutoTArray<RefPtr<Promise>, 8> promises;

  for (auto& script : aScripts) {
    if (RefPtr<Promise> promise = ExecuteContentScript(aWindow, *script)) {
      promises.AppendElement(std::move(promise));
    }
  }

  RefPtr<Promise> promise = Promise::All(aCx, promises, IgnoreErrors());
  MOZ_RELEASE_ASSERT(promise);
  return promise;
}

// Use browser's MessageManagerGroup to decide if we care about it, to inject
// extension APIs or content scripts.  Tabs use "browsers", and all custom
// extension browsers use "webext-browsers", including popups & sidebars,
// background & options pages, and xpcshell tests.
static bool IsTabOrExtensionBrowser(dom::BrowsingContext* aBC) {
  const auto& group = aBC->Top()->GetMessageManagerGroup();
  bool rv = group == u"browsers"_ns || group == u"webext-browsers"_ns;

#ifdef MOZ_THUNDERBIRD
  // ...unless it's Thunderbird, which has extra groups for unrelated reasons.
  rv = rv || group == u"single-site"_ns || group == u"single-page"_ns;
#endif

  return rv;
}

static nsTArray<RefPtr<dom::BrowsingContext>> GetAllInProcessContentBCs() {
  nsTArray<RefPtr<dom::BrowsingContext>> contentBCs;
  nsTArray<RefPtr<dom::BrowsingContextGroup>> groups;
  dom::BrowsingContextGroup::GetAllGroups(groups);
  for (const auto& group : groups) {
    for (const auto& toplevel : group->Toplevels()) {
      if (!toplevel->IsContent() || toplevel->IsDiscarded() ||
          !IsTabOrExtensionBrowser(toplevel)) {
        continue;
      }

      toplevel->PreOrderWalk([&](dom::BrowsingContext* aContext) {
        contentBCs.AppendElement(aContext);
      });
    }
  }
  return contentBCs;
}

nsresult ExtensionPolicyService::InjectContentScripts(
    WebExtensionPolicy* aExtension) {
  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));

  auto contentBCs = GetAllInProcessContentBCs();
  for (dom::BrowsingContext* bc : contentBCs) {
    auto* win = bc->GetDOMWindow();

    if (bc->Top()->IsDiscarded() || !win || !win->GetDocumentURI()) {
      continue;
    }
    DocInfo docInfo(win);

    using RunAt = dom::ContentScriptRunAt;
    namespace RunAtValues = dom::ContentScriptRunAtValues;
    using Scripts = AutoTArray<RefPtr<WebExtensionContentScript>, 8>;

    Scripts scripts[RunAtValues::Count];

    auto GetScripts = [&](RunAt aRunAt) -> Scripts&& {
      static_assert(sizeof(aRunAt) == 1, "Our cast is wrong");
      return std::move(scripts[uint8_t(aRunAt)]);
    };

    for (const auto& script : aExtension->ContentScripts()) {
      if (script->Matches(docInfo)) {
        GetScripts(script->RunAt()).AppendElement(script);
      }
    }

    nsCOMPtr<nsPIDOMWindowInner> inner = win->GetCurrentInnerWindow();

    MOZ_TRY(ExecuteContentScripts(jsapi.cx(), inner,
                                  GetScripts(RunAt::Document_start))
                ->ThenWithCycleCollectedArgs(
                    [](JSContext* aCx, JS::Handle<JS::Value> aValue,
                       ErrorResult& aRv, ExtensionPolicyService* aSelf,
                       nsPIDOMWindowInner* aInner, Scripts&& aScripts) {
                      return aSelf->ExecuteContentScripts(aCx, aInner, aScripts)
                          .forget();
                    },
                    this, inner, GetScripts(RunAt::Document_end))
                .andThen([&](auto aPromise) {
                  return aPromise->ThenWithCycleCollectedArgs(
                      [](JSContext* aCx, JS::Handle<JS::Value> aValue,
                         ErrorResult& aRv, ExtensionPolicyService* aSelf,
                         nsPIDOMWindowInner* aInner, Scripts&& aScripts) {
                        return aSelf
                            ->ExecuteContentScripts(aCx, aInner, aScripts)
                            .forget();
                      },
                      this, inner, GetScripts(RunAt::Document_idle));
                }));
  }
  return NS_OK;
}

// Checks a request for matching content scripts, and begins pre-loading them
// if necessary.
void ExtensionPolicyService::CheckRequest(nsIChannel* aChannel) {
  nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
  auto loadType = loadInfo->GetExternalContentPolicyType();
  if (loadType != ExtContentPolicy::TYPE_DOCUMENT &&
      loadType != ExtContentPolicy::TYPE_SUBDOCUMENT) {
    return;
  }

  nsCOMPtr<nsIURI> uri;
  if (NS_FAILED(aChannel->GetURI(getter_AddRefs(uri)))) {
    return;
  }

  CheckContentScripts({uri.get(), loadInfo}, true);
}

static bool CheckParentFrames(nsPIDOMWindowOuter* aWindow,
                              WebExtensionPolicy& aPolicy) {
  nsCOMPtr<nsIURI> aboutAddons;
  if (NS_FAILED(NS_NewURI(getter_AddRefs(aboutAddons), "about:addons"))) {
    return false;
  }
  nsCOMPtr<nsIURI> htmlAboutAddons;
  if (NS_FAILED(
          NS_NewURI(getter_AddRefs(htmlAboutAddons),
                    "chrome://mozapps/content/extensions/aboutaddons.html"))) {
    return false;
  }

  dom::WindowContext* wc = aWindow->GetCurrentInnerWindow()->GetWindowContext();
  while ((wc = wc->GetParentWindowContext())) {
    if (!wc->IsInProcess()) {
      return false;
    }

    nsGlobalWindowInner* win = wc->GetInnerWindow();

    auto* principal = BasePrincipal::Cast(win->GetPrincipal());
    if (principal->IsSystemPrincipal()) {
      // The add-on manager is a special case, since it contains extension
      // options pages in same-type <browser> frames.
      nsIURI* uri = win->GetDocumentURI();
      bool equals;
      if ((NS_SUCCEEDED(uri->Equals(aboutAddons, &equals)) && equals) ||
          (NS_SUCCEEDED(uri->Equals(htmlAboutAddons, &equals)) && equals)) {
        return true;
      }
    }

    if (principal->AddonPolicy() != &aPolicy) {
      return false;
    }
  }

  return true;
}

// Checks a document, just after the document element has been inserted, for
// matching content scripts or extension principals, and loads them if
// necessary.
void ExtensionPolicyService::CheckDocument(Document* aDocument) {
  nsCOMPtr<nsPIDOMWindowOuter> win = aDocument->GetWindow();
  if (win) {
    if (!IsTabOrExtensionBrowser(win->GetBrowsingContext())) {
      return;
    }

    if (win->GetDocumentURI()) {
      CheckContentScripts(win.get(), false);
    }

    nsIPrincipal* principal = aDocument->NodePrincipal();

    RefPtr<WebExtensionPolicy> policy =
        BasePrincipal::Cast(principal)->AddonPolicy();
    if (policy) {
      bool privileged = IsExtensionProcess() && CheckParentFrames(win, *policy);

      ProcessScript().InitExtensionDocument(policy, aDocument, privileged);
    }
  }
}

void ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo,
                                                 bool aIsPreload) {
  nsCOMPtr<nsPIDOMWindowInner> win;
  if (!aIsPreload) {
    win = aDocInfo.GetWindow()->GetCurrentInnerWindow();
  }

  nsTArray<RefPtr<WebExtensionContentScript>> scriptsToLoad;

  for (RefPtr<WebExtensionPolicy> policy : mExtensions.Values()) {
    for (auto& script : policy->ContentScripts()) {
      if (script->Matches(aDocInfo)) {
        if (aIsPreload) {
          ProcessScript().PreloadContentScript(script);
        } else {
          // Collect the content scripts to load instead of loading them
          // right away (to prevent a loaded content script from being
          // able to invalidate the iterator by triggering a call to
          // policy->UnregisterContentScript while we are still iterating
          // over all its content scripts). See Bug 1593240.
          scriptsToLoad.AppendElement(script);
        }
      }
    }

    for (auto& script : scriptsToLoad) {
      if (!win->IsCurrentInnerWindow()) {
        break;
      }

      RefPtr<Promise> promise;
      ProcessScript().LoadContentScript(script, win, getter_AddRefs(promise));
    }

    scriptsToLoad.ClearAndRetainStorage();
  }

  for (RefPtr<DocumentObserver> observer : mObservers.Values()) {
    for (auto& matcher : observer->Matchers()) {
      if (matcher->Matches(aDocInfo)) {
        if (aIsPreload) {
          observer->NotifyMatch(*matcher, aDocInfo.GetLoadInfo());
        } else {
          observer->NotifyMatch(*matcher, aDocInfo.GetWindow());
        }
      }
    }
  }
}

/* static */
RefPtr<AtomSet> ExtensionPolicyService::RestrictedDomains() {
  StaticAutoReadLock lock(sEPSLock);
  return sRestrictedDomains;
}

/* static */
RefPtr<AtomSet> ExtensionPolicyService::QuarantinedDomains() {
  StaticAutoReadLock lock(sEPSLock);
  return sQuarantinedDomains;
}

void ExtensionPolicyService::UpdateRestrictedDomains() {
  nsAutoCString eltsString;
  Unused << Preferences::GetCString(RESTRICTED_DOMAINS_PREF, eltsString);

  AutoTArray<nsString, 32> elts;
  for (const nsACString& elt : eltsString.Split(',')) {
    elts.AppendElement(NS_ConvertUTF8toUTF16(elt));
    elts.LastElement().StripWhitespace();
  }
  RefPtr<AtomSet> atomSet = new AtomSet(elts);

  StaticAutoWriteLock lock(sEPSLock);
  sRestrictedDomains = atomSet;
}

void ExtensionPolicyService::UpdateQuarantinedDomains() {
  if (!Preferences::GetBool(QUARANTINED_DOMAINS_ENABLED)) {
    StaticAutoWriteLock lock(sEPSLock);
    sQuarantinedDomains = nullptr;
    return;
  }

  nsAutoCString eltsString;
  AutoTArray<nsString, 32> elts;
  if (NS_SUCCEEDED(
          Preferences::GetCString(QUARANTINED_DOMAINS_PREF, eltsString))) {
    for (const nsACString& elt : eltsString.Split(',')) {
      elts.AppendElement(NS_ConvertUTF8toUTF16(elt));
      elts.LastElement().StripWhitespace();
    }
  }
  RefPtr<AtomSet> atomSet = new AtomSet(elts);

  StaticAutoWriteLock lock(sEPSLock);
  sQuarantinedDomains = atomSet;
}

/*****************************************************************************
 * nsIAddonPolicyService
 *****************************************************************************/

nsresult ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP) {
  if (mDefaultCSP.IsVoid()) {
    nsresult rv = Preferences::GetString(DEFAULT_CSP_PREF, mDefaultCSP);
    if (NS_FAILED(rv)) {
      mDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP);
    }
    mDefaultCSP.SetIsVoid(false);
  }

  aDefaultCSP.Assign(mDefaultCSP);
  return NS_OK;
}

nsresult ExtensionPolicyService::GetDefaultCSPV3(nsAString& aDefaultCSP) {
  if (mDefaultCSPV3.IsVoid()) {
    nsresult rv = Preferences::GetString(DEFAULT_CSP_PREF_V3, mDefaultCSPV3);
    if (NS_FAILED(rv)) {
      mDefaultCSPV3.AssignLiteral(DEFAULT_DEFAULT_CSP_V3);
    }
    mDefaultCSPV3.SetIsVoid(false);
  }

  aDefaultCSP.Assign(mDefaultCSPV3);
  return NS_OK;
}

nsresult ExtensionPolicyService::GetBaseCSP(const nsAString& aAddonId,
                                            nsAString& aResult) {
  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
    policy->GetBaseCSP(aResult);
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::GetExtensionPageCSP(const nsAString& aAddonId,
                                                     nsAString& aResult) {
  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
    policy->GetExtensionPageCSP(aResult);
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::GetGeneratedBackgroundPageUrl(
    const nsACString& aHostname, nsACString& aResult) {
  if (WebExtensionPolicy* policy = GetByHost(aHostname)) {
    nsAutoCString url("data:text/html,");

    nsCString html = policy->BackgroundPageHTML();
    nsAutoCString escaped;

    url.Append(NS_EscapeURL(html, esc_Minimal, escaped));

    aResult = url;
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::AddonHasPermission(const nsAString& aAddonId,
                                                    const nsAString& aPerm,
                                                    bool* aResult) {
  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
    *aResult = policy->HasPermission(aPerm);
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::AddonMayLoadURI(const nsAString& aAddonId,
                                                 nsIURI* aURI, bool aExplicit,
                                                 bool* aResult) {
  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
    *aResult = policy->CanAccessURI(aURI, aExplicit);
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::GetExtensionName(const nsAString& aAddonId,
                                                  nsAString& aName) {
  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
    aName.Assign(policy->Name());
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::SourceMayLoadExtensionURI(
    nsIURI* aSourceURI, nsIURI* aExtensionURI, bool* aResult) {
  URLInfo source(aSourceURI);
  URLInfo url(aExtensionURI);
  if (WebExtensionPolicy* policy = GetByURL(url)) {
    *aResult = policy->SourceMayAccessPath(source, url.FilePath());
    return NS_OK;
  }
  return NS_ERROR_INVALID_ARG;
}

nsresult ExtensionPolicyService::ExtensionURIToAddonId(nsIURI* aURI,
                                                       nsAString& aResult) {
  if (WebExtensionPolicy* policy = GetByURL(aURI)) {
    policy->GetId(aResult);
  } else {
    aResult.SetIsVoid(true);
  }
  return NS_OK;
}

NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mObservers)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService)
  NS_INTERFACE_MAP_ENTRY(nsIAddonPolicyService)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsIMemoryReporter)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAddonPolicyService)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPolicyService)

}  // namespace mozilla
