summaryrefslogtreecommitdiffstats
path: root/extensions/permissions/PermissionManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/permissions/PermissionManager.cpp')
-rw-r--r--extensions/permissions/PermissionManager.cpp3669
1 files changed, 3669 insertions, 0 deletions
diff --git a/extensions/permissions/PermissionManager.cpp b/extensions/permissions/PermissionManager.cpp
new file mode 100644
index 0000000000..bc69579ecc
--- /dev/null
+++ b/extensions/permissions/PermissionManager.cpp
@@ -0,0 +1,3669 @@
+/* -*- 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/AbstractThread.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ContentPrincipal.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/ExpandedPrincipal.h"
+#include "mozilla/net/NeckoMessageUtils.h"
+#include "mozilla/Permission.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_permissions.h"
+#include "mozilla/Telemetry.h"
+
+#include "mozIStorageService.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageCID.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsEffectiveTLDService.h"
+#include "nsIConsoleService.h"
+#include "nsIUserIdleService.h"
+#include "nsIInputStream.h"
+#include "nsINavHistoryService.h"
+#include "nsIObserverService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrincipal.h"
+#include "nsIURIMutator.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsReadLine.h"
+#include "nsToolkitCompsCID.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+#define PERMISSIONS_FILE_NAME "permissions.sqlite"
+#define HOSTS_SCHEMA_VERSION 11
+
+// 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.
+constexpr char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl";
+
+constexpr char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION;
+
+// A special value for a permission ID that indicates the ID was loaded as
+// a default value. These will never be written to the database, but may
+// be overridden with an explicit permission (including UNKNOWN_ACTION)
+constexpr int64_t cIDPermissionIsDefault = -1;
+
+static StaticRefPtr<PermissionManager> gPermissionManager;
+
+#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_(;)
+
+#define EXPIRY_NOW PR_Now() / 1000
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+bool IsChildProcess() { return XRE_IsContentProcess(); }
+
+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());
+}
+
+// NOTE: an empty string can be passed as aType - if it is this function will
+// return "false" unconditionally.
+bool HasDefaultPref(const nsACString& aType) {
+ // A list of permissions that can have a fallback default permission
+ // set under the permissions.default.* pref.
+ static const nsLiteralCString kPermissionsWithDefaults[] = {
+ "camera"_ns, "microphone"_ns, "geo"_ns, "desktop-notification"_ns,
+ "shortcuts"_ns};
+
+ if (!aType.IsEmpty()) {
+ for (const auto& perm : kPermissionsWithDefaults) {
+ if (perm.Equals(aType)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// These permissions are special permissions which must be transmitted to the
+// content process before documents with their principals have loaded within
+// that process.
+//
+// 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 nsLiteralCString kPreloadPermissions[] = {
+ // This permission is preloaded to support properly blocking service worker
+ // interception when a user has disabled storage for a specific site. Once
+ // service worker interception moves to the parent process this should be
+ // removed. See bug 1428130.
+ "cookie"_ns};
+
+// Certain permissions should never be persisted to disk under GeckoView; it's
+// the responsibility of the app to manage storing these beyond the scope of
+// a single session.
+#ifdef ANDROID
+static const nsLiteralCString kGeckoViewRestrictedPermissions[] = {
+ "MediaManagerVideo"_ns, "geolocation"_ns,
+ "desktop-notification"_ns, "persistent-storage"_ns,
+ "trackingprotection"_ns, "trackingprotection-pb"_ns};
+#endif
+
+// NOTE: nullptr can be passed as aType - if it is this function will return
+// "false" unconditionally.
+bool IsPreloadPermission(const nsACString& aType) {
+ if (!aType.IsEmpty()) {
+ for (const auto& perm : kPreloadPermissions) {
+ if (perm.Equals(aType)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// Array of permission types which should not be isolated by origin attributes,
+// for user context and private browsing.
+// Keep this array in sync with 'STRIPPED_PERMS' in
+// 'test_permmanager_oa_strip.js'
+// Currently only preloaded permissions are supported.
+// This is because perms are sent to the content process in bulk by perm key.
+// Non-preloaded, but OA stripped permissions would not be accessible by sites
+// in private browsing / non-default user context.
+static constexpr std::array<nsLiteralCString, 1> kStripOAPermissions = {
+ {"cookie"_ns}};
+
+bool IsOAForceStripPermission(const nsACString& aType) {
+ if (aType.IsEmpty()) {
+ return false;
+ }
+ for (const auto& perm : kStripOAPermissions) {
+ if (perm.Equals(aType)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Strip origin attributes depending on pref state
+ * @param aForceStrip If true, strips user context and private browsing id,
+ * ignoring stripping prefs.
+ * @param aOriginAttributes object to strip.
+ */
+void MaybeStripOAs(bool aForceStrip, OriginAttributes& aOriginAttributes) {
+ uint32_t flags = 0;
+
+ if (aForceStrip || !StaticPrefs::permissions_isolateBy_privateBrowsing()) {
+ flags |= OriginAttributes::STRIP_PRIVATE_BROWSING_ID;
+ }
+
+ if (aForceStrip || !StaticPrefs::permissions_isolateBy_userContext()) {
+ flags |= OriginAttributes::STRIP_USER_CONTEXT_ID;
+ }
+
+ if (flags != 0) {
+ aOriginAttributes.StripAttributes(flags);
+ }
+}
+
+void OriginAppendOASuffix(OriginAttributes aOriginAttributes,
+ bool aForceStripOA, nsACString& aOrigin) {
+ MaybeStripOAs(aForceStripOA, aOriginAttributes);
+
+ nsAutoCString oaSuffix;
+ aOriginAttributes.CreateSuffix(oaSuffix);
+ aOrigin.Append(oaSuffix);
+}
+
+nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA,
+ 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);
+
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(suffix)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ OriginAppendOASuffix(attrs, aForceStripOA, aOrigin);
+
+ return NS_OK;
+}
+
+nsresult GetOriginFromURIAndOA(nsIURI* aURI,
+ const OriginAttributes* aOriginAttributes,
+ bool aForceStripOA, nsACString& aOrigin) {
+ nsAutoCString origin(aOrigin);
+ nsresult rv = ContentPrincipal::GenerateOriginNoSuffixFromURI(aURI, origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OriginAppendOASuffix(*aOriginAttributes, aForceStripOA, origin);
+
+ aOrigin = origin;
+
+ return NS_OK;
+}
+
+nsresult GetPrincipalFromOrigin(const nsACString& aOrigin, bool aForceStripOA,
+ nsIPrincipal** aPrincipal) {
+ nsAutoCString originNoSuffix;
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromOrigin(aOrigin, originNoSuffix)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MaybeStripOAs(aForceStripOA, attrs);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+ principal.forget(aPrincipal);
+ return NS_OK;
+}
+
+nsresult GetPrincipal(nsIURI* aURI, bool aIsInIsolatedMozBrowserElement,
+ nsIPrincipal** aPrincipal) {
+ OriginAttributes attrs(aIsInIsolatedMozBrowserElement);
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aURI, attrs);
+ NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);
+
+ principal.forget(aPrincipal);
+ return NS_OK;
+}
+
+nsresult GetPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal) {
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aURI, attrs);
+ NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);
+
+ principal.forget(aPrincipal);
+ return NS_OK;
+}
+
+nsCString GetNextSubDomainForHost(const nsACString& aHost) {
+ nsCString subDomain;
+ nsresult rv =
+ nsEffectiveTLDService::GetInstance()->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 ""_ns;
+ }
+
+ 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 = NS_MutateURI(aURI).SetHost(domain).Finalize(uri);
+ if (NS_FAILED(rv) || !uri) {
+ return nullptr;
+ }
+
+ return uri.forget();
+}
+
+nsresult UpgradeHostToOriginAndInsert(
+ const nsACString& aHost, const nsCString& aType, uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime,
+ bool aIsInIsolatedMozBrowserElement,
+ std::function<nsresult(const nsACString& aOrigin, const nsCString& aType,
+ uint32_t aPermission, uint32_t aExpireType,
+ int64_t aExpireTime, int64_t aModificationTime)>&&
+ aCallback) {
+ 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
+ if (uri->SchemeIs("moz-nullprincipal")) {
+ NS_WARNING("A moz-nullprincipal: permission is being discarded.");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement,
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString origin;
+ rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType),
+ origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aCallback(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;
+ rv = nsEffectiveTLDService::GetInstance()->GetBaseDomainFromHost(aHost, 0,
+ eTLD1);
+
+ if (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 = NS_MutateURI(uri).SetHost(aHost).Finalize(uri);
+ 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, aIsInIsolatedMozBrowserElement,
+ getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) continue;
+
+ nsAutoCString origin;
+ rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType),
+ 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 = aCallback(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), "http://"_ns + hostSegment);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement,
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType),
+ origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aCallback(origin, aType, aPermission, aExpireType, aExpireTime,
+ aModificationTime);
+
+ // https:// URI default
+ rv = NS_NewURI(getter_AddRefs(uri), "https://"_ns + hostSegment);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement,
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType),
+ origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aCallback(origin, aType, aPermission, aExpireType, aExpireTime,
+ aModificationTime);
+ }
+
+ return NS_OK;
+}
+
+bool IsExpandedPrincipal(nsIPrincipal* aPrincipal) {
+ nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(aPrincipal);
+ return !!ep;
+}
+
+// We only want to persist permissions which don't have session or policy
+// expiration.
+bool IsPersistentExpire(uint32_t aExpire, const nsACString& aType) {
+ bool res = (aExpire != nsIPermissionManager::EXPIRE_SESSION &&
+ aExpire != nsIPermissionManager::EXPIRE_POLICY);
+#ifdef ANDROID
+ for (const auto& perm : kGeckoViewRestrictedPermissions) {
+ res = res && !perm.Equals(aType);
+ }
+#endif
+ return res;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+PermissionManager::PermissionKey*
+PermissionManager::PermissionKey::CreateFromPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ nsresult& aResult) {
+ nsAutoCString origin;
+ aResult = GetOriginFromPrincipal(aPrincipal, aForceStripOA, origin);
+ if (NS_WARN_IF(NS_FAILED(aResult))) {
+ return nullptr;
+ }
+
+ return new PermissionKey(origin);
+}
+
+PermissionManager::PermissionKey*
+PermissionManager::PermissionKey::CreateFromURIAndOriginAttributes(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes, bool aForceStripOA,
+ nsresult& aResult) {
+ nsAutoCString origin;
+ aResult =
+ GetOriginFromURIAndOA(aURI, aOriginAttributes, aForceStripOA, origin);
+ if (NS_WARN_IF(NS_FAILED(aResult))) {
+ return nullptr;
+ }
+
+ return new PermissionKey(origin);
+}
+
+PermissionManager::PermissionKey*
+PermissionManager::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);
+}
+
+/* static */
+void PermissionManager::Startup() {
+ nsCOMPtr<nsIPermissionManager> permManager =
+ do_GetService("@mozilla.org/permissionmanager;1");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// PermissionManager Implementation
+
+NS_IMPL_ISUPPORTS(PermissionManager, nsIPermissionManager, nsIObserver,
+ nsISupportsWeakReference, nsIAsyncShutdownBlocker)
+
+PermissionManager::PermissionManager()
+ : mMonitor("PermissionManager::mMonitor"),
+ mState(eInitializing),
+ mMemoryOnlyDB(false),
+ mBlockerAdded(false),
+ mLargestID(0) {}
+
+PermissionManager::~PermissionManager() {
+ // 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();
+
+ if (mThread) {
+ mThread->Shutdown();
+ mThread = nullptr;
+ }
+}
+
+// static
+already_AddRefed<nsIPermissionManager> PermissionManager::GetXPCOMSingleton() {
+ if (gPermissionManager) {
+ return do_AddRef(gPermissionManager);
+ }
+
+ // Create a new singleton PermissionManager.
+ // 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.
+ auto permManager = MakeRefPtr<PermissionManager>();
+ if (NS_SUCCEEDED(permManager->Init())) {
+ gPermissionManager = permManager.get();
+ return permManager.forget();
+ }
+
+ return nullptr;
+}
+
+// static
+PermissionManager* PermissionManager::GetInstance() {
+ if (!gPermissionManager) {
+ // Hand off the creation of the permission manager to GetXPCOMSingleton.
+ nsCOMPtr<nsIPermissionManager> permManager = GetXPCOMSingleton();
+ }
+
+ return gPermissionManager;
+}
+
+nsresult PermissionManager::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 = 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.
+ mState = eReady;
+
+ // We use ClearOnShutdown on the content process only because on the parent
+ // process we need to block the shutdown for the final closeDB() call.
+ ClearOnShutdown(&gPermissionManager);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "profile-before-change", true);
+ observerService->AddObserver(this, "profile-do-change", true);
+ observerService->AddObserver(this, "testonly-reload-permissions-from-disk",
+ true);
+ }
+
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetShutdownPhase();
+ if (asc) {
+ nsAutoString blockerName;
+ MOZ_ALWAYS_SUCCEEDS(GetName(blockerName));
+
+ // This method can fail during some xpcshell-tests.
+ nsresult rv =
+ asc->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, blockerName);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ if (NS_SUCCEEDED(rv)) {
+ mBlockerAdded = true;
+ }
+ }
+
+ if (!mBlockerAdded) {
+ ClearOnShutdown(&gPermissionManager);
+ }
+ }
+
+ AddIdleDailyMaintenanceJob();
+
+ MOZ_ASSERT(!mThread);
+ NS_ENSURE_SUCCESS(NS_NewNamedThread("Permission", getter_AddRefs(mThread)),
+ NS_ERROR_FAILURE);
+
+ PRThread* prThread;
+ MOZ_ALWAYS_SUCCEEDS(mThread->GetPRThread(&prThread));
+ MOZ_ASSERT(prThread);
+
+ mThreadBoundData.Transfer(prThread);
+
+ InitDB(false);
+
+ return NS_OK;
+}
+
+nsresult PermissionManager::OpenDatabase(nsIFile* aPermissionsFile) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ auto data = mThreadBoundData.Access();
+
+ 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(kMozStorageMemoryStorageKey, VoidCString(),
+ getter_AddRefs(data->mDBConn));
+ } else {
+ rv = storage->OpenDatabase(aPermissionsFile, getter_AddRefs(data->mDBConn));
+ }
+ return rv;
+}
+
+void PermissionManager::InitDB(bool aRemoveFile) {
+ mState = eInitializing;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ mReadEntries.Clear();
+ }
+
+ auto readyIfFailed = MakeScopeExit([&]() {
+ // 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?
+ mState = eReady;
+ });
+
+ if (!mPermissionsFile) {
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_PERMISSION_PARENT_DIR,
+ getter_AddRefs(mPermissionsFile));
+ if (NS_FAILED(rv)) {
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mPermissionsFile));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ rv =
+ mPermissionsFile->AppendNative(nsLiteralCString(PERMISSIONS_FILE_NAME));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+
+ nsCOMPtr<nsIInputStream> defaultsInputStream = GetDefaultsInputStream();
+
+ RefPtr<PermissionManager> self = this;
+ mThread->Dispatch(NS_NewRunnableFunction(
+ "PermissionManager::InitDB", [self, aRemoveFile, defaultsInputStream] {
+ nsresult rv = self->TryInitDB(aRemoveFile, defaultsInputStream);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ // This extra runnable calls EnsureReadCompleted to finialize the
+ // initialization. If there is something blocked by the monitor, it will
+ // be NOP.
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("PermissionManager::InitDB-MainThread",
+ [self] { self->EnsureReadCompleted(); }));
+
+ self->mMonitor.Notify();
+ }));
+
+ readyIfFailed.release();
+}
+
+nsresult PermissionManager::TryInitDB(bool aRemoveFile,
+ nsIInputStream* aDefaultsInputStream) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ MonitorAutoLock lock(mMonitor);
+
+ auto raii = MakeScopeExit([&]() {
+ if (aDefaultsInputStream) {
+ aDefaultsInputStream->Close();
+ }
+
+ mState = eDBInitialized;
+ });
+
+ auto data = mThreadBoundData.Access();
+
+ auto raiiFailure = MakeScopeExit([&]() {
+ if (data->mDBConn) {
+ DebugOnly<nsresult> rv = data->mDBConn->Close();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ data->mDBConn = nullptr;
+ }
+ });
+
+ nsresult rv;
+
+ if (aRemoveFile) {
+ bool exists = false;
+ rv = mPermissionsFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (exists) {
+ rv = mPermissionsFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ rv = OpenDatabase(mPermissionsFile);
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ LogToConsole(u"permissions.sqlite is corrupted! Try again!"_ns);
+
+ // Add telemetry probe
+ Telemetry::Accumulate(Telemetry::PERMISSIONS_SQL_CORRUPTED, 1);
+
+ // delete corrupted permissions.sqlite and try again
+ rv = mPermissionsFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LogToConsole(u"Corrupted permissions.sqlite has been removed."_ns);
+
+ rv = OpenDatabase(mPermissionsFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LogToConsole(u"OpenDatabase to permissions.sqlite is successful!"_ns);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool ready;
+ data->mDBConn->GetConnectionReady(&ready);
+ if (!ready) {
+ LogToConsole(nsLiteralString(
+ u"Fail to get connection to permissions.sqlite! Try again!"));
+
+ // delete and try again
+ rv = mPermissionsFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LogToConsole(u"Defective permissions.sqlite has been removed."_ns);
+
+ // Add telemetry probe
+ Telemetry::Accumulate(Telemetry::DEFECTIVE_PERMISSIONS_SQL_REMOVED, 1);
+
+ rv = OpenDatabase(mPermissionsFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LogToConsole(u"OpenDatabase to permissions.sqlite is successful!"_ns);
+
+ data->mDBConn->GetConnectionReady(&ready);
+ if (!ready) return NS_ERROR_UNEXPECTED;
+ }
+
+ bool tableExists = false;
+ data->mDBConn->TableExists("moz_perms"_ns, &tableExists);
+ if (!tableExists) {
+ data->mDBConn->TableExists("moz_hosts"_ns, &tableExists);
+ }
+ if (!tableExists) {
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // table already exists; check the schema version before reading
+ int32_t dbSchemaVersion;
+ rv = data->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 = data->mDBConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_hosts ADD expireType INTEGER"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_hosts ADD expireTime INTEGER"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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 = data->mDBConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_hosts ADD appId INTEGER"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_hosts ADD isInBrowserElement INTEGER"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->SetSchemaVersion(3);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[fallthrough]];
+
+ // Version 3->4 is the creation of the modificationTime field.
+ case 3: {
+ rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "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 = data->mDBConn->SetSchemaVersion(4);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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;
+ data->mDBConn->TableExists("moz_perms"_ns, &permsTableExists);
+ if (!permsTableExists) {
+ // Move the upgraded database to moz_perms
+ rv = data->mDBConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_hosts RENAME TO moz_perms"_ns);
+ 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 = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_hosts"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+#ifdef DEBUG
+ // The moz_hosts table shouldn't exist anymore
+ bool hostsTableExists = false;
+ data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists);
+ MOZ_ASSERT(!hostsTableExists);
+#endif
+
+ // Rename moz_hosts_v4 back to it's original location, if it
+ // exists
+ bool v4TableExists = false;
+ data->mDBConn->TableExists("moz_hosts_v4"_ns, &v4TableExists);
+ if (v4TableExists) {
+ rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_hosts_v4 RENAME TO moz_hosts"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = data->mDBConn->SetSchemaVersion(6);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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;
+ data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists);
+ if (hostsTableExists) {
+ // 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 = data->mDBConn->BeginTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool tableExists = false;
+ data->mDBConn->TableExists("moz_hosts_new"_ns, &tableExists);
+ if (tableExists) {
+ NS_WARNING(
+ "The temporary database moz_hosts_new already exists, "
+ "dropping "
+ "it.");
+ rv = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_hosts_new"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = data->mDBConn->ExecuteSimpleSQL(
+ nsLiteralCString("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 = data->mDBConn->CreateStatement(
+ nsLiteralCString(
+ "SELECT host, type, permission, expireType, "
+ "expireTime, "
+ "modificationTime, isInBrowserElement FROM moz_hosts"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t id = 0;
+ bool hasResult;
+
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ MigrationEntry entry;
+
+ // Read in the old row
+ rv = stmt->GetUTF8String(0, entry.mHost);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ rv = stmt->GetUTF8String(1, entry.mType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ entry.mId = id++;
+ entry.mPermission = stmt->AsInt32(2);
+ entry.mExpireType = stmt->AsInt32(3);
+ entry.mExpireTime = stmt->AsInt64(4);
+ entry.mModificationTime = stmt->AsInt64(5);
+ entry.mIsInBrowserElement = static_cast<bool>(stmt->AsInt32(6));
+
+ mMigrationEntries.AppendElement(entry);
+ }
+
+ // We don't drop the moz_hosts table such that it is available
+ // 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 = data->mDBConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE moz_hosts_is_backup (dummy "
+ "INTEGER PRIMARY KEY)"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool permsTableExists = false;
+ data->mDBConn->TableExists("moz_perms"_ns, &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 = data->mDBConn->CreateStatement(
+ "SELECT COUNT(*) FROM moz_perms"_ns, getter_AddRefs(countStmt));
+ bool hasResult = false;
+ if (NS_FAILED(rv) ||
+ NS_FAILED(countStmt->ExecuteStep(&hasResult)) || !hasResult) {
+ 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 = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_perms RENAME TO moz_perms_v6"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_hosts_new RENAME TO moz_perms"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->CommitTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // We don't have a moz_hosts table, so we create one for
+ // downgrading purposes. This table is empty.
+ rv = data->mDBConn->ExecuteSimpleSQL(
+ nsLiteralCString("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;
+ data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists);
+ data->mDBConn->TableExists("moz_perms"_ns, &permsTableExists);
+ MOZ_ASSERT(hostsTableExists && permsTableExists);
+ }
+#endif
+
+ rv = data->mDBConn->SetSchemaVersion(7);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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;
+ data->mDBConn->TableExists("moz_hosts_is_backup"_ns,
+ &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<mozIStorageStatement> stmt;
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString(
+ "SELECT host, type, permission, expireType, "
+ "expireTime, "
+ "modificationTime, isInBrowserElement FROM moz_hosts"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> idStmt;
+ rv = data->mDBConn->CreateStatement(
+ "SELECT MAX(id) FROM moz_hosts"_ns, 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;
+ }
+
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ MigrationEntry entry;
+
+ // Read in the old row
+ rv = stmt->GetUTF8String(0, entry.mHost);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsAutoCString eTLD1;
+ rv = nsEffectiveTLDService::GetInstance()->GetBaseDomainFromHost(
+ entry.mHost, 0, eTLD1);
+ if (NS_SUCCEEDED(rv)) {
+ // We only care about entries which the tldService can't
+ // handle
+ continue;
+ }
+
+ rv = stmt->GetUTF8String(1, entry.mType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ entry.mId = id++;
+ entry.mPermission = stmt->AsInt32(2);
+ entry.mExpireType = stmt->AsInt32(3);
+ entry.mExpireTime = stmt->AsInt64(4);
+ entry.mModificationTime = stmt->AsInt64(5);
+ entry.mIsInBrowserElement = static_cast<bool>(stmt->AsInt32(6));
+
+ mMigrationEntries.AppendElement(entry);
+ }
+ }
+
+ // Even if we didn't perform the migration, we want to bump the
+ // schema version to 8.
+ rv = data->mDBConn->SetSchemaVersion(8);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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;
+ data->mDBConn->TableExists("moz_hosts_is_backup"_ns,
+ &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 = data->mDBConn->ExecuteSimpleSQL("DELETE FROM moz_hosts"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The table is no longer a backup, so get rid of it.
+ rv = data->mDBConn->ExecuteSimpleSQL(
+ "DROP TABLE moz_hosts_is_backup"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = data->mDBConn->SetSchemaVersion(9);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[fallthrough]];
+
+ case 9: {
+ rv = data->mDBConn->SetSchemaVersion(10);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[fallthrough]];
+
+ case 10: {
+ // Filter out the rows with storage access API permissions with a
+ // granted origin, and remove the granted origin part from the
+ // permission type.
+ rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString(
+ "UPDATE moz_perms "
+ "SET type=SUBSTR(type, 0, INSTR(SUBSTR(type, INSTR(type, "
+ "'^') + "
+ "1), '^') + INSTR(type, '^')) "
+ "WHERE INSTR(SUBSTR(type, INSTR(type, '^') + 1), '^') AND "
+ "SUBSTR(type, 0, 18) == \"storageAccessAPI^\";"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[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 = data->mDBConn->CreateStatement(
+ nsLiteralCString("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 = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_perms"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } break;
+ }
+ }
+
+ // cache frequently used statements (for insertion, deletion, and
+ // updating)
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("INSERT INTO moz_perms "
+ "(id, origin, type, permission, expireType, "
+ "expireTime, modificationTime) "
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"),
+ getter_AddRefs(data->mStmtInsert));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->CreateStatement(nsLiteralCString("DELETE FROM moz_perms "
+ "WHERE id = ?1"),
+ getter_AddRefs(data->mStmtDelete));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("UPDATE moz_perms "
+ "SET permission = ?2, expireType= ?3, expireTime = "
+ "?4, modificationTime = ?5 WHERE id = ?1"),
+ getter_AddRefs(data->mStmtUpdate));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Always import default permissions.
+ ConsumeDefaultsInputStream(aDefaultsInputStream, lock);
+
+ // check whether to import or just read in the db
+ if (tableExists) {
+ rv = Read(lock);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ raiiFailure.release();
+
+ return NS_OK;
+}
+
+void PermissionManager::AddIdleDailyMaintenanceJob() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ NS_ENSURE_TRUE_VOID(observerService);
+
+ nsresult rv =
+ observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, false);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+void PermissionManager::RemoveIdleDailyMaintenanceJob() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ NS_ENSURE_TRUE_VOID(observerService);
+
+ nsresult rv =
+ observerService->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+void PermissionManager::PerformIdleDailyMaintenance() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<PermissionManager> self = this;
+ mThread->Dispatch(NS_NewRunnableFunction(
+ "PermissionManager::PerformIdleDailyMaintenance", [self] {
+ auto data = self->mThreadBoundData.Access();
+
+ if (self->mState == eClosed || !data->mDBConn) {
+ return;
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmtDeleteExpired;
+ nsresult rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("DELETE FROM moz_perms WHERE expireType = "
+ "?1 AND expireTime <= ?2"),
+ getter_AddRefs(stmtDeleteExpired));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = stmtDeleteExpired->BindInt32ByIndex(
+ 0, nsIPermissionManager::EXPIRE_TIME);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = stmtDeleteExpired->BindInt64ByIndex(1, EXPIRY_NOW);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = stmtDeleteExpired->Execute();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }));
+}
+
+// sets the schema version and creates the moz_perms table.
+nsresult PermissionManager::CreateTable() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ auto data = mThreadBoundData.Access();
+
+ // set the schema version, before creating the table
+ nsresult rv = data->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 = data->mDBConn->ExecuteSimpleSQL(
+ nsLiteralCString("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 data->mDBConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE moz_hosts ("
+ " id INTEGER PRIMARY KEY"
+ ",host TEXT"
+ ",type TEXT"
+ ",permission INTEGER"
+ ",expireType INTEGER"
+ ",expireTime INTEGER"
+ ",modificationTime INTEGER"
+ ",isInBrowserElement INTEGER"
+ ")"));
+}
+
+// Returns whether the given combination of expire type and expire time are
+// expired. Note that EXPIRE_SESSION only honors expireTime if it is nonzero.
+bool PermissionManager::HasExpired(uint32_t aExpireType, int64_t aExpireTime) {
+ return (aExpireType == nsIPermissionManager::EXPIRE_TIME ||
+ (aExpireType == nsIPermissionManager::EXPIRE_SESSION &&
+ aExpireTime != 0)) &&
+ aExpireTime <= EXPIRY_NOW;
+}
+
+NS_IMETHODIMP
+PermissionManager::AddFromPrincipal(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t aPermission, uint32_t aExpireType,
+ int64_t aExpireTime) {
+ ENSURE_NOT_CHILD_PROCESS;
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_TRUE(aExpireType == nsIPermissionManager::EXPIRE_NEVER ||
+ aExpireType == nsIPermissionManager::EXPIRE_TIME ||
+ aExpireType == nsIPermissionManager::EXPIRE_SESSION ||
+ aExpireType == nsIPermissionManager::EXPIRE_POLICY,
+ NS_ERROR_INVALID_ARG);
+
+ // Skip addition if the permission is already expired.
+ if (HasExpired(aExpireType, aExpireTime)) {
+ return NS_OK;
+ }
+
+ // We don't add the system principal because it actually has no URI and we
+ // always allow action for them.
+ if (aPrincipal->IsSystemPrincipal()) {
+ 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, aType, aPermission, 0, aExpireType,
+ aExpireTime, modificationTime, eNotify, eWriteToDB);
+}
+
+nsresult PermissionManager::AddInternal(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aPermission,
+ int64_t aID, uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime, NotifyOperationType aNotifyOperation,
+ DBOperationType aDBOperation, const bool aIgnoreSessionPermissions,
+ const nsACString* aOriginString) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ EnsureReadCompleted();
+
+ nsresult rv = NS_OK;
+ nsAutoCString origin;
+ // Only attempt to compute the origin string when it is going to be needed
+ // later on in the function.
+ if (!IsChildProcess() ||
+ (aDBOperation == eWriteToDB && IsPersistentExpire(aExpireType, aType))) {
+ if (aOriginString) {
+ // Use the origin string provided by the caller.
+ origin = *aOriginString;
+ } else {
+ // Compute it from the principal provided.
+ rv = GetOriginFromPrincipal(aPrincipal, IsOAForceStripPermission(aType),
+ origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // For private browsing only store permissions for the session
+ if (aExpireType != EXPIRE_SESSION) {
+ uint32_t privateBrowsingId =
+ nsScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID;
+ nsresult rv = aPrincipal->GetPrivateBrowsingId(&privateBrowsingId);
+ if (NS_SUCCEEDED(rv) &&
+ privateBrowsingId !=
+ nsScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) {
+ aExpireType = EXPIRE_SESSION;
+ }
+ }
+
+ // Let's send the new permission to the content process only if it has to be
+ // notified.
+ if (!IsChildProcess() && aNotifyOperation == eNotify) {
+ IPC::Permission permission(origin, aType, aPermission, aExpireType,
+ aExpireTime);
+
+ nsAutoCString permissionKey;
+ GetKeyForPermission(aPrincipal, aType, 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(PermissionAvailable(aPrincipal, aType));
+
+ // look up the type index
+ int32_t typeIndex = GetTypeIndex(aType, 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, IsOAForceStripPermission(aType), 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 = EXPIRY_NOW;
+ }
+
+ 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));
+
+ if (aDBOperation == eWriteToDB &&
+ IsPersistentExpire(aExpireType, aType)) {
+ UpdateDB(op, id, origin, aType, aPermission, aExpireType, aExpireTime,
+ aModificationTime);
+ }
+
+ if (aNotifyOperation == eNotify) {
+ NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
+ aPermission, aExpireType, aExpireTime,
+ aModificationTime, u"added");
+ }
+
+ break;
+ }
+
+ case eOperationRemoving: {
+ PermissionEntry oldPermissionEntry = entry->GetPermissions()[index];
+ id = oldPermissionEntry.mID;
+
+ // If the type we want to remove is EXPIRE_POLICY, we need to reject
+ // attempts to change the permission.
+ if (entry->GetPermissions()[index].mExpireType == EXPIRE_POLICY) {
+ NS_WARNING("Attempting to remove EXPIRE_POLICY permission");
+ break;
+ }
+
+ entry->GetPermissions().RemoveElementAt(index);
+
+ if (aDBOperation == eWriteToDB)
+ // We care only about the id here so we pass dummy values for all other
+ // parameters.
+ UpdateDB(op, id, ""_ns, ""_ns, 0, nsIPermissionManager::EXPIRE_NEVER, 0,
+ 0);
+
+ if (aNotifyOperation == eNotify) {
+ NotifyObserversWithPermission(
+ aPrincipal, mTypeArray[typeIndex], oldPermissionEntry.mPermission,
+ oldPermissionEntry.mExpireType, oldPermissionEntry.mExpireTime,
+ oldPermissionEntry.mModificationTime, 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 existing type is EXPIRE_POLICY, we need to reject attempts to
+ // change the permission.
+ if (entry->GetPermissions()[index].mExpireType == EXPIRE_POLICY) {
+ NS_WARNING("Attempting to modify EXPIRE_POLICY permission");
+ break;
+ }
+
+ PermissionEntry oldPermissionEntry = entry->GetPermissions()[index];
+
+ // 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) {
+ bool newIsPersistentExpire = IsPersistentExpire(aExpireType, aType);
+ bool oldIsPersistentExpire =
+ IsPersistentExpire(oldPermissionEntry.mExpireType, aType);
+
+ if (!newIsPersistentExpire && oldIsPersistentExpire) {
+ // Maybe we have to remove the previous permission if that was
+ // persistent.
+ UpdateDB(eOperationRemoving, id, ""_ns, ""_ns, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0);
+ } else if (newIsPersistentExpire && !oldIsPersistentExpire) {
+ // It could also be that the previous permission was session-only but
+ // this needs to be written into the DB. In this case, we have to run
+ // an Adding operation.
+ UpdateDB(eOperationAdding, id, origin, aType, aPermission,
+ aExpireType, aExpireTime, aModificationTime);
+ } else if (newIsPersistentExpire) {
+ // This is the a simple update. We care only about the id, the
+ // permission and expireType/expireTime/modificationTime here. We pass
+ // dummy values for all other parameters.
+ UpdateDB(op, id, ""_ns, ""_ns, aPermission, aExpireType, aExpireTime,
+ aModificationTime);
+ }
+ }
+
+ if (aNotifyOperation == eNotify) {
+ NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
+ aPermission, aExpireType, aExpireTime,
+ aModificationTime, 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 or
+ // policy expiry.
+ NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType !=
+ nsIPermissionManager::EXPIRE_SESSION,
+ NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType !=
+ nsIPermissionManager::EXPIRE_POLICY,
+ 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 &&
+ IsPersistentExpire(aExpireType, aType)) {
+ UpdateDB(eOperationAdding, id, origin, aType, aPermission, aExpireType,
+ aExpireTime, aModificationTime);
+ }
+
+ if (aNotifyOperation == eNotify) {
+ NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex],
+ aPermission, aExpireType, aExpireTime,
+ aModificationTime, u"changed");
+ }
+
+ } break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PermissionManager::RemoveFromPrincipal(nsIPrincipal* aPrincipal,
+ const nsACString& aType) {
+ ENSURE_NOT_CHILD_PROCESS;
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+
+ // System principals are never added to the database, no need to remove them.
+ if (aPrincipal->IsSystemPrincipal()) {
+ 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, aType, nsIPermissionManager::UNKNOWN_ACTION, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0, eNotify,
+ eWriteToDB);
+}
+
+NS_IMETHODIMP
+PermissionManager::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);
+}
+
+NS_IMETHODIMP
+PermissionManager::RemoveAll() {
+ ENSURE_NOT_CHILD_PROCESS;
+ return RemoveAllInternal(true);
+}
+
+NS_IMETHODIMP
+PermissionManager::RemoveAllSince(int64_t aSince) {
+ ENSURE_NOT_CHILD_PROCESS;
+ return RemoveAllModifiedSince(aSince);
+}
+
+template <class T>
+nsresult PermissionManager::RemovePermissionEntries(T aCondition) {
+ EnsureReadCompleted();
+
+ Vector<Tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10> array;
+ for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
+ PermissionHashKey* entry = iter.Get();
+ for (const auto& permEntry : entry->GetPermissions()) {
+ if (!aCondition(permEntry)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = GetPrincipalFromOrigin(
+ entry->GetKey()->mOrigin,
+ IsOAForceStripPermission(mTypeArray[permEntry.mType]),
+ getter_AddRefs(principal));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ if (!array.emplaceBack(principal, mTypeArray[permEntry.mType],
+ entry->GetKey()->mOrigin)) {
+ continue;
+ }
+ }
+ }
+
+ for (auto& i : array) {
+ // AddInternal handles removal, so let it do the work...
+ AddInternal(Get<0>(i), Get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0,
+ PermissionManager::eNotify, PermissionManager::eWriteToDB,
+ false, &Get<2>(i));
+ }
+
+ // now re-import any defaults as they may now be required if we just deleted
+ // an override.
+ ImportLatestDefaults();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PermissionManager::RemoveByType(const nsACString& aType) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ 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;
+ }
+
+ return RemovePermissionEntries(
+ [typeIndex](const PermissionEntry& aPermEntry) {
+ return static_cast<uint32_t>(typeIndex) == aPermEntry.mType;
+ });
+}
+
+NS_IMETHODIMP
+PermissionManager::RemoveByTypeSince(const nsACString& aType,
+ int64_t aModificationTime) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ 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;
+ }
+
+ return RemovePermissionEntries(
+ [typeIndex, aModificationTime](const PermissionEntry& aPermEntry) {
+ return uint32_t(typeIndex) == aPermEntry.mType &&
+ aModificationTime <= aPermEntry.mModificationTime;
+ });
+}
+
+void PermissionManager::CloseDB(CloseDBNextOp aNextOp) {
+ EnsureReadCompleted();
+
+ mState = eClosed;
+
+ nsCOMPtr<nsIInputStream> defaultsInputStream;
+ if (aNextOp == eRebuldOnSuccess) {
+ defaultsInputStream = GetDefaultsInputStream();
+ }
+
+ RefPtr<PermissionManager> self = this;
+ mThread->Dispatch(NS_NewRunnableFunction(
+ "PermissionManager::CloseDB", [self, aNextOp, defaultsInputStream] {
+ auto data = self->mThreadBoundData.Access();
+ // Null the statements, this will finalize them.
+ data->mStmtInsert = nullptr;
+ data->mStmtDelete = nullptr;
+ data->mStmtUpdate = nullptr;
+ if (data->mDBConn) {
+ DebugOnly<nsresult> rv = data->mDBConn->Close();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ data->mDBConn = nullptr;
+
+ if (aNextOp == eRebuldOnSuccess) {
+ self->TryInitDB(true, defaultsInputStream);
+ }
+ }
+
+ if (aNextOp == eShutdown) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "PermissionManager::MaybeCompleteShutdown",
+ [self] { self->MaybeCompleteShutdown(); }));
+ }
+ }));
+}
+
+nsresult PermissionManager::RemoveAllFromIPC() {
+ MOZ_ASSERT(IsChildProcess());
+
+ // 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();
+
+ return NS_OK;
+}
+
+nsresult PermissionManager::RemoveAllInternal(bool aNotifyObservers) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ EnsureReadCompleted();
+
+ // Let's broadcast the removeAll() to any content process.
+ nsTArray<ContentParent*> parents;
+ ContentParent::GetAll(parents);
+ for (ContentParent* parent : parents) {
+ Unused << parent->SendRemoveAllPermissions();
+ }
+
+ // 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
+ ImportLatestDefaults();
+
+ if (aNotifyObservers) {
+ NotifyObservers(nullptr, u"cleared");
+ }
+
+ RefPtr<PermissionManager> self = this;
+ mThread->Dispatch(
+ NS_NewRunnableFunction("PermissionManager::RemoveAllInternal", [self] {
+ auto data = self->mThreadBoundData.Access();
+
+ if (self->mState == eClosed || !data->mDBConn) {
+ return;
+ }
+
+ // clear the db
+ nsresult rv =
+ data->mDBConn->ExecuteSimpleSQL("DELETE FROM moz_perms"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "PermissionManager::RemoveAllInternal-Failure",
+ [self] { self->CloseDB(eRebuldOnSuccess); }));
+ }
+ }));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PermissionManager::TestExactPermissionFromPrincipal(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t* aPermission) {
+ return CommonTestPermission(aPrincipal, -1, aType, aPermission,
+ nsIPermissionManager::UNKNOWN_ACTION, false, true,
+ true);
+}
+
+NS_IMETHODIMP
+PermissionManager::TestExactPermanentPermission(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t* aPermission) {
+ return CommonTestPermission(aPrincipal, -1, aType, aPermission,
+ nsIPermissionManager::UNKNOWN_ACTION, false, true,
+ false);
+}
+
+nsresult PermissionManager::LegacyTestPermissionFromURI(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes,
+ const nsACString& aType, uint32_t* aPermission) {
+ return CommonTestPermission(aURI, aOriginAttributes, -1, aType, aPermission,
+ nsIPermissionManager::UNKNOWN_ACTION, false,
+ false, true);
+}
+
+NS_IMETHODIMP
+PermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t* aPermission) {
+ return CommonTestPermission(aPrincipal, -1, aType, aPermission,
+ nsIPermissionManager::UNKNOWN_ACTION, false,
+ false, true);
+}
+
+NS_IMETHODIMP
+PermissionManager::GetPermissionObject(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ bool aExactHostMatch,
+ nsIPermission** aResult) {
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ *aResult = nullptr;
+
+ EnsureReadCompleted();
+
+ if (aPrincipal->IsSystemPrincipal()) {
+ return NS_OK;
+ }
+
+ // Querying the permission object of an nsEP is non-sensical.
+ if (IsExpandedPrincipal(aPrincipal)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ MOZ_ASSERT(PermissionAvailable(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,
+ IsOAForceStripPermission(aType),
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PermissionEntry& perm = entry->GetPermissions()[idx];
+ nsCOMPtr<nsIPermission> r = Permission::Create(
+ principal, mTypeArray[perm.mType], perm.mPermission, perm.mExpireType,
+ perm.mExpireTime, perm.mModificationTime);
+ if (NS_WARN_IF(!r)) {
+ return NS_ERROR_FAILURE;
+ }
+ r.forget(aResult);
+ return NS_OK;
+}
+
+nsresult PermissionManager::CommonTestPermissionInternal(
+ nsIPrincipal* aPrincipal, nsIURI* aURI,
+ const OriginAttributes* aOriginAttributes, int32_t aTypeIndex,
+ const nsACString& aType, uint32_t* aPermission, bool aExactHostMatch,
+ bool aIncludingSession) {
+ MOZ_ASSERT(aPrincipal || aURI);
+ NS_ENSURE_ARG_POINTER(aPrincipal || aURI);
+ MOZ_ASSERT_IF(aPrincipal, !aURI && !aOriginAttributes);
+ MOZ_ASSERT_IF(aURI || aOriginAttributes, !aPrincipal);
+
+ EnsureReadCompleted();
+
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsIPrincipal> prin = aPrincipal;
+ if (!prin) {
+ if (aURI) {
+ prin = BasePrincipal::CreateContentPrincipal(aURI, OriginAttributes());
+ }
+ }
+ MOZ_ASSERT(prin);
+ MOZ_ASSERT(PermissionAvailable(prin, aType));
+ }
+#endif
+
+ PermissionHashKey* entry =
+ aPrincipal ? GetPermissionHashKey(aPrincipal, aTypeIndex, aExactHostMatch)
+ : GetPermissionHashKey(aURI, aOriginAttributes, aTypeIndex,
+ aExactHostMatch);
+ if (!entry || (!aIncludingSession &&
+ entry->GetPermission(aTypeIndex).mNonSessionExpireType ==
+ nsIPermissionManager::EXPIRE_SESSION)) {
+ return NS_OK;
+ }
+
+ *aPermission = aIncludingSession
+ ? entry->GetPermission(aTypeIndex).mPermission
+ : entry->GetPermission(aTypeIndex).mNonSessionPermission;
+
+ return NS_OK;
+}
+
+// Helper function to filter permissions using a condition function.
+template <class T>
+nsresult PermissionManager::GetPermissionEntries(
+ T aCondition, nsTArray<RefPtr<nsIPermission>>& aResult) {
+ aResult.Clear();
+ if (XRE_IsContentProcess()) {
+ NS_WARNING(
+ "Iterating over all permissions is not available in the "
+ "content process, as not all permissions may be available.");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ EnsureReadCompleted();
+
+ 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;
+ }
+
+ // If the permission is expired, skip it. We're not deleting it here
+ // because we're iterating over a lot of permissions.
+ // It will be removed as part of the daily maintenance later.
+ if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) {
+ continue;
+ }
+
+ if (!aCondition(permEntry)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = GetPrincipalFromOrigin(
+ entry->GetKey()->mOrigin,
+ IsOAForceStripPermission(mTypeArray[permEntry.mType]),
+ getter_AddRefs(principal));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ RefPtr<nsIPermission> permission = Permission::Create(
+ principal, mTypeArray[permEntry.mType], permEntry.mPermission,
+ permEntry.mExpireType, permEntry.mExpireTime,
+ permEntry.mModificationTime);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+ aResult.AppendElement(std::move(permission));
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP PermissionManager::GetAll(
+ nsTArray<RefPtr<nsIPermission>>& aResult) {
+ return GetPermissionEntries(
+ [](const PermissionEntry& aPermEntry) { return true; }, aResult);
+}
+
+NS_IMETHODIMP PermissionManager::GetAllByTypeSince(
+ const nsACString& aPrefix, int64_t aSince,
+ nsTArray<RefPtr<nsIPermission>>& aResult) {
+ return GetPermissionEntries(
+ [&](const PermissionEntry& aPermEntry) {
+ return mTypeArray[aPermEntry.mType].Equals(aPrefix) &&
+ aSince <= aPermEntry.mModificationTime;
+ },
+ aResult);
+}
+
+NS_IMETHODIMP PermissionManager::GetAllWithTypePrefix(
+ const nsACString& aPrefix, nsTArray<RefPtr<nsIPermission>>& aResult) {
+ return GetPermissionEntries(
+ [&](const PermissionEntry& aPermEntry) {
+ return StringBeginsWith(mTypeArray[aPermEntry.mType], aPrefix);
+ },
+ aResult);
+}
+
+NS_IMETHODIMP
+PermissionManager::GetAllForPrincipal(
+ nsIPrincipal* aPrincipal, nsTArray<RefPtr<nsIPermission>>& aResult) {
+ aResult.Clear();
+ EnsureReadCompleted();
+
+ MOZ_ASSERT(PermissionAvailable(aPrincipal, ""_ns));
+
+ nsresult rv;
+ RefPtr<PermissionKey> key =
+ PermissionKey::CreateFromPrincipal(aPrincipal, false, rv);
+ if (!key) {
+ MOZ_ASSERT(NS_FAILED(rv));
+ return rv;
+ }
+ PermissionHashKey* entry = mPermissionTable.GetEntry(key);
+
+ nsTArray<PermissionEntry> strippedPerms;
+ rv = GetStripPermsForPrincipal(aPrincipal, strippedPerms);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (entry) {
+ for (const auto& permEntry : entry->GetPermissions()) {
+ // Only return custom permissions
+ if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
+ continue;
+ }
+
+ // If the permission is expired, skip it. We're not deleting it here
+ // because we're iterating over a lot of permissions.
+ // It will be removed as part of the daily maintenance later.
+ if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) {
+ continue;
+ }
+
+ // Stripped principal permissions overwrite regular ones
+ // For each permission check if there is a stripped permission we should
+ // use instead
+ PermissionEntry perm = permEntry;
+ nsTArray<PermissionEntry>::index_type index = 0;
+ for (const auto& strippedPerm : strippedPerms) {
+ if (strippedPerm.mType == permEntry.mType) {
+ perm = strippedPerm;
+ strippedPerms.RemoveElementAt(index);
+ break;
+ }
+ index++;
+ }
+
+ RefPtr<nsIPermission> permission = Permission::Create(
+ aPrincipal, mTypeArray[perm.mType], perm.mPermission,
+ perm.mExpireType, perm.mExpireTime, perm.mModificationTime);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+ aResult.AppendElement(permission);
+ }
+ }
+
+ for (const auto& perm : strippedPerms) {
+ RefPtr<nsIPermission> permission = Permission::Create(
+ aPrincipal, mTypeArray[perm.mType], perm.mPermission, perm.mExpireType,
+ perm.mExpireTime, perm.mModificationTime);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+ aResult.AppendElement(permission);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP PermissionManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
+ if (!mBlockerAdded) {
+ // The profile is about to change and the shutdown blocker has not been
+ // added yet (we are probably in a xpcshell-test).
+ RemoveIdleDailyMaintenanceJob();
+ RemoveAllFromMemory();
+ CloseDB(eNone);
+ }
+ } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) {
+ // the profile has already changed; init the db from the new location
+ InitDB(false);
+ } else if (!nsCRT::strcmp(aTopic, "testonly-reload-permissions-from-disk")) {
+ // Testing mechanism to reload all permissions from disk. Because the
+ // permission manager automatically initializes itself at startup, tests
+ // that directly manipulate the permissions database need some way to reload
+ // the database for their changes to have any effect. This mechanism was
+ // introduced when moving the permissions manager from on-demand startup to
+ // always being initialized. This is not guarded by a pref because it's not
+ // dangerous to reload permissions from disk, just bad for performance.
+ RemoveAllFromMemory();
+ CloseDB(eNone);
+ InitDB(false);
+ } else if (!nsCRT::strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
+ PerformIdleDailyMaintenance();
+ }
+
+ return NS_OK;
+}
+
+nsresult PermissionManager::RemoveAllModifiedSince(int64_t aModificationTime) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ return RemovePermissionEntries(
+ [aModificationTime](const PermissionEntry& aPermEntry) {
+ return aModificationTime <= aPermEntry.mModificationTime;
+ });
+}
+
+NS_IMETHODIMP
+PermissionManager::RemovePermissionsWithAttributes(const nsAString& aPattern) {
+ ENSURE_NOT_CHILD_PROCESS;
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return RemovePermissionsWithAttributes(pattern);
+}
+
+nsresult PermissionManager::RemovePermissionsWithAttributes(
+ OriginAttributesPattern& aPattern) {
+ EnsureReadCompleted();
+
+ Vector<Tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10> permissions;
+ for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
+ PermissionHashKey* entry = iter.Get();
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, false,
+ getter_AddRefs(principal));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(principal->OriginAttributesRef())) {
+ continue;
+ }
+
+ for (const auto& permEntry : entry->GetPermissions()) {
+ if (!permissions.emplaceBack(principal, mTypeArray[permEntry.mType],
+ entry->GetKey()->mOrigin)) {
+ continue;
+ }
+ }
+ }
+
+ for (auto& i : permissions) {
+ AddInternal(Get<0>(i), Get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0,
+ PermissionManager::eNotify, PermissionManager::eWriteToDB,
+ false, &Get<2>(i));
+ }
+
+ return NS_OK;
+}
+
+nsresult PermissionManager::GetStripPermsForPrincipal(
+ nsIPrincipal* aPrincipal, nsTArray<PermissionEntry>& aResult) {
+ aResult.Clear();
+ aResult.SetCapacity(kStripOAPermissions.size());
+
+ // No special strip permissions
+ if (kStripOAPermissions.empty()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ // Create a key for the principal, but strip any origin attributes
+ RefPtr<PermissionKey> key =
+ PermissionKey::CreateFromPrincipal(aPrincipal, true, rv);
+ if (!key) {
+ MOZ_ASSERT(NS_FAILED(rv));
+ return rv;
+ }
+
+ PermissionHashKey* hashKey = mPermissionTable.GetEntry(key);
+ if (!hashKey) {
+ return NS_OK;
+ }
+
+ for (const auto& permType : kStripOAPermissions) {
+ int32_t index = GetTypeIndex(permType, false);
+ if (index == -1) {
+ continue;
+ }
+ PermissionEntry perm = hashKey->GetPermission(index);
+ if (perm.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
+ continue;
+ }
+ aResult.AppendElement(perm);
+ }
+
+ return NS_OK;
+}
+
+int32_t PermissionManager::GetTypeIndex(const nsACString& 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
+ if (!mTypeArray.emplaceBack(aType)) {
+ return -1;
+ }
+
+ return mTypeArray.length() - 1;
+}
+
+PermissionManager::PermissionHashKey* PermissionManager::GetPermissionHashKey(
+ nsIPrincipal* aPrincipal, uint32_t aType, bool aExactHostMatch) {
+ EnsureReadCompleted();
+
+ MOZ_ASSERT(PermissionAvailable(aPrincipal, mTypeArray[aType]));
+
+ nsresult rv;
+ RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal(
+ aPrincipal, IsOAForceStripPermission(mTypeArray[aType]), 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.
+ if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) {
+ entry = nullptr;
+ RemoveFromPrincipal(aPrincipal, mTypeArray[aType]);
+ } 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 = aPrincipal->GetNextSubDomainPrincipal();
+ if (principal) {
+ return GetPermissionHashKey(principal, aType, aExactHostMatch);
+ }
+ }
+
+ // No entry, really...
+ return nullptr;
+}
+
+PermissionManager::PermissionHashKey* PermissionManager::GetPermissionHashKey(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes, uint32_t aType,
+ bool aExactHostMatch) {
+ MOZ_ASSERT(aURI);
+
+#ifdef DEBUG
+ {
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = NS_OK;
+ if (aURI) {
+ rv = GetPrincipal(aURI, getter_AddRefs(principal));
+ }
+ MOZ_ASSERT_IF(NS_SUCCEEDED(rv),
+ PermissionAvailable(principal, mTypeArray[aType]));
+ }
+#endif
+
+ nsresult rv;
+ RefPtr<PermissionKey> key;
+
+ if (aOriginAttributes) {
+ key = PermissionKey::CreateFromURIAndOriginAttributes(
+ aURI, aOriginAttributes, IsOAForceStripPermission(mTypeArray[aType]),
+ rv);
+ } else {
+ 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.
+ if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) {
+ 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;
+ if (aURI) {
+ nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ }
+ RemoveFromPrincipal(principal, mTypeArray[aType]);
+ } 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;
+ if (aURI) {
+ uri = GetNextSubDomainURI(aURI);
+ }
+ if (uri) {
+ return GetPermissionHashKey(uri, aOriginAttributes, aType,
+ aExactHostMatch);
+ }
+ }
+
+ // No entry, really...
+ return nullptr;
+}
+
+nsresult PermissionManager::RemoveAllFromMemory() {
+ mLargestID = 0;
+ mTypeArray.clear();
+ mPermissionTable.Clear();
+
+ return NS_OK;
+}
+
+// wrapper function for mangling (host,type,perm,expireType,expireTime)
+// set into an nsIPermission.
+void PermissionManager::NotifyObserversWithPermission(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime,
+ const char16_t* aData) {
+ nsCOMPtr<nsIPermission> permission =
+ Permission::Create(aPrincipal, aType, aPermission, aExpireType,
+ aExpireTime, aModificationTime);
+ 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 PermissionManager::NotifyObservers(nsIPermission* aPermission,
+ const char16_t* aData) {
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService)
+ observerService->NotifyObservers(aPermission, kPermissionChangeNotification,
+ aData);
+}
+
+nsresult PermissionManager::Read(const MonitorAutoLock& aProofOfLock) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ MOZ_ASSERT(!NS_IsMainThread());
+ auto data = mThreadBoundData.Access();
+
+ nsresult rv;
+ bool hasResult;
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ // Let's retrieve the last used ID.
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("SELECT MAX(id) FROM moz_perms"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t id = stmt->AsInt64(0);
+ mLargestID = id;
+ }
+
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString(
+ "SELECT id, origin, type, permission, expireType, "
+ "expireTime, modificationTime "
+ "FROM moz_perms WHERE expireType != ?1 OR expireTime > ?2"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByIndex(0, nsIPermissionManager::EXPIRE_TIME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByIndex(1, EXPIRY_NOW);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool readError = false;
+
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ ReadEntry entry;
+
+ // explicitly set our entry id counter for use in AddInternal(),
+ // and keep track of the largest id so we know where to pick up.
+ entry.mId = stmt->AsInt64(0);
+ MOZ_ASSERT(entry.mId <= mLargestID);
+
+ rv = stmt->GetUTF8String(1, entry.mOrigin);
+ if (NS_FAILED(rv)) {
+ readError = true;
+ continue;
+ }
+
+ rv = stmt->GetUTF8String(2, entry.mType);
+ if (NS_FAILED(rv)) {
+ readError = true;
+ continue;
+ }
+
+ entry.mPermission = stmt->AsInt32(3);
+ entry.mExpireType = stmt->AsInt32(4);
+
+ // convert into int64_t values (milliseconds)
+ entry.mExpireTime = stmt->AsInt64(5);
+ entry.mModificationTime = stmt->AsInt64(6);
+
+ entry.mFromMigration = false;
+
+ mReadEntries.AppendElement(entry);
+ }
+
+ if (readError) {
+ NS_ERROR("Error occured while reading the permissions database!");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void PermissionManager::CompleteMigrations() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mState == eReady);
+
+ nsresult rv;
+
+ nsTArray<MigrationEntry> entries;
+ {
+ MonitorAutoLock lock(mMonitor);
+ entries = std::move(mMigrationEntries);
+ }
+
+ for (const MigrationEntry& entry : entries) {
+ rv = UpgradeHostToOriginAndInsert(
+ entry.mHost, entry.mType, entry.mPermission, entry.mExpireType,
+ entry.mExpireTime, entry.mModificationTime, entry.mIsInBrowserElement,
+ [&](const nsACString& aOrigin, const nsCString& aType,
+ uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime) {
+ MaybeAddReadEntryFromMigration(aOrigin, aType, aPermission,
+ aExpireType, aExpireTime,
+ aModificationTime, entry.mId);
+ return NS_OK;
+ });
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+}
+
+void PermissionManager::CompleteRead() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mState == eReady);
+
+ nsresult rv;
+
+ nsTArray<ReadEntry> entries;
+ {
+ MonitorAutoLock lock(mMonitor);
+ entries = std::move(mReadEntries);
+ }
+
+ for (const ReadEntry& entry : entries) {
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = GetPrincipalFromOrigin(entry.mOrigin,
+ IsOAForceStripPermission(entry.mType),
+ getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ DBOperationType op = entry.mFromMigration ? eWriteToDB : eNoDBOperation;
+
+ rv = AddInternal(principal, entry.mType, entry.mPermission, entry.mId,
+ entry.mExpireType, entry.mExpireTime,
+ entry.mModificationTime, eDontNotify, op, false,
+ &entry.mOrigin);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+}
+
+void PermissionManager::MaybeAddReadEntryFromMigration(
+ const nsACString& aOrigin, const nsCString& aType, uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime,
+ int64_t aId) {
+ MonitorAutoLock lock(mMonitor);
+
+ // We convert a migration to a ReadEntry only if we don't have an existing
+ // ReadEntry for the same origin + type.
+ for (const ReadEntry& entry : mReadEntries) {
+ if (entry.mOrigin == aOrigin && entry.mType == aType) {
+ return;
+ }
+ }
+
+ ReadEntry entry;
+ entry.mId = aId;
+ entry.mOrigin = aOrigin;
+ entry.mType = aType;
+ entry.mPermission = aPermission;
+ entry.mExpireType = aExpireType;
+ entry.mExpireTime = aExpireTime;
+ entry.mModificationTime = aModificationTime;
+ entry.mFromMigration = true;
+
+ mReadEntries.AppendElement(entry);
+}
+
+void PermissionManager::UpdateDB(OperationType aOp, 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;
+
+ MOZ_ASSERT(NS_IsMainThread());
+ EnsureReadCompleted();
+
+ nsCString origin(aOrigin);
+ nsCString type(aType);
+
+ RefPtr<PermissionManager> self = this;
+ mThread->Dispatch(NS_NewRunnableFunction(
+ "PermissionManager::UpdateDB",
+ [self, aOp, aID, origin, type, aPermission, aExpireType, aExpireTime,
+ aModificationTime] {
+ nsresult rv;
+
+ auto data = self->mThreadBoundData.Access();
+
+ if (self->mState == eClosed || !data->mDBConn) {
+ // no statement is ok - just means we don't have a profile
+ return;
+ }
+
+ mozIStorageStatement* stmt = nullptr;
+ switch (aOp) {
+ case eOperationAdding: {
+ stmt = data->mStmtInsert;
+
+ rv = stmt->BindInt64ByIndex(0, aID);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindUTF8StringByIndex(1, origin);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindUTF8StringByIndex(2, type);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt32ByIndex(3, aPermission);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt32ByIndex(4, aExpireType);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt64ByIndex(5, aExpireTime);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt64ByIndex(6, aModificationTime);
+ break;
+ }
+
+ case eOperationRemoving: {
+ stmt = data->mStmtDelete;
+ rv = stmt->BindInt64ByIndex(0, aID);
+ break;
+ }
+
+ case eOperationChanging: {
+ stmt = data->mStmtUpdate;
+
+ rv = stmt->BindInt64ByIndex(0, aID);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt32ByIndex(1, aPermission);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt32ByIndex(2, aExpireType);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt64ByIndex(3, aExpireTime);
+ if (NS_FAILED(rv)) break;
+
+ rv = stmt->BindInt64ByIndex(4, aModificationTime);
+ break;
+ }
+
+ default: {
+ MOZ_ASSERT_UNREACHABLE("need a valid operation in UpdateDB()!");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("db change failed!");
+ return;
+ }
+
+ rv = stmt->Execute();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }));
+}
+
+bool PermissionManager::GetPermissionsFromOriginOrKey(
+ const nsACString& aOrigin, const nsACString& aKey,
+ nsTArray<IPC::Permission>& aPerms) {
+ EnsureReadCompleted();
+
+ aPerms.Clear();
+ if (NS_WARN_IF(XRE_IsContentProcess())) {
+ return false;
+ }
+
+ for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) {
+ PermissionHashKey* entry = iter.Get();
+
+ nsAutoCString permissionKey;
+ if (aOrigin.IsEmpty()) {
+ // We can't check for individual OA strip perms here.
+ // Don't force strip origin attributes.
+ GetKeyForOrigin(entry->GetKey()->mOrigin, false, 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 (aKey != permissionKey && !aKey.IsEmpty()) {
+ continue;
+ }
+ } else if (aOrigin != entry->GetKey()->mOrigin) {
+ // If the origins don't match, then we can exit early. We have to keep
+ // looking if we're getting the default origin, as we may see a preload
+ // permission which should be transmitted.
+ 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]);
+ bool shouldAppend;
+ if (aOrigin.IsEmpty()) {
+ shouldAppend = (isPreload && aKey.IsEmpty()) ||
+ (!isPreload && aKey == permissionKey);
+ } else {
+ shouldAppend = (!isPreload && aOrigin == entry->GetKey()->mOrigin);
+ }
+ if (shouldAppend) {
+ aPerms.AppendElement(
+ IPC::Permission(entry->GetKey()->mOrigin,
+ mTypeArray[permEntry.mType], permEntry.mPermission,
+ permEntry.mExpireType, permEntry.mExpireTime));
+ }
+ }
+ }
+
+ return true;
+}
+
+void PermissionManager::SetPermissionsWithKey(
+ const nsACString& aPermissionKey, nsTArray<IPC::Permission>& aPerms) {
+ if (NS_WARN_IF(XRE_IsParentProcess())) {
+ return;
+ }
+
+ RefPtr<GenericNonExclusivePromise::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;
+ }
+ mPermissionKeyPromiseMap.Put(aPermissionKey,
+ RefPtr<GenericNonExclusivePromise::Private>{});
+
+ // Add the permissions locally to our process
+ for (IPC::Permission& perm : aPerms) {
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv =
+ GetPrincipalFromOrigin(perm.origin, IsOAForceStripPermission(perm.type),
+ getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+#ifdef DEBUG
+ nsAutoCString permissionKey;
+ GetKeyForPermission(principal, perm.type, 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 */);
+ }
+}
+
+/* static */
+void PermissionManager::GetKeyForOrigin(const nsACString& aOrigin,
+ bool aForceStripOA, 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, "http:"_ns) &&
+ !StringBeginsWith(aOrigin, "https:"_ns) &&
+ !StringBeginsWith(aOrigin, "ftp:"_ns)) {
+ 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;
+ }
+
+ MaybeStripOAs(aForceStripOA, attrs);
+
+#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, aForceStripOA,
+ getter_AddRefs(dbgPrincipal)));
+ MOZ_ASSERT(dbgPrincipal->SchemeIs("http") ||
+ dbgPrincipal->SchemeIs("https") || dbgPrincipal->SchemeIs("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 PermissionManager::GetKeyForPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ nsACString& aKey) {
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aKey.Truncate();
+ return;
+ }
+ GetKeyForOrigin(origin, aForceStripOA, aKey);
+}
+
+/* static */
+void PermissionManager::GetKeyForPermission(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ nsACString& aKey) {
+ // Preload permissions have the "" key.
+ if (IsPreloadPermission(aType)) {
+ aKey.Truncate();
+ return;
+ }
+
+ GetKeyForPrincipal(aPrincipal, IsOAForceStripPermission(aType), aKey);
+}
+
+/* static */
+nsTArray<std::pair<nsCString, nsCString>>
+PermissionManager::GetAllKeysForPrincipal(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsTArray<std::pair<nsCString, nsCString>> pairs;
+ nsCOMPtr<nsIPrincipal> prin = aPrincipal;
+ while (prin) {
+ // Add the pair to the list
+ std::pair<nsCString, nsCString>* pair =
+ pairs.AppendElement(std::make_pair(""_ns, ""_ns));
+ // We can't check for individual OA strip perms here.
+ // Don't force strip origin attributes.
+ GetKeyForPrincipal(prin, false, pair->first);
+
+ Unused << GetOriginFromPrincipal(prin, false, pair->second);
+ prin = prin->GetNextSubDomainPrincipal();
+ // Get the next subdomain principal and loop back around.
+ }
+
+ MOZ_ASSERT(pairs.Length() >= 1,
+ "Every principal should have at least one pair item.");
+ return pairs;
+}
+
+NS_IMETHODIMP
+PermissionManager::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 PermissionManager::PermissionAvailable(nsIPrincipal* aPrincipal,
+ const nsACString& aType) {
+ EnsureReadCompleted();
+
+ 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 available, so report a warning and return false.
+ RefPtr<GenericNonExclusivePromise::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;
+}
+
+void PermissionManager::WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
+ nsIRunnable* aRunnable) {
+ MOZ_ASSERT(aRunnable);
+
+ if (!XRE_IsContentProcess()) {
+ aRunnable->Run();
+ return;
+ }
+
+ nsTArray<RefPtr<GenericNonExclusivePromise>> promises;
+ for (auto& pair : GetAllKeysForPrincipal(aPrincipal)) {
+ RefPtr<GenericNonExclusivePromise::Private> promise;
+ if (!mPermissionKeyPromiseMap.Get(pair.first, getter_AddRefs(promise))) {
+ // In this case we have found a permission which isn't available 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 GenericNonExclusivePromise::Private(__func__);
+ mPermissionKeyPromiseMap.Put(pair.first, RefPtr{promise});
+ }
+
+ if (promise) {
+ promises.AppendElement(std::move(promise));
+ }
+ }
+
+ // If all of our permissions are available, immediately run the runnable. This
+ // avoids any extra overhead during fetch interception which is performance
+ // sensitive.
+ if (promises.IsEmpty()) {
+ aRunnable->Run();
+ return;
+ }
+
+ auto* thread = AbstractThread::MainThread();
+
+ RefPtr<nsIRunnable> runnable = aRunnable;
+ GenericNonExclusivePromise::All(thread, promises)
+ ->Then(
+ thread, __func__, [runnable]() { runnable->Run(); },
+ []() {
+ NS_WARNING(
+ "PermissionManager permission promise rejected. We're "
+ "probably shutting down.");
+ });
+}
+
+void PermissionManager::EnsureReadCompleted() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState == eInitializing) {
+ MonitorAutoLock lock(mMonitor);
+
+ while (mState == eInitializing) {
+ mMonitor.Wait();
+ }
+ }
+
+ switch (mState) {
+ case eInitializing:
+ MOZ_CRASH("This state is impossible!");
+
+ case eDBInitialized:
+ mState = eReady;
+
+ CompleteMigrations();
+ ImportLatestDefaults();
+ CompleteRead();
+
+ [[fallthrough]];
+
+ case eReady:
+ [[fallthrough]];
+
+ case eClosed:
+ return;
+
+ default:
+ MOZ_CRASH("Invalid state");
+ }
+}
+
+already_AddRefed<nsIInputStream> PermissionManager::GetDefaultsInputStream() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString defaultsURL;
+ Preferences::GetCString(kDefaultsUrlPrefName, defaultsURL);
+ if (defaultsURL.IsEmpty()) { // == Don't use built-in permissions.
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> defaultsURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), defaultsURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = channel->Open(getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return inputStream.forget();
+}
+
+void PermissionManager::ConsumeDefaultsInputStream(
+ nsIInputStream* aInputStream, const MonitorAutoLock& aProofOfLock) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ constexpr char kMatchTypeHost[] = "host";
+ constexpr char kMatchTypeOrigin[] = "origin";
+
+ mDefaultEntries.Clear();
+
+ if (!aInputStream) {
+ return;
+ }
+
+ nsresult rv;
+
+ /* 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(aInputStream, &lineBuffer, line, &isMore);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (line.IsEmpty() || line.First() == '#') {
+ continue;
+ }
+
+ nsTArray<nsCString> lineArray;
+
+ // Split the line at tabs
+ ParseString(line, '\t', lineArray);
+
+ if (lineArray.Length() != 4) {
+ continue;
+ }
+
+ nsresult error = NS_OK;
+ uint32_t permission = lineArray[2].ToInteger(&error);
+ if (NS_FAILED(error)) {
+ continue;
+ }
+
+ DefaultEntry::Op op;
+
+ if (lineArray[0].EqualsLiteral(kMatchTypeHost)) {
+ op = DefaultEntry::eImportMatchTypeHost;
+ } else if (lineArray[0].EqualsLiteral(kMatchTypeOrigin)) {
+ op = DefaultEntry::eImportMatchTypeOrigin;
+ } else {
+ continue;
+ }
+
+ DefaultEntry* entry = mDefaultEntries.AppendElement();
+ MOZ_ASSERT(entry);
+
+ entry->mOp = op;
+ entry->mPermission = permission;
+ entry->mHostOrOrigin = lineArray[3];
+ entry->mType = lineArray[1];
+ } while (isMore);
+}
+
+// ImportLatestDefaults will import the latest default cookies read during the
+// last DB initialization.
+nsresult PermissionManager::ImportLatestDefaults() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mState == eReady);
+
+ nsresult rv;
+
+ MonitorAutoLock lock(mMonitor);
+
+ for (const DefaultEntry& entry : mDefaultEntries) {
+ if (entry.mOp == DefaultEntry::eImportMatchTypeHost) {
+ // the import file format doesn't handle modification times, so we use
+ // 0, which AddInternal will convert to now()
+ int64_t modificationTime = 0;
+
+ rv = UpgradeHostToOriginAndInsert(
+ entry.mHostOrOrigin, entry.mType, entry.mPermission,
+ nsIPermissionManager::EXPIRE_NEVER, 0, modificationTime, false,
+ [&](const nsACString& aOrigin, const nsCString& aType,
+ uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime) {
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv =
+ GetPrincipalFromOrigin(aOrigin, IsOAForceStripPermission(aType),
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return AddInternal(
+ principal, aType, aPermission, cIDPermissionIsDefault,
+ aExpireType, aExpireTime, aModificationTime,
+ PermissionManager::eDontNotify,
+ PermissionManager::eNoDBOperation, false, &aOrigin);
+ });
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("There was a problem importing a host permission");
+ }
+ continue;
+ }
+
+ MOZ_ASSERT(entry.mOp == DefaultEntry::eImportMatchTypeOrigin);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = GetPrincipalFromOrigin(entry.mHostOrOrigin,
+ IsOAForceStripPermission(entry.mType),
+ getter_AddRefs(principal));
+ if (NS_FAILED(rv)) {
+ 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;
+
+ rv = AddInternal(principal, entry.mType, entry.mPermission,
+ cIDPermissionIsDefault, nsIPermissionManager::EXPIRE_NEVER,
+ 0, modificationTime, eDontNotify, eNoDBOperation);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("There was a problem importing an origin permission");
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Perform the early steps of a permission check and determine whether we need
+ * to call CommonTestPermissionInternal() for the actual permission check.
+ *
+ * @param aPrincipal optional principal argument to check the permission for,
+ * can be nullptr if we aren't performing a principal-based
+ * check.
+ * @param aTypeIndex if the caller isn't sure what the index of the permission
+ * type to check for is in the mTypeArray member variable,
+ * it should pass -1, otherwise this would be the index of
+ * the type inside mTypeArray. This would only be something
+ * other than -1 in recursive invocations of this function.
+ * @param aType the permission type to test.
+ * @param aPermission out argument which will be a permission type that we
+ * will return from this function once the function is
+ * done.
+ * @param aDefaultPermission the default permission to be used if we can't
+ * determine the result of the permission check.
+ * @param aDefaultPermissionIsValid whether the previous argument contains a
+ * valid value.
+ * @param aExactHostMatch whether to look for the exact host name or also for
+ * subdomains that can have the same permission.
+ * @param aIncludingSession whether to include session permissions when
+ * testing for the permission.
+ */
+PermissionManager::TestPreparationResult
+PermissionManager::CommonPrepareToTestPermission(
+ nsIPrincipal* aPrincipal, int32_t aTypeIndex, const nsACString& aType,
+ uint32_t* aPermission, uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid, bool aExactHostMatch,
+ bool aIncludingSession) {
+ auto* basePrin = BasePrincipal::Cast(aPrincipal);
+ if (basePrin && basePrin->IsSystemPrincipal()) {
+ *aPermission = ALLOW_ACTION;
+ return AsVariant(NS_OK);
+ }
+
+ EnsureReadCompleted();
+
+ // 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.
+ int32_t defaultPermission =
+ aDefaultPermissionIsValid ? aDefaultPermission : UNKNOWN_ACTION;
+ if (!aDefaultPermissionIsValid && HasDefaultPref(aType)) {
+ Unused << mDefaultPrefBranch->GetIntPref(PromiseFlatCString(aType).get(),
+ &defaultPermission);
+ }
+
+ // Set the default.
+ *aPermission = defaultPermission;
+
+ int32_t typeIndex =
+ aTypeIndex == -1 ? GetTypeIndex(aType, false) : aTypeIndex;
+
+ // For expanded principals, we want to iterate over the allowlist and see
+ // if the permission is granted for any of them.
+ if (basePrin && basePrin->Is<ExpandedPrincipal>()) {
+ auto ep = basePrin->As<ExpandedPrincipal>();
+ for (auto& prin : ep->AllowList()) {
+ uint32_t perm;
+ nsresult rv =
+ CommonTestPermission(prin, typeIndex, aType, &perm, defaultPermission,
+ true, aExactHostMatch, aIncludingSession);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return AsVariant(rv);
+ }
+
+ if (perm == nsIPermissionManager::ALLOW_ACTION) {
+ *aPermission = perm;
+ return AsVariant(NS_OK);
+ }
+ if (perm == nsIPermissionManager::PROMPT_ACTION) {
+ // Store it, but keep going to see if we can do better.
+ *aPermission = perm;
+ }
+ }
+
+ return AsVariant(NS_OK);
+ }
+
+ // If type == -1, the type isn't known, just signal that we are done.
+ if (typeIndex == -1) {
+ return AsVariant(NS_OK);
+ }
+
+ return AsVariant(typeIndex);
+}
+
+// If aTypeIndex is passed -1, we try to inder the type index from aType.
+nsresult PermissionManager::CommonTestPermission(
+ nsIPrincipal* aPrincipal, int32_t aTypeIndex, const nsACString& aType,
+ uint32_t* aPermission, uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid, bool aExactHostMatch,
+ bool aIncludingSession) {
+ auto preparationResult = CommonPrepareToTestPermission(
+ aPrincipal, aTypeIndex, aType, aPermission, aDefaultPermission,
+ aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession);
+ if (preparationResult.is<nsresult>()) {
+ return preparationResult.as<nsresult>();
+ }
+
+ return CommonTestPermissionInternal(
+ aPrincipal, nullptr, nullptr, preparationResult.as<int32_t>(), aType,
+ aPermission, aExactHostMatch, aIncludingSession);
+}
+
+// If aTypeIndex is passed -1, we try to inder the type index from aType.
+nsresult PermissionManager::CommonTestPermission(
+ nsIURI* aURI, int32_t aTypeIndex, const nsACString& aType,
+ uint32_t* aPermission, uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid, bool aExactHostMatch,
+ bool aIncludingSession) {
+ auto preparationResult = CommonPrepareToTestPermission(
+ nullptr, aTypeIndex, aType, aPermission, aDefaultPermission,
+ aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession);
+ if (preparationResult.is<nsresult>()) {
+ return preparationResult.as<nsresult>();
+ }
+
+ return CommonTestPermissionInternal(
+ nullptr, aURI, nullptr, preparationResult.as<int32_t>(), aType,
+ aPermission, aExactHostMatch, aIncludingSession);
+}
+
+nsresult PermissionManager::CommonTestPermission(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes, int32_t aTypeIndex,
+ const nsACString& aType, uint32_t* aPermission, uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid, bool aExactHostMatch,
+ bool aIncludingSession) {
+ auto preparationResult = CommonPrepareToTestPermission(
+ nullptr, aTypeIndex, aType, aPermission, aDefaultPermission,
+ aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession);
+ if (preparationResult.is<nsresult>()) {
+ return preparationResult.as<nsresult>();
+ }
+
+ return CommonTestPermissionInternal(
+ nullptr, aURI, aOriginAttributes, preparationResult.as<int32_t>(), aType,
+ aPermission, aExactHostMatch, aIncludingSession);
+}
+
+nsresult PermissionManager::TestPermissionWithoutDefaultsFromPrincipal(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t* aPermission) {
+ MOZ_ASSERT(!HasDefaultPref(aType));
+
+ return CommonTestPermission(aPrincipal, -1, aType, aPermission,
+ nsIPermissionManager::UNKNOWN_ACTION, true, false,
+ true);
+}
+
+void PermissionManager::MaybeCompleteShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetShutdownPhase();
+ MOZ_ASSERT(asc);
+
+ DebugOnly<nsresult> rv = asc->RemoveBlocker(this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+// Async shutdown blocker methods
+
+NS_IMETHODIMP PermissionManager::GetName(nsAString& aName) {
+ aName = u"PermissionManager: Flushing data"_ns;
+ return NS_OK;
+}
+
+NS_IMETHODIMP PermissionManager::BlockShutdown(
+ nsIAsyncShutdownClient* aClient) {
+ RemoveIdleDailyMaintenanceJob();
+ RemoveAllFromMemory();
+ CloseDB(eShutdown);
+
+ gPermissionManager = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PermissionManager::GetState(nsIPropertyBag** aBagOut) {
+ nsCOMPtr<nsIWritablePropertyBag2> propertyBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+
+ nsresult rv = propertyBag->SetPropertyAsInt32(u"state"_ns, mState);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ propertyBag.forget(aBagOut);
+
+ return NS_OK;
+}
+
+nsCOMPtr<nsIAsyncShutdownClient> PermissionManager::GetShutdownPhase() const {
+ nsresult rv;
+ nsCOMPtr<nsIAsyncShutdownService> svc =
+ do_GetService("@mozilla.org/async-shutdown-service;1", &rv);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ rv = svc->GetProfileBeforeChange(getter_AddRefs(client));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+
+ return client;
+}
+
+} // namespace mozilla