/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/Attributes.h"
#include "mozilla/DebugOnly.h"

#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/Services.h"
#include "mozilla/SystemGroup.h"
#include "mozilla/Unused.h"
#include "nsPermissionManager.h"
#include "nsPermission.h"
#include "nsCRT.h"
#include "nsNetUtil.h"
#include "nsCOMArray.h"
#include "nsArrayEnumerator.h"
#include "nsTArray.h"
#include "nsReadableUtils.h"
#include "nsILineInputStream.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceDefs.h"
#include "mozilla/storage.h"
#include "mozilla/Attributes.h"
#include "nsXULAppAPI.h"
#include "nsIPrincipal.h"
#include "nsContentUtils.h"
#include "nsIScriptSecurityManager.h"
#include "nsIEffectiveTLDService.h"
#include "nsPIDOMWindow.h"
#include "nsIDocument.h"
#include "mozilla/net/NeckoMessageUtils.h"
#include "mozilla/Preferences.h"
#include "nsReadLine.h"
#include "mozilla/Telemetry.h"
#include "nsIConsoleService.h"
#include "nsINavHistoryService.h"
#include "nsToolkitCompsCID.h"
#include "nsIObserverService.h"
#include "nsPrintfCString.h"
#include "mozilla/AbstractThread.h"
#include "ContentPrincipal.h"

static nsPermissionManager *gPermissionManager = nullptr;

using namespace mozilla;
using namespace mozilla::dom;

static bool
IsChildProcess()
{
  return XRE_IsContentProcess();
}

static void
LogToConsole(const nsAString& aMsg)
{
  nsCOMPtr<nsIConsoleService> console(do_GetService("@mozilla.org/consoleservice;1"));
  if (!console) {
    NS_WARNING("Failed to log message to console.");
    return;
  }

  nsAutoString msg(aMsg);
  console->LogStringMessage(msg.get());
}

#define ENSURE_NOT_CHILD_PROCESS_(onError) \
  PR_BEGIN_MACRO \
  if (IsChildProcess()) { \
    NS_ERROR("Cannot perform action in content process!"); \
    onError \
  } \
  PR_END_MACRO

#define ENSURE_NOT_CHILD_PROCESS \
  ENSURE_NOT_CHILD_PROCESS_({ return NS_ERROR_NOT_AVAILABLE; })

#define ENSURE_NOT_CHILD_PROCESS_NORET \
  ENSURE_NOT_CHILD_PROCESS_(;)

////////////////////////////////////////////////////////////////////////////////

namespace {

// The number of permissions from the kPreloadPermissions list which are present
// in the permission manager. Used to determine if the permission manager should
// be checked for one of these preload permissions in nsContentBlocker.
static int32_t sPreloadPermissionCount = 0;

// These permissions are special permissions which must be transmitted to the
// content process before documents with their principals have loaded within
// that process. This is because these permissions are used for content
// blocking in nsContentBlocker.
//
// Permissions which are in this list are considered to have a "" permission
// key, even if their principal would not normally have that key.
static const char* kPreloadPermissions[] = {
  // NOTE: These permissions are the different nsContentBlocker permissions for
  // allowing or denying certain content types from being loaded. Every
  // permission listed in the `kTypeString` array in nsContentBlocker.cpp should
  // appear in this list.
  "other",
  "script",
  "image",
  "stylesheet",
  "object",
  "document",
  "subdocument",
  "refresh",
  "xbl",
  "ping",
  "xmlhttprequest",
  "objectsubrequest",
  "dtd",
  "font",
  "media",
  "websocket",
  "csp_report",
  "xslt",
  "beacon",
  "fetch",
  "image",
  "manifest"
};

// NOTE: nullptr can be passed as aType - if it is this function will return
// "false" unconditionally.
bool
IsPreloadPermission(const char* aType)
{
  if (aType) {
    for (uint32_t i = 0; i < mozilla::ArrayLength(kPreloadPermissions); ++i) {
      if (!strcmp(aType, kPreloadPermissions[i])) {
        return true;
      }
    }
  }

  return false;
}

nsresult
GetOriginFromPrincipal(nsIPrincipal* aPrincipal, nsACString& aOrigin)
{
  nsresult rv = aPrincipal->GetOriginNoSuffix(aOrigin);
  // The principal may belong to the about:blank content viewer, so this can be
  // expected to fail.
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsAutoCString suffix;
  rv = aPrincipal->GetOriginSuffix(suffix);
  NS_ENSURE_SUCCESS(rv, rv);

  mozilla::OriginAttributes attrs;
  if (!attrs.PopulateFromSuffix(suffix)) {
    return NS_ERROR_FAILURE;
  }

  // mPrivateBrowsingId must be set to false because PermissionManager is not supposed to have
  // any knowledge of private browsing. Allowing it to be true changes the suffix being hashed.
  attrs.mPrivateBrowsingId = 0;

  // Disable userContext and firstParty isolation for permissions.
  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);

  attrs.CreateSuffix(suffix);
  aOrigin.Append(suffix);
  return NS_OK;
}

nsresult
GetPrincipalFromOrigin(const nsACString& aOrigin, nsIPrincipal** aPrincipal)
{
  nsAutoCString originNoSuffix;
  mozilla::OriginAttributes attrs;
  if (!attrs.PopulateFromOrigin(aOrigin, originNoSuffix)) {
    return NS_ERROR_FAILURE;
  }

  // mPrivateBrowsingId must be set to false because PermissionManager is not supposed to have
  // any knowledge of private browsing. Allowing it to be true changes the suffix being hashed.
  attrs.mPrivateBrowsingId = 0;

  // Disable userContext and firstParty isolation for permissions.
  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIPrincipal> principal = mozilla::BasePrincipal::CreateCodebasePrincipal(uri, attrs);
  principal.forget(aPrincipal);
  return NS_OK;
}

nsresult
GetPrincipal(nsIURI* aURI, uint32_t aAppId, bool aIsInIsolatedMozBrowserElement, nsIPrincipal** aPrincipal)
{
  mozilla::OriginAttributes attrs(aAppId, aIsInIsolatedMozBrowserElement);
  nsCOMPtr<nsIPrincipal> principal = mozilla::BasePrincipal::CreateCodebasePrincipal(aURI, attrs);
  NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);

  principal.forget(aPrincipal);
  return NS_OK;
}

nsresult
GetPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal)
{
  mozilla::OriginAttributes attrs;
  nsCOMPtr<nsIPrincipal> principal = mozilla::BasePrincipal::CreateCodebasePrincipal(aURI, attrs);
  NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);

  principal.forget(aPrincipal);
  return NS_OK;
}

nsCString
GetNextSubDomainForHost(const nsACString& aHost)
{
  nsCOMPtr<nsIEffectiveTLDService> tldService =
    do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
  if (!tldService) {
    NS_ERROR("Should have a tld service!");
    return EmptyCString();
  }

  nsCString subDomain;
  nsresult rv = tldService->GetNextSubDomain(aHost, subDomain);
  // We can fail if there is no more subdomain or if the host can't have a
  // subdomain.
  if (NS_FAILED(rv)) {
    return EmptyCString();
  }

  return subDomain;
}

// This function produces a nsIURI which is identical to the current
// nsIURI, except that it has one less subdomain segment. It returns
// `nullptr` if there are no more segments to remove.
already_AddRefed<nsIURI>
GetNextSubDomainURI(nsIURI* aURI)
{
  nsAutoCString host;
  nsresult rv = aURI->GetHost(host);
  if (NS_FAILED(rv)) {
    return nullptr;
  }

  nsCString domain = GetNextSubDomainForHost(host);
  if (domain.IsEmpty()) {
    return nullptr;
  }

  nsCOMPtr<nsIURI> uri;
  rv = aURI->Clone(getter_AddRefs(uri));
  if (NS_FAILED(rv) || !uri) {
    return nullptr;
  }

  rv = uri->SetHost(domain);
  if (NS_FAILED(rv)) {
    return nullptr;
  }

  return uri.forget();
}

// This function produces a nsIPrincipal which is identical to the current
// nsIPrincipal, except that it has one less subdomain segment. It returns
// `nullptr` if there are no more segments to remove.
already_AddRefed<nsIPrincipal>
GetNextSubDomainPrincipal(nsIPrincipal* aPrincipal)
{
  nsCOMPtr<nsIURI> uri;
  nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri));
  if (NS_FAILED(rv) || !uri) {
    return nullptr;
  }

  // Create a new principal which is identical to the current one, but with the new host
  nsCOMPtr<nsIURI> newURI = GetNextSubDomainURI(uri);
  if (!newURI) {
    return nullptr;
  }

  // Copy the attributes over
  mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef();

  // Disable userContext and firstParty isolation for permissions.
  attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID |
                        mozilla::OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);

  nsCOMPtr<nsIPrincipal> principal =
    mozilla::BasePrincipal::CreateCodebasePrincipal(newURI, attrs);

  return principal.forget();
}

class ClearOriginDataObserver final : public nsIObserver {
  ~ClearOriginDataObserver() {}

public:
  NS_DECL_ISUPPORTS

  // nsIObserver implementation.
  NS_IMETHOD
  Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) override
  {
    MOZ_ASSERT(!nsCRT::strcmp(aTopic, "clear-origin-attributes-data"));

    nsCOMPtr<nsIPermissionManager> permManager = do_GetService("@mozilla.org/permissionmanager;1");
    return permManager->RemovePermissionsWithAttributes(nsDependentString(aData));
  }
};

NS_IMPL_ISUPPORTS(ClearOriginDataObserver, nsIObserver)

class MOZ_STACK_CLASS UpgradeHostToOriginHelper {
public:
  virtual nsresult Insert(const nsACString& aOrigin, const nsCString& aType,
                          uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
                          int64_t aModificationTime) = 0;
};

