gecko-dev/extensions/cookie/nsPermissionManager.cpp
Johann Hofmann a98b34501b Bug 1379560 - Part 1 - Add a default permission pref in the permission manager. r=mystor,Paolo
This patch enables support for setting prefs with the pattern
permissions.default.* to provide a custom default permission
for arbitrary permission types in nsPermissionManager.

The previous default of UNKNOWN_ACTION is honored if no pref is set.

A default value is provided if no permission entry can be found in the db.
Accordingly, the patch does not affect the behavior of functions
that return permission objects from the db such as GetPermissionObject,
which returns null if no entry was found.

MozReview-Commit-ID: 3JECI6kXqGf

--HG--
extra : rebase_source : 9fbcfc740a85c02cf4245956e69ae13c8f90b5ab
2017-07-10 23:13:43 +02:00

3431 lines
115 KiB
C++

/* -*- 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"
#include "ExpandedPrincipal.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"
};
// A list of permissions that can have a fallback default permission
// set under the permissions.default.* pref.
static const char* kPermissionsWithDefaults[] = {
"camera",
"microphone",
"geo",
"desktop-notification"
};
// NOTE: nullptr can be passed as aType - if it is this function will return
// "false" unconditionally.
bool
HasDefaultPref(const char* aType)
{
if (aType) {
for (const char* perm : kPermissionsWithDefaults) {
if (!strcmp(aType, perm)) {
return true;
}
}
}
return false;
}
// 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 likelihood 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.AssignLiteral("[");
hostSegment.Append(aHost);
hostSegment.AppendLiteral("]");
} 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);
nsresult rv;
nsCOMPtr<nsIPrefService> prefService = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = prefService->GetBranch("permissions.default.", getter_AddRefs(mDefaultPrefBranch));
NS_ENSURE_SUCCESS(rv, rv);
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 some permissions, query the default from a pref. We want to avoid
// doing this for all permissions so that permissions can opt into having
// the pref lookup overhead on each call.
if (HasDefaultPref(aType)) {
int32_t defaultPermission = nsIPermissionManager::UNKNOWN_ACTION;
nsresult rv = mDefaultPrefBranch->GetIntPref(aType, &defaultPermission);
if (NS_SUCCEEDED(rv)) {
*aPermission = defaultPermission;
}
}
// For expanded principals, we want to iterate over the whitelist and see
// if the permission is granted for any of them.
auto* basePrin = BasePrincipal::Cast(aPrincipal);
if (basePrin && basePrin->Is<ExpandedPrincipal>()) {
auto ep = basePrin->As<ExpandedPrincipal>();
for (auto& prin : ep->WhiteList()) {
uint32_t perm;
nsresult rv = CommonTestPermission(prin, 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;
}