class MOZ_STACK_CLASS UpgradeHostToOriginDBMigration final : public UpgradeHostToOriginHelper {
public:
  UpgradeHostToOriginDBMigration(mozIStorageConnection* aDBConn, int64_t* aID) : mDBConn(aDBConn)
                                                                               , mID(aID)
  {
    mDBConn->CreateStatement(NS_LITERAL_CSTRING(
      "INSERT INTO moz_hosts_new "
      "(id, origin, type, permission, expireType, expireTime, modificationTime) "
      "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmt));
  }

  nsresult
  Insert(const nsACString& aOrigin, const nsCString& aType,
         uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
         int64_t aModificationTime) final
  {
    nsresult rv = mStmt->BindInt64ByIndex(0, *mID);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindUTF8StringByIndex(1, aOrigin);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindUTF8StringByIndex(2, aType);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt32ByIndex(3, aPermission);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt32ByIndex(4, aExpireType);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt64ByIndex(5, aExpireTime);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt64ByIndex(6, aModificationTime);
    NS_ENSURE_SUCCESS(rv, rv);

    // Increment the working identifier, as we are about to use this one
    (*mID)++;

    rv = mStmt->Execute();
    NS_ENSURE_SUCCESS(rv, rv);

    return NS_OK;
  }

private:
  nsCOMPtr<mozIStorageStatement> mStmt;
  nsCOMPtr<mozIStorageConnection> mDBConn;
  int64_t* mID;
};

class MOZ_STACK_CLASS UpgradeHostToOriginHostfileImport final : public UpgradeHostToOriginHelper {
public:
  UpgradeHostToOriginHostfileImport(nsPermissionManager* aPm,
                                    nsPermissionManager::DBOperationType aOperation,
                                    int64_t aID) : mPm(aPm)
                                                 , mOperation(aOperation)
                                                 , mID(aID)
  {}

  nsresult
  Insert(const nsACString& aOrigin, const nsCString& aType,
         uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
         int64_t aModificationTime) final
  {
    nsCOMPtr<nsIPrincipal> principal;
    nsresult rv = GetPrincipalFromOrigin(aOrigin, getter_AddRefs(principal));
    NS_ENSURE_SUCCESS(rv, rv);

    return mPm->AddInternal(principal, aType, aPermission, mID,
                            aExpireType, aExpireTime, aModificationTime,
                            nsPermissionManager::eDontNotify, mOperation);
  }

private:
  RefPtr<nsPermissionManager> mPm;
  nsPermissionManager::DBOperationType mOperation;
  int64_t mID;
};

class MOZ_STACK_CLASS UpgradeIPHostToOriginDB final : public UpgradeHostToOriginHelper {
public:
  UpgradeIPHostToOriginDB(mozIStorageConnection* aDBConn, int64_t* aID) : mDBConn(aDBConn)
                                                                        , mID(aID)
  {
    mDBConn->CreateStatement(NS_LITERAL_CSTRING(
      "INSERT INTO moz_perms"
      "(id, origin, type, permission, expireType, expireTime, modificationTime) "
      "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmt));

    mDBConn->CreateStatement(NS_LITERAL_CSTRING(
      "SELECT id FROM moz_perms WHERE origin = ?1 AND type = ?2"),
      getter_AddRefs(mLookupStmt));
  }

  nsresult
  Insert(const nsACString& aOrigin, const nsCString& aType,
         uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
         int64_t aModificationTime) final
  {
    // Every time the migration code wants to insert an origin into
    // the database we need to check to see if someone has already
    // created a permissions entry for that permission. If they have,
    // we don't want to insert a duplicate row.
    //
    // We can afford to do this lookup unconditionally and not perform
    // caching, as a origin type pair should only be attempted to be
    // inserted once.

    nsresult rv = mLookupStmt->Reset();
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mLookupStmt->BindUTF8StringByIndex(0, aOrigin);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mLookupStmt->BindUTF8StringByIndex(1, aType);
    NS_ENSURE_SUCCESS(rv, rv);

    // Check if we already have the row in the database, if we do, then
    // we don't want to be inserting it again.
    bool moreStmts = false;
    if (NS_FAILED(mLookupStmt->ExecuteStep(&moreStmts)) || moreStmts) {
      mLookupStmt->Reset();
      NS_WARNING("A permissions entry was going to be re-migrated, "
                 "but was already found in the permissions database.");
      return NS_OK;
    }

    // Actually insert the statement into the database.
    rv = mStmt->BindInt64ByIndex(0, *mID);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindUTF8StringByIndex(1, aOrigin);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindUTF8StringByIndex(2, aType);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt32ByIndex(3, aPermission);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt32ByIndex(4, aExpireType);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt64ByIndex(5, aExpireTime);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mStmt->BindInt64ByIndex(6, aModificationTime);
    NS_ENSURE_SUCCESS(rv, rv);

    // Increment the working identifier, as we are about to use this one
    (*mID)++;

    rv = mStmt->Execute();
    NS_ENSURE_SUCCESS(rv, rv);

    return NS_OK;
  }

private:
  nsCOMPtr<mozIStorageStatement> mStmt;
  nsCOMPtr<mozIStorageStatement> mLookupStmt;
  nsCOMPtr<mozIStorageConnection> mDBConn;
  int64_t* mID;
};


nsresult
UpgradeHostToOriginAndInsert(const nsACString& aHost, const nsCString& aType,
                             uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
                             int64_t aModificationTime, uint32_t aAppId, bool aIsInIsolatedMozBrowserElement,
                             UpgradeHostToOriginHelper* aHelper)
{
  if (aHost.EqualsLiteral("<file>")) {
    // We no longer support the magic host <file>
    NS_WARNING("The magic host <file> is no longer supported. "
               "It is being removed from the permissions database.");
    return NS_OK;
  }

  // First, we check to see if the host is a valid URI. If it is, it can be imported directly
  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), aHost);
  if (NS_SUCCEEDED(rv)) {
    // It was previously possible to insert useless entries to your permissions database
    // for URIs which have a null principal. This acts as a cleanup, getting rid of
    // these useless database entries
    bool nullpScheme = false;
    if (NS_SUCCEEDED(uri->SchemeIs("moz-nullprincipal", &nullpScheme)) && nullpScheme) {
      NS_WARNING("A moz-nullprincipal: permission is being discarded.");
      return NS_OK;
    }

    nsCOMPtr<nsIPrincipal> principal;
    rv = GetPrincipal(uri, aAppId, aIsInIsolatedMozBrowserElement, getter_AddRefs(principal));
    NS_ENSURE_SUCCESS(rv, rv);

    nsAutoCString origin;
    rv = GetOriginFromPrincipal(principal, origin);
    NS_ENSURE_SUCCESS(rv, rv);

    return aHelper->Insert(origin, aType, aPermission,
                           aExpireType, aExpireTime, aModificationTime);
    return NS_OK;
  }

  // The user may use this host at non-standard ports or protocols, we can use their history
  // to guess what ports and protocols we want to add permissions for.
  // We find every URI which they have visited with this host (or a subdomain of this host),
  // and try to add it as a principal.
  bool foundHistory = false;

  nsCOMPtr<nsINavHistoryService> histSrv = do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);

  if (histSrv) {
    nsCOMPtr<nsINavHistoryQuery> histQuery;
    rv = histSrv->GetNewQuery(getter_AddRefs(histQuery));
    NS_ENSURE_SUCCESS(rv, rv);

    // Get the eTLD+1 of the domain
    nsAutoCString eTLD1;
    nsCOMPtr<nsIEffectiveTLDService> tldService =
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
    MOZ_ASSERT(tldService); // We should always have a tldService
    if (tldService) {
      rv = tldService->GetBaseDomainFromHost(aHost, 0, eTLD1);
    }

    if (!tldService || NS_FAILED(rv)) {
      // If the lookup on the tldService for the base domain for the host failed,
      // that means that we just want to directly use the host as the host name
      // for the lookup.
      eTLD1 = aHost;
    }

    // We want to only find history items for this particular eTLD+1, and subdomains
    rv = histQuery->SetDomain(eTLD1);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = histQuery->SetDomainIsHost(false);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts;
    rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts));
    NS_ENSURE_SUCCESS(rv, rv);

    // We want to get the URIs for every item in the user's history with the given host
    rv = histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_URI);
    NS_ENSURE_SUCCESS(rv, rv);

    // We only search history, because searching both bookmarks and history
    // is not supported, and history tends to be more comprehensive.
    rv = histQueryOpts->SetQueryType(nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY);
    NS_ENSURE_SUCCESS(rv, rv);

    // We include hidden URIs (such as those visited via iFrames) as they may have permissions too
    rv = histQueryOpts->SetIncludeHidden(true);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsINavHistoryResult> histResult;
    rv = histSrv->ExecuteQuery(histQuery, histQueryOpts, getter_AddRefs(histResult));
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsINavHistoryContainerResultNode> histResultContainer;
    rv = histResult->GetRoot(getter_AddRefs(histResultContainer));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = histResultContainer->SetContainerOpen(true);
    NS_ENSURE_SUCCESS(rv, rv);

    uint32_t childCount = 0;
    rv = histResultContainer->GetChildCount(&childCount);
    NS_ENSURE_SUCCESS(rv, rv);

    nsTHashtable<nsCStringHashKey> insertedOrigins;
    for (uint32_t i = 0; i < childCount; i++) {
      nsCOMPtr<nsINavHistoryResultNode> child;
      histResultContainer->GetChild(i, getter_AddRefs(child));
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      uint32_t type;
      rv = child->GetType(&type);
      if (NS_WARN_IF(NS_FAILED(rv)) || type != nsINavHistoryResultNode::RESULT_TYPE_URI) {
        NS_WARNING("Unexpected non-RESULT_TYPE_URI node in "
                   "UpgradeHostToOriginAndInsert()");
        continue;
      }

      nsAutoCString uriSpec;
      rv = child->GetUri(uriSpec);
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      nsCOMPtr<nsIURI> uri;
      rv = NS_NewURI(getter_AddRefs(uri), uriSpec);
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      // Use the provided host - this URI may be for a subdomain, rather than the host we care about.
      rv = uri->SetHost(aHost);
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      // We now have a URI which we can make a nsIPrincipal out of
      nsCOMPtr<nsIPrincipal> principal;
      rv = GetPrincipal(uri, aAppId, aIsInIsolatedMozBrowserElement, getter_AddRefs(principal));
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      nsAutoCString origin;
      rv = GetOriginFromPrincipal(principal, origin);
      if (NS_WARN_IF(NS_FAILED(rv))) continue;

      // Ensure that we don't insert the same origin repeatedly
      if (insertedOrigins.Contains(origin)) {
        continue;
      }

      foundHistory = true;
      rv = aHelper->Insert(origin, aType, aPermission,
                           aExpireType, aExpireTime, aModificationTime);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Insert failed");
      insertedOrigins.PutEntry(origin);
    }

    rv = histResultContainer->SetContainerOpen(false);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // If we didn't find any origins for this host in the poermissions database,
  // we can insert the default http:// and https:// permissions into the database.
  // This has a relatively high liklihood of applying the permission to the correct
  // origin.
  if (!foundHistory) {
    nsAutoCString hostSegment;
    nsCOMPtr<nsIPrincipal> principal;
    nsAutoCString origin;

    // If this is an ipv6 URI, we need to surround it in '[', ']' before trying to
    // parse it as a URI.
    if (aHost.FindChar(':') != -1) {
      hostSegment.Assign("[");
      hostSegment.Append(aHost);
      hostSegment.Append("]");
    } else {
      hostSegment.Assign(aHost);
    }

    // http:// URI default
    rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("http://") + hostSegment);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GetPrincipal(uri, aAppId, aIsInIsolatedMozBrowserElement, getter_AddRefs(principal));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GetOriginFromPrincipal(principal, origin);
    NS_ENSURE_SUCCESS(rv, rv);

    aHelper->Insert(origin, aType, aPermission,
                    aExpireType, aExpireTime, aModificationTime);

    // https:// URI default
    rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + hostSegment);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GetPrincipal(uri, aAppId, aIsInIsolatedMozBrowserElement, getter_AddRefs(principal));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GetOriginFromPrincipal(principal, origin);
    NS_ENSURE_SUCCESS(rv, rv);

    aHelper->Insert(origin, aType, aPermission,
                    aExpireType, aExpireTime, aModificationTime);
  }

  return NS_OK;
}

static bool
IsExpandedPrincipal(nsIPrincipal* aPrincipal)
{
  nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(aPrincipal);
  return !!ep;
}

} // namespace

////////////////////////////////////////////////////////////////////////////////

nsPermissionManager::PermissionKey*
nsPermissionManager::PermissionKey::CreateFromPrincipal(nsIPrincipal* aPrincipal,
                                                        nsresult& aResult)
{
  nsAutoCString origin;
  aResult = GetOriginFromPrincipal(aPrincipal, origin);
  if (NS_WARN_IF(NS_FAILED(aResult))) {
    return nullptr;
  }

  return new PermissionKey(origin);
}

nsPermissionManager::PermissionKey*
nsPermissionManager::PermissionKey::CreateFromURI(nsIURI* aURI, nsresult& aResult)
{
  nsAutoCString origin;
  aResult = ContentPrincipal::GenerateOriginNoSuffixFromURI(aURI, origin);
  if (NS_WARN_IF(NS_FAILED(aResult))) {
    return nullptr;
  }

  return new PermissionKey(origin);
}

/**
 * Simple callback used by |AsyncClose| to trigger a treatment once
 * the database is closed.
 *
 * Note: Beware that, if you hold onto a |CloseDatabaseListener| from a
 * |nsPermissionManager|, this will create a cycle.
 *
 * Note: Once the callback has been called this DeleteFromMozHostListener cannot
 * be reused.
 */
class CloseDatabaseListener final : public mozIStorageCompletionCallback
{
  ~CloseDatabaseListener() {}

public:
  NS_DECL_ISUPPORTS
  NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
  /**
   * @param aManager The owning manager.
   * @param aRebuildOnSuccess If |true|, reinitialize the database once
   * it has been closed. Otherwise, do nothing such.
   */
  CloseDatabaseListener(nsPermissionManager* aManager,
                        bool aRebuildOnSuccess);

protected:
  RefPtr<nsPermissionManager> mManager;
  bool mRebuildOnSuccess;
};

NS_IMPL_ISUPPORTS(CloseDatabaseListener, mozIStorageCompletionCallback)

CloseDatabaseListener::CloseDatabaseListener(nsPermissionManager* aManager,
                                             bool aRebuildOnSuccess)
  : mManager(aManager)
  , mRebuildOnSuccess(aRebuildOnSuccess)
{
}

NS_IMETHODIMP
CloseDatabaseListener::Complete(nsresult, nsISupports*)
{
  // Help breaking cycles
  RefPtr<nsPermissionManager> manager = mManager.forget();
  if (mRebuildOnSuccess && !manager->mIsShuttingDown) {
    return manager->InitDB(true);
  }
  return NS_OK;
}


/**
 * Simple callback used by |RemoveAllInternal| to trigger closing
 * the database and reinitializing it.
 *
 * Note: Beware that, if you hold onto a |DeleteFromMozHostListener| from a
 * |nsPermissionManager|, this will create a cycle.
 *
 * Note: Once the callback has been called this DeleteFromMozHostListener cannot
 * be reused.
 */
class DeleteFromMozHostListener final : public mozIStorageStatementCallback
{
  ~DeleteFromMozHostListener() {}

public:
  NS_DECL_ISUPPORTS
  NS_DECL_MOZISTORAGESTATEMENTCALLBACK

  /**
   * @param aManager The owning manager.
   */
  explicit DeleteFromMozHostListener(nsPermissionManager* aManager);

protected:
  RefPtr<nsPermissionManager> mManager;
};

NS_IMPL_ISUPPORTS(DeleteFromMozHostListener, mozIStorageStatementCallback)

DeleteFromMozHostListener::
DeleteFromMozHostListener(nsPermissionManager* aManager)
  : mManager(aManager)
{
}

NS_IMETHODIMP DeleteFromMozHostListener::HandleResult(mozIStorageResultSet *)
{
  MOZ_CRASH("Should not get any results");
}

NS_IMETHODIMP DeleteFromMozHostListener::HandleError(mozIStorageError *)
{
  // Errors are handled in |HandleCompletion|
  return NS_OK;
}

NS_IMETHODIMP DeleteFromMozHostListener::HandleCompletion(uint16_t aReason)
{
  // Help breaking cycles
  RefPtr<nsPermissionManager> manager = mManager.forget();

  if (aReason == REASON_ERROR) {
    manager->CloseDB(true);
  }

  return NS_OK;
}

/* static */ void
nsPermissionManager::ClearOriginDataObserverInit()
{
  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();
  observerService->AddObserver(new ClearOriginDataObserver(), "clear-origin-attributes-data", /* ownsWeak= */ false);
}

////////////////////////////////////////////////////////////////////////////////
// nsPermissionManager Implementation

#define PERMISSIONS_FILE_NAME "permissions.sqlite"
#define HOSTS_SCHEMA_VERSION 9

#define HOSTPERM_FILE_NAME "hostperm.1"

// Default permissions are read from a URL - this is the preference we read
// to find that URL. If not set, don't use any default permissions.
static const char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl";

static const char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION;

NS_IMPL_ISUPPORTS(nsPermissionManager, nsIPermissionManager, nsIObserver, nsISupportsWeakReference)

nsPermissionManager::nsPermissionManager()
 : mMemoryOnlyDB(false)
 , mLargestID(0)
 , mIsShuttingDown(false)
{
}

nsPermissionManager::~nsPermissionManager()
{
  // NOTE: Make sure to reject each of the promises in mPermissionKeyPromiseMap
  // before destroying.
  for (auto iter = mPermissionKeyPromiseMap.Iter(); !iter.Done(); iter.Next()) {
    if (iter.Data()) {
      iter.Data()->Reject(NS_ERROR_FAILURE, __func__);
    }
  }
  mPermissionKeyPromiseMap.Clear();

  RemoveAllFromMemory();
  gPermissionManager = nullptr;
}

// static
nsIPermissionManager*
nsPermissionManager::GetXPCOMSingleton()
{
  if (gPermissionManager) {
    NS_ADDREF(gPermissionManager);
    return gPermissionManager;
  }

  // Create a new singleton nsPermissionManager.
  // We AddRef only once since XPCOM has rules about the ordering of module
  // teardowns - by the time our module destructor is called, it's too late to
  // Release our members, since GC cycles have already been completed and
  // would result in serious leaks.
  // See bug 209571.
  gPermissionManager = new nsPermissionManager();
  if (gPermissionManager) {
    NS_ADDREF(gPermissionManager);
    if (NS_FAILED(gPermissionManager->Init())) {
      NS_RELEASE(gPermissionManager);
    }
  }

  return gPermissionManager;
}

nsresult
nsPermissionManager::Init()
{
  // If the 'permissions.memory_only' pref is set to true, then don't write any
  // permission settings to disk, but keep them in a memory-only database.
  mMemoryOnlyDB = mozilla::Preferences::GetBool("permissions.memory_only", false);

  if (IsChildProcess()) {
    // Stop here; we don't need the DB in the child process. Instead we will be
    // sent permissions as we need them by our parent process.
    return NS_OK;
  }

  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();
  if (observerService) {
    observerService->AddObserver(this, "profile-before-change", true);
    observerService->AddObserver(this, "profile-do-change", true);
  }

  // ignore failure here, since it's non-fatal (we can run fine without
  // persistent storage - e.g. if there's no profile).
  // XXX should we tell the user about this?
  InitDB(false);

  return NS_OK;
}

nsresult
nsPermissionManager::OpenDatabase(nsIFile* aPermissionsFile)
{
  nsresult rv;
  nsCOMPtr<mozIStorageService> storage = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
  if (!storage) {
    return NS_ERROR_UNEXPECTED;
  }
  // cache a connection to the hosts database
  if (mMemoryOnlyDB) {
    rv = storage->OpenSpecialDatabase("memory", getter_AddRefs(mDBConn));
  } else {
    rv = storage->OpenDatabase(aPermissionsFile, getter_AddRefs(mDBConn));
  }
  return rv;
}

nsresult
nsPermissionManager::InitDB(bool aRemoveFile)
{
  nsCOMPtr<nsIFile> permissionsFile;
  nsresult rv = NS_GetSpecialDirectory(NS_APP_PERMISSION_PARENT_DIR, getter_AddRefs(permissionsFile));
  if (NS_FAILED(rv)) {
    rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(permissionsFile));
  }
  NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);

  rv = permissionsFile->AppendNative(NS_LITERAL_CSTRING(PERMISSIONS_FILE_NAME));
  NS_ENSURE_SUCCESS(rv, rv);

  if (aRemoveFile) {
    bool exists = false;
    rv = permissionsFile->Exists(&exists);
    NS_ENSURE_SUCCESS(rv, rv);
    if (exists) {
      rv = permissionsFile->Remove(false);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  rv = OpenDatabase(permissionsFile);
  if (rv == NS_ERROR_FILE_CORRUPTED) {
    LogToConsole(NS_LITERAL_STRING("permissions.sqlite is corrupted! Try again!"));

    // Add telemetry probe
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERMISSIONS_SQL_CORRUPTED, 1);

    // delete corrupted permissions.sqlite and try again
    rv = permissionsFile->Remove(false);
    NS_ENSURE_SUCCESS(rv, rv);
    LogToConsole(NS_LITERAL_STRING("Corrupted permissions.sqlite has been removed."));

    rv = OpenDatabase(permissionsFile);
    NS_ENSURE_SUCCESS(rv, rv);
    LogToConsole(NS_LITERAL_STRING("OpenDatabase to permissions.sqlite is successful!"));
  } else if (NS_FAILED(rv)) {
    return rv;
  }

  bool ready;
  mDBConn->GetConnectionReady(&ready);
  if (!ready) {
    LogToConsole(NS_LITERAL_STRING("Fail to get connection to permissions.sqlite! Try again!"));

    // delete and try again
    rv = permissionsFile->Remove(false);
    NS_ENSURE_SUCCESS(rv, rv);
    LogToConsole(NS_LITERAL_STRING("Defective permissions.sqlite has been removed."));

    // Add telemetry probe
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::DEFECTIVE_PERMISSIONS_SQL_REMOVED, 1);

    rv = OpenDatabase(permissionsFile);
    NS_ENSURE_SUCCESS(rv, rv);
    LogToConsole(NS_LITERAL_STRING("OpenDatabase to permissions.sqlite is successful!"));

    mDBConn->GetConnectionReady(&ready);
    if (!ready)
      return NS_ERROR_UNEXPECTED;
  }


  bool tableExists = false;
  mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &tableExists);
  if (!tableExists) {
    mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &tableExists);
  }
  if (!tableExists) {
    rv = CreateTable();
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    // table already exists; check the schema version before reading
    int32_t dbSchemaVersion;
    rv = mDBConn->GetSchemaVersion(&dbSchemaVersion);
    NS_ENSURE_SUCCESS(rv, rv);

    switch (dbSchemaVersion) {
    // upgrading.
    // every time you increment the database schema, you need to implement
    // the upgrading code from the previous version to the new one.
    // fall through to current version

    case 1:
      {
        // previous non-expiry version of database.  Upgrade it by adding the
        // expiration columns
        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_hosts ADD expireType INTEGER"));
        NS_ENSURE_SUCCESS(rv, rv);

        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_hosts ADD expireTime INTEGER"));
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // TODO: we want to make default version as version 2 in order to fix bug 784875.
    case 0:
    case 2:
      {
        // Add appId/isInBrowserElement fields.
        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_hosts ADD appId INTEGER"));
        NS_ENSURE_SUCCESS(rv, rv);

        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_hosts ADD isInBrowserElement INTEGER"));
        NS_ENSURE_SUCCESS(rv, rv);

        rv = mDBConn->SetSchemaVersion(3);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // Version 3->4 is the creation of the modificationTime field.
    case 3:
      {
        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_hosts ADD modificationTime INTEGER"));
        NS_ENSURE_SUCCESS(rv, rv);

        // We leave the modificationTime at zero for all existing records; using
        // now() would mean, eg, that doing "remove all from the last hour"
        // within the first hour after migration would remove all permissions.

        rv = mDBConn->SetSchemaVersion(4);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // In version 5, host appId, and isInBrowserElement were merged into a
    // single origin entry
    //
    // In version 6, the tables were renamed for backwards compatability reasons
    // with version 4 and earlier.
    //
    // In version 7, a bug in the migration used for version 4->5 was discovered
    // which could have triggered data-loss. Because of that, all users with a
    // version 4, 5, or 6 database will be re-migrated from the backup database.
    // (bug 1186034). This migration bug is not present after bug 1185340, and the
    // re-migration ensures that all users have the fix.
    case 5:
      // This branch could also be reached via dbSchemaVersion == 3, in which case
      // we want to fall through to the dbSchemaVersion == 4 case.
      // The easiest way to do that is to perform this extra check here to make
      // sure that we didn't get here via a fallthrough from v3
      if (dbSchemaVersion == 5) {
        // In version 5, the backup database is named moz_hosts_v4. We perform
        // the version 5->6 migration to get the tables to have consistent
        // naming conventions.

        // Version 5->6 is the renaming of moz_hosts to moz_perms, and
        // moz_hosts_v4 to moz_hosts (bug 1185343)
        //
        // In version 5, we performed the modifications to the permissions
        // database in place, this meant that if you upgraded to a version which
        // used V5, and then downgraded to a version which used v4 or earlier,
        // the fallback path would drop the table, and your permissions data
        // would be lost. This migration undoes that mistake, by restoring the
        // old moz_hosts table (if it was present), and instead using the new
        // table moz_perms for the new permissions schema.
        //
        // NOTE: If you downgrade, store new permissions, and then upgrade
        // again, these new permissions won't be migrated or reflected in the
        // updated database. This migration only occurs once, as if moz_perms
        // exists, it will skip creating it. In addition, permissions added
        // after the migration will not be visible in previous versions of
        // firefox.

        bool permsTableExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists);
        if (!permsTableExists) {
          // Move the upgraded database to moz_perms
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
                 "ALTER TABLE moz_hosts RENAME TO moz_perms"));
          NS_ENSURE_SUCCESS(rv, rv);
        } else {
          NS_WARNING("moz_hosts was not renamed to moz_perms, "
                     "as a moz_perms table already exists");

          // In the situation where a moz_perms table already exists, but the
          // schema is lower than 6, a migration has already previously occured
          // to V6, but a downgrade has caused the moz_hosts table to be
          // dropped. This should only occur in the case of a downgrade to a V5
          // database, which was only present in a few day's nightlies. As that
          // version was likely used only on a temporary basis, we assume that
          // the database from the previous V6 has the permissions which the
          // user actually wants to use. We have to get rid of moz_hosts such
          // that moz_hosts_v4 can be moved into its place if it exists.
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts"));
          NS_ENSURE_SUCCESS(rv, rv);
        }

#ifdef DEBUG
        // The moz_hosts table shouldn't exist anymore
        bool hostsTableExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists);
        MOZ_ASSERT(!hostsTableExists);
#endif

        // Rename moz_hosts_v4 back to it's original location, if it exists
        bool v4TableExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_v4"), &v4TableExists);
        if (v4TableExists) {
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
                 "ALTER TABLE moz_hosts_v4 RENAME TO moz_hosts"));
          NS_ENSURE_SUCCESS(rv, rv);
        }

        rv = mDBConn->SetSchemaVersion(6);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // At this point, the version 5 table has been migrated to a version 6 table
    // We are guaranteed to have at least one of moz_hosts and moz_perms. If
    // we have moz_hosts, we will migrate moz_hosts into moz_perms (even if
    // we already have a moz_perms, as we need a re-migration due to bug 1186034).
    //
    // After this migration, we are guaranteed to have both a moz_hosts (for backwards
    // compatability), and a moz_perms table. The moz_hosts table will have a v4 schema,
    // and the moz_perms table will have a v6 schema.
    case 4:
    case 6:
      {
        bool hostsTableExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists);
        if (hostsTableExists) {
          bool migrationError = false;

          // Both versions 4 and 6 have a version 4 formatted hosts table named
          // moz_hosts. We can migrate this table to our version 7 table moz_perms.
          // If moz_perms is present, then we can use it as a basis for comparison.

          rv = mDBConn->BeginTransaction();
          NS_ENSURE_SUCCESS(rv, rv);

          bool tableExists = false;
          mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_new"), &tableExists);
          if (tableExists) {
            NS_WARNING("The temporary database moz_hosts_new already exists, dropping it.");
            rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts_new"));
            NS_ENSURE_SUCCESS(rv, rv);
          }
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
            "CREATE TABLE moz_hosts_new ("
              " id INTEGER PRIMARY KEY"
              ",origin TEXT"
              ",type TEXT"
              ",permission INTEGER"
              ",expireType INTEGER"
              ",expireTime INTEGER"
              ",modificationTime INTEGER"
            ")"));
          NS_ENSURE_SUCCESS(rv, rv);

          nsCOMPtr<mozIStorageStatement> stmt;
          rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
            "SELECT host, type, permission, expireType, expireTime, "
            "modificationTime, appId, isInBrowserElement FROM moz_hosts"),
             getter_AddRefs(stmt));
          NS_ENSURE_SUCCESS(rv, rv);

          int64_t id = 0;
          nsAutoCString host, type;
          uint32_t permission;
          uint32_t expireType;
          int64_t expireTime;
          int64_t modificationTime;
          uint32_t appId;
          bool isInBrowserElement;
          bool hasResult;

          while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
            // Read in the old row
            rv = stmt->GetUTF8String(0, host);
            if (NS_WARN_IF(NS_FAILED(rv))) {
              migrationError = true;
              continue;
            }
            rv = stmt->GetUTF8String(1, type);
            if (NS_WARN_IF(NS_FAILED(rv))) {
              migrationError = true;
              continue;
            }
            permission = stmt->AsInt32(2);
            expireType = stmt->AsInt32(3);
            expireTime = stmt->AsInt64(4);
            modificationTime = stmt->AsInt64(5);
            if (NS_WARN_IF(stmt->AsInt64(6) < 0)) {
              migrationError = true;
              continue;
            }
            appId = static_cast<uint32_t>(stmt->AsInt64(6));
            isInBrowserElement = static_cast<bool>(stmt->AsInt32(7));

            // Perform the meat of the migration by deferring to the
            // UpgradeHostToOriginAndInsert function.
            UpgradeHostToOriginDBMigration upHelper(mDBConn, &id);
            rv = UpgradeHostToOriginAndInsert(host, type, permission,
                                              expireType, expireTime,
                                              modificationTime, appId,
                                              isInBrowserElement,
                                              &upHelper);
            if (NS_FAILED(rv)) {
              NS_WARNING("Unexpected failure when upgrading migrating permission "
                         "from host to origin");
              migrationError = true;
            }
          }

          // We don't drop the moz_hosts table such that it is avaliable for
          // backwards-compatability and for future migrations in case of
          // migration errors in the current code.
          // Create a marker empty table which will indicate that the moz_hosts
          // table is intended to act as a backup. If this table is not present,
          // then the moz_hosts table was created as a random empty table.
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
            "CREATE TABLE moz_hosts_is_backup (dummy INTEGER PRIMARY KEY)"));
          NS_ENSURE_SUCCESS(rv, rv);

          bool permsTableExists = false;
          mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists);
          if (permsTableExists) {
            // The user already had a moz_perms table, and we are performing a
            // re-migration. We count the rows in the old table for telemetry,
            // and then back up their old database as moz_perms_v6

            nsCOMPtr<mozIStorageStatement> countStmt;
            rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT COUNT(*) FROM moz_perms"),
                                          getter_AddRefs(countStmt));
            bool hasResult = false;
            if (NS_SUCCEEDED(rv) &&
                NS_SUCCEEDED(countStmt->ExecuteStep(&hasResult)) &&
                hasResult) {
              int32_t permsCount = countStmt->AsInt32(0);

              // The id variable contains the number of rows inserted into the
              // moz_hosts_new table (as one ID was used per entry)
              uint32_t telemetryValue;
              if (permsCount > id) {
                telemetryValue = 3; // NEW > OLD
              } else if (permsCount == id) {
                telemetryValue = 2; // NEW == OLD
              } else if (permsCount == 0) {
                telemetryValue = 0; // NEW = 0
              } else {
                telemetryValue = 1; // NEW < OLD
              }

              // Report the telemetry value to telemetry
              mozilla::Telemetry::Accumulate(
                  mozilla::Telemetry::PERMISSIONS_REMIGRATION_COMPARISON,
                  telemetryValue);
            } else {
              NS_WARNING("Could not count the rows in moz_perms");
            }

            // Back up the old moz_perms database as moz_perms_v6 before we
            // move the new table into its position
            rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
              "ALTER TABLE moz_perms RENAME TO moz_perms_v6"));
            NS_ENSURE_SUCCESS(rv, rv);
          }

          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
            "ALTER TABLE moz_hosts_new RENAME TO moz_perms"));
          NS_ENSURE_SUCCESS(rv, rv);

          rv = mDBConn->CommitTransaction();
          NS_ENSURE_SUCCESS(rv, rv);

          mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERMISSIONS_MIGRATION_7_ERROR,
                                         NS_WARN_IF(migrationError));
        } else {
          // We don't have a moz_hosts table, so we create one for downgrading purposes.
          // This table is empty.
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
            "CREATE TABLE moz_hosts ("
              " id INTEGER PRIMARY KEY"
              ",host TEXT"
              ",type TEXT"
              ",permission INTEGER"
              ",expireType INTEGER"
              ",expireTime INTEGER"
              ",modificationTime INTEGER"
              ",appId INTEGER"
              ",isInBrowserElement INTEGER"
            ")"));
          NS_ENSURE_SUCCESS(rv, rv);

          // We are guaranteed to have a moz_perms table at this point.
        }

#ifdef DEBUG
        {
          // At this point, both the moz_hosts and moz_perms tables should exist
          bool hostsTableExists = false;
          bool permsTableExists = false;
          mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists);
          mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists);
          MOZ_ASSERT(hostsTableExists && permsTableExists);
        }
#endif

        rv = mDBConn->SetSchemaVersion(7);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // The version 7-8 migration is the re-migration of localhost and ip-address
    // entries due to errors in the previous version 7 migration which caused
    // localhost and ip-address entries to be incorrectly discarded.
    // The version 7 migration logic has been corrected, and thus this logic only
    // needs to execute if the user is currently on version 7.
    case 7:
      {
        // This migration will be relatively expensive as we need to perform
        // database lookups for each origin which we want to insert. Fortunately,
        // it shouldn't be too expensive as we only want to insert a small number
        // of entries created for localhost or IP addresses.

        // We only want to perform the re-migration if moz_hosts is a backup
        bool hostsIsBackupExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_is_backup"),
                             &hostsIsBackupExists);

        // Only perform this migration if the original schema version was 7, and
        // the moz_hosts table is a backup.
        if (dbSchemaVersion == 7 && hostsIsBackupExists) {
          nsCOMPtr<nsIEffectiveTLDService> tldService =
            do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
          MOZ_ASSERT(tldService); // We should always have a tldService

          nsCOMPtr<mozIStorageStatement> stmt;
          rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
            "SELECT host, type, permission, expireType, expireTime, "
            "modificationTime, appId, isInBrowserElement FROM moz_hosts"),
             getter_AddRefs(stmt));
          NS_ENSURE_SUCCESS(rv, rv);

          nsCOMPtr<mozIStorageStatement> idStmt;
          rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
            "SELECT MAX(id) FROM moz_hosts"), getter_AddRefs(idStmt));
          int64_t id = 0;
          bool hasResult = false;
          if (NS_SUCCEEDED(rv) &&
              NS_SUCCEEDED(idStmt->ExecuteStep(&hasResult)) &&
              hasResult) {
            id = idStmt->AsInt32(0) + 1;
          }

          nsAutoCString host, type;
          uint32_t permission;
          uint32_t expireType;
          int64_t expireTime;
          int64_t modificationTime;
          uint32_t appId;
          bool isInBrowserElement;

          while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
            // Read in the old row
            rv = stmt->GetUTF8String(0, host);
            if (NS_WARN_IF(NS_FAILED(rv))) {
              continue;
            }

            nsAutoCString eTLD1;
            rv = tldService->GetBaseDomainFromHost(host, 0, eTLD1);
            if (NS_SUCCEEDED(rv)) {
              // We only care about entries which the tldService can't handle
              continue;
            }

            rv = stmt->GetUTF8String(1, type);
            if (NS_WARN_IF(NS_FAILED(rv))) {
              continue;
            }
            permission = stmt->AsInt32(2);
            expireType = stmt->AsInt32(3);
            expireTime = stmt->AsInt64(4);
            modificationTime = stmt->AsInt64(5);
            if (NS_WARN_IF(stmt->AsInt64(6) < 0)) {
              continue;
            }
            appId = static_cast<uint32_t>(stmt->AsInt64(6));
            isInBrowserElement = static_cast<bool>(stmt->AsInt32(7));

            // Perform the meat of the migration by deferring to the
            // UpgradeHostToOriginAndInsert function.
            UpgradeIPHostToOriginDB upHelper(mDBConn, &id);
            rv = UpgradeHostToOriginAndInsert(host, type, permission,
                                              expireType, expireTime,
                                              modificationTime, appId,
                                              isInBrowserElement,
                                              &upHelper);
            if (NS_FAILED(rv)) {
              NS_WARNING("Unexpected failure when upgrading migrating permission "
                         "from host to origin");
            }
          }
        }

        // Even if we didn't perform the migration, we want to bump the schema
        // version to 8.
        rv = mDBConn->SetSchemaVersion(8);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // The version 8-9 migration removes the unnecessary backup moz-hosts database contents.
    // as the data no longer needs to be migrated
    case 8:
      {
        // We only want to clear out the old table if it is a backup. If it isn't a backup,
        // we don't need to touch it.
        bool hostsIsBackupExists = false;
        mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_is_backup"),
                             &hostsIsBackupExists);
        if (hostsIsBackupExists) {
          // Delete everything from the backup, we want to keep around the table so that
          // you can still downgrade and not break things, but we don't need to keep the
          // rows around.
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_hosts"));
          NS_ENSURE_SUCCESS(rv, rv);

          // The table is no longer a backup, so get rid of it.
          rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts_is_backup"));
          NS_ENSURE_SUCCESS(rv, rv);
        }

        rv = mDBConn->SetSchemaVersion(9);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // fall through to the next upgrade
      MOZ_FALLTHROUGH;

    // current version.
    case HOSTS_SCHEMA_VERSION:
      break;

    // downgrading.
    // if columns have been added to the table, we can still use the ones we
    // understand safely. if columns have been deleted or altered, just
    // blow away the table and start from scratch! if you change the way
    // a column is interpreted, make sure you also change its name so this
    // check will catch it.
    default:
      {
        // check if all the expected columns exist
        nsCOMPtr<mozIStorageStatement> stmt;
        rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
          "SELECT origin, type, permission, expireType, expireTime, "
          "modificationTime FROM moz_perms"),
           getter_AddRefs(stmt));
        if (NS_SUCCEEDED(rv))
          break;

        // our columns aren't there - drop the table!
        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_perms"));
        NS_ENSURE_SUCCESS(rv, rv);

        rv = CreateTable();
        NS_ENSURE_SUCCESS(rv, rv);
      }
      break;
    }
  }

  // cache frequently used statements (for insertion, deletion, and updating)
  rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
    "INSERT INTO moz_perms "
    "(id, origin, type, permission, expireType, expireTime, modificationTime) "
    "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmtInsert));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
    "DELETE FROM moz_perms "
    "WHERE id = ?1"), getter_AddRefs(mStmtDelete));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
    "UPDATE moz_perms "
    "SET permission = ?2, expireType= ?3, expireTime = ?4, modificationTime = ?5 WHERE id = ?1"),
    getter_AddRefs(mStmtUpdate));
  NS_ENSURE_SUCCESS(rv, rv);

  // Always import default permissions.
  ImportDefaults();
  // check whether to import or just read in the db
  if (tableExists)
    return Read();

  return Import();
}

// sets the schema version and creates the moz_perms table.
nsresult
nsPermissionManager::CreateTable()
{
  // set the schema version, before creating the table
  nsresult rv = mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION);
  if (NS_FAILED(rv)) return rv;

  // create the table
  // SQL also lives in automation.py.in. If you change this SQL change that
  // one too
  rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
    "CREATE TABLE moz_perms ("
      " id INTEGER PRIMARY KEY"
      ",origin TEXT"
      ",type TEXT"
      ",permission INTEGER"
      ",expireType INTEGER"
      ",expireTime INTEGER"
      ",modificationTime INTEGER"
    ")"));
  if (NS_FAILED(rv)) return rv;

  // We also create a legacy V4 table, for backwards compatability,
  // and to ensure that downgrades don't trigger a schema version change.
  return mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
    "CREATE TABLE moz_hosts ("
      " id INTEGER PRIMARY KEY"
      ",host TEXT"
      ",type TEXT"
      ",permission INTEGER"
      ",expireType INTEGER"
      ",expireTime INTEGER"
      ",modificationTime INTEGER"
      ",appId INTEGER"
      ",isInBrowserElement INTEGER"
    ")"));
}

NS_IMETHODIMP
nsPermissionManager::Add(nsIURI     *aURI,
                         const char *aType,
                         uint32_t    aPermission,
                         uint32_t    aExpireType,
                         int64_t     aExpireTime)
{
  NS_ENSURE_ARG_POINTER(aURI);

  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  return AddFromPrincipal(principal, aType, aPermission, aExpireType, aExpireTime);
}

NS_IMETHODIMP
nsPermissionManager::AddFromPrincipal(nsIPrincipal* aPrincipal,
                                      const char* aType, uint32_t aPermission,
                                      uint32_t aExpireType, int64_t aExpireTime)
{
  ENSURE_NOT_CHILD_PROCESS;
  NS_ENSURE_ARG_POINTER(aPrincipal);
  NS_ENSURE_ARG_POINTER(aType);
  NS_ENSURE_TRUE(aExpireType == nsIPermissionManager::EXPIRE_NEVER ||
                 aExpireType == nsIPermissionManager::EXPIRE_TIME ||
                 aExpireType == nsIPermissionManager::EXPIRE_SESSION,
                 NS_ERROR_INVALID_ARG);

  // Skip addition if the permission is already expired. Note that EXPIRE_SESSION only
  // honors expireTime if it is nonzero.
  if ((aExpireType == nsIPermissionManager::EXPIRE_TIME ||
       (aExpireType == nsIPermissionManager::EXPIRE_SESSION && aExpireTime != 0)) &&
      aExpireTime <= (PR_Now() / 1000)) {
    return NS_OK;
  }

  // We don't add the system principal because it actually has no URI and we
  // always allow action for them.
  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    return NS_OK;
  }

  // Null principals can't meaningfully have persisted permissions attached to
  // them, so we don't allow adding permissions for them.
  if (aPrincipal->GetIsNullPrincipal()) {
    return NS_OK;
  }

  // Permissions may not be added to expanded principals.
  if (IsExpandedPrincipal(aPrincipal)) {
    return NS_ERROR_INVALID_ARG;
  }

  // A modificationTime of zero will cause AddInternal to use now().
  int64_t modificationTime = 0;

  return AddInternal(aPrincipal, nsDependentCString(aType), aPermission, 0,
                     aExpireType, aExpireTime, modificationTime, eNotify, eWriteToDB);
}

nsresult
nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal,
                                 const nsCString& aType,
                                 uint32_t              aPermission,
                                 int64_t               aID,
                                 uint32_t              aExpireType,
                                 int64_t               aExpireTime,
                                 int64_t               aModificationTime,
                                 NotifyOperationType   aNotifyOperation,
                                 DBOperationType       aDBOperation,
                                 const bool            aIgnoreSessionPermissions)
{
  nsAutoCString origin;
  nsresult rv = GetOriginFromPrincipal(aPrincipal, origin);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!IsChildProcess()) {
    IPC::Permission permission(origin, aType, aPermission,
                               aExpireType, aExpireTime);

    nsAutoCString permissionKey;
    GetKeyForPermission(aPrincipal, aType.get(), permissionKey);

    nsTArray<ContentParent*> cplist;
    ContentParent::GetAll(cplist);
    for (uint32_t i = 0; i < cplist.Length(); ++i) {
      ContentParent* cp = cplist[i];
      if (cp->NeedsPermissionsUpdate(permissionKey))
        Unused << cp->SendAddPermission(permission);
    }
  }

  MOZ_ASSERT(PermissionAvaliable(aPrincipal, aType.get()));

  // look up the type index
  int32_t typeIndex = GetTypeIndex(aType.get(), true);
  NS_ENSURE_TRUE(typeIndex != -1, NS_ERROR_OUT_OF_MEMORY);

  // When an entry already exists, PutEntry will return that, instead
  // of adding a new one
  RefPtr<PermissionKey> key =
    PermissionKey::CreateFromPrincipal(aPrincipal, rv);
  if (!key) {
    MOZ_ASSERT(NS_FAILED(rv));
    return rv;
  }

  PermissionHashKey* entry = mPermissionTable.PutEntry(key);
  if (!entry) return NS_ERROR_FAILURE;
  if (!entry->GetKey()) {
    mPermissionTable.RemoveEntry(entry);
    return NS_ERROR_OUT_OF_MEMORY;
  }

  // figure out the transaction type, and get any existing permission value
  OperationType op;
  int32_t index = entry->GetPermissionIndex(typeIndex);
  if (index == -1) {
    if (aPermission == nsIPermissionManager::UNKNOWN_ACTION)
      op = eOperationNone;
    else
      op = eOperationAdding;

  } else {
    PermissionEntry oldPermissionEntry = entry->GetPermissions()[index];

    // remove the permission if the permission is UNKNOWN, update the
    // permission if its value or expire type have changed OR if the time has
    // changed and the expire type is time, otherwise, don't modify.  There's
    // no need to modify a permission that doesn't expire with time when the
    // only thing changed is the expire time.
    if (aPermission == oldPermissionEntry.mPermission &&
        aExpireType == oldPermissionEntry.mExpireType &&
        (aExpireType == nsIPermissionManager::EXPIRE_NEVER ||
         aExpireTime == oldPermissionEntry.mExpireTime))
      op = eOperationNone;
    else if (oldPermissionEntry.mID == cIDPermissionIsDefault)
      // The existing permission is one added as a default and the new permission
      // doesn't exactly match so we are replacing the default.  This is true
      // even if the new permission is UNKNOWN_ACTION (which means a "logical
      // remove" of the default)
      op = eOperationReplacingDefault;
    else if (aID == cIDPermissionIsDefault)
      // We are adding a default permission but a "real" permission already
      // exists.  This almost-certainly means we just did a removeAllSince and
      // are re-importing defaults - so we can ignore this.
      op = eOperationNone;
    else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION)
      op = eOperationRemoving;
    else
      op = eOperationChanging;
  }

  // child processes should *always* be passed a modificationTime of zero.
  MOZ_ASSERT(!IsChildProcess() || aModificationTime == 0);

  // do the work for adding, deleting, or changing a permission:
  // update the in-memory list, write to the db, and notify consumers.
  int64_t id;
  if (aModificationTime == 0) {
    aModificationTime = PR_Now() / 1000;
  }

  switch (op) {
  case eOperationNone:
    {
      // nothing to do
      return NS_OK;
    }

  case eOperationAdding:
    {
      if (aDBOperation == eWriteToDB) {
        // we'll be writing to the database - generate a known unique id
        id = ++mLargestID;
      } else {
        // we're reading from the database - use the id already assigned
        id = aID;
      }

      entry->GetPermissions().AppendElement(PermissionEntry(id, typeIndex, aPermission,
                                                            aExpireType, aExpireTime,
                                                            aModificationTime));

      // Record a count of the number of preload permissions present in the
      // content process.
      if (IsPreloadPermission(mTypeArray[typeIndex].get())) {
        sPreloadPermissionCount++;
      }

      if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION) {
        UpdateDB(op, mStmtInsert, id, origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime);
      }

      if (aNotifyOperation == eNotify) {
        NotifyObserversWithPermission(aPrincipal,
                                      mTypeArray[typeIndex],
                                      aPermission,
                                      aExpireType,
                                      aExpireTime,
                                      u"added");
      }

      break;
    }

  case eOperationRemoving:
    {
      PermissionEntry oldPermissionEntry = entry->GetPermissions()[index];
      id = oldPermissionEntry.mID;
      entry->GetPermissions().RemoveElementAt(index);

      // Record a count of the number of preload permissions present in the
      // content process.
      if (IsPreloadPermission(mTypeArray[typeIndex].get())) {
        sPreloadPermissionCount--;
      }

      if (aDBOperation == eWriteToDB)
        // We care only about the id here so we pass dummy values for all other
        // parameters.
        UpdateDB(op, mStmtDelete, id, EmptyCString(), EmptyCString(), 0,
                 nsIPermissionManager::EXPIRE_NEVER, 0, 0);

      if (aNotifyOperation == eNotify) {
        NotifyObserversWithPermission(aPrincipal,
                                      mTypeArray[typeIndex],
                                      oldPermissionEntry.mPermission,
                                      oldPermissionEntry.mExpireType,
                                      oldPermissionEntry.mExpireTime,
                                      u"deleted");
      }

      // If there are no more permissions stored for that entry, clear it.
      if (entry->GetPermissions().IsEmpty()) {
        mPermissionTable.RemoveEntry(entry);
      }

      break;
    }

  case eOperationChanging:
    {
      id = entry->GetPermissions()[index].mID;

      // If the new expireType is EXPIRE_SESSION, then we have to keep a
      // copy of the previous permission/expireType values. This cached value will be
      // used when restoring the permissions of an app.
      if (entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION &&
          aExpireType == nsIPermissionManager::EXPIRE_SESSION) {
        entry->GetPermissions()[index].mNonSessionPermission = entry->GetPermissions()[index].mPermission;
        entry->GetPermissions()[index].mNonSessionExpireType = entry->GetPermissions()[index].mExpireType;
        entry->GetPermissions()[index].mNonSessionExpireTime = entry->GetPermissions()[index].mExpireTime;
      } else if (aExpireType != nsIPermissionManager::EXPIRE_SESSION) {
        entry->GetPermissions()[index].mNonSessionPermission = aPermission;
        entry->GetPermissions()[index].mNonSessionExpireType = aExpireType;
        entry->GetPermissions()[index].mNonSessionExpireTime = aExpireTime;
      }

      entry->GetPermissions()[index].mPermission = aPermission;
      entry->GetPermissions()[index].mExpireType = aExpireType;
      entry->GetPermissions()[index].mExpireTime = aExpireTime;
      entry->GetPermissions()[index].mModificationTime = aModificationTime;

      if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION)
        // We care only about the id, the permission and expireType/expireTime/modificationTime here.
        // We pass dummy values for all other parameters.
        UpdateDB(op, mStmtUpdate, id, EmptyCString(), EmptyCString(),
                 aPermission, aExpireType, aExpireTime, aModificationTime);

      if (aNotifyOperation == eNotify) {
        NotifyObserversWithPermission(aPrincipal,
                                      mTypeArray[typeIndex],
                                      aPermission,
                                      aExpireType,
                                      aExpireTime,
                                      u"changed");
      }

      break;
    }
  case eOperationReplacingDefault:
    {
      // this is handling the case when we have an existing permission
      // entry that was created as a "default" (and thus isn't in the DB) with
      // an explicit permission (that may include UNKNOWN_ACTION.)
      // Note we will *not* get here if we are replacing an already replaced
      // default value - that is handled as eOperationChanging.

      // So this is a hybrid of eOperationAdding (as we are writing a new entry
      // to the DB) and eOperationChanging (as we are replacing the in-memory
      // repr and sending a "changed" notification).

      // We want a new ID even if not writing to the DB, so the modified entry
      // in memory doesn't have the magic cIDPermissionIsDefault value.
      id = ++mLargestID;

      // The default permission being replaced can't have session expiry.
      NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION,
                     NS_ERROR_UNEXPECTED);
      // We don't support the new entry having any expiry - supporting that would
      // make things far more complex and none of the permissions we set as a
      // default support that.
      NS_ENSURE_TRUE(aExpireType == EXPIRE_NEVER, NS_ERROR_UNEXPECTED);

      // update the existing entry in memory.
      entry->GetPermissions()[index].mID = id;
      entry->GetPermissions()[index].mPermission = aPermission;
      entry->GetPermissions()[index].mExpireType = aExpireType;
      entry->GetPermissions()[index].mExpireTime = aExpireTime;
      entry->GetPermissions()[index].mModificationTime = aModificationTime;

      // If requested, create the entry in the DB.
      if (aDBOperation == eWriteToDB) {
        UpdateDB(eOperationAdding, mStmtInsert, id, origin, aType, aPermission,
                 aExpireType, aExpireTime, aModificationTime);
      }

      if (aNotifyOperation == eNotify) {
        NotifyObserversWithPermission(aPrincipal,
                                      mTypeArray[typeIndex],
                                      aPermission,
                                      aExpireType,
                                      aExpireTime,
                                      u"changed");
      }

    }
    break;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::Remove(nsIURI*     aURI,
                            const char* aType)
{
  NS_ENSURE_ARG_POINTER(aURI);

  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  return RemoveFromPrincipal(principal, aType);
}

NS_IMETHODIMP
nsPermissionManager::RemoveFromPrincipal(nsIPrincipal* aPrincipal,
                                         const char* aType)
{
  ENSURE_NOT_CHILD_PROCESS;
  NS_ENSURE_ARG_POINTER(aPrincipal);
  NS_ENSURE_ARG_POINTER(aType);

  // System principals are never added to the database, no need to remove them.
  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    return NS_OK;
  }

  // Permissions may not be added to expanded principals.
  if (IsExpandedPrincipal(aPrincipal)) {
    return NS_ERROR_INVALID_ARG;
  }

  // AddInternal() handles removal, just let it do the work
  return AddInternal(aPrincipal,
                     nsDependentCString(aType),
                     nsIPermissionManager::UNKNOWN_ACTION,
                     0,
                     nsIPermissionManager::EXPIRE_NEVER,
                     0,
                     0,
                     eNotify,
                     eWriteToDB);
}

NS_IMETHODIMP
nsPermissionManager::RemovePermission(nsIPermission* aPerm)
{
  if (!aPerm) {
    return NS_OK;
  }
  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = aPerm->GetPrincipal(getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString type;
  rv = aPerm->GetType(type);
  NS_ENSURE_SUCCESS(rv, rv);

  // Permissions are uniquely identified by their principal and type.
  // We remove the permission using these two pieces of data.
  return RemoveFromPrincipal(principal, type.get());
}

NS_IMETHODIMP
nsPermissionManager::RemoveAll()
{
  ENSURE_NOT_CHILD_PROCESS;
  return RemoveAllInternal(true);
}

NS_IMETHODIMP
nsPermissionManager::RemoveAllSince(int64_t aSince)
{
  ENSURE_NOT_CHILD_PROCESS;
  return RemoveAllModifiedSince(aSince);
}

void
nsPermissionManager::CloseDB(bool aRebuildOnSuccess)
{
  // Null the statements, this will finalize them.
  mStmtInsert = nullptr;
  mStmtDelete = nullptr;
  mStmtUpdate = nullptr;
  if (mDBConn) {
    mozIStorageCompletionCallback* cb = new CloseDatabaseListener(this,
           aRebuildOnSuccess);
    mozilla::DebugOnly<nsresult> rv = mDBConn->AsyncClose(cb);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    mDBConn = nullptr; // Avoid race conditions
  }
}

nsresult
nsPermissionManager::RemoveAllInternal(bool aNotifyObservers)
{
  // Remove from memory and notify immediately. Since the in-memory
  // database is authoritative, we do not need confirmation from the
  // on-disk database to notify observers.
  RemoveAllFromMemory();

  // Re-import the defaults
  ImportDefaults();

  if (aNotifyObservers) {
    NotifyObservers(nullptr, u"cleared");
  }

  // clear the db
  if (mDBConn) {
    nsCOMPtr<mozIStorageAsyncStatement> removeStmt;
    nsresult rv = mDBConn->
      CreateAsyncStatement(NS_LITERAL_CSTRING(
         "DELETE FROM moz_perms"
      ), getter_AddRefs(removeStmt));
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    if (!removeStmt) {
      return NS_ERROR_UNEXPECTED;
    }
    nsCOMPtr<mozIStoragePendingStatement> pending;
    mozIStorageStatementCallback* cb = new DeleteFromMozHostListener(this);
    rv = removeStmt->ExecuteAsync(cb, getter_AddRefs(pending));
    MOZ_ASSERT(NS_SUCCEEDED(rv));

    return rv;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::TestExactPermission(nsIURI     *aURI,
                                         const char *aType,
                                         uint32_t   *aPermission)
{
  return CommonTestPermission(aURI, aType, aPermission, true, true);
}

NS_IMETHODIMP
nsPermissionManager::TestExactPermissionFromPrincipal(nsIPrincipal* aPrincipal,
                                                      const char* aType,
                                                      uint32_t* aPermission)
{
  return CommonTestPermission(aPrincipal, aType, aPermission, true, true);
}

NS_IMETHODIMP
nsPermissionManager::TestExactPermanentPermission(nsIPrincipal* aPrincipal,
                                                  const char* aType,
                                                  uint32_t* aPermission)
{
  return CommonTestPermission(aPrincipal, aType, aPermission, true, false);
}

NS_IMETHODIMP
nsPermissionManager::TestPermission(nsIURI     *aURI,
                                    const char *aType,
                                    uint32_t   *aPermission)
{
  return CommonTestPermission(aURI, aType, aPermission, false, true);
}

NS_IMETHODIMP
nsPermissionManager::TestPermissionFromWindow(mozIDOMWindow* aWindow,
                                              const char* aType,
                                              uint32_t* aPermission)
{
  NS_ENSURE_ARG(aWindow);
  nsCOMPtr<nsPIDOMWindowInner> window = nsPIDOMWindowInner::From(aWindow);

  // Get the document for security check
  nsCOMPtr<nsIDocument> document = window->GetExtantDoc();
  NS_ENSURE_TRUE(document, NS_NOINTERFACE);

  nsCOMPtr<nsIPrincipal> principal = document->NodePrincipal();
  return TestPermissionFromPrincipal(principal, aType, aPermission);
}

NS_IMETHODIMP
nsPermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal,
                                                 const char* aType,
                                                 uint32_t* aPermission)
{
  return CommonTestPermission(aPrincipal, aType, aPermission, false, true);
}

NS_IMETHODIMP
nsPermissionManager::GetPermissionObjectForURI(nsIURI* aURI,
                                               const char* aType,
                                               bool aExactHostMatch,
                                               nsIPermission** aResult)
{
  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  return GetPermissionObject(principal, aType, aExactHostMatch, aResult);
}

NS_IMETHODIMP
nsPermissionManager::GetPermissionObject(nsIPrincipal* aPrincipal,
                                         const char* aType,
                                         bool aExactHostMatch,
                                         nsIPermission** aResult)
{
  NS_ENSURE_ARG_POINTER(aPrincipal);
  NS_ENSURE_ARG_POINTER(aType);

  *aResult = nullptr;

  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    return NS_OK;
  }

  // Querying the permission object of an nsEP is non-sensical.
  if (IsExpandedPrincipal(aPrincipal)) {
    return NS_ERROR_INVALID_ARG;
  }

  MOZ_ASSERT(PermissionAvaliable(aPrincipal, aType));

  int32_t typeIndex = GetTypeIndex(aType, false);
  // If type == -1, the type isn't known,
  // so just return NS_OK
  if (typeIndex == -1) return NS_OK;

  PermissionHashKey* entry = GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch);
  if (!entry) {
    return NS_OK;
  }

  // We don't call GetPermission(typeIndex) because that returns a fake
  // UNKNOWN_ACTION entry if there is no match.
  int32_t idx = entry->GetPermissionIndex(typeIndex);
  if (-1 == idx) {
    return NS_OK;
  }

  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  PermissionEntry& perm = entry->GetPermissions()[idx];
  nsCOMPtr<nsIPermission> r = nsPermission::Create(principal,
                                                   mTypeArray.ElementAt(perm.mType),
                                                   perm.mPermission,
                                                   perm.mExpireType,
                                                   perm.mExpireTime);
  if (NS_WARN_IF(!r)) {
    return NS_ERROR_FAILURE;
  }
  r.forget(aResult);
  return NS_OK;
}

nsresult
nsPermissionManager::CommonTestPermissionInternal(nsIPrincipal* aPrincipal,
                                                  nsIURI      * aURI,
                                                  const char  * aType,
                                                  uint32_t    * aPermission,
                                                  bool          aExactHostMatch,
                                                  bool          aIncludingSession)
{
  MOZ_ASSERT(aPrincipal || aURI);
  MOZ_ASSERT_IF(aPrincipal, !aURI);
  NS_ENSURE_ARG_POINTER(aPrincipal || aURI);
  NS_ENSURE_ARG_POINTER(aType);

  if (aPrincipal && nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    *aPermission = nsIPermissionManager::ALLOW_ACTION;
    return NS_OK;
  }

  // Set the default.
  *aPermission = nsIPermissionManager::UNKNOWN_ACTION;

  // For expanded principals, we want to iterate over the whitelist and see
  // if the permission is granted for any of them.
  nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(aPrincipal);
  if (ep) {
    nsTArray<nsCOMPtr<nsIPrincipal>>* whitelist;
    nsresult rv = ep->GetWhiteList(&whitelist);
    NS_ENSURE_SUCCESS(rv, rv);

    for (size_t i = 0; i < whitelist->Length(); ++i) {
      uint32_t perm;
      rv = CommonTestPermission(whitelist->ElementAt(i), aType, &perm,
                                aExactHostMatch, aIncludingSession);
      NS_ENSURE_SUCCESS(rv, rv);
      if (perm == nsIPermissionManager::ALLOW_ACTION) {
        *aPermission = perm;
        return NS_OK;
      } else if (perm == nsIPermissionManager::PROMPT_ACTION) {
        // Store it, but keep going to see if we can do better.
        *aPermission = perm;
      }
    }

    return NS_OK;
  }

#ifdef DEBUG
  {
    nsCOMPtr<nsIPrincipal> prin = aPrincipal;
    if (!prin) {
      prin = mozilla::BasePrincipal::CreateCodebasePrincipal(aURI, OriginAttributes());
    }
    MOZ_ASSERT(PermissionAvaliable(prin, aType));
  }
#endif

  int32_t typeIndex = GetTypeIndex(aType, false);
  // If type == -1, the type isn't known,
  // so just return NS_OK
  if (typeIndex == -1) return NS_OK;

  PermissionHashKey* entry = aPrincipal ?
    GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch) :
    GetPermissionHashKey(aURI, typeIndex, aExactHostMatch);
  if (!entry ||
      (!aIncludingSession &&
       entry->GetPermission(typeIndex).mNonSessionExpireType ==
         nsIPermissionManager::EXPIRE_SESSION)) {
    return NS_OK;
  }

  *aPermission = aIncludingSession
                   ? entry->GetPermission(typeIndex).mPermission
                   : entry->GetPermission(typeIndex).mNonSessionPermission;

  return NS_OK;
}

// Returns PermissionHashKey for a given { host, appId, isInBrowserElement } tuple.
// This is not simply using PermissionKey because we will walk-up domains in
// case of |host| contains sub-domains.
// Returns null if nothing found.
// Also accepts host on the format "<foo>". This will perform an exact match
// lookup as the string doesn't contain any dots.
nsPermissionManager::PermissionHashKey*
nsPermissionManager::GetPermissionHashKey(nsIPrincipal* aPrincipal,
                                          uint32_t aType,
                                          bool aExactHostMatch)
{
  MOZ_ASSERT(PermissionAvaliable(aPrincipal, mTypeArray[aType].get()));

  nsresult rv;
  RefPtr<PermissionKey> key =
    PermissionKey::CreateFromPrincipal(aPrincipal, rv);
  if (!key) {
    return nullptr;
  }

  PermissionHashKey* entry = mPermissionTable.GetEntry(key);

  if (entry) {
    PermissionEntry permEntry = entry->GetPermission(aType);

    // if the entry is expired, remove and keep looking for others.
    // Note that EXPIRE_SESSION only honors expireTime if it is nonzero.
    if ((permEntry.mExpireType == nsIPermissionManager::EXPIRE_TIME ||
         (permEntry.mExpireType == nsIPermissionManager::EXPIRE_SESSION &&
          permEntry.mExpireTime != 0)) &&
        permEntry.mExpireTime <= (PR_Now() / 1000)) {
      entry = nullptr;
      RemoveFromPrincipal(aPrincipal, mTypeArray[aType].get());
    } else if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
      entry = nullptr;
    }
  }

  if (entry) {
    return entry;
  }

  // If aExactHostMatch wasn't true, we can check if the base domain has a permission entry.
  if (!aExactHostMatch) {
    nsCOMPtr<nsIPrincipal> principal =
      GetNextSubDomainPrincipal(aPrincipal);
    if (principal) {
      return GetPermissionHashKey(principal, aType, aExactHostMatch);
    }
  }

  // No entry, really...
  return nullptr;
}

// Returns PermissionHashKey for a given { host, appId, isInBrowserElement } tuple.
// This is not simply using PermissionKey because we will walk-up domains in
// case of |host| contains sub-domains.
// Returns null if nothing found.
// Also accepts host on the format "<foo>". This will perform an exact match
// lookup as the string doesn't contain any dots.
nsPermissionManager::PermissionHashKey*
nsPermissionManager::GetPermissionHashKey(nsIURI* aURI,
                                          uint32_t aType,
                                          bool aExactHostMatch)
{
#ifdef DEBUG
  {
    nsCOMPtr<nsIPrincipal> principal;
    nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
    MOZ_ASSERT_IF(NS_SUCCEEDED(rv),
                  PermissionAvaliable(principal, mTypeArray[aType].get()));
  }
#endif

  nsresult rv;
  RefPtr<PermissionKey> key =
    PermissionKey::CreateFromURI(aURI, rv);
  if (!key) {
    return nullptr;
  }

  PermissionHashKey* entry = mPermissionTable.GetEntry(key);

  if (entry) {
    PermissionEntry permEntry = entry->GetPermission(aType);

    // if the entry is expired, remove and keep looking for others.
    // Note that EXPIRE_SESSION only honors expireTime if it is nonzero.
    if ((permEntry.mExpireType == nsIPermissionManager::EXPIRE_TIME ||
         (permEntry.mExpireType == nsIPermissionManager::EXPIRE_SESSION &&
          permEntry.mExpireTime != 0)) &&
        permEntry.mExpireTime <= (PR_Now() / 1000)) {
      entry = nullptr;
      // If we need to remove a permission we mint a principal.  This is a bit
      // inefficient, but hopefully this code path isn't super common.
      nsCOMPtr<nsIPrincipal> principal;
      nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return nullptr;
      }
      RemoveFromPrincipal(principal, mTypeArray[aType].get());
    } else if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
      entry = nullptr;
    }
  }

  if (entry) {
    return entry;
  }

  // If aExactHostMatch wasn't true, we can check if the base domain has a permission entry.
  if (!aExactHostMatch) {
    nsCOMPtr<nsIURI> uri = GetNextSubDomainURI(aURI);
    if (uri) {
      return GetPermissionHashKey(uri, aType, aExactHostMatch);
    }
  }

  // No entry, really...
  return nullptr;
}

NS_IMETHODIMP nsPermissionManager::GetEnumerator(nsISimpleEnumerator **aEnum)
{
  if (XRE_IsContentProcess()) {
    NS_WARNING("nsPermissionManager's enumerator is not avaliable in the "
               "content process, as not all permissions may be avaliable.");
    *aEnum = nullptr;
    return NS_ERROR_NOT_AVAILABLE;
  }

  // roll an nsCOMArray of all our permissions, then hand out an enumerator
  nsCOMArray<nsIPermission> array;

  for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
    PermissionHashKey* entry = iter.Get();
    for (const auto& permEntry : entry->GetPermissions()) {
      // Given how "default" permissions work and the possibility of them being
      // overridden with UNKNOWN_ACTION, we might see this value here - but we
      // do *not* want to return them via the enumerator.
      if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
        continue;
      }

      nsCOMPtr<nsIPrincipal> principal;
      nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin,
                                           getter_AddRefs(principal));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsCOMPtr<nsIPermission> permission =
        nsPermission::Create(principal,
                             mTypeArray.ElementAt(permEntry.mType),
                             permEntry.mPermission,
                             permEntry.mExpireType,
                             permEntry.mExpireTime);
      if (NS_WARN_IF(!permission)) {
        continue;
      }
      array.AppendObject(permission);
    }
  }

  return NS_NewArrayEnumerator(aEnum, array);
}

NS_IMETHODIMP nsPermissionManager::GetAllForURI(nsIURI* aURI, nsISimpleEnumerator **aEnum)
{
  nsCOMArray<nsIPermission> array;

  nsCOMPtr<nsIPrincipal> principal;
  nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
  NS_ENSURE_SUCCESS(rv, rv);

  MOZ_ASSERT(PermissionAvaliable(principal, nullptr));

  RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal(principal, rv);
  if (!key) {
    MOZ_ASSERT(NS_FAILED(rv));
    return rv;
  }

  PermissionHashKey* entry = mPermissionTable.GetEntry(key);

  if (entry) {
    for (const auto& permEntry : entry->GetPermissions()) {
      // Only return custom permissions
      if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
        continue;
      }

      nsCOMPtr<nsIPermission> permission =
        nsPermission::Create(principal,
                             mTypeArray.ElementAt(permEntry.mType),
                             permEntry.mPermission,
                             permEntry.mExpireType,
                             permEntry.mExpireTime);
      if (NS_WARN_IF(!permission)) {
        continue;
      }
      array.AppendObject(permission);
    }
  }

  return NS_NewArrayEnumerator(aEnum, array);
}

NS_IMETHODIMP nsPermissionManager::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *someData)
{
  ENSURE_NOT_CHILD_PROCESS;

  if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
    // The profile is about to change,
    // or is going away because the application is shutting down.
    mIsShuttingDown = true;
    RemoveAllFromMemory();
    CloseDB(false);
  } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) {
    // the profile has already changed; init the db from the new location
    InitDB(false);
  }

  return NS_OK;
}

nsresult
nsPermissionManager::RemoveAllModifiedSince(int64_t aModificationTime)
{
  ENSURE_NOT_CHILD_PROCESS;

  nsCOMArray<nsIPermission> array;
  for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
    PermissionHashKey* entry = iter.Get();
    for (const auto& permEntry : entry->GetPermissions()) {
      if (aModificationTime > permEntry.mModificationTime) {
        continue;
      }

      nsCOMPtr<nsIPrincipal> principal;
      nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin,
                                           getter_AddRefs(principal));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsCOMPtr<nsIPermission> permission =
        nsPermission::Create(principal,
                             mTypeArray.ElementAt(permEntry.mType),
                             permEntry.mPermission,
                             permEntry.mExpireType,
                             permEntry.mExpireTime);
      if (NS_WARN_IF(!permission)) {
        continue;
      }
      array.AppendObject(permission);
    }
  }

  for (int32_t i = 0; i<array.Count(); ++i) {
    nsCOMPtr<nsIPrincipal> principal;
    nsAutoCString type;

    nsresult rv = array[i]->GetPrincipal(getter_AddRefs(principal));
    if (NS_FAILED(rv)) {
      NS_ERROR("GetPrincipal() failed!");
      continue;
    }

    rv = array[i]->GetType(type);
    if (NS_FAILED(rv)) {
      NS_ERROR("GetType() failed!");
      continue;
    }

    // AddInternal handles removal, so let it do the work...
    AddInternal(
      principal,
      type,
      nsIPermissionManager::UNKNOWN_ACTION,
      0,
      nsIPermissionManager::EXPIRE_NEVER, 0, 0,
      nsPermissionManager::eNotify,
      nsPermissionManager::eWriteToDB);
  }
  // now re-import any defaults as they may now be required if we just deleted
  // an override.
  ImportDefaults();
  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::RemovePermissionsWithAttributes(const nsAString& aPattern)
{
  ENSURE_NOT_CHILD_PROCESS;
  mozilla::OriginAttributesPattern pattern;
  if (!pattern.Init(aPattern)) {
    return NS_ERROR_INVALID_ARG;
  }

  return RemovePermissionsWithAttributes(pattern);
}

nsresult
nsPermissionManager::RemovePermissionsWithAttributes(mozilla::OriginAttributesPattern& aPattern)
{
  nsCOMArray<nsIPermission> permissions;
  for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
    PermissionHashKey* entry = iter.Get();

    nsCOMPtr<nsIPrincipal> principal;
    nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin,
                                         getter_AddRefs(principal));
    if (NS_FAILED(rv)) {
      continue;
    }

    if (!aPattern.Matches(principal->OriginAttributesRef())) {
      continue;
    }

    for (const auto& permEntry : entry->GetPermissions()) {
      nsCOMPtr<nsIPermission> permission =
        nsPermission::Create(principal,
                             mTypeArray.ElementAt(permEntry.mType),
                             permEntry.mPermission,
                             permEntry.mExpireType,
                             permEntry.mExpireTime);
      if (NS_WARN_IF(!permission)) {
        continue;
      }
      permissions.AppendObject(permission);
    }
  }

  for (int32_t i = 0; i < permissions.Count(); ++i) {
    nsCOMPtr<nsIPrincipal> principal;
    nsAutoCString type;

    permissions[i]->GetPrincipal(getter_AddRefs(principal));
    permissions[i]->GetType(type);

    AddInternal(principal,
                type,
                nsIPermissionManager::UNKNOWN_ACTION,
                0,
                nsIPermissionManager::EXPIRE_NEVER,
                0,
                0,
                nsPermissionManager::eNotify,
                nsPermissionManager::eWriteToDB);
  }

  return NS_OK;
}

//*****************************************************************************
//*** nsPermissionManager private methods
//*****************************************************************************

nsresult
nsPermissionManager::RemoveAllFromMemory()
{
  mLargestID = 0;
  mTypeArray.Clear();
  mPermissionTable.Clear();

  return NS_OK;
}

// Returns -1 on failure
int32_t
nsPermissionManager::GetTypeIndex(const char *aType,
                                  bool        aAdd)
{
  for (uint32_t i = 0; i < mTypeArray.Length(); ++i)
    if (mTypeArray[i].Equals(aType))
      return i;

  if (!aAdd) {
    // Not found, but that is ok - we were just looking.
    return -1;
  }

  // This type was not registered before.
  // append it to the array, without copy-constructing the string
  nsCString *elem = mTypeArray.AppendElement();
  if (!elem)
    return -1;

  elem->Assign(aType);
  return mTypeArray.Length() - 1;
}

// wrapper function for mangling (host,type,perm,expireType,expireTime)
// set into an nsIPermission.
void
nsPermissionManager::NotifyObserversWithPermission(nsIPrincipal*     aPrincipal,
                                                   const nsCString  &aType,
                                                   uint32_t          aPermission,
                                                   uint32_t          aExpireType,
                                                   int64_t           aExpireTime,
                                                   const char16_t  *aData)
{
  nsCOMPtr<nsIPermission> permission =
    nsPermission::Create(aPrincipal, aType, aPermission,
                         aExpireType, aExpireTime);
  if (permission)
    NotifyObservers(permission, aData);
}

// notify observers that the permission list changed. there are four possible
// values for aData:
// "deleted" means a permission was deleted. aPermission is the deleted permission.
// "added"   means a permission was added. aPermission is the added permission.
// "changed" means a permission was altered. aPermission is the new permission.
// "cleared" means the entire permission list was cleared. aPermission is null.
void
nsPermissionManager::NotifyObservers(nsIPermission   *aPermission,
                                     const char16_t *aData)
{
  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();
  if (observerService)
    observerService->NotifyObservers(aPermission,
                                     kPermissionChangeNotification,
                                     aData);
}

nsresult
nsPermissionManager::Read()
{
  ENSURE_NOT_CHILD_PROCESS;

  nsresult rv;

  // delete expired permissions before we read in the db
  {
    // this deletion has its own scope so the write lock is released when done.
    nsCOMPtr<mozIStorageStatement> stmtDeleteExpired;
    rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
          "DELETE FROM moz_perms WHERE expireType = ?1 AND expireTime <= ?2"),
          getter_AddRefs(stmtDeleteExpired));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = stmtDeleteExpired->BindInt32ByIndex(0, nsIPermissionManager::EXPIRE_TIME);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = stmtDeleteExpired->BindInt64ByIndex(1, PR_Now() / 1000);
    NS_ENSURE_SUCCESS(rv, rv);

    bool hasResult;
    rv = stmtDeleteExpired->ExecuteStep(&hasResult);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  nsCOMPtr<mozIStorageStatement> stmt;
  rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
    "SELECT id, origin, type, permission, expireType, expireTime, modificationTime "
    "FROM moz_perms"), getter_AddRefs(stmt));
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t id;
  nsAutoCString origin, type;
  uint32_t permission;
  uint32_t expireType;
  int64_t expireTime;
  int64_t modificationTime;
  bool hasResult;
  bool readError = false;

  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
    // explicitly set our entry id counter for use in AddInternal(),
    // and keep track of the largest id so we know where to pick up.
    id = stmt->AsInt64(0);
    if (id > mLargestID)
      mLargestID = id;

    rv = stmt->GetUTF8String(1, origin);
    if (NS_FAILED(rv)) {
      readError = true;
      continue;
    }

    rv = stmt->GetUTF8String(2, type);
    if (NS_FAILED(rv)) {
      readError = true;
      continue;
    }

    permission = stmt->AsInt32(3);
    expireType = stmt->AsInt32(4);

    // convert into int64_t values (milliseconds)
    expireTime = stmt->AsInt64(5);
    modificationTime = stmt->AsInt64(6);

    nsCOMPtr<nsIPrincipal> principal;
    nsresult rv = GetPrincipalFromOrigin(origin, getter_AddRefs(principal));
    if (NS_FAILED(rv)) {
      readError = true;
      continue;
    }

    rv = AddInternal(principal, type, permission, id, expireType, expireTime,
                     modificationTime, eDontNotify, eNoDBOperation);
    if (NS_FAILED(rv)) {
      readError = true;
      continue;
    }
  }

  if (readError) {
    NS_ERROR("Error occured while reading the permissions database!");
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

static const char kMatchTypeHost[] = "host";
static const char kMatchTypeOrigin[] = "origin";

// Import() will read a file from the profile directory and add them to the
// database before deleting the file - ie, this is a one-shot operation that
// will not succeed on subsequent runs as the file imported from is removed.
nsresult
nsPermissionManager::Import()
{
  nsresult rv;

  nsCOMPtr<nsIFile> permissionsFile;
  rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(permissionsFile));
  if (NS_FAILED(rv)) return rv;

  rv = permissionsFile->AppendNative(NS_LITERAL_CSTRING(HOSTPERM_FILE_NAME));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIInputStream> fileInputStream;
  rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream),
                                  permissionsFile);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = _DoImport(fileInputStream, mDBConn);
  NS_ENSURE_SUCCESS(rv, rv);

  // we successfully imported and wrote to the DB - delete the old file.
  permissionsFile->Remove(false);
  return NS_OK;
}

// ImportDefaults will read a URL with default permissions and add them to the
// in-memory copy of permissions.  The database is *not* written to.
nsresult
nsPermissionManager::ImportDefaults()
{
  nsAutoCString defaultsURL;
  mozilla::Preferences::GetCString(kDefaultsUrlPrefName, defaultsURL);
  if (defaultsURL.IsEmpty()) { // == Don't use built-in permissions.
    return NS_OK;
  }

  nsCOMPtr<nsIURI> defaultsURI;
  nsresult rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIChannel> channel;
  rv = NS_NewChannel(getter_AddRefs(channel),
                     defaultsURI,
                     nsContentUtils::GetSystemPrincipal(),
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                     nsIContentPolicy::TYPE_OTHER);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIInputStream> inputStream;
  rv = channel->Open2(getter_AddRefs(inputStream));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = _DoImport(inputStream, nullptr);
  inputStream->Close();
  return rv;
}

// _DoImport reads the specified stream and adds the parsed elements.  If
// |conn| is passed, the imported data will be written to the database, but if
// |conn| is null the data will be added only to the in-memory copy of the
// database.
nsresult
nsPermissionManager::_DoImport(nsIInputStream *inputStream, mozIStorageConnection *conn)
{
  ENSURE_NOT_CHILD_PROCESS;

  nsresult rv;
  // start a transaction on the storage db, to optimize insertions.
  // transaction will automically commit on completion
  // (note the transaction is a no-op if a null connection is passed)
  mozStorageTransaction transaction(conn, true);

  // The DB operation - we only try and write if a connection was passed.
  DBOperationType operation = conn ? eWriteToDB : eNoDBOperation;
  // and if no DB connection was passed we assume this is a "default" permission,
  // so use the special ID which indicates this.
  int64_t id = conn ? 0 : cIDPermissionIsDefault;

  /* format is:
   * matchtype \t type \t permission \t host
   * Only "host" is supported for matchtype
   * type is a string that identifies the type of permission (e.g. "cookie")
   * permission is an integer between 1 and 15
   */

  // Ideally we'd do this with nsILineInputString, but this is called with an
  // nsIInputStream that comes from a resource:// URI, which doesn't support
  // that interface.  So NS_ReadLine to the rescue...
  nsLineBuffer<char> lineBuffer;
  nsCString line;
  bool isMore = true;
  do {
    rv = NS_ReadLine(inputStream, &lineBuffer, line, &isMore);
    NS_ENSURE_SUCCESS(rv, rv);

    if (line.IsEmpty() || line.First() == '#') {
      continue;
    }

    nsTArray<nsCString> lineArray;

    // Split the line at tabs
    ParseString(line, '\t', lineArray);

    if (lineArray[0].EqualsLiteral(kMatchTypeHost) &&
        lineArray.Length() == 4) {
      nsresult error = NS_OK;
      uint32_t permission = lineArray[2].ToInteger(&error);
      if (NS_FAILED(error))
        continue;

      // the import file format doesn't handle modification times, so we use
      // 0, which AddInternal will convert to now()
      int64_t modificationTime = 0;

      UpgradeHostToOriginHostfileImport upHelper(this, operation, id);
      error = UpgradeHostToOriginAndInsert(lineArray[3], lineArray[1], permission,
                                           nsIPermissionManager::EXPIRE_NEVER, 0,
                                           modificationTime, nsIScriptSecurityManager::NO_APP_ID,
                                           false, &upHelper);
      if (NS_FAILED(error)) {
        NS_WARNING("There was a problem importing a host permission");
      }
    } else if (lineArray[0].EqualsLiteral(kMatchTypeOrigin) &&
               lineArray.Length() == 4) {
      nsresult error = NS_OK;
      uint32_t permission = lineArray[2].ToInteger(&error);
      if (NS_FAILED(error))
        continue;

      nsCOMPtr<nsIPrincipal> principal;
      error = GetPrincipalFromOrigin(lineArray[3], getter_AddRefs(principal));
      if (NS_FAILED(error)) {
        NS_WARNING("Couldn't import an origin permission - malformed origin");
        continue;
      }

      // the import file format doesn't handle modification times, so we use
      // 0, which AddInternal will convert to now()
      int64_t modificationTime = 0;

      error = AddInternal(principal, lineArray[1], permission, id,
                          nsIPermissionManager::EXPIRE_NEVER, 0,
                          modificationTime,
                          eDontNotify, operation);
      if (NS_FAILED(error)) {
        NS_WARNING("There was a problem importing an origin permission");
      }
    }

  } while (isMore);

  return NS_OK;
}

void
nsPermissionManager::UpdateDB(OperationType aOp,
                              mozIStorageAsyncStatement* aStmt,
                              int64_t aID,
                              const nsACString &aOrigin,
                              const nsACString &aType,
                              uint32_t aPermission,
                              uint32_t aExpireType,
                              int64_t aExpireTime,
                              int64_t aModificationTime)
{
  ENSURE_NOT_CHILD_PROCESS_NORET;

  nsresult rv;

  // no statement is ok - just means we don't have a profile
  if (!aStmt)
    return;

  switch (aOp) {
  case eOperationAdding:
    {
      rv = aStmt->BindInt64ByIndex(0, aID);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindUTF8StringByIndex(1, aOrigin);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindUTF8StringByIndex(2, aType);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt32ByIndex(3, aPermission);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt32ByIndex(4, aExpireType);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt64ByIndex(5, aExpireTime);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt64ByIndex(6, aModificationTime);
      break;
    }

  case eOperationRemoving:
    {
      rv = aStmt->BindInt64ByIndex(0, aID);
      break;
    }

  case eOperationChanging:
    {
      rv = aStmt->BindInt64ByIndex(0, aID);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt32ByIndex(1, aPermission);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt32ByIndex(2, aExpireType);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt64ByIndex(3, aExpireTime);
      if (NS_FAILED(rv)) break;

      rv = aStmt->BindInt64ByIndex(4, aModificationTime);
      break;
    }

  default:
    {
      NS_NOTREACHED("need a valid operation in UpdateDB()!");
      rv = NS_ERROR_UNEXPECTED;
      break;
    }
  }

  if (NS_FAILED(rv)) {
    NS_WARNING("db change failed!");
    return;
  }

  nsCOMPtr<mozIStoragePendingStatement> pending;
  rv = aStmt->ExecuteAsync(nullptr, getter_AddRefs(pending));
  MOZ_ASSERT(NS_SUCCEEDED(rv));
}

NS_IMETHODIMP
nsPermissionManager::UpdateExpireTime(nsIPrincipal* aPrincipal,
                                     const char* aType,
                                     bool aExactHostMatch,
                                     uint64_t aSessionExpireTime,
                                     uint64_t aPersistentExpireTime)
{
  NS_ENSURE_ARG_POINTER(aPrincipal);
  NS_ENSURE_ARG_POINTER(aType);

  uint64_t nowms = PR_Now() / 1000;
  if (aSessionExpireTime < nowms || aPersistentExpireTime < nowms) {
    return NS_ERROR_INVALID_ARG;
  }

  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    return NS_OK;
  }

  // Setting the expire time of an nsEP is non-sensical.
  if (IsExpandedPrincipal(aPrincipal)) {
    return NS_ERROR_INVALID_ARG;
  }

  MOZ_ASSERT(PermissionAvaliable(aPrincipal, aType));

  int32_t typeIndex = GetTypeIndex(aType, false);
  // If type == -1, the type isn't known,
  // so just return NS_OK
  if (typeIndex == -1) return NS_OK;

  PermissionHashKey* entry = GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch);
  if (!entry) {
    return NS_OK;
  }

  int32_t idx = entry->GetPermissionIndex(typeIndex);
  if (-1 == idx) {
    return NS_OK;
  }

  PermissionEntry& perm = entry->GetPermissions()[idx];
  if (perm.mExpireType == EXPIRE_TIME) {
    perm.mExpireTime = aPersistentExpireTime;
  } else if (perm.mExpireType == EXPIRE_SESSION && perm.mExpireTime != 0) {
    perm.mExpireTime = aSessionExpireTime;
  }
  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::GetPermissionsWithKey(const nsACString& aPermissionKey,
                                           nsTArray<IPC::Permission>& aPerms)
{
  aPerms.Clear();
  if (NS_WARN_IF(XRE_IsContentProcess())) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
    PermissionHashKey* entry = iter.Get();

    nsAutoCString permissionKey;
    GetKeyForOrigin(entry->GetKey()->mOrigin, permissionKey);

    // If the keys don't match, and we aren't getting the default "" key, then
    // we can exit early. We have to keep looking if we're getting the default
    // key, as we may see a preload permission which should be transmitted.
    if (aPermissionKey != permissionKey && !aPermissionKey.IsEmpty()) {
      continue;
    }

    for (const auto& permEntry : entry->GetPermissions()) {
      // Given how "default" permissions work and the possibility of them being
      // overridden with UNKNOWN_ACTION, we might see this value here - but we
      // do not want to send it to the content process.
      if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
        continue;
      }

      bool isPreload = IsPreloadPermission(mTypeArray[permEntry.mType].get());
      if ((isPreload && aPermissionKey.IsEmpty()) || (!isPreload && aPermissionKey == permissionKey)) {
        aPerms.AppendElement(IPC::Permission(entry->GetKey()->mOrigin,
                                             mTypeArray.ElementAt(permEntry.mType),
                                             permEntry.mPermission,
                                             permEntry.mExpireType,
                                             permEntry.mExpireTime));
      }
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::SetPermissionsWithKey(const nsACString& aPermissionKey,
                                           nsTArray<IPC::Permission>& aPerms)
{
  if (NS_WARN_IF(XRE_IsParentProcess())) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  RefPtr<GenericPromise::Private> promise;
  bool foundKey = mPermissionKeyPromiseMap.Get(aPermissionKey, getter_AddRefs(promise));
  if (promise) {
    MOZ_ASSERT(foundKey);
    // NOTE: This will resolve asynchronously, so we can mark it as resolved
    // now, and be confident that we will have filled in the database before any
    // callbacks run.
    promise->Resolve(true, __func__);
  } else if (foundKey) {
    // NOTE: We shouldn't be sent two InitializePermissionsWithKey for the same
    // key, but it's possible.
    return NS_OK;
  }
  mPermissionKeyPromiseMap.Put(aPermissionKey, nullptr);

  // Add the permissions locally to our process
  for (IPC::Permission& perm : aPerms) {
    nsCOMPtr<nsIPrincipal> principal;
    nsresult rv = GetPrincipalFromOrigin(perm.origin, getter_AddRefs(principal));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }

#ifdef DEBUG
    nsAutoCString permissionKey;
    GetKeyForPermission(principal, perm.type.get(), permissionKey);
    MOZ_ASSERT(permissionKey == aPermissionKey,
               "The permission keys which were sent over should match!");
#endif

    // The child process doesn't care about modification times - it neither
    // reads nor writes, nor removes them based on the date - so 0 (which
    // will end up as now()) is fine.
    uint64_t modificationTime = 0;
    AddInternal(principal, perm.type, perm.capability, 0, perm.expireType,
                perm.expireTime, modificationTime, eNotify, eNoDBOperation,
                true /* ignoreSessionPermissions */);
  }
  return NS_OK;
}

/* static */ void
nsPermissionManager::GetKeyForOrigin(const nsACString& aOrigin, nsACString& aKey)
{
  aKey.Truncate();

  // We only key origins for http, https, and ftp URIs. All origins begin with
  // the URL which they apply to, which means that they should begin with their
  // scheme in the case where they are one of these interesting URIs. We don't
  // want to actually parse the URL here however, because this can be called on
  // hot paths.
  if (!StringBeginsWith(aOrigin, NS_LITERAL_CSTRING("http:")) &&
      !StringBeginsWith(aOrigin, NS_LITERAL_CSTRING("https:")) &&
      !StringBeginsWith(aOrigin, NS_LITERAL_CSTRING("ftp:"))) {
    return;
  }

  // We need to look at the originAttributes if they are present, to make sure
  // to remove any which we don't want. We put the rest of the origin, not
  // including the attributes, into the key.
  OriginAttributes attrs;
  if (!attrs.PopulateFromOrigin(aOrigin, aKey)) {
    aKey.Truncate();
    return;
  }

  // mPrivateBrowsingId must be set to false because PermissionManager is not supposed to have
  // any knowledge of private browsing. Allowing it to be true changes the suffix being hashed.
  attrs.mPrivateBrowsingId = 0;

  // Disable userContext and firstParty isolation for permissions.
  attrs.StripAttributes(OriginAttributes::STRIP_USER_CONTEXT_ID |
                        OriginAttributes::STRIP_FIRST_PARTY_DOMAIN);

#ifdef DEBUG
  // Parse the origin string into a principal, and extract some useful
  // information from it for assertions.
  nsCOMPtr<nsIPrincipal> dbgPrincipal;
  MOZ_ALWAYS_SUCCEEDS(GetPrincipalFromOrigin(aOrigin, getter_AddRefs(dbgPrincipal)));
  nsCOMPtr<nsIURI> dbgUri;
  MOZ_ALWAYS_SUCCEEDS(dbgPrincipal->GetURI(getter_AddRefs(dbgUri)));
  nsAutoCString dbgScheme;
  MOZ_ALWAYS_SUCCEEDS(dbgUri->GetScheme(dbgScheme));
  MOZ_ASSERT(dbgScheme.EqualsLiteral("http") ||
             dbgScheme.EqualsLiteral("https") ||
             dbgScheme.EqualsLiteral("ftp"));
  MOZ_ASSERT(dbgPrincipal->OriginAttributesRef() == attrs);
#endif

  // Append the stripped suffix to the output origin key.
  nsAutoCString suffix;
  attrs.CreateSuffix(suffix);
  aKey.Append(suffix);
}

/* static */ void
nsPermissionManager::GetKeyForPrincipal(nsIPrincipal* aPrincipal, nsACString& aKey)
{
  nsAutoCString origin;
  nsresult rv = aPrincipal->GetOrigin(origin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    aKey.Truncate();
    return;
  }
  GetKeyForOrigin(origin, aKey);
}

/* static */ void
nsPermissionManager::GetKeyForPermission(nsIPrincipal* aPrincipal, const char* aType, nsACString& aKey)
{
  // Preload permissions have the "" key.
  if (IsPreloadPermission(aType)) {
    aKey.Truncate();
    return;
  }

  GetKeyForPrincipal(aPrincipal, aKey);
}

/* static */ nsTArray<nsCString>
nsPermissionManager::GetAllKeysForPrincipal(nsIPrincipal* aPrincipal)
{
  MOZ_ASSERT(aPrincipal);

  nsTArray<nsCString> keys;
  nsCOMPtr<nsIPrincipal> prin = aPrincipal;
  while (prin) {
    // Add the key to the list
    nsCString* key = keys.AppendElement();
    GetKeyForPrincipal(prin, *key);

    // Get the next subdomain principal and loop back around.
    prin = GetNextSubDomainPrincipal(prin);
  }

  MOZ_ASSERT(keys.Length() >= 1,
             "Every principal should have at least one key.");
  return keys;
}

NS_IMETHODIMP
nsPermissionManager::BroadcastPermissionsForPrincipalToAllContentProcesses(nsIPrincipal* aPrincipal)
{
  nsTArray<ContentParent*> cps;
  ContentParent::GetAll(cps);
  for (ContentParent* cp : cps) {
    nsresult rv = cp->TransmitPermissionsForPrincipal(aPrincipal);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

bool
nsPermissionManager::PermissionAvaliable(nsIPrincipal* aPrincipal, const char* aType)
{
  if (XRE_IsContentProcess()) {
    nsAutoCString permissionKey;
    // NOTE: GetKeyForPermission accepts a null aType.
    GetKeyForPermission(aPrincipal, aType, permissionKey);

    // If we have a pending promise for the permission key in question, we don't
    // have the permission avaliable, so report a warning and return false.
    RefPtr<GenericPromise::Private> promise;
    if (!mPermissionKeyPromiseMap.Get(permissionKey, getter_AddRefs(promise)) || promise) {
      // Emit a useful diagnostic warning with the permissionKey for the process
      // which hasn't received permissions yet.
      NS_WARNING(nsPrintfCString("This content process hasn't received the "
                                 "permissions for %s yet", permissionKey.get()).get());
      return false;
    }
  }
  return true;
}

NS_IMETHODIMP
nsPermissionManager::WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
                                              nsIRunnable* aRunnable)
{
  MOZ_ASSERT(aRunnable);

  if (!XRE_IsContentProcess()) {
    aRunnable->Run();
    return NS_OK;
  }

  nsTArray<RefPtr<GenericPromise>> promises;
  for (auto& key : GetAllKeysForPrincipal(aPrincipal)) {
    RefPtr<GenericPromise::Private> promise;
    if (!mPermissionKeyPromiseMap.Get(key, getter_AddRefs(promise))) {
      // In this case we have found a permission which isn't avaliable in the
      // content process and hasn't been requested yet. We need to create a new
      // promise, and send the request to the parent (if we have not already
      // done so).
      promise = new GenericPromise::Private(__func__);
      mPermissionKeyPromiseMap.Put(key, RefPtr<GenericPromise::Private>(promise).forget());
    }

    if (promise) {
      promises.AppendElement(Move(promise));
    }
  }

  // If all of our permissions are avaliable, immediately run the runnable. This
  // avoids any extra overhead during fetch interception which is performance
  // sensitive.
  if (promises.IsEmpty()) {
    aRunnable->Run();
    return NS_OK;
  }

  auto* thread = SystemGroup::AbstractMainThreadFor(TaskCategory::Other);

  RefPtr<nsIRunnable> runnable = aRunnable;
  GenericPromise::All(thread, promises)->Then(
    thread, __func__,
    [runnable] () { runnable->Run(); },
    [] () {
      NS_WARNING("nsPermissionManager permission promise rejected. We're probably shutting down.");
    });
  return NS_OK;
}

NS_IMETHODIMP
nsPermissionManager::GetHasPreloadPermissions(bool* aResult)
{
  *aResult = sPreloadPermissionCount > 0;
  return NS_OK;
}
