summaryrefslogtreecommitdiffstats
path: root/extensions/permissions
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--extensions/permissions/Permission.cpp122
-rw-r--r--extensions/permissions/Permission.h49
-rw-r--r--extensions/permissions/PermissionDelegateHandler.cpp403
-rw-r--r--extensions/permissions/PermissionDelegateHandler.h203
-rw-r--r--extensions/permissions/PermissionDelegateIPCUtils.h40
-rw-r--r--extensions/permissions/PermissionManager.cpp3928
-rw-r--r--extensions/permissions/PermissionManager.h682
-rw-r--r--extensions/permissions/components.conf25
-rw-r--r--extensions/permissions/moz.build39
-rw-r--r--extensions/permissions/test/PermissionTestUtils.sys.mjs111
-rw-r--r--extensions/permissions/test/browser.ini8
-rw-r--r--extensions/permissions/test/browser_permmgr_sync.js448
-rw-r--r--extensions/permissions/test/browser_permmgr_viewsrc.js28
-rw-r--r--extensions/permissions/test/gtest/PermissionManagerTest.cpp52
-rw-r--r--extensions/permissions/test/gtest/moz.build11
-rw-r--r--extensions/permissions/test/moz.build15
-rw-r--r--extensions/permissions/test/unit/head.js26
-rw-r--r--extensions/permissions/test/unit/test_permmanager_cleardata.js93
-rw-r--r--extensions/permissions/test/unit/test_permmanager_default_pref.js76
-rw-r--r--extensions/permissions/test/unit/test_permmanager_defaults.js485
-rw-r--r--extensions/permissions/test/unit/test_permmanager_expiration.js189
-rw-r--r--extensions/permissions/test/unit/test_permmanager_getAllByTypeSince.js79
-rw-r--r--extensions/permissions/test/unit/test_permmanager_getAllByTypes.js124
-rw-r--r--extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js72
-rw-r--r--extensions/permissions/test/unit/test_permmanager_getAllWithTypePrefix.js84
-rw-r--r--extensions/permissions/test/unit/test_permmanager_getPermissionObject.js98
-rw-r--r--extensions/permissions/test/unit/test_permmanager_idn.js75
-rw-r--r--extensions/permissions/test/unit/test_permmanager_ipc.js89
-rw-r--r--extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js264
-rw-r--r--extensions/permissions/test/unit/test_permmanager_local_files.js74
-rw-r--r--extensions/permissions/test/unit/test_permmanager_matches.js203
-rw-r--r--extensions/permissions/test/unit/test_permmanager_matchesuri.js252
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_10-11.js196
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_11-12.js230
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_4-7.js264
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_4-7_no_history.js279
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_5-7a.js365
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_5-7b.js203
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_6-7a.js366
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_6-7b.js199
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_7-8.js328
-rw-r--r--extensions/permissions/test/unit/test_permmanager_migrate_9-10.js262
-rw-r--r--extensions/permissions/test/unit/test_permmanager_notifications.js139
-rw-r--r--extensions/permissions/test/unit/test_permmanager_oa_strip.js220
-rw-r--r--extensions/permissions/test/unit/test_permmanager_remove_add_update.js84
-rw-r--r--extensions/permissions/test/unit/test_permmanager_removeall.js47
-rw-r--r--extensions/permissions/test/unit/test_permmanager_removebytype.js76
-rw-r--r--extensions/permissions/test/unit/test_permmanager_removebytypesince.js89
-rw-r--r--extensions/permissions/test/unit/test_permmanager_removepermission.js58
-rw-r--r--extensions/permissions/test/unit/test_permmanager_removesince.js83
-rw-r--r--extensions/permissions/test/unit/test_permmanager_site_scope.js116
-rw-r--r--extensions/permissions/test/unit/test_permmanager_subdomains.js106
-rw-r--r--extensions/permissions/test/unit/xpcshell.ini60
53 files changed, 12217 insertions, 0 deletions
diff --git a/extensions/permissions/Permission.cpp b/extensions/permissions/Permission.cpp
new file mode 100644
index 0000000000..adec3689cf
--- /dev/null
+++ b/extensions/permissions/Permission.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Permission.h"
+#include "nsIClassInfoImpl.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_permissions.h"
+#include "mozilla/PermissionManager.h"
+
+namespace mozilla {
+
+// Permission Implementation
+
+NS_IMPL_CLASSINFO(Permission, nullptr, 0, {0})
+NS_IMPL_ISUPPORTS_CI(Permission, nsIPermission)
+
+Permission::Permission(nsIPrincipal* aPrincipal, const nsACString& aType,
+ uint32_t aCapability, uint32_t aExpireType,
+ int64_t aExpireTime, int64_t aModificationTime)
+ : mPrincipal(aPrincipal),
+ mType(aType),
+ mCapability(aCapability),
+ mExpireType(aExpireType),
+ mExpireTime(aExpireTime),
+ mModificationTime(aModificationTime) {}
+
+already_AddRefed<nsIPrincipal> Permission::ClonePrincipalForPermission(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+ PermissionManager::MaybeStripOriginAttributes(false, attrs);
+
+ nsAutoCString originNoSuffix;
+ nsresult rv = aPrincipal->GetOriginNoSuffix(originNoSuffix);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return mozilla::BasePrincipal::CreateContentPrincipal(uri, attrs);
+}
+
+already_AddRefed<Permission> Permission::Create(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aCapability,
+ uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) {
+ NS_ENSURE_TRUE(aPrincipal, nullptr);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ Permission::ClonePrincipalForPermission(aPrincipal);
+ NS_ENSURE_TRUE(principal, nullptr);
+
+ RefPtr<Permission> permission =
+ new Permission(principal, aType, aCapability, aExpireType, aExpireTime,
+ aModificationTime);
+ return permission.forget();
+}
+
+NS_IMETHODIMP
+Permission::GetPrincipal(nsIPrincipal** aPrincipal) {
+ nsCOMPtr<nsIPrincipal> copy = mPrincipal;
+ copy.forget(aPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::GetType(nsACString& aType) {
+ aType = mType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::GetCapability(uint32_t* aCapability) {
+ *aCapability = mCapability;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::GetExpireType(uint32_t* aExpireType) {
+ *aExpireType = mExpireType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::GetExpireTime(int64_t* aExpireTime) {
+ *aExpireTime = mExpireTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::GetModificationTime(int64_t* aModificationTime) {
+ *aModificationTime = mModificationTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Permission::Matches(nsIPrincipal* aPrincipal, bool aExactHost, bool* aMatches) {
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_ARG_POINTER(aMatches);
+
+ return mPrincipal->EqualsForPermission(aPrincipal, aExactHost, aMatches);
+}
+
+NS_IMETHODIMP
+Permission::MatchesURI(nsIURI* aURI, bool aExactHost, bool* aMatches) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ mozilla::OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(aURI, attrs);
+ NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);
+
+ return Matches(principal, aExactHost, aMatches);
+}
+
+} // namespace mozilla
diff --git a/extensions/permissions/Permission.h b/extensions/permissions/Permission.h
new file mode 100644
index 0000000000..0d304a48c7
--- /dev/null
+++ b/extensions/permissions/Permission.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_Permission_h
+#define mozilla_Permission_h
+
+#include "nsIPermission.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+////////////////////////////////////////////////////////////////////////////////
+
+class Permission : public nsIPermission {
+ public:
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERMISSION
+
+ static already_AddRefed<Permission> Create(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aCapability,
+ uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime);
+
+ // This method creates a new nsIPrincipal with a stripped OriginAttributes (no
+ // userContextId) and a content principal equal to the origin of 'aPrincipal'.
+ static already_AddRefed<nsIPrincipal> ClonePrincipalForPermission(
+ nsIPrincipal* aPrincipal);
+
+ protected:
+ Permission(nsIPrincipal* aPrincipal, const nsACString& aType,
+ uint32_t aCapability, uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime);
+
+ virtual ~Permission() = default;
+
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCString mType;
+ uint32_t mCapability;
+ uint32_t mExpireType;
+ int64_t mExpireTime;
+ int64_t mModificationTime;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_Permission_h
diff --git a/extensions/permissions/PermissionDelegateHandler.cpp b/extensions/permissions/PermissionDelegateHandler.cpp
new file mode 100644
index 0000000000..b81943c9b9
--- /dev/null
+++ b/extensions/permissions/PermissionDelegateHandler.cpp
@@ -0,0 +1,403 @@
+/* -*- 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/PermissionDelegateHandler.h"
+
+#include "nsGlobalWindowInner.h"
+#include "nsPIDOMWindow.h"
+#include "nsIPrincipal.h"
+#include "nsContentPermissionHelper.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_permissions.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/PermissionManager.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+typedef PermissionDelegateHandler::PermissionDelegatePolicy DelegatePolicy;
+typedef PermissionDelegateHandler::PermissionDelegateInfo DelegateInfo;
+
+// Particular type of permissions to care about. We decide cases by case and
+// give various types of controls over each of these.
+static const DelegateInfo sPermissionsMap[] = {
+ // Permissions API map. All permission names have to be in lowercase.
+ {"geo", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy},
+ // The same with geo, but we support both to save some conversions between
+ // "geo" and "geolocation"
+ {"geolocation", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy},
+ {"desktop-notification", nullptr,
+ DelegatePolicy::ePersistDeniedCrossOrigin},
+ {"persistent-storage", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
+ {"vibration", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
+ {"midi", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
+ // Like "midi" but with sysex support.
+ {"midi-sysex", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
+ {"storage-access", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
+ {"camera", u"camera", DelegatePolicy::eDelegateUseFeaturePolicy},
+ {"microphone", u"microphone", DelegatePolicy::eDelegateUseFeaturePolicy},
+ {"screen", u"display-capture", DelegatePolicy::eDelegateUseFeaturePolicy},
+ {"xr", u"xr-spatial-tracking", DelegatePolicy::eDelegateUseFeaturePolicy},
+};
+
+static_assert(PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT ==
+ (sizeof(sPermissionsMap) / sizeof(DelegateInfo)),
+ "The PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT must "
+ "match to the "
+ "length of sPermissionsMap. Please update it.");
+
+NS_IMPL_CYCLE_COLLECTION(PermissionDelegateHandler)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PermissionDelegateHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PermissionDelegateHandler)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PermissionDelegateHandler)
+ NS_INTERFACE_MAP_ENTRY(nsIPermissionDelegateHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+PermissionDelegateHandler::PermissionDelegateHandler(dom::Document* aDocument)
+ : mDocument(aDocument) {
+ MOZ_ASSERT(aDocument);
+}
+
+/* static */
+const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo(
+ const nsAString& aPermissionName) {
+ nsAutoString lowerContent(aPermissionName);
+ ToLowerCase(lowerContent);
+
+ for (const auto& perm : sPermissionsMap) {
+ if (lowerContent.EqualsASCII(perm.mPermissionName)) {
+ return &perm;
+ }
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+PermissionDelegateHandler::MaybeUnsafePermissionDelegate(
+ const nsTArray<nsCString>& aTypes, bool* aMaybeUnsafe) {
+ *aMaybeUnsafe = false;
+ if (!StaticPrefs::permissions_delegation_enabled()) {
+ return NS_OK;
+ }
+
+ for (auto& type : aTypes) {
+ const DelegateInfo* info =
+ GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(type));
+ if (!info) {
+ continue;
+ }
+
+ nsAutoString featureName(info->mFeatureName);
+ if (FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(mDocument, featureName)) {
+ *aMaybeUnsafe = true;
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PermissionDelegateHandler::GetPermissionDelegateFPEnabled(bool* aEnabled) {
+ MOZ_ASSERT(NS_IsMainThread());
+ *aEnabled = StaticPrefs::permissions_delegation_enabled();
+ return NS_OK;
+}
+
+/* static */
+nsresult PermissionDelegateHandler::GetDelegatePrincipal(
+ const nsACString& aType, nsIContentPermissionRequest* aRequest,
+ nsIPrincipal** aResult) {
+ MOZ_ASSERT(aRequest);
+
+ if (!StaticPrefs::permissions_delegation_enabled()) {
+ return aRequest->GetPrincipal(aResult);
+ }
+
+ const DelegateInfo* info =
+ GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
+ if (!info) {
+ *aResult = nullptr;
+ return NS_OK;
+ }
+
+ if (info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin ||
+ info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) {
+ return aRequest->GetTopLevelPrincipal(aResult);
+ }
+
+ return aRequest->GetPrincipal(aResult);
+}
+
+bool PermissionDelegateHandler::Initialize() {
+ MOZ_ASSERT(mDocument);
+
+ mPermissionManager = PermissionManager::GetInstance();
+ if (!mPermissionManager) {
+ return false;
+ }
+
+ mPrincipal = mDocument->NodePrincipal();
+ return true;
+}
+
+static bool IsCrossOriginContentToTop(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+
+ RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext();
+ if (!bc) {
+ return true;
+ }
+ RefPtr<BrowsingContext> topBC = bc->Top();
+
+ // In Fission, we can know if it is cross-origin by checking whether both
+ // contexts in the same process. So, If they are not in the same process, we
+ // can say that it's cross-origin.
+ if (!topBC->IsInProcess()) {
+ return true;
+ }
+
+ RefPtr<Document> topDoc = topBC->GetDocument();
+ if (!topDoc) {
+ return true;
+ }
+
+ nsCOMPtr<nsIPrincipal> topLevelPrincipal = topDoc->NodePrincipal();
+
+ return !aDocument->NodePrincipal()->Subsumes(topLevelPrincipal);
+}
+
+bool PermissionDelegateHandler::HasFeaturePolicyAllowed(
+ const DelegateInfo* info) const {
+ if (info->mPolicy != DelegatePolicy::eDelegateUseFeaturePolicy ||
+ !info->mFeatureName) {
+ return true;
+ }
+
+ nsAutoString featureName(info->mFeatureName);
+ return FeaturePolicyUtils::IsFeatureAllowed(mDocument, featureName);
+}
+
+bool PermissionDelegateHandler::HasPermissionDelegated(
+ const nsACString& aType) const {
+ MOZ_ASSERT(mDocument);
+
+ // System principal should have right to make permission request
+ if (mPrincipal->IsSystemPrincipal()) {
+ return true;
+ }
+
+ const DelegateInfo* info =
+ GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
+ if (!info || !HasFeaturePolicyAllowed(info)) {
+ return false;
+ }
+
+ if (!StaticPrefs::permissions_delegation_enabled()) {
+ return true;
+ }
+
+ if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
+ !mDocument->IsTopLevelContentDocument() &&
+ IsCrossOriginContentToTop(mDocument)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult PermissionDelegateHandler::GetPermission(const nsACString& aType,
+ uint32_t* aPermission,
+ bool aExactHostMatch) {
+ MOZ_ASSERT(mDocument);
+ MOZ_ASSERT(mPrincipal);
+
+ if (mPrincipal->IsSystemPrincipal()) {
+ *aPermission = nsIPermissionManager::ALLOW_ACTION;
+ return NS_OK;
+ }
+
+ const DelegateInfo* info =
+ GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
+ if (!info || !HasFeaturePolicyAllowed(info)) {
+ *aPermission = nsIPermissionManager::DENY_ACTION;
+ return NS_OK;
+ }
+
+ nsresult (NS_STDCALL nsIPermissionManager::*testPermission)(
+ nsIPrincipal*, const nsACString&, uint32_t*) =
+ aExactHostMatch ? &nsIPermissionManager::TestExactPermissionFromPrincipal
+ : &nsIPermissionManager::TestPermissionFromPrincipal;
+
+ if (!StaticPrefs::permissions_delegation_enabled()) {
+ return (mPermissionManager->*testPermission)(mPrincipal, aType,
+ aPermission);
+ }
+
+ if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
+ !mDocument->IsTopLevelContentDocument() &&
+ IsCrossOriginContentToTop(mDocument)) {
+ *aPermission = nsIPermissionManager::DENY_ACTION;
+ return NS_OK;
+ }
+
+ nsIPrincipal* principal = mPrincipal;
+ // If we cannot get the browsing context from the document, we fallback to use
+ // the prinicpal of the document to test the permission.
+ RefPtr<BrowsingContext> bc = mDocument->GetBrowsingContext();
+
+ if ((info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin ||
+ info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) &&
+ bc) {
+ RefPtr<WindowContext> topWC = bc->GetTopWindowContext();
+
+ if (topWC && topWC->IsInProcess()) {
+ // If the top-level window context is in the same process, we directly get
+ // the node principal from the top-level document to test the permission.
+ // We cannot check the lists in the window context in this case since the
+ // 'perm-changed' could be notified in the iframe before the top-level in
+ // certain cases, for example, request permissions in first-party iframes.
+ // In this case, the list in window context hasn't gotten updated, so it
+ // would has an out-dated value until the top-level window get the
+ // observer. So, we have to test permission manager directly if we can.
+ RefPtr<Document> topDoc = topWC->GetBrowsingContext()->GetDocument();
+
+ if (topDoc) {
+ principal = topDoc->NodePrincipal();
+ }
+ } else if (topWC) {
+ // Get the delegated permissions from the top-level window context.
+ DelegatedPermissionList list =
+ aExactHostMatch ? topWC->GetDelegatedExactHostMatchPermissions()
+ : topWC->GetDelegatedPermissions();
+ size_t idx = std::distance(sPermissionsMap, info);
+ *aPermission = list.mPermissions[idx];
+ return NS_OK;
+ }
+ }
+
+ return (mPermissionManager->*testPermission)(principal, aType, aPermission);
+}
+
+nsresult PermissionDelegateHandler::GetPermissionForPermissionsAPI(
+ const nsACString& aType, uint32_t* aPermission) {
+ return GetPermission(aType, aPermission, false);
+}
+
+void PermissionDelegateHandler::PopulateAllDelegatedPermissions() {
+ MOZ_ASSERT(mDocument);
+ MOZ_ASSERT(mPermissionManager);
+
+ // We only populate the delegated permissions for the top-level content.
+ if (!mDocument->IsTopLevelContentDocument()) {
+ return;
+ }
+
+ RefPtr<WindowContext> wc = mDocument->GetWindowContext();
+ NS_ENSURE_TRUE_VOID(wc && !wc->IsDiscarded());
+
+ DelegatedPermissionList list;
+ DelegatedPermissionList exactHostMatchList;
+
+ for (const auto& perm : sPermissionsMap) {
+ size_t idx = std::distance(sPermissionsMap, &perm);
+
+ nsDependentCString type(perm.mPermissionName);
+ // Populate the permission.
+ uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
+ Unused << mPermissionManager->TestPermissionFromPrincipal(mPrincipal, type,
+ &permission);
+ list.mPermissions[idx] = permission;
+
+ // Populate the exact-host-match permission.
+ permission = nsIPermissionManager::UNKNOWN_ACTION;
+ Unused << mPermissionManager->TestExactPermissionFromPrincipal(
+ mPrincipal, type, &permission);
+ exactHostMatchList.mPermissions[idx] = permission;
+ }
+
+ WindowContext::Transaction txn;
+ txn.SetDelegatedPermissions(list);
+ txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList);
+ MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc));
+}
+
+void PermissionDelegateHandler::UpdateDelegatedPermission(
+ const nsACString& aType) {
+ MOZ_ASSERT(mDocument);
+ MOZ_ASSERT(mPermissionManager);
+
+ // We only update the delegated permission for the top-level content.
+ if (!mDocument->IsTopLevelContentDocument()) {
+ return;
+ }
+
+ RefPtr<WindowContext> wc = mDocument->GetWindowContext();
+ NS_ENSURE_TRUE_VOID(wc);
+
+ const DelegateInfo* info =
+ GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
+ if (!info) {
+ return;
+ }
+ size_t idx = std::distance(sPermissionsMap, info);
+
+ WindowContext::Transaction txn;
+ bool changed = false;
+ DelegatedPermissionList list = wc->GetDelegatedPermissions();
+
+ if (UpdateDelegatePermissionInternal(
+ list, aType, idx,
+ &nsIPermissionManager::TestPermissionFromPrincipal)) {
+ txn.SetDelegatedPermissions(list);
+ changed = true;
+ }
+
+ DelegatedPermissionList exactHostMatchList =
+ wc->GetDelegatedExactHostMatchPermissions();
+
+ if (UpdateDelegatePermissionInternal(
+ exactHostMatchList, aType, idx,
+ &nsIPermissionManager::TestExactPermissionFromPrincipal)) {
+ txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList);
+ changed = true;
+ }
+
+ // We only commit if there is any change of permissions.
+ if (changed) {
+ MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc));
+ }
+}
+
+bool PermissionDelegateHandler::UpdateDelegatePermissionInternal(
+ PermissionDelegateHandler::DelegatedPermissionList& aList,
+ const nsACString& aType, size_t aIdx,
+ nsresult (NS_STDCALL nsIPermissionManager::*aTestFunc)(nsIPrincipal*,
+ const nsACString&,
+ uint32_t*)) {
+ MOZ_ASSERT(aTestFunc);
+ MOZ_ASSERT(mPermissionManager);
+ MOZ_ASSERT(mPrincipal);
+
+ uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
+ Unused << (mPermissionManager->*aTestFunc)(mPrincipal, aType, &permission);
+
+ if (aList.mPermissions[aIdx] != permission) {
+ aList.mPermissions[aIdx] = permission;
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace mozilla
diff --git a/extensions/permissions/PermissionDelegateHandler.h b/extensions/permissions/PermissionDelegateHandler.h
new file mode 100644
index 0000000000..a07fbceaeb
--- /dev/null
+++ b/extensions/permissions/PermissionDelegateHandler.h
@@ -0,0 +1,203 @@
+/* -*- 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/. */
+
+/*
+ * Permission delegate handler provides a policy of how top-level can
+ * delegate permission to embedded iframes.
+ *
+ * This class includes a mechanism to delegate permission using feature
+ * policy. Feature policy will assure that only cross-origin iframes which
+ * have been explicitly granted access will have the opportunity to request
+ * permission.
+ *
+ * For example if an iframe has not been granted access to geolocation by
+ * Feature Policy, geolocation request from the iframe will be automatically
+ * denied. if the top-level origin already has access to geolocation and the
+ * iframe has been granted access to geolocation by Feature Policy, the iframe
+ * will also have access to geolocation. If the top-level frame did not have
+ * access to geolocation, and the iframe has been granted access to geolocation
+ * by Feature Policy, a request from the cross-origin iframe would trigger a
+ * prompt using of the top-level origin.
+ */
+
+#ifndef mozilla_PermissionDelegateHandler_h
+#define mozilla_PermissionDelegateHandler_h
+
+#include "mozilla/Array.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsIPermissionDelegateHandler.h"
+#include "nsIPermissionManager.h"
+#include "nsCOMPtr.h"
+
+class nsIPrincipal;
+class nsIContentPermissionRequest;
+
+namespace mozilla {
+namespace dom {
+class Document;
+class WindowContext;
+} // namespace dom
+
+class PermissionDelegateHandler final : public nsIPermissionDelegateHandler {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(PermissionDelegateHandler)
+
+ NS_DECL_NSIPERMISSIONDELEGATEHANDLER
+
+ explicit PermissionDelegateHandler() = default;
+ explicit PermissionDelegateHandler(mozilla::dom::Document* aDocument);
+
+ static constexpr size_t DELEGATED_PERMISSION_COUNT = 12;
+
+ typedef struct DelegatedPermissionList {
+ Array<uint32_t, DELEGATED_PERMISSION_COUNT> mPermissions;
+
+ bool operator==(const DelegatedPermissionList& aOther) const {
+ return mPermissions == aOther.mPermissions;
+ }
+ } DelegatedPermissionList;
+
+ bool Initialize();
+
+ /*
+ * Indicates if we has the right to make permission request with aType
+ */
+ bool HasPermissionDelegated(const nsACString& aType) const;
+
+ /*
+ * Get permission state, which applied permission delegate policy.
+ *
+ * @param aType the permission type to get
+ * @param aPermission out argument which will be a permission type that we
+ * will return from this function.
+ * @param aExactHostMatch whether to look for the exact host name or also for
+ * subdomains that can have the same permission.
+ */
+ nsresult GetPermission(const nsACString& aType, uint32_t* aPermission,
+ bool aExactHostMatch);
+
+ /*
+ * Get permission state for permission api, which applied
+ * permission delegate policy.
+ *
+ * @param aType the permission type to get
+ * @param aExactHostMatch whether to look for the exact host name or also for
+ * subdomains that can have the same permission.
+ * @param aPermission out argument which will be a permission type that we
+ * will return from this function.
+ */
+ nsresult GetPermissionForPermissionsAPI(const nsACString& aType,
+ uint32_t* aPermission);
+
+ enum PermissionDelegatePolicy {
+ /* Always delegate permission from top level to iframe and the iframe
+ * should use top level origin to get/set permission.*/
+ eDelegateUseTopOrigin,
+
+ /* Permission is delegated using Feature Policy. Permission is denied by
+ * default in cross origin iframe and the iframe only could get/set
+ * permission if there's allow attribute set in iframe. e.g allow =
+ * "geolocation" */
+ eDelegateUseFeaturePolicy,
+
+ /* Persistent denied permissions in cross origin iframe */
+ ePersistDeniedCrossOrigin,
+
+ /* This is the old behavior of cross origin iframe permission. The
+ * permission delegation should not have an effect on iframe. The cross
+ * origin iframe get/set permissions by its origin */
+ eDelegateUseIframeOrigin,
+ };
+
+ /*
+ * Indicates matching between Feature Policy and Permissions name defined in
+ * Permissions Manager, not DOM Permissions API. Permissions API exposed in
+ * DOM only supports "geo" at the moment but Permissions Manager also supports
+ * "camera", "microphone".
+ */
+ typedef struct {
+ const char* mPermissionName;
+ const char16_t* mFeatureName;
+ PermissionDelegatePolicy mPolicy;
+ } PermissionDelegateInfo;
+
+ /**
+ * The loader maintains a weak reference to the document with
+ * which it is initialized. This call forces the reference to
+ * be dropped.
+ */
+ void DropDocumentReference() { mDocument = nullptr; }
+
+ /*
+ * Helper function to return the delegate info value for aPermissionName.
+ * @param aPermissionName the permission name to get
+ */
+ static const PermissionDelegateInfo* GetPermissionDelegateInfo(
+ const nsAString& aPermissionName);
+
+ /*
+ * Helper function to return the delegate principal. This will return nullptr,
+ * or request's principal or top level principal based on the delegate policy
+ * will be applied for a given type.
+ * We use this function when prompting, no need to perform permission check
+ * (deny/allow).
+ *
+ * @param aType the permission type to get
+ * @param aRequest The request which the principal is get from.
+ * @param aResult out argument which will be a principal that we
+ * will return from this function.
+ */
+ static nsresult GetDelegatePrincipal(const nsACString& aType,
+ nsIContentPermissionRequest* aRequest,
+ nsIPrincipal** aResult);
+
+ /**
+ * Populate all delegated permissions to the WindowContext of the associated
+ * document. We only populate the permissions for the top-level content.
+ */
+ void PopulateAllDelegatedPermissions();
+
+ /**
+ * Update the given delegated permission to the WindowContext. We only
+ * update it for the top-level content.
+ */
+ void UpdateDelegatedPermission(const nsACString& aType);
+
+ private:
+ ~PermissionDelegateHandler() = default;
+
+ /*
+ * Check whether the permission is blocked by FeaturePolicy directive.
+ * Default allowlist for a featureName of permission used in permissions
+ * delegate should be set to eSelf, to ensure that permission is denied by
+ * default and only have the opportunity to request permission with allow
+ * attribute.
+ */
+ bool HasFeaturePolicyAllowed(const PermissionDelegateInfo* info) const;
+
+ /**
+ * A helper function to test the permission and set the result to the given
+ * list. It will return true if the permission is changed, otherwise false.
+ */
+ bool UpdateDelegatePermissionInternal(
+ PermissionDelegateHandler::DelegatedPermissionList& aList,
+ const nsACString& aType, size_t aIdx,
+ nsresult (NS_STDCALL nsIPermissionManager::*aTestFunc)(nsIPrincipal*,
+ const nsACString&,
+ uint32_t*));
+
+ // A weak pointer to our document. Nulled out by DropDocumentReference.
+ mozilla::dom::Document* mDocument;
+
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ RefPtr<nsIPermissionManager> mPermissionManager;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PermissionDelegateHandler_h
diff --git a/extensions/permissions/PermissionDelegateIPCUtils.h b/extensions/permissions/PermissionDelegateIPCUtils.h
new file mode 100644
index 0000000000..c570a587f5
--- /dev/null
+++ b/extensions/permissions/PermissionDelegateIPCUtils.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef mozilla_permissiondelegateipcutils_h
+#define mozilla_permissiondelegateipcutils_h
+
+#include "ipc/IPCMessageUtils.h"
+
+#include "mozilla/PermissionDelegateHandler.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<
+ mozilla::PermissionDelegateHandler::DelegatedPermissionList> {
+ typedef mozilla::PermissionDelegateHandler::DelegatedPermissionList paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ for (auto& permission : aParam.mPermissions) {
+ WriteParam(aWriter, permission);
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ for (auto& permission : aResult->mPermissions) {
+ if (!ReadParam(aReader, &permission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_permissiondelegateipcutils_h
diff --git a/extensions/permissions/PermissionManager.cpp b/extensions/permissions/PermissionManager.cpp
new file mode 100644
index 0000000000..f81d823470
--- /dev/null
+++ b/extensions/permissions/PermissionManager.cpp
@@ -0,0 +1,3928 @@
+/* -*- 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/AppShutdown.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 "nsTHashSet.h"
+#include "nsToolkitCompsCID.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+#define PERMISSIONS_FILE_NAME "permissions.sqlite"
+#define HOSTS_SCHEMA_VERSION 12
+
+// 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};
+
+// 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;
+}
+
+// Array of permission prefixes which should be isolated only by site.
+// These site-scoped permissions are stored under their site's principal.
+// GetAllForPrincipal also needs to look for these especially.
+static constexpr std::array<nsLiteralCString, 2> kSiteScopedPermissions = {
+ {"3rdPartyStorage^"_ns, "AllowStorageAccessRequest^"_ns}};
+
+bool IsSiteScopedPermission(const nsACString& aType) {
+ if (aType.IsEmpty()) {
+ return false;
+ }
+ for (const auto& perm : kSiteScopedPermissions) {
+ if (aType.Length() >= perm.Length() &&
+ Substring(aType, 0, perm.Length()) == perm) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void OriginAppendOASuffix(OriginAttributes aOriginAttributes,
+ bool aForceStripOA, nsACString& aOrigin) {
+ PermissionManager::MaybeStripOriginAttributes(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;
+ NS_ENSURE_TRUE(attrs.PopulateFromSuffix(suffix), NS_ERROR_FAILURE);
+
+ OriginAppendOASuffix(attrs, aForceStripOA, aOrigin);
+
+ return NS_OK;
+}
+
+// Returns the site of the principal, including OA, given a principal.
+nsresult GetSiteFromPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA,
+ nsACString& aSite) {
+ nsCOMPtr<nsIURI> uri = aPrincipal->GetURI();
+ nsEffectiveTLDService* etld = nsEffectiveTLDService::GetInstance();
+ NS_ENSURE_TRUE(etld, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
+ nsresult rv = etld->GetSite(uri, aSite);
+
+ // The principal may belong to the about:blank content viewer, so this can be
+ // expected to fail.
+ if (NS_FAILED(rv)) {
+ rv = aPrincipal->GetOrigin(aSite);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ nsAutoCString suffix;
+ rv = aPrincipal->GetOriginSuffix(suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OriginAttributes attrs;
+ NS_ENSURE_TRUE(attrs.PopulateFromSuffix(suffix), NS_ERROR_FAILURE);
+
+ OriginAppendOASuffix(attrs, aForceStripOA, aSite);
+
+ 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;
+ }
+
+ PermissionManager::MaybeStripOriginAttributes(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);
+
+ nsTHashSet<nsCString> 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.Insert(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);
+ return res;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+PermissionManager::PermissionKey*
+PermissionManager::PermissionKey::CreateFromPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ bool aScopeToSite,
+ nsresult& aResult) {
+ nsAutoCString keyString;
+ if (aScopeToSite) {
+ aResult = GetSiteFromPrincipal(aPrincipal, aForceStripOA, keyString);
+ } else {
+ aResult = GetOriginFromPrincipal(aPrincipal, aForceStripOA, keyString);
+ }
+ if (NS_WARN_IF(NS_FAILED(aResult))) {
+ return nullptr;
+ }
+ return new PermissionKey(keyString);
+}
+
+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);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// PermissionManager Implementation
+
+NS_IMPL_ISUPPORTS(PermissionManager, nsIPermissionManager, nsIObserver,
+ nsISupportsWeakReference, nsIAsyncShutdownBlocker)
+
+PermissionManager::PermissionManager()
+ : mMonitor("PermissionManager::mMonitor"),
+ mState(eInitializing),
+ mMemoryOnlyDB(false),
+ mLargestID(0) {}
+
+PermissionManager::~PermissionManager() {
+ // NOTE: Make sure to reject each of the promises in mPermissionKeyPromiseMap
+ // before destroying.
+ for (const auto& promise : mPermissionKeyPromiseMap.Values()) {
+ if (promise) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ }
+ }
+ mPermissionKeyPromiseMap.Clear();
+
+ if (mThread) {
+ mThread->Shutdown();
+ mThread = nullptr;
+ }
+}
+
+/* static */
+StaticMutex PermissionManager::sCreationMutex;
+
+// static
+already_AddRefed<nsIPermissionManager> PermissionManager::GetXPCOMSingleton() {
+ // The lazy initialization could race.
+ StaticMutexAutoLock lock(sCreationMutex);
+
+ 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() {
+ // TODO: There is a minimal chance that we can race here with a
+ // GetXPCOMSingleton call that did not yet set gPermissionManager.
+ // See bug 1745056.
+ if (!gPermissionManager) {
+ // Hand off the creation of the permission manager to GetXPCOMSingleton.
+ nsCOMPtr<nsIPermissionManager> permManager = GetXPCOMSingleton();
+ }
+
+ return gPermissionManager;
+}
+
+nsresult PermissionManager::Init() {
+ // If we are already shutting down, do not permit a creation.
+ // This must match the phase in GetAsyncShutdownBarrier.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ // 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-do-change", true);
+ observerService->AddObserver(this, "testonly-reload-permissions-from-disk",
+ true);
+ }
+
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier();
+ if (!asc) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ nsAutoString blockerName;
+ MOZ_ALWAYS_SUCCEEDS(GetName(blockerName));
+
+ nsresult rv = asc->AddBlocker(
+ this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, blockerName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ 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(),
+ mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(data->mDBConn));
+ } else {
+ rv = storage->OpenDatabase(aPermissionsFile,
+ mozIStorageService::CONNECTION_DEFAULT,
+ 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(11);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // fall through to the next upgrade
+ [[fallthrough]];
+
+ case 11: {
+ // Migrate 3rdPartyStorage keys to a site scope
+ rv = data->mDBConn->BeginTransaction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageStatement> updateStmt;
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("UPDATE moz_perms SET origin = ?2 WHERE id = ?1"),
+ getter_AddRefs(updateStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> deleteStmt;
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("DELETE FROM moz_perms WHERE id = ?1"),
+ getter_AddRefs(deleteStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> selectStmt;
+ rv = data->mDBConn->CreateStatement(
+ nsLiteralCString("SELECT id, origin, type FROM moz_perms WHERE "
+ " SUBSTR(type, 0, 17) == \"3rdPartyStorage^\""),
+ getter_AddRefs(selectStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTHashSet<nsCStringHashKey> deduplicationSet;
+ bool hasResult;
+ while (NS_SUCCEEDED(selectStmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t id;
+ rv = selectStmt->GetInt64(0, &id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString origin;
+ rv = selectStmt->GetUTF8String(1, origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString type;
+ rv = selectStmt->GetUTF8String(2, type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), origin);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCString site;
+ rv = nsEffectiveTLDService::GetInstance()->GetSite(uri, site);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsCString deduplicationKey =
+ nsPrintfCString("%s,%s", site.get(), type.get());
+ if (deduplicationSet.Contains(deduplicationKey)) {
+ rv = deleteStmt->BindInt64ByIndex(0, id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = deleteStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ deduplicationSet.Insert(deduplicationKey);
+ rv = updateStmt->BindInt64ByIndex(0, id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = updateStmt->BindUTF8StringByIndex(1, site);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = updateStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ rv = data->mDBConn->CommitTransaction();
+ 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::AddFromPrincipalAndPersistInPrivateBrowsing(
+ nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aPermission) {
+ ENSURE_NOT_CHILD_PROCESS;
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ // 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,
+ nsIPermissionManager::EXPIRE_NEVER,
+ /* aExpireTime */ 0, modificationTime, eNotify, eWriteToDB,
+ /* aIgnoreSessionPermissions */ false,
+ /* aOriginString*/ nullptr,
+ /* aAllowPersistInPrivateBrowsing */ true);
+}
+
+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,
+ const bool aAllowPersistInPrivateBrowsing) {
+ 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 {
+ if (IsSiteScopedPermission(aType)) {
+ rv = GetSiteFromPrincipal(aPrincipal, IsOAForceStripPermission(aType),
+ origin);
+ } else {
+ // Compute it from the principal provided.
+ rv = GetOriginFromPrincipal(aPrincipal, IsOAForceStripPermission(aType),
+ origin);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Unless the caller sets aAllowPersistInPrivateBrowsing, only store
+ // permissions for the session in Private Browsing. Except for default
+ // permissions which are stored in-memory only and imported each startup. We
+ // also allow setting persistent UKNOWN_ACTION, to support removing default
+ // private browsing permissions.
+ if (!aAllowPersistInPrivateBrowsing && aID != cIDPermissionIsDefault &&
+ aPermission != UNKNOWN_ACTION && 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),
+ IsSiteScopedPermission(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<std::tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10> array;
+ for (const PermissionHashKey& entry : mPermissionTable) {
+ 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(
+ std::get<0>(i), std::get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0, PermissionManager::eNotify,
+ PermissionManager::eWriteToDB, false, &std::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 (const PermissionHashKey& entry : mPermissionTable) {
+ 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) {
+ // Check that aSince is a reasonable point in time, not in the future
+ if (aSince > (PR_Now() / PR_USEC_PER_MSEC)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ 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::GetAllByTypes(
+ const nsTArray<nsCString>& aTypes,
+ nsTArray<RefPtr<nsIPermission>>& aResult) {
+ if (aTypes.IsEmpty()) {
+ return NS_OK;
+ }
+
+ return GetPermissionEntries(
+ [&](const PermissionEntry& aPermEntry) {
+ return aTypes.Contains(mTypeArray[aPermEntry.mType]);
+ },
+ aResult);
+}
+
+nsresult PermissionManager::GetAllForPrincipalHelper(
+ nsIPrincipal* aPrincipal, bool aSiteScopePermissions,
+ nsTArray<RefPtr<nsIPermission>>& aResult) {
+ nsresult rv;
+ RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal(
+ aPrincipal, false, aSiteScopePermissions, rv);
+ if (!key) {
+ MOZ_ASSERT(NS_FAILED(rv));
+ return rv;
+ }
+ PermissionHashKey* entry = mPermissionTable.GetEntry(key);
+
+ nsTArray<PermissionEntry> strippedPerms;
+ rv = GetStripPermsForPrincipal(aPrincipal, aSiteScopePermissions,
+ 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;
+ }
+
+ // Make sure that we only get site scoped permissions if this
+ // helper is being invoked for that purpose.
+ if (aSiteScopePermissions !=
+ IsSiteScopedPermission(mTypeArray[permEntry.mType])) {
+ 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::GetAllForPrincipal(
+ nsIPrincipal* aPrincipal, nsTArray<RefPtr<nsIPermission>>& aResult) {
+ nsresult rv;
+ aResult.Clear();
+ EnsureReadCompleted();
+
+ MOZ_ASSERT(PermissionAvailable(aPrincipal, ""_ns));
+
+ // First, append the non-site-scoped permissions.
+ rv = GetAllForPrincipalHelper(aPrincipal, false, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Second, append the site-scoped permissions.
+ return GetAllForPrincipalHelper(aPrincipal, true, aResult);
+}
+
+NS_IMETHODIMP PermissionManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ ENSURE_NOT_CHILD_PROCESS;
+
+ if (!nsCRT::strcmp(aTopic, "profile-do-change") && !mPermissionsFile) {
+ // profile startup is complete, and we didn't have the permissions file
+ // before; 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<std::tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10>
+ permissions;
+ for (const PermissionHashKey& entry : mPermissionTable) {
+ 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(
+ std::get<0>(i), std::get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0, PermissionManager::eNotify,
+ PermissionManager::eWriteToDB, false, &std::get<2>(i));
+ }
+
+ return NS_OK;
+}
+
+nsresult PermissionManager::GetStripPermsForPrincipal(
+ nsIPrincipal* aPrincipal, bool aSiteScopePermissions,
+ nsTArray<PermissionEntry>& aResult) {
+ aResult.Clear();
+ aResult.SetCapacity(kStripOAPermissions.size());
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunreachable-code-return"
+#endif
+ // No special strip permissions
+ if (kStripOAPermissions.empty()) {
+ return NS_OK;
+ }
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+ nsresult rv;
+ // Create a key for the principal, but strip any origin attributes.
+ // The key must be created aware of whether or not we are scoping to site.
+ RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal(
+ aPrincipal, true, aSiteScopePermissions, 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) {
+ // if the permission type's site scoping does not match this function call,
+ // we don't care about it, so continue.
+ // As of time of writing, this never happens when aSiteScopePermissions
+ // is true because there is no common permission between kStripOAPermissions
+ // and kSiteScopedPermissions
+ if (aSiteScopePermissions != IsSiteScopedPermission(permType)) {
+ continue;
+ }
+ 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]),
+ IsSiteScopedPermission(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 (const PermissionHashKey& entry : mPermissionTable) {
+ 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, 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.InsertOrUpdate(
+ 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 */
+nsresult PermissionManager::GetKeyForOrigin(const nsACString& aOrigin,
+ bool aForceStripOA,
+ bool aSiteScopePermissions,
+ 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 NS_OK;
+ }
+
+ // 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 NS_OK;
+ }
+
+ MaybeStripOriginAttributes(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
+
+ // If it is needed, turn the origin into its site-origin
+ if (aSiteScopePermissions) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aKey);
+ if (!NS_WARN_IF(NS_FAILED(rv))) {
+ nsCString site;
+ rv = nsEffectiveTLDService::GetInstance()->GetSite(uri, site);
+ if (!NS_WARN_IF(NS_FAILED(rv))) {
+ aKey = site;
+ }
+ }
+ }
+
+ // Append the stripped suffix to the output origin key.
+ nsAutoCString suffix;
+ attrs.CreateSuffix(suffix);
+ aKey.Append(suffix);
+
+ return NS_OK;
+}
+
+/* static */
+nsresult PermissionManager::GetKeyForPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ bool aSiteScopePermissions,
+ nsACString& aKey) {
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aKey.Truncate();
+ return rv;
+ }
+ return GetKeyForOrigin(origin, aForceStripOA, aSiteScopePermissions, aKey);
+}
+
+/* static */
+nsresult PermissionManager::GetKeyForPermission(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ nsACString& aKey) {
+ // Preload permissions have the "" key.
+ if (IsPreloadPermission(aType)) {
+ aKey.Truncate();
+ return NS_OK;
+ }
+
+ return GetKeyForPrincipal(aPrincipal, IsOAForceStripPermission(aType),
+ IsSiteScopedPermission(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, false, pair->first);
+
+ // On origins with a derived key set to an empty string
+ // (basically any non-web URI scheme), we want to make sure
+ // to return earlier, and leave [("", "")] as the resulting
+ // pairs (but still run the same debug assertions near the
+ // end of this method).
+ if (pair->first.IsEmpty()) {
+ break;
+ }
+
+ 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;
+}
+
+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.InsertOrUpdate(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);
+ rv =
+ AddInternal(principal, aType, aPermission,
+ cIDPermissionIsDefault, aExpireType, aExpireTime,
+ aModificationTime, PermissionManager::eDontNotify,
+ PermissionManager::eNoDBOperation, false, &aOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StaticPrefs::permissions_isolateBy_privateBrowsing()) {
+ // Also import the permission for private browsing.
+ OriginAttributes attrs =
+ OriginAttributes(principal->OriginAttributesRef());
+ attrs.mPrivateBrowsingId = 1;
+ nsCOMPtr<nsIPrincipal> pbPrincipal =
+ BasePrincipal::Cast(principal)->CloneForcingOriginAttributes(
+ attrs);
+
+ rv = AddInternal(
+ pbPrincipal, aType, aPermission, cIDPermissionIsDefault,
+ aExpireType, aExpireTime, aModificationTime,
+ PermissionManager::eDontNotify,
+ PermissionManager::eNoDBOperation, false, &aOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ });
+
+ 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");
+ }
+
+ if (StaticPrefs::permissions_isolateBy_privateBrowsing()) {
+ // Also import the permission for private browsing.
+ OriginAttributes attrs =
+ OriginAttributes(principal->OriginAttributesRef());
+ attrs.mPrivateBrowsingId = 1;
+ nsCOMPtr<nsIPrincipal> pbPrincipal =
+ BasePrincipal::Cast(principal)->CloneForcingOriginAttributes(attrs);
+ // May return nullptr if clone fails.
+ NS_ENSURE_TRUE(pbPrincipal, NS_ERROR_FAILURE);
+
+ rv = AddInternal(pbPrincipal, 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 for private "
+ "browsing");
+ }
+ }
+ }
+
+ 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 = GetAsyncShutdownBarrier();
+ 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::GetAsyncShutdownBarrier()
+ const {
+ nsresult rv;
+ nsCOMPtr<nsIAsyncShutdownService> svc =
+ do_GetService("@mozilla.org/async-shutdown-service;1", &rv);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ // This feels very late but there seem to be other services that rely on
+ // us later than "profile-before-change".
+ rv = svc->GetXpcomWillShutdown(getter_AddRefs(client));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+
+ return client;
+}
+
+void PermissionManager::MaybeStripOriginAttributes(
+ 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);
+ }
+}
+
+} // namespace mozilla
diff --git a/extensions/permissions/PermissionManager.h b/extensions/permissions/PermissionManager.h
new file mode 100644
index 0000000000..a92d583e49
--- /dev/null
+++ b/extensions/permissions/PermissionManager.h
@@ -0,0 +1,682 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef mozilla_PermissionManager_h
+#define mozilla_PermissionManager_h
+
+#include "nsIPermissionManager.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsTHashtable.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsHashKeys.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/ThreadBound.h"
+#include "mozilla/Variant.h"
+#include "mozilla/Vector.h"
+
+#include <utility>
+
+class mozIStorageConnection;
+class mozIStorageStatement;
+class nsIInputStream;
+class nsIPermission;
+class nsIPrefBranch;
+
+namespace IPC {
+struct Permission;
+}
+
+namespace mozilla {
+class OriginAttributesPattern;
+
+namespace dom {
+class ContentChild;
+} // namespace dom
+
+////////////////////////////////////////////////////////////////////////////////
+
+class PermissionManager final : public nsIPermissionManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIAsyncShutdownBlocker {
+ friend class dom::ContentChild;
+
+ public:
+ class PermissionEntry {
+ public:
+ PermissionEntry(int64_t aID, uint32_t aType, uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime)
+ : mID(aID),
+ mExpireTime(aExpireTime),
+ mModificationTime(aModificationTime),
+ mType(aType),
+ mPermission(aPermission),
+ mExpireType(aExpireType),
+ mNonSessionPermission(aPermission),
+ mNonSessionExpireType(aExpireType),
+ mNonSessionExpireTime(aExpireTime) {}
+
+ int64_t mID;
+ int64_t mExpireTime;
+ int64_t mModificationTime;
+ uint32_t mType;
+ uint32_t mPermission;
+ uint32_t mExpireType;
+ uint32_t mNonSessionPermission;
+ uint32_t mNonSessionExpireType;
+ uint32_t mNonSessionExpireTime;
+ };
+
+ /**
+ * PermissionKey is the key used by PermissionHashKey hash table.
+ */
+ class PermissionKey {
+ public:
+ static PermissionKey* CreateFromPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ bool aScopeToSite,
+ nsresult& aResult);
+ static PermissionKey* CreateFromURI(nsIURI* aURI, nsresult& aResult);
+ static PermissionKey* CreateFromURIAndOriginAttributes(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes,
+ bool aForceStripOA, nsresult& aResult);
+
+ explicit PermissionKey(const nsACString& aOrigin)
+ : mOrigin(aOrigin), mHashCode(HashString(aOrigin)) {}
+
+ bool operator==(const PermissionKey& aKey) const {
+ return mOrigin.Equals(aKey.mOrigin);
+ }
+
+ PLDHashNumber GetHashCode() const { return mHashCode; }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PermissionKey)
+
+ const nsCString mOrigin;
+ const PLDHashNumber mHashCode;
+
+ private:
+ // Default ctor shouldn't be used.
+ PermissionKey() = delete;
+
+ // Dtor shouldn't be used outside of the class.
+ ~PermissionKey(){};
+ };
+
+ class PermissionHashKey : public nsRefPtrHashKey<PermissionKey> {
+ public:
+ explicit PermissionHashKey(const PermissionKey* aPermissionKey)
+ : nsRefPtrHashKey<PermissionKey>(aPermissionKey) {}
+
+ PermissionHashKey(PermissionHashKey&& toCopy)
+ : nsRefPtrHashKey<PermissionKey>(std::move(toCopy)),
+ mPermissions(std::move(toCopy.mPermissions)) {}
+
+ bool KeyEquals(const PermissionKey* aKey) const {
+ return *aKey == *GetKey();
+ }
+
+ static PLDHashNumber HashKey(const PermissionKey* aKey) {
+ return aKey->GetHashCode();
+ }
+
+ // Force the hashtable to use the copy constructor when shuffling entries
+ // around, otherwise the Auto part of our AutoTArray won't be happy!
+ enum { ALLOW_MEMMOVE = false };
+
+ inline nsTArray<PermissionEntry>& GetPermissions() { return mPermissions; }
+ inline const nsTArray<PermissionEntry>& GetPermissions() const {
+ return mPermissions;
+ }
+
+ inline int32_t GetPermissionIndex(uint32_t aType) const {
+ for (uint32_t i = 0; i < mPermissions.Length(); ++i)
+ if (mPermissions[i].mType == aType) return i;
+
+ return -1;
+ }
+
+ inline PermissionEntry GetPermission(uint32_t aType) const {
+ for (uint32_t i = 0; i < mPermissions.Length(); ++i)
+ if (mPermissions[i].mType == aType) return mPermissions[i];
+
+ // unknown permission... return relevant data
+ return PermissionEntry(-1, aType, nsIPermissionManager::UNKNOWN_ACTION,
+ nsIPermissionManager::EXPIRE_NEVER, 0, 0);
+ }
+
+ private:
+ AutoTArray<PermissionEntry, 1> mPermissions;
+ };
+
+ // nsISupports
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPERMISSIONMANAGER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ PermissionManager();
+ static already_AddRefed<nsIPermissionManager> GetXPCOMSingleton();
+ static PermissionManager* GetInstance();
+ nsresult Init();
+
+ // enums for AddInternal()
+ enum OperationType {
+ eOperationNone,
+ eOperationAdding,
+ eOperationRemoving,
+ eOperationChanging,
+ eOperationReplacingDefault
+ };
+
+ enum DBOperationType { eNoDBOperation, eWriteToDB };
+
+ enum NotifyOperationType { eDontNotify, eNotify };
+
+ // Similar to TestPermissionFromPrincipal, except that it is used only for
+ // permissions which can never have default values.
+ nsresult TestPermissionWithoutDefaultsFromPrincipal(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t* aPermission);
+
+ nsresult LegacyTestPermissionFromURI(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes,
+ const nsACString& aType, uint32_t* aPermission);
+
+ nsresult RemovePermissionsWithAttributes(OriginAttributesPattern& aAttrs);
+
+ /**
+ * See `nsIPermissionManager::GetPermissionsWithKey` for more info on
+ * permission keys.
+ *
+ * Get the permission key corresponding to the given Principal. This method is
+ * intentionally infallible, as we want to provide an permission key to every
+ * principal. Principals which don't have meaningful URIs with http://,
+ * https://, or ftp:// schemes are given the default "" Permission Key.
+ *
+ * @param aPrincipal The Principal which the key is to be extracted from.
+ * @param aForceStripOA Whether to force stripping the principals origin
+ * attributes prior to generating the key.
+ * @param aSiteScopePermissions Whether to prepare the key for permissions
+ * scoped to the Principal's site, rather than origin. These are looked
+ * up independently. Scoping of a permission is fully determined by its
+ * type and determined by calls to the function IsSiteScopedPermission.
+ * @param aKey A string which will be filled with the permission
+ * key.
+ */
+ static nsresult GetKeyForPrincipal(nsIPrincipal* aPrincipal,
+ bool aForceStripOA,
+ bool aSiteScopePermissions,
+ nsACString& aKey);
+
+ /**
+ * See `nsIPermissionManager::GetPermissionsWithKey` for more info on
+ * permission keys.
+ *
+ * Get the permission key corresponding to the given Origin. This method is
+ * like GetKeyForPrincipal, except that it avoids creating a nsIPrincipal
+ * object when you already have access to an origin string.
+ *
+ * If this method is passed a nonsensical origin string it may produce a
+ * nonsensical permission key result.
+ *
+ * @param aOrigin The origin which the key is to be extracted from.
+ * @param aForceStripOA Whether to force stripping the origins attributes
+ * prior to generating the key.
+ * @param aSiteScopePermissions Whether to prepare the key for permissions
+ * scoped to the Principal's site, rather than origin. These are looked
+ * up independently. Scoping of a permission is fully determined by its
+ * type and determined by calls to the function IsSiteScopedPermission.
+ * @param aKey A string which will be filled with the permission
+ * key.
+ */
+ static nsresult GetKeyForOrigin(const nsACString& aOrigin, bool aForceStripOA,
+ bool aSiteScopePermissions, nsACString& aKey);
+
+ /**
+ * See `nsIPermissionManager::GetPermissionsWithKey` for more info on
+ * permission keys.
+ *
+ * Get the permission key corresponding to the given Principal and type. This
+ * method is intentionally infallible, as we want to provide an permission key
+ * to every principal. Principals which don't have meaningful URIs with
+ * http://, https://, or ftp:// schemes are given the default "" Permission
+ * Key.
+ *
+ * This method is different from GetKeyForPrincipal in that it also takes
+ * permissions which must be sent down before loading a document into account.
+ *
+ * @param aPrincipal The Principal which the key is to be extracted from.
+ * @param aType The type of the permission to get the key for.
+ * @param aPermissionKey A string which will be filled with the permission
+ * key.
+ */
+ static nsresult GetKeyForPermission(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ nsACString& aKey);
+
+ /**
+ * See `nsIPermissionManager::GetPermissionsWithKey` for more info on
+ * permission keys.
+ *
+ * Get all permissions keys which could correspond to the given principal.
+ * This method, like GetKeyForPrincipal, is infallible and should always
+ * produce at least one (key, origin) pair.
+ *
+ * Unlike GetKeyForPrincipal, this method also gets the keys for base domains
+ * of the given principal. All keys returned by this method must be available
+ * in the content process for a given URL to successfully have its permissions
+ * checked in the `aExactHostMatch = false` situation.
+ *
+ * @param aPrincipal The Principal which the key is to be extracted from.
+ * @return returns an array of (key, origin) pairs.
+ */
+ static nsTArray<std::pair<nsCString, nsCString>> GetAllKeysForPrincipal(
+ nsIPrincipal* aPrincipal);
+
+ // From ContentChild.
+ nsresult RemoveAllFromIPC();
+
+ /**
+ * Returns false if this permission manager wouldn't have the permission
+ * requested available.
+ *
+ * If aType is empty, checks that the permission manager would have all
+ * permissions available for the given principal.
+ */
+ bool PermissionAvailable(nsIPrincipal* aPrincipal, const nsACString& aType);
+
+ /**
+ * The content process doesn't have access to every permission. Instead, when
+ * LOAD_DOCUMENT_URI channels for http://, https://, and ftp:// URIs are
+ * opened, the permissions for those channels are sent down to the content
+ * process before the OnStartRequest message. Permissions for principals with
+ * other schemes are sent down at process startup.
+ *
+ * Permissions are keyed and grouped by "Permission Key"s.
+ * `PermissionManager::GetKeyForPrincipal` provides the mechanism for
+ * determining the permission key for a given principal.
+ *
+ * This method may only be called in the parent process. It fills the nsTArray
+ * argument with the IPC::Permission objects which have a matching origin.
+ *
+ * @param origin The origin to use to find the permissions of interest.
+ * @param key The key to use to find the permissions of interest. Only used
+ * when the origin argument is empty.
+ * @param perms An array which will be filled with the permissions which
+ * match the given origin.
+ */
+ bool GetPermissionsFromOriginOrKey(const nsACString& aOrigin,
+ const nsACString& aKey,
+ nsTArray<IPC::Permission>& aPerms);
+
+ /**
+ * See `PermissionManager::GetPermissionsWithKey` for more info on
+ * Permission keys.
+ *
+ * `SetPermissionsWithKey` may only be called in the Child process, and
+ * initializes the permission manager with the permissions for a given
+ * Permission key. marking permissions with that key as available.
+ *
+ * @param permissionKey The key for the permissions which have been sent
+ * over.
+ * @param perms An array with the permissions which match the given key.
+ */
+ void SetPermissionsWithKey(const nsACString& aPermissionKey,
+ nsTArray<IPC::Permission>& aPerms);
+
+ /**
+ * Add a callback which should be run when all permissions are available for
+ * the given nsIPrincipal. This method invokes the callback runnable
+ * synchronously when the permissions are already available. Otherwise the
+ * callback will be run asynchronously in SystemGroup when all permissions
+ * are available in the future.
+ *
+ * NOTE: This method will not request the permissions be sent by the parent
+ * process. This should only be used to wait for permissions which may not
+ * have arrived yet in order to ensure they are present.
+ *
+ * @param aPrincipal The principal to wait for permissions to be available
+ * for.
+ * @param aRunnable The runnable to run when permissions are available for
+ * the given principal.
+ */
+ void WhenPermissionsAvailable(nsIPrincipal* aPrincipal,
+ nsIRunnable* aRunnable);
+
+ /**
+ * Strip origin attributes for permissions, depending on permission isolation
+ * pref state.
+ * @param aForceStrip If true, strips user context and private browsing id,
+ * ignoring permission isolation prefs.
+ * @param aOriginAttributes object to strip.
+ */
+ static void MaybeStripOriginAttributes(bool aForceStrip,
+ OriginAttributes& aOriginAttributes);
+
+ private:
+ ~PermissionManager();
+ static StaticMutex sCreationMutex MOZ_UNANNOTATED;
+
+ /**
+ * Get all permissions for a given principal, which should not be isolated
+ * by user context or private browsing. The principal has its origin
+ * attributes stripped before perm db lookup. This is currently only affects
+ * the "cookie" permission.
+ * @param aPrincipal Used for creating the permission key.
+ * @param aSiteScopePermissions Used to specify whether to get strip perms for
+ * site scoped permissions (defined in IsSiteScopedPermission) or all other
+ * permissions. Also used to create the permission key.
+ */
+ nsresult GetStripPermsForPrincipal(nsIPrincipal* aPrincipal,
+ bool aSiteScopePermissions,
+ nsTArray<PermissionEntry>& aResult);
+
+ // Returns -1 on failure
+ int32_t GetTypeIndex(const nsACString& aType, bool aAdd);
+
+ // 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 HasExpired(uint32_t aExpireType, int64_t aExpireTime);
+
+ // Appends the permissions associated with this principal to aResult.
+ // If the onlySiteScopePermissions argument is true, the permissions searched
+ // are those for the site of the principal and only the permissions that are
+ // site-scoped are used.
+ nsresult GetAllForPrincipalHelper(nsIPrincipal* aPrincipal,
+ bool aSiteScopePermissions,
+ nsTArray<RefPtr<nsIPermission>>& aResult);
+
+ // Returns PermissionHashKey for a given { host, isInBrowserElement } tuple.
+ // This is not simply using PermissionKey because we will walk-up domains in
+ // case of |host| contains sub-domains. Returns null if nothing found. Also
+ // accepts host on the format "<foo>". This will perform an exact match lookup
+ // as the string doesn't contain any dots.
+ PermissionHashKey* GetPermissionHashKey(nsIPrincipal* aPrincipal,
+ uint32_t aType, bool aExactHostMatch);
+
+ // Returns PermissionHashKey for a given { host, isInBrowserElement } tuple.
+ // This is not simply using PermissionKey because we will walk-up domains in
+ // case of |host| contains sub-domains. Returns null if nothing found. Also
+ // accepts host on the format "<foo>". This will perform an exact match lookup
+ // as the string doesn't contain any dots.
+ PermissionHashKey* GetPermissionHashKey(
+ nsIURI* aURI, const OriginAttributes* aOriginAttributes, uint32_t aType,
+ bool aExactHostMatch);
+
+ // The int32_t is the type index, the nsresult is an early bail-out return
+ // code.
+ typedef Variant<int32_t, nsresult> TestPreparationResult;
+ TestPreparationResult CommonPrepareToTestPermission(
+ nsIPrincipal* aPrincipal, int32_t aTypeIndex, const nsACString& aType,
+ uint32_t* aPermission, uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid, bool aExactHostMatch,
+ bool aIncludingSession);
+
+ // If aTypeIndex is passed -1, we try to inder the type index from aType.
+ nsresult CommonTestPermission(nsIPrincipal* aPrincipal, int32_t aTypeIndex,
+ const nsACString& aType, uint32_t* aPermission,
+ uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid,
+ bool aExactHostMatch, bool aIncludingSession);
+
+ // If aTypeIndex is passed -1, we try to inder the type index from aType.
+ nsresult CommonTestPermission(nsIURI* aURI, int32_t aTypeIndex,
+ const nsACString& aType, uint32_t* aPermission,
+ uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid,
+ bool aExactHostMatch, bool aIncludingSession);
+
+ nsresult CommonTestPermission(nsIURI* aURI,
+ const OriginAttributes* aOriginAttributes,
+ int32_t aTypeIndex, const nsACString& aType,
+ uint32_t* aPermission,
+ uint32_t aDefaultPermission,
+ bool aDefaultPermissionIsValid,
+ bool aExactHostMatch, bool aIncludingSession);
+
+ // Only one of aPrincipal or aURI is allowed to be passed in.
+ nsresult CommonTestPermissionInternal(
+ nsIPrincipal* aPrincipal, nsIURI* aURI,
+ const OriginAttributes* aOriginAttributes, int32_t aTypeIndex,
+ const nsACString& aType, uint32_t* aPermission, bool aExactHostMatch,
+ bool aIncludingSession);
+
+ nsresult OpenDatabase(nsIFile* permissionsFile);
+
+ void InitDB(bool aRemoveFile);
+ nsresult TryInitDB(bool aRemoveFile, nsIInputStream* aDefaultsInputStream);
+
+ void AddIdleDailyMaintenanceJob();
+ void RemoveIdleDailyMaintenanceJob();
+ void PerformIdleDailyMaintenance();
+
+ nsresult ImportLatestDefaults();
+ already_AddRefed<nsIInputStream> GetDefaultsInputStream();
+ void ConsumeDefaultsInputStream(nsIInputStream* aDefaultsInputStream,
+ const MonitorAutoLock& aProofOfLock);
+
+ nsresult CreateTable();
+ void NotifyObserversWithPermission(nsIPrincipal* aPrincipal,
+ const nsACString& aType,
+ uint32_t aPermission, uint32_t aExpireType,
+ int64_t aExpireTime,
+ int64_t aModificationTime,
+ const char16_t* aData);
+ void NotifyObservers(nsIPermission* aPermission, const char16_t* aData);
+
+ // Finalize all statements, close the DB and null it.
+ enum CloseDBNextOp {
+ eNone,
+ eRebuldOnSuccess,
+ eShutdown,
+ };
+ void CloseDB(CloseDBNextOp aNextOp);
+
+ nsresult RemoveAllInternal(bool aNotifyObservers);
+ nsresult RemoveAllFromMemory();
+
+ void UpdateDB(OperationType aOp, int64_t aID, const nsACString& aOrigin,
+ const nsACString& aType, uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime);
+
+ /**
+ * This method removes all permissions modified after the specified time.
+ */
+ nsresult RemoveAllModifiedSince(int64_t aModificationTime);
+
+ template <class T>
+ nsresult RemovePermissionEntries(T aCondition);
+
+ template <class T>
+ nsresult GetPermissionEntries(T aCondition,
+ nsTArray<RefPtr<nsIPermission>>& aResult);
+
+ // This method must be called before doing any operation to be sure that the
+ // DB reading has been completed. This method is also in charge to complete
+ // the migrations if needed.
+ void EnsureReadCompleted();
+
+ nsresult 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 = false,
+ const nsACString* aOriginString = nullptr,
+ const bool aAllowPersistInPrivateBrowsing = false);
+
+ void MaybeAddReadEntryFromMigration(const nsACString& aOrigin,
+ const nsCString& aType,
+ uint32_t aPermission,
+ uint32_t aExpireType, int64_t aExpireTime,
+ int64_t aModificationTime, int64_t aId);
+
+ nsCOMPtr<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() const;
+
+ void MaybeCompleteShutdown();
+
+ nsRefPtrHashtable<nsCStringHashKey, GenericNonExclusivePromise::Private>
+ mPermissionKeyPromiseMap;
+
+ nsCOMPtr<nsIFile> mPermissionsFile;
+
+ // This monitor is used to ensure the database reading before any other
+ // operation. The reading of the database happens OMT. See |State| to know the
+ // steps of the database reading.
+ Monitor mMonitor MOZ_UNANNOTATED;
+
+ enum State {
+ // Initial state. The database has not been read yet.
+ // |TryInitDB| is called at startup time to read the database OMT.
+ // During the reading, |mReadEntries| will be populated with all the
+ // existing permissions.
+ eInitializing,
+
+ // At the end of the database reading, we are in this state. A runnable is
+ // executed to call |EnsureReadCompleted| on the main thread.
+ // |EnsureReadCompleted| processes |mReadEntries| and goes to the next
+ // state.
+ eDBInitialized,
+
+ // The permissions are fully read and any pending operation can proceed.
+ eReady,
+
+ // The permission manager has been terminated. No extra database operations
+ // will be allowed.
+ eClosed,
+ };
+ Atomic<State> mState;
+
+ // A single entry, from the database.
+ struct ReadEntry {
+ ReadEntry()
+ : mId(0),
+ mPermission(0),
+ mExpireType(0),
+ mExpireTime(0),
+ mModificationTime(0) {}
+
+ nsCString mOrigin;
+ nsCString mType;
+ int64_t mId;
+ uint32_t mPermission;
+ uint32_t mExpireType;
+ int64_t mExpireTime;
+ int64_t mModificationTime;
+
+ // true if this entry is the result of a migration.
+ bool mFromMigration;
+ };
+
+ // List of entries read from the database. It will be populated OMT and
+ // consumed on the main-thread.
+ // This array is protected by the monitor.
+ nsTArray<ReadEntry> mReadEntries;
+
+ // A single entry, from the database.
+ struct MigrationEntry {
+ MigrationEntry()
+ : mId(0),
+ mPermission(0),
+ mExpireType(0),
+ mExpireTime(0),
+ mModificationTime(0),
+ mIsInBrowserElement(false) {}
+
+ nsCString mHost;
+ nsCString mType;
+ int64_t mId;
+ uint32_t mPermission;
+ uint32_t mExpireType;
+ int64_t mExpireTime;
+ int64_t mModificationTime;
+
+ // Legacy, for migration.
+ bool mIsInBrowserElement;
+ };
+
+ // List of entries read from the database. It will be populated OMT and
+ // consumed on the main-thread. The migration entries will be converted to
+ // ReadEntry in |CompleteMigrations|.
+ // This array is protected by the monitor.
+ nsTArray<MigrationEntry> mMigrationEntries;
+
+ // A single entry from the defaults URL.
+ struct DefaultEntry {
+ DefaultEntry() : mOp(eImportMatchTypeHost), mPermission(0) {}
+
+ enum Op {
+ eImportMatchTypeHost,
+ eImportMatchTypeOrigin,
+ };
+
+ Op mOp;
+
+ nsCString mHostOrOrigin;
+ nsCString mType;
+ uint32_t mPermission;
+ };
+
+ // List of entries read from the default settings.
+ // This array is protected by the monitor.
+ nsTArray<DefaultEntry> mDefaultEntries;
+
+ nsresult Read(const MonitorAutoLock& aProofOfLock);
+ void CompleteRead();
+
+ void CompleteMigrations();
+
+ bool mMemoryOnlyDB;
+
+ nsTHashtable<PermissionHashKey> mPermissionTable;
+ // a unique, monotonically increasing id used to identify each database entry
+ int64_t mLargestID;
+
+ nsCOMPtr<nsIPrefBranch> mDefaultPrefBranch;
+
+ // NOTE: Ensure this is the last member since it has a large inline buffer.
+ // An array to store the strings identifying the different types.
+ Vector<nsCString, 512> mTypeArray;
+
+ nsCOMPtr<nsIThread> mThread;
+
+ struct ThreadBoundData {
+ nsCOMPtr<mozIStorageConnection> mDBConn;
+
+ nsCOMPtr<mozIStorageStatement> mStmtInsert;
+ nsCOMPtr<mozIStorageStatement> mStmtDelete;
+ nsCOMPtr<mozIStorageStatement> mStmtUpdate;
+ };
+ ThreadBound<ThreadBoundData> mThreadBoundData;
+
+ friend class DeleteFromMozHostListener;
+ friend class CloseDatabaseListener;
+};
+
+// {4F6B5E00-0C36-11d5-A535-0010A401EB10}
+#define NS_PERMISSIONMANAGER_CID \
+ { \
+ 0x4f6b5e00, 0xc36, 0x11d5, { \
+ 0xa5, 0x35, 0x0, 0x10, 0xa4, 0x1, 0xeb, 0x10 \
+ } \
+ }
+
+} // namespace mozilla
+
+#endif // mozilla_PermissionManager_h
diff --git a/extensions/permissions/components.conf b/extensions/permissions/components.conf
new file mode 100644
index 0000000000..62f4f7ba1c
--- /dev/null
+++ b/extensions/permissions/components.conf
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'name': 'PermissionManager',
+ 'js_name': 'perms',
+ 'cid': '{4f6b5e00-0c36-11d5-a535-0010a401eb10}',
+ 'contract_ids': ['@mozilla.org/permissionmanager;1'],
+ 'interfaces': ['nsIPermissionManager'],
+ 'singleton': True,
+ 'type': 'nsIPermissionManager',
+ 'constructor': 'mozilla::PermissionManager::GetXPCOMSingleton',
+ 'headers': ['/extensions/permissions/PermissionManager.h'],
+ },
+ {
+ 'cid': '{07611dc6-bf4d-4d8a-a64b-f3a5904dddc7}',
+ 'contract_ids': ['@mozilla.org/permissiondelegatehandler;1'],
+ 'type': 'PermissionDelegateHandler',
+ 'headers': ['/extensions/permissions/PermissionDelegateHandler.h'],
+ },
+]
diff --git a/extensions/permissions/moz.build b/extensions/permissions/moz.build
new file mode 100644
index 0000000000..6cdac60926
--- /dev/null
+++ b/extensions/permissions/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ["test"]
+
+TESTING_JS_MODULES += [
+ "test/PermissionTestUtils.sys.mjs",
+]
+
+EXPORTS.mozilla += [
+ "Permission.h",
+ "PermissionDelegateHandler.h",
+ "PermissionDelegateIPCUtils.h",
+ "PermissionManager.h",
+]
+
+UNIFIED_SOURCES += [
+ "Permission.cpp",
+ "PermissionDelegateHandler.cpp",
+ "PermissionManager.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+LOCAL_INCLUDES += [
+ "/caps",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Permission Manager")
diff --git a/extensions/permissions/test/PermissionTestUtils.sys.mjs b/extensions/permissions/test/PermissionTestUtils.sys.mjs
new file mode 100644
index 0000000000..84601376ba
--- /dev/null
+++ b/extensions/permissions/test/PermissionTestUtils.sys.mjs
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Utility module for tests to access the PermissionManager
+ * with uri or origin string parameters.
+ */
+
+let pm = Services.perms;
+
+let secMan = Services.scriptSecurityManager;
+
+/**
+ * Convert origin string or uri to principal.
+ * If passed an nsIPrincipal it will be returned without conversion.
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject - Subject to convert to principal
+ * @returns {Ci.nsIPrincipal} Principal created from subject
+ */
+function convertToPrincipal(subject) {
+ if (subject instanceof Ci.nsIPrincipal) {
+ return subject;
+ }
+ if (typeof subject === "string") {
+ return secMan.createContentPrincipalFromOrigin(subject);
+ }
+ if (subject === null || subject instanceof Ci.nsIURI) {
+ return secMan.createContentPrincipal(subject, {});
+ }
+ throw new Error(
+ "subject parameter must be an nsIURI an origin string or a principal."
+ );
+}
+
+export let PermissionTestUtils = {
+ /**
+ * Add permission information for a given subject.
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ add(subject, ...args) {
+ return pm.addFromPrincipal(convertToPrincipal(subject), ...args);
+ },
+ /**
+ * Get all custom permissions for a given subject.
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ getAll(subject, ...args) {
+ return pm.getAllForPrincipal(convertToPrincipal(subject), ...args);
+ },
+ /**
+ * Remove permission information for a given subject and permission type
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ remove(subject, ...args) {
+ return pm.removeFromPrincipal(convertToPrincipal(subject), ...args);
+ },
+ /**
+ * Test whether a website has permission to perform the given action.
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ testPermission(subject, ...args) {
+ return pm.testPermissionFromPrincipal(convertToPrincipal(subject), ...args);
+ },
+ /**
+ * Test whether a website has permission to perform the given action.
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ testExactPermission(subject, ...args) {
+ return pm.testExactPermissionFromPrincipal(
+ convertToPrincipal(subject),
+ ...args
+ );
+ },
+ /**
+ * Get the permission object associated with the given subject and action.
+ * Subject can be a principal, uri or origin string.
+ *
+ * @see nsIPermissionManager for documentation
+ *
+ * @param {Ci.nsIPrincipal|Ci.nsIURI|string} subject
+ * @param {*} args
+ */
+ getPermissionObject(subject, type, exactHost = false) {
+ return pm.getPermissionObject(convertToPrincipal(subject), type, exactHost);
+ },
+};
diff --git a/extensions/permissions/test/browser.ini b/extensions/permissions/test/browser.ini
new file mode 100644
index 0000000000..b71b10dfaf
--- /dev/null
+++ b/extensions/permissions/test/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+
+[browser_permmgr_sync.js]
+# The browser_permmgr_sync test runs code
+# paths which would hit the debug only assertion in
+# PermissionManager::PermissionKey::CreateFromPrincipal.
+skip-if = debug
+[browser_permmgr_viewsrc.js]
diff --git a/extensions/permissions/test/browser_permmgr_sync.js b/extensions/permissions/test/browser_permmgr_sync.js
new file mode 100644
index 0000000000..619c29842c
--- /dev/null
+++ b/extensions/permissions/test/browser_permmgr_sync.js
@@ -0,0 +1,448 @@
+function addPerm(aOrigin, aName) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(aOrigin);
+ Services.perms.addFromPrincipal(
+ principal,
+ aName,
+ Services.perms.ALLOW_ACTION
+ );
+}
+
+add_task(async function () {
+ // Make sure that we get a new process for the tab which we create. This is
+ // important, because we want to assert information about the initial state
+ // of the local permissions cache.
+
+ addPerm("http://example.com", "perm1");
+ addPerm("http://foo.bar.example.com", "perm2");
+ addPerm("about:home", "perm3");
+ addPerm("https://example.com", "perm4");
+ // NOTE: This permission is a preload permission, so it should be available in
+ // the content process from startup.
+ addPerm("https://somerandomwebsite.com", "cookie");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank", forceNewProcess: true },
+ async function (aBrowser) {
+ await SpecialPowers.spawn(aBrowser, [], async function () {
+ // Before the load http URIs shouldn't have been sent down yet
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm1-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "perm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm2-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "perm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm3-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm4-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://somerandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "cookie-1"
+ );
+
+ let iframe = content.document.createElement("iframe");
+
+ // Perform a load of example.com
+ await new Promise(resolve => {
+ iframe.setAttribute("src", "http://example.com");
+ iframe.onload = resolve;
+ content.document.body.appendChild(iframe);
+ });
+
+ // After the load finishes, the iframe process should know about example.com, but not foo.bar.example.com
+ await content.SpecialPowers.spawn(iframe, [], async function () {
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm1-2"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "perm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm2-2"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "perm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm3-2"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm4-2"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://somerandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "cookie-2"
+ );
+ });
+
+ // In Fission only, the parent process should have no knowledge about the child
+ // process permissions
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "perm1-3"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "perm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm2-3"
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm4-3"
+ );
+ });
+
+ addPerm("http://example.com", "newperm1");
+ addPerm("http://foo.bar.example.com", "newperm2");
+ addPerm("about:home", "newperm3");
+ addPerm("https://example.com", "newperm4");
+ addPerm("https://someotherrandomwebsite.com", "cookie");
+
+ await SpecialPowers.spawn(aBrowser, [], async function () {
+ // The new permissions should be available, but only for
+ // http://example.com (without Fission), and about:home.
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "perm1-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "newperm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "newperm1-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "perm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm2-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "newperm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "newperm2-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "perm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm3-3"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "newperm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "newperm3-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm4-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "newperm4"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "newperm4-1"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://somerandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "cookie-3"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://someotherrandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "othercookie-3"
+ );
+
+ let iframe = content.document.createElement("iframe");
+ // Loading a subdomain now, on https
+ await new Promise(resolve => {
+ iframe.setAttribute("src", "https://sub1.test1.example.com");
+ iframe.onload = resolve;
+ content.document.body.appendChild(iframe);
+ });
+
+ // After the load finishes, the iframe process should not know about
+ // the permissions of its base domain.
+ await content.SpecialPowers.spawn(iframe, [], async function () {
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm4-5"
+ );
+
+ // In Fission not across schemes, though.
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "perm1-5"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "newperm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "newperm1-2"
+ );
+ });
+
+ // The parent process should still have no permission under Fission.
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "perm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "perm1-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ ),
+ "newperm1"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "newperm1-3"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "perm4"
+ ),
+ SpecialPowers.useRemoteSubframes
+ ? Services.perms.UNKNOWN_ACTION
+ : Services.perms.ALLOW_ACTION,
+ "perm4-6"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "perm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "perm2-5"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://foo.bar.example.com"
+ ),
+ "newperm2"
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "newperm2-2"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "perm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "perm3-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "about:home"
+ ),
+ "newperm3"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "newperm3-2"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://somerandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "cookie-4"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://someotherrandomwebsite.com"
+ ),
+ "cookie"
+ ),
+ Services.perms.ALLOW_ACTION,
+ "othercookie-4"
+ );
+ });
+ }
+ );
+});
diff --git a/extensions/permissions/test/browser_permmgr_viewsrc.js b/extensions/permissions/test/browser_permmgr_viewsrc.js
new file mode 100644
index 0000000000..64dcb0413c
--- /dev/null
+++ b/extensions/permissions/test/browser_permmgr_viewsrc.js
@@ -0,0 +1,28 @@
+add_task(async function () {
+ // Add a permission for example.com, start a new content process, and make
+ // sure that the permission has been sent down.
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "viewsourceTestingPerm",
+ Services.perms.ALLOW_ACTION
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "view-source:https://example.com",
+ /* waitForLoad */ true,
+ /* waitForStateStop */ false,
+ /* forceNewProcess */ true
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [principal], async function (p) {
+ is(
+ Services.perms.testPermissionFromPrincipal(p, "viewsourceTestingPerm"),
+ Services.perms.ALLOW_ACTION
+ );
+ });
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/extensions/permissions/test/gtest/PermissionManagerTest.cpp b/extensions/permissions/test/gtest/PermissionManagerTest.cpp
new file mode 100644
index 0000000000..b69a7d46f2
--- /dev/null
+++ b/extensions/permissions/test/gtest/PermissionManagerTest.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; 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 "nsNetUtil.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "gtest/gtest.h"
+#include "gtest/MozGTestBench.h"
+
+using namespace mozilla;
+
+class PermissionManagerTester : public ::testing::Test {
+ protected:
+ PermissionManagerTester()
+ : mNonExistentType("permissionTypeThatIsGuaranteedToNeverExist"_ns) {}
+ void SetUp() override {
+ mPermissionManager = PermissionManager::GetInstance();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv =
+ NS_NewURI(getter_AddRefs(uri),
+ "https://test.origin.with.subdomains.example.com"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ mPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(uri, OriginAttributes());
+ }
+
+ void TearDown() override {
+ mPermissionManager = nullptr;
+ mPrincipal = nullptr;
+ }
+
+ static const unsigned kNumIterations = 100000;
+
+ nsLiteralCString mNonExistentType;
+ RefPtr<PermissionManager> mPermissionManager;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+};
+
+MOZ_GTEST_BENCH_F(PermissionManagerTester,
+ TestNonExistentPermissionFromPrincipal, [this] {
+ for (unsigned i = 0; i < kNumIterations; ++i) {
+ uint32_t result = 0;
+ Unused << mPermissionManager->TestPermissionFromPrincipal(
+ mPrincipal, mNonExistentType, &result);
+ }
+ });
diff --git a/extensions/permissions/test/gtest/moz.build b/extensions/permissions/test/gtest/moz.build
new file mode 100644
index 0000000000..132c384597
--- /dev/null
+++ b/extensions/permissions/test/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "PermissionManagerTest.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/extensions/permissions/test/moz.build b/extensions/permissions/test/moz.build
new file mode 100644
index 0000000000..5c68459575
--- /dev/null
+++ b/extensions/permissions/test/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += [
+ "gtest",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell.ini",
+]
+
+BROWSER_CHROME_MANIFESTS += ["browser.ini"]
diff --git a/extensions/permissions/test/unit/head.js b/extensions/permissions/test/unit/head.js
new file mode 100644
index 0000000000..b5b2518e22
--- /dev/null
+++ b/extensions/permissions/test/unit/head.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+// Helper to step a generator function and catch a StopIteration exception.
+function do_run_generator(generator) {
+ try {
+ generator.next();
+ } catch (e) {
+ do_throw("caught exception " + e, Components.stack.caller);
+ }
+}
+
+// Helper to finish a generator function test.
+function do_finish_generator_test(generator) {
+ executeSoon(function () {
+ generator.return();
+ do_test_finished();
+ });
+}
+
+function do_count_array(all) {
+ return all.length;
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_cleardata.js b/extensions/permissions/test/unit/test_permmanager_cleardata.js
new file mode 100644
index 0000000000..2bd4d11319
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_cleardata.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var pm;
+
+// Create a principal based on the { origin, originAttributes }.
+function createPrincipal(aOrigin, aOriginAttributes) {
+ return Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(aOrigin),
+ aOriginAttributes
+ );
+}
+
+function getData(aPattern) {
+ return JSON.stringify(aPattern);
+}
+
+// Use aEntries to create principals, add permissions to them and check that they have them.
+// Then, it is removing origin attributes with the given aData and check if the permissions
+// of principals[i] matches the permission in aResults[i].
+function test(aEntries, aData, aResults) {
+ let principals = [];
+
+ for (const entry of aEntries) {
+ principals.push(createPrincipal(entry.origin, entry.originAttributes));
+ }
+
+ for (const principal of principals) {
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, "test/clear-origin"),
+ pm.UNKNOWN_ACTION
+ );
+ pm.addFromPrincipal(
+ principal,
+ "test/clear-origin",
+ pm.ALLOW_ACTION,
+ pm.EXPIRE_NEVER,
+ 0
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, "test/clear-origin"),
+ pm.ALLOW_ACTION
+ );
+ }
+
+ // `clear-origin-attributes-data` notification is removed from permission
+ // manager
+ pm.removePermissionsWithAttributes(aData);
+
+ var length = aEntries.length;
+ for (let i = 0; i < length; ++i) {
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principals[i], "test/clear-origin"),
+ aResults[i]
+ );
+
+ // Remove allowed actions.
+ if (aResults[i] == pm.ALLOW_ACTION) {
+ pm.removeFromPrincipal(principals[i], "test/clear-origin");
+ }
+ }
+}
+
+function run_test() {
+ do_get_profile();
+
+ pm = Services.perms;
+
+ let entries = [
+ { origin: "http://example.com", originAttributes: {} },
+ {
+ origin: "http://example.com",
+ originAttributes: { inIsolatedMozBrowser: true },
+ },
+ ];
+
+ // In that case, all permissions should be removed.
+ test(entries, getData({}), [
+ pm.UNKNOWN_ACTION,
+ pm.UNKNOWN_ACTION,
+ pm.ALLOW_ACTION,
+ pm.ALLOW_ACTION,
+ ]);
+
+ // In that case, only the permissions related to a browserElement should be removed.
+ // All the other permissions should stay.
+ test(entries, getData({ inIsolatedMozBrowser: true }), [
+ pm.ALLOW_ACTION,
+ pm.UNKNOWN_ACTION,
+ pm.ALLOW_ACTION,
+ pm.ALLOW_ACTION,
+ ]);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_default_pref.js b/extensions/permissions/test/unit/test_permmanager_default_pref.js
new file mode 100644
index 0000000000..3759594bf5
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_default_pref.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.org"
+ );
+
+ // Check that without a pref the default return value is UNKNOWN.
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.UNKNOWN_ACTION
+ );
+
+ // Check that the default return value changed after setting the pref.
+ Services.prefs.setIntPref(
+ "permissions.default.camera",
+ Services.perms.DENY_ACTION
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.DENY_ACTION
+ );
+
+ // Check that functions that do not directly return a permission value still
+ // consider the permission as being set to its default.
+ Assert.equal(
+ null,
+ Services.perms.getPermissionObject(principal, "camera", false)
+ );
+
+ // Check that other permissions still return UNKNOWN.
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "geo"),
+ Services.perms.UNKNOWN_ACTION
+ );
+
+ // Check that the default return value changed after changing the pref.
+ Services.prefs.setIntPref(
+ "permissions.default.camera",
+ Services.perms.ALLOW_ACTION
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Check that the preference is ignored if there is a value.
+ Services.perms.addFromPrincipal(
+ principal,
+ "camera",
+ Services.perms.DENY_ACTION
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.DENY_ACTION
+ );
+ Assert.ok(
+ Services.perms.getPermissionObject(principal, "camera", false) != null
+ );
+
+ // The preference should be honored again, after resetting the permissions.
+ Services.perms.removeAll();
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Should be UNKNOWN after clearing the pref.
+ Services.prefs.clearUserPref("permissions.default.camera");
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(principal, "camera"),
+ Services.perms.UNKNOWN_ACTION
+ );
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_defaults.js b/extensions/permissions/test/unit/test_permmanager_defaults.js
new file mode 100644
index 0000000000..0f608e2151
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_defaults.js
@@ -0,0 +1,485 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The origin we use in most of the tests.
+const TEST_ORIGIN = NetUtil.newURI("http://example.org");
+const TEST_ORIGIN_HTTPS = NetUtil.newURI("https://example.org");
+const TEST_ORIGIN_2 = NetUtil.newURI("http://example.com");
+const TEST_ORIGIN_3 = NetUtil.newURI("https://example2.com:8080");
+const TEST_PERMISSION = "test-permission";
+
+function promiseTimeout(delay) {
+ return new Promise(resolve => {
+ do_timeout(delay, resolve);
+ });
+}
+
+add_task(async function do_test() {
+ // setup a profile.
+ do_get_profile();
+
+ // create a file in the temp directory with the defaults.
+ let file = do_get_tempdir();
+ file.append("test_default_permissions");
+
+ // write our test data to it.
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(file, -1, 0o666, 0);
+ let conv = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(
+ Ci.nsIConverterOutputStream
+ );
+ conv.init(ostream, "UTF-8");
+
+ conv.writeString("# this is a comment\n");
+ conv.writeString("\n"); // a blank line!
+ conv.writeString(
+ "host\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN.host + "\n"
+ );
+ conv.writeString(
+ "host\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN_2.host + "\n"
+ );
+ conv.writeString(
+ "origin\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN_3.spec + "\n"
+ );
+ conv.writeString(
+ "origin\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN.spec + "^inBrowser=1\n"
+ );
+ ostream.close();
+
+ // Set the preference used by the permission manager so the file is read.
+ Services.prefs.setCharPref(
+ "permissions.manager.defaultsUrl",
+ "file://" + file.path
+ );
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ let permIsolateUserContext = Services.prefs.getBoolPref(
+ "permissions.isolateBy.userContext"
+ );
+ let permIsolatePrivateBrowsing = Services.prefs.getBoolPref(
+ "permissions.isolateBy.privateBrowsing"
+ );
+
+ let pm = Services.perms;
+
+ // test the default permission was applied.
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN,
+ {}
+ );
+ let principalHttps = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN_HTTPS,
+ {}
+ );
+ let principal2 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN_2,
+ {}
+ );
+ let principal3 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN_3,
+ {}
+ );
+
+ let attrs = { inIsolatedMozBrowser: true };
+ let principal4 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN,
+ attrs
+ );
+ let principal5 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN_3,
+ attrs
+ );
+
+ attrs = { userContextId: 1 };
+ let principal1UserContext =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_ORIGIN, attrs);
+ attrs = { privateBrowsingId: 1 };
+ let principal1PrivateBrowsing =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_ORIGIN, attrs);
+ attrs = { firstPartyDomain: "cnn.com" };
+ let principal7 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN,
+ attrs
+ );
+ attrs = { userContextId: 1, firstPartyDomain: "cnn.com" };
+ let principal8 = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_ORIGIN,
+ attrs
+ );
+
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principalHttps, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal3, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal4, TEST_PERMISSION)
+ );
+ // Depending on the prefs there are two scenarios here:
+ // 1. We isolate by private browsing: The permission mgr should
+ // add default permissions for these principals too.
+ // 2. We don't isolate by private browsing: The permission
+ // check will strip the private browsing origin attribute.
+ // In this case the used internally for the lookup is always principal1.
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+
+ // Didn't add
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal5, TEST_PERMISSION)
+ );
+
+ // the permission should exist in the enumerator.
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ findCapabilityViaEnum(TEST_ORIGIN)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ findCapabilityViaEnum(TEST_ORIGIN_3)
+ );
+
+ // but should not have been written to the DB
+ await checkCapabilityViaDB(null);
+
+ // remove all should not throw and the default should remain
+ pm.removeAll();
+
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal3, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal4, TEST_PERMISSION)
+ );
+ // Default permission should have also been added for private browsing.
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+ // make sure principals with userContextId use the same / different permissions
+ // depending on pref state
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal1UserContext, TEST_PERMISSION)
+ );
+ // make sure principals with a firstPartyDomain use different permissions
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION)
+ );
+
+ // Asking for this permission to be removed should result in that permission
+ // having UNKNOWN_ACTION
+ pm.removeFromPrincipal(principal, TEST_PERMISSION);
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ // make sure principals with userContextId use the correct permissions
+ // (Should be unknown with and without OA stripping )
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal1UserContext, TEST_PERMISSION)
+ );
+ // If we isolate by private browsing, the permission should still be present
+ // for the private browsing principal.
+ Assert.equal(
+ permIsolatePrivateBrowsing
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+ // and we should have this UNKNOWN_ACTION reflected in the DB
+ await checkCapabilityViaDB(Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ // but the permission should *not* appear in the enumerator.
+ Assert.equal(null, findCapabilityViaEnum());
+
+ // and a subsequent RemoveAll should restore the default
+ pm.removeAll();
+
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ // Make sure default imports work for private browsing after removeAll.
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+ // make sure principals with userContextId share permissions depending on pref state
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal1UserContext, TEST_PERMISSION)
+ );
+ // make sure principals with firstPartyDomain use different permissions
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION)
+ );
+ // and allow it to again be seen in the enumerator.
+ Assert.equal(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum());
+
+ // now explicitly add a permission - this too should override the default.
+ pm.addFromPrincipal(
+ principal,
+ TEST_PERMISSION,
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+
+ // it should be reflected in a permission check, in the enumerator and the DB
+ Assert.equal(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ // make sure principals with userContextId use the same / different permissions
+ // depending on pref state
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal1UserContext, TEST_PERMISSION)
+ );
+ // If we isolate by private browsing, we should still have the default perm
+ // for the private browsing principal.
+ Assert.equal(
+ permIsolatePrivateBrowsing
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+ // make sure principals with firstPartyDomain use different permissions
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION)
+ );
+ Assert.equal(Ci.nsIPermissionManager.DENY_ACTION, findCapabilityViaEnum());
+ await checkCapabilityViaDB(Ci.nsIPermissionManager.DENY_ACTION);
+
+ // explicitly add a different permission - in this case we are no longer
+ // replacing the default, but instead replacing the replacement!
+ pm.addFromPrincipal(
+ principal,
+ TEST_PERMISSION,
+ Ci.nsIPermissionManager.PROMPT_ACTION
+ );
+
+ // it should be reflected in a permission check, in the enumerator and the DB
+ Assert.equal(
+ Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ // make sure principals with userContextId use the same / different permissions
+ // depending on pref state
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principal1UserContext, TEST_PERMISSION)
+ );
+ // If we isolate by private browsing, we should still have the default perm
+ // for the private browsing principal.
+ Assert.equal(
+ permIsolatePrivateBrowsing
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principal1PrivateBrowsing, TEST_PERMISSION)
+ );
+ // make sure principals with firstPartyDomain use different permissions
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal7, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(principal8, TEST_PERMISSION)
+ );
+ Assert.equal(Ci.nsIPermissionManager.PROMPT_ACTION, findCapabilityViaEnum());
+ await checkCapabilityViaDB(Ci.nsIPermissionManager.PROMPT_ACTION);
+
+ // --------------------------------------------------------------
+ // check default permissions and removeAllSince work as expected.
+ pm.removeAll(); // ensure only defaults are there.
+
+ // default for both principals is allow.
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION)
+ );
+
+ // Add a default override for TEST_ORIGIN_2 - this one should *not* be
+ // restored in removeAllSince()
+ pm.addFromPrincipal(
+ principal2,
+ TEST_PERMISSION,
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION)
+ );
+ await promiseTimeout(20);
+
+ let since = Number(Date.now());
+ await promiseTimeout(20);
+
+ // explicitly add a permission which overrides the default for the first
+ // principal - this one *should* be removed by removeAllSince.
+ pm.addFromPrincipal(
+ principal,
+ TEST_PERMISSION,
+ Ci.nsIPermissionManager.DENY_ACTION
+ );
+ Assert.equal(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+
+ // do a removeAllSince.
+ pm.removeAllSince(since);
+
+ // the default for the first principal should re-appear as we modified it
+ // later then |since|
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+
+ // but the permission for principal2 should remain as we added that before |since|.
+ Assert.equal(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION)
+ );
+
+ // remove the temp file we created.
+ file.remove(false);
+});
+
+// use an enumerator to find the requested permission. Returns the permission
+// value (ie, the "capability" in nsIPermission parlance) or null if it can't
+// be found.
+function findCapabilityViaEnum(origin = TEST_ORIGIN, type = TEST_PERMISSION) {
+ let result = undefined;
+ for (let perm of Services.perms.all) {
+ if (perm.matchesURI(origin, true) && perm.type == type) {
+ if (result !== undefined) {
+ // we've already found one previously - that's bad!
+ do_throw("enumerator found multiple entries");
+ }
+ result = perm.capability;
+ }
+ }
+ return result || null;
+}
+
+// A function to check the DB has the specified capability. As the permission
+// manager uses async DB operations without a completion callback, the
+// distinct possibility exists that our checking of the DB will happen before
+// the permission manager update has completed - so we just retry a few times.
+// Returns a promise.
+function checkCapabilityViaDB(
+ expected,
+ origin = TEST_ORIGIN,
+ type = TEST_PERMISSION
+) {
+ return new Promise(resolve => {
+ let count = 0;
+ let max = 20;
+ let do_check = () => {
+ let got = findCapabilityViaDB(origin, type);
+ if (got == expected) {
+ // the do_check_eq() below will succeed - which is what we want.
+ Assert.equal(got, expected, "The database has the expected value");
+ resolve();
+ return;
+ }
+ // value isn't correct - see if we've retried enough
+ if (count++ == max) {
+ // the do_check_eq() below will fail - which is what we want.
+ Assert.equal(
+ got,
+ expected,
+ "The database wasn't updated with the expected value"
+ );
+ resolve();
+ return;
+ }
+ // we can retry...
+ do_timeout(100, do_check);
+ };
+ do_check();
+ });
+}
+
+// use the DB to find the requested permission. Returns the permission
+// value (ie, the "capability" in nsIPermission parlance) or null if it can't
+// be found.
+function findCapabilityViaDB(origin = TEST_ORIGIN, type = TEST_PERMISSION) {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ origin,
+ {}
+ );
+ let originStr = principal.origin;
+
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("permissions.sqlite");
+
+ let connection = Services.storage.openDatabase(file);
+
+ let query = connection.createStatement(
+ "SELECT permission FROM moz_perms WHERE origin = :origin AND type = :type"
+ );
+ query.bindByName("origin", originStr);
+ query.bindByName("type", type);
+
+ if (!query.executeStep()) {
+ // no row
+ return null;
+ }
+ let result = query.getInt32(0);
+ if (query.executeStep()) {
+ // this is bad - we never expect more than 1 row here.
+ do_throw("More than 1 row found!");
+ }
+ return result;
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_expiration.js b/extensions/permissions/test/unit/test_permmanager_expiration.js
new file mode 100644
index 0000000000..24ff366730
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_expiration.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that permissions with specific expiry times behave as expected.
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function continue_test() {
+ do_run_generator(test_generator);
+}
+
+function* do_run_test() {
+ let pm = Services.perms;
+ let permURI = NetUtil.newURI("http://example.com");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ permURI,
+ {}
+ );
+
+ let now = Number(Date.now());
+
+ // add a permission with *now* expiration
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-perm-exp",
+ 1,
+ pm.EXPIRE_TIME,
+ now
+ );
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-session-exp",
+ 1,
+ pm.EXPIRE_SESSION,
+ now
+ );
+
+ // add a permission with future expiration (100 milliseconds)
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-perm-exp2",
+ 1,
+ pm.EXPIRE_TIME,
+ now + 100
+ );
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-session-exp2",
+ 1,
+ pm.EXPIRE_SESSION,
+ now + 100
+ );
+
+ // add a permission with future expiration (1000 seconds)
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-perm-exp3",
+ 1,
+ pm.EXPIRE_TIME,
+ now + 1e6
+ );
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-session-exp3",
+ 1,
+ pm.EXPIRE_SESSION,
+ now + 1e6
+ );
+
+ // add a permission without expiration
+ pm.addFromPrincipal(
+ principal,
+ "test/expiration-perm-nexp",
+ 1,
+ pm.EXPIRE_NEVER,
+ 0
+ );
+
+ // check that the second two haven't expired yet
+ Assert.equal(
+ 1,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-perm-exp3")
+ );
+ Assert.equal(
+ 1,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-session-exp3")
+ );
+ Assert.equal(
+ 1,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-perm-nexp")
+ );
+ Assert.equal(1, pm.getAllWithTypePrefix("test/expiration-perm-exp3").length);
+ Assert.equal(
+ 1,
+ pm.getAllWithTypePrefix("test/expiration-session-exp3").length
+ );
+ Assert.equal(1, pm.getAllWithTypePrefix("test/expiration-perm-nexp").length);
+ Assert.equal(5, pm.getAllForPrincipal(principal).length);
+
+ // ... and the first one has
+ do_timeout(10, continue_test);
+ yield;
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-perm-exp")
+ );
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-session-exp")
+ );
+
+ // ... and that the short-term one will
+ do_timeout(200, continue_test);
+ yield;
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-perm-exp2")
+ );
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal, "test/expiration-session-exp2")
+ );
+ Assert.equal(0, pm.getAllWithTypePrefix("test/expiration-perm-exp2").length);
+ Assert.equal(
+ 0,
+ pm.getAllWithTypePrefix("test/expiration-session-exp2").length
+ );
+
+ Assert.equal(3, pm.getAllForPrincipal(principal).length);
+
+ // Check that .getPermission returns a matching result
+ Assert.equal(
+ null,
+ pm.getPermissionObject(principal, "test/expiration-perm-exp", false)
+ );
+ Assert.equal(
+ null,
+ pm.getPermissionObject(principal, "test/expiration-session-exp", false)
+ );
+ Assert.equal(
+ null,
+ pm.getPermissionObject(principal, "test/expiration-perm-exp2", false)
+ );
+ Assert.equal(
+ null,
+ pm.getPermissionObject(principal, "test/expiration-session-exp2", false)
+ );
+
+ // Add a persistent permission for private browsing
+ let principalPB = Services.scriptSecurityManager.createContentPrincipal(
+ permURI,
+ { privateBrowsingId: 1 }
+ );
+ pm.addFromPrincipal(
+ principalPB,
+ "test/expiration-session-pb",
+ pm.ALLOW_ACTION
+ );
+
+ // The permission should be set to session expiry
+ let perm = pm.getPermissionObject(
+ principalPB,
+ "test/expiration-session-pb",
+ true
+ );
+ Assert.equal(perm.expireType, pm.EXPIRE_SESSION);
+
+ // Add a persistent permission for private browsing using
+ // addFromPrincipalAndPersistInPrivateBrowsing
+ pm.addFromPrincipalAndPersistInPrivateBrowsing(
+ principalPB,
+ "test/expiration-session-pb",
+ pm.ALLOW_ACTION
+ );
+
+ // The permission should be set to never expire
+ perm = pm.getPermissionObject(
+ principalPB,
+ "test/expiration-session-pb",
+ true
+ );
+ Assert.equal(perm.expireType, pm.EXPIRE_NEVER);
+
+ do_finish_generator_test(test_generator);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_getAllByTypeSince.js b/extensions/permissions/test/unit/test_permmanager_getAllByTypeSince.js
new file mode 100644
index 0000000000..918f184673
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_getAllByTypeSince.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(prefix, from, permissions) {
+ let pm = Services.perms;
+
+ let array = pm.getAllByTypeSince(prefix, from);
+ Assert.equal(array.length, permissions.length);
+ for (let [principal, type, capability] of permissions) {
+ let perm = array.find(p => p.principal.equals(principal));
+ Assert.ok(perm != null);
+ Assert.equal(perm.type, type);
+ Assert.equal(perm.capability, capability);
+ Assert.equal(perm.expireType, pm.EXPIRE_NEVER);
+ }
+}
+
+add_task(async function test() {
+ let pm = Services.perms;
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+ let subPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://sub.example.com"
+ );
+
+ check_enumerator("test/", 0, []);
+
+ pm.addFromPrincipal(principal, "test/getAllByTypeSince", pm.ALLOW_ACTION);
+
+ // These shouldn't show up anywhere, the name doesn't match.
+ pm.addFromPrincipal(
+ subPrincipal,
+ "other-test/getAllByTypeSince",
+ pm.PROMPT_ACTION
+ );
+ pm.addFromPrincipal(principal, "test/getAllByTypeSince1", pm.PROMPT_ACTION);
+
+ check_enumerator("test/getAllByTypeSince", 0, [
+ [principal, "test/getAllByTypeSince", pm.ALLOW_ACTION],
+ ]);
+
+ // Add some time in between taking the snapshot of the timestamp
+ // to avoid flakyness.
+ await new Promise(c => do_timeout(100, c));
+ let timestamp = Date.now();
+ await new Promise(c => do_timeout(100, c));
+
+ pm.addFromPrincipal(subPrincipal, "test/getAllByTypeSince", pm.DENY_ACTION);
+
+ check_enumerator("test/getAllByTypeSince", 0, [
+ [subPrincipal, "test/getAllByTypeSince", pm.DENY_ACTION],
+ [principal, "test/getAllByTypeSince", pm.ALLOW_ACTION],
+ ]);
+
+ check_enumerator("test/getAllByTypeSince", timestamp, [
+ [subPrincipal, "test/getAllByTypeSince", pm.DENY_ACTION],
+ ]);
+
+ // check that UNKNOWN_ACTION permissions are ignored
+ pm.addFromPrincipal(
+ subPrincipal,
+ "test/getAllByTypeSince",
+ pm.UNKNOWN_ACTION
+ );
+
+ check_enumerator("test/getAllByTypeSince", 0, [
+ [principal, "test/getAllByTypeSince", pm.ALLOW_ACTION],
+ ]);
+
+ // check that permission removals are reflected
+ pm.removeFromPrincipal(principal, "test/getAllByTypeSince");
+ check_enumerator("test/", 0, []);
+
+ pm.removeAll();
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_getAllByTypes.js b/extensions/permissions/test/unit/test_permmanager_getAllByTypes.js
new file mode 100644
index 0000000000..ab40c4b12a
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_getAllByTypes.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(permissionTypes, expectedPermissions) {
+ const permissions = Services.perms.getAllByTypes(permissionTypes);
+
+ Assert.equal(
+ permissions.length,
+ expectedPermissions.length,
+ `getAllByTypes returned the expected number of permissions for ${JSON.stringify(
+ permissionTypes
+ )}`
+ );
+
+ for (const perm of permissions) {
+ Assert.ok(perm != null);
+
+ // For some reason, the order in which we get the permissions doesn't seem to be
+ // stable when running the test with --verify. As a result, we need to retrieve the
+ // expected permission for the origin and type.
+ const expectedPermission = expectedPermissions.find(
+ ([expectedPrincipal, expectedType]) =>
+ perm.principal.equals(expectedPrincipal) && perm.type === expectedType
+ );
+
+ Assert.ok(expectedPermission !== null, "Found the expected permission");
+ Assert.equal(perm.capability, expectedPermission[2]);
+ Assert.equal(perm.expireType, Services.perms.EXPIRE_NEVER);
+ }
+}
+
+function run_test() {
+ let pm = Services.perms;
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+ let subPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://sub.example.com"
+ );
+
+ const PERM_TYPE_1 = "test/getallbytypes_1";
+ const PERM_TYPE_2 = "test/getallbytypes_2";
+
+ info("check default state");
+ check_enumerator([], []);
+ check_enumerator([PERM_TYPE_1], []);
+ check_enumerator([PERM_TYPE_2], []);
+ check_enumerator([PERM_TYPE_1, PERM_TYPE_2], []);
+
+ info("check that expected permissions are retrieved");
+ pm.addFromPrincipal(principal, PERM_TYPE_1, pm.ALLOW_ACTION);
+ pm.addFromPrincipal(
+ subPrincipal,
+ "other-test/getallbytypes_1",
+ pm.PROMPT_ACTION
+ );
+ check_enumerator([PERM_TYPE_1], [[principal, PERM_TYPE_1, pm.ALLOW_ACTION]]);
+ check_enumerator(
+ [PERM_TYPE_1, PERM_TYPE_2],
+ [[principal, PERM_TYPE_1, pm.ALLOW_ACTION]]
+ );
+ check_enumerator([], []);
+ check_enumerator([PERM_TYPE_2], []);
+
+ pm.addFromPrincipal(subPrincipal, PERM_TYPE_1, pm.PROMPT_ACTION);
+ check_enumerator(
+ [PERM_TYPE_1],
+ [
+ [subPrincipal, PERM_TYPE_1, pm.PROMPT_ACTION],
+ [principal, PERM_TYPE_1, pm.ALLOW_ACTION],
+ ]
+ );
+ check_enumerator(
+ [PERM_TYPE_1, PERM_TYPE_2],
+ [
+ [subPrincipal, PERM_TYPE_1, pm.PROMPT_ACTION],
+ [principal, PERM_TYPE_1, pm.ALLOW_ACTION],
+ ]
+ );
+ check_enumerator([], []);
+ check_enumerator([PERM_TYPE_2], []);
+
+ pm.addFromPrincipal(principal, PERM_TYPE_2, pm.PROMPT_ACTION);
+ check_enumerator(
+ [PERM_TYPE_1, PERM_TYPE_2],
+ [
+ [subPrincipal, PERM_TYPE_1, pm.PROMPT_ACTION],
+ [principal, PERM_TYPE_1, pm.ALLOW_ACTION],
+ [principal, PERM_TYPE_2, pm.PROMPT_ACTION],
+ ]
+ );
+ check_enumerator([], []);
+ check_enumerator([PERM_TYPE_2], [[principal, PERM_TYPE_2, pm.PROMPT_ACTION]]);
+
+ info("check that UNKNOWN_ACTION permissions are ignored");
+ pm.addFromPrincipal(subPrincipal, PERM_TYPE_2, pm.UNKNOWN_ACTION);
+ check_enumerator([PERM_TYPE_2], [[principal, PERM_TYPE_2, pm.PROMPT_ACTION]]);
+
+ info("check that permission updates are reflected");
+ pm.addFromPrincipal(subPrincipal, PERM_TYPE_2, pm.PROMPT_ACTION);
+ check_enumerator(
+ [PERM_TYPE_2],
+ [
+ [subPrincipal, PERM_TYPE_2, pm.PROMPT_ACTION],
+ [principal, PERM_TYPE_2, pm.PROMPT_ACTION],
+ ]
+ );
+
+ info("check that permission removals are reflected");
+ pm.removeFromPrincipal(principal, PERM_TYPE_1);
+ check_enumerator(
+ [PERM_TYPE_1],
+ [[subPrincipal, PERM_TYPE_1, pm.PROMPT_ACTION]]
+ );
+
+ pm.removeAll();
+ check_enumerator([], []);
+ check_enumerator([PERM_TYPE_1], []);
+ check_enumerator([PERM_TYPE_2], []);
+ check_enumerator([PERM_TYPE_1, PERM_TYPE_2], []);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js b/extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js
new file mode 100644
index 0000000000..ad9db37c91
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(principal, permissions) {
+ let perms = Services.perms.getAllForPrincipal(principal);
+ for (let [type, capability] of permissions) {
+ let perm = perms.shift();
+ Assert.ok(perm != null);
+ Assert.equal(perm.type, type);
+ Assert.equal(perm.capability, capability);
+ Assert.equal(perm.expireType, Services.perms.EXPIRE_NEVER);
+ }
+ Assert.ok(!perms.length);
+}
+
+function run_test() {
+ let pm = Services.perms;
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+ let subPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://sub.example.com"
+ );
+
+ check_enumerator(principal, []);
+
+ pm.addFromPrincipal(principal, "test/getallforuri", pm.ALLOW_ACTION);
+ check_enumerator(principal, [["test/getallforuri", pm.ALLOW_ACTION]]);
+
+ // check that uris are matched exactly
+ check_enumerator(subPrincipal, []);
+
+ pm.addFromPrincipal(subPrincipal, "test/getallforuri", pm.PROMPT_ACTION);
+ pm.addFromPrincipal(subPrincipal, "test/getallforuri2", pm.DENY_ACTION);
+
+ check_enumerator(subPrincipal, [
+ ["test/getallforuri", pm.PROMPT_ACTION],
+ ["test/getallforuri2", pm.DENY_ACTION],
+ ]);
+
+ // check that the original uri list has not changed
+ check_enumerator(principal, [["test/getallforuri", pm.ALLOW_ACTION]]);
+
+ // check that UNKNOWN_ACTION permissions are ignored
+ pm.addFromPrincipal(principal, "test/getallforuri2", pm.UNKNOWN_ACTION);
+ pm.addFromPrincipal(principal, "test/getallforuri3", pm.DENY_ACTION);
+
+ check_enumerator(principal, [
+ ["test/getallforuri", pm.ALLOW_ACTION],
+ ["test/getallforuri3", pm.DENY_ACTION],
+ ]);
+
+ // check that permission updates are reflected
+ pm.addFromPrincipal(principal, "test/getallforuri", pm.PROMPT_ACTION);
+
+ check_enumerator(principal, [
+ ["test/getallforuri", pm.PROMPT_ACTION],
+ ["test/getallforuri3", pm.DENY_ACTION],
+ ]);
+
+ // check that permission removals are reflected
+ pm.removeFromPrincipal(principal, "test/getallforuri");
+
+ check_enumerator(principal, [["test/getallforuri3", pm.DENY_ACTION]]);
+
+ pm.removeAll();
+ check_enumerator(principal, []);
+ check_enumerator(subPrincipal, []);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_getAllWithTypePrefix.js b/extensions/permissions/test/unit/test_permmanager_getAllWithTypePrefix.js
new file mode 100644
index 0000000000..e3708cf445
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_getAllWithTypePrefix.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(prefix, permissions) {
+ let pm = Services.perms;
+
+ let array = pm.getAllWithTypePrefix(prefix);
+ for (let [principal, type, capability] of permissions) {
+ let perm = array.shift();
+ Assert.ok(perm != null);
+ Assert.ok(perm.principal.equals(principal));
+ Assert.equal(perm.type, type);
+ Assert.equal(perm.capability, capability);
+ Assert.equal(perm.expireType, pm.EXPIRE_NEVER);
+ }
+ Assert.equal(array.length, 0);
+}
+
+function run_test() {
+ let pm = Services.perms;
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+ let subPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://sub.example.com"
+ );
+
+ check_enumerator("test/", []);
+
+ pm.addFromPrincipal(principal, "test/getallwithtypeprefix", pm.ALLOW_ACTION);
+ pm.addFromPrincipal(
+ subPrincipal,
+ "other-test/getallwithtypeprefix",
+ pm.PROMPT_ACTION
+ );
+ check_enumerator("test/", [
+ [principal, "test/getallwithtypeprefix", pm.ALLOW_ACTION],
+ ]);
+
+ pm.addFromPrincipal(
+ subPrincipal,
+ "test/getallwithtypeprefix",
+ pm.PROMPT_ACTION
+ );
+ check_enumerator("test/", [
+ [subPrincipal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ [principal, "test/getallwithtypeprefix", pm.ALLOW_ACTION],
+ ]);
+
+ check_enumerator("test/getallwithtypeprefix", [
+ [subPrincipal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ [principal, "test/getallwithtypeprefix", pm.ALLOW_ACTION],
+ ]);
+
+ // check that UNKNOWN_ACTION permissions are ignored
+ pm.addFromPrincipal(
+ principal,
+ "test/getallwithtypeprefix2",
+ pm.UNKNOWN_ACTION
+ );
+ check_enumerator("test/", [
+ [subPrincipal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ [principal, "test/getallwithtypeprefix", pm.ALLOW_ACTION],
+ ]);
+
+ // check that permission updates are reflected
+ pm.addFromPrincipal(principal, "test/getallwithtypeprefix", pm.PROMPT_ACTION);
+ check_enumerator("test/", [
+ [subPrincipal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ [principal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ ]);
+
+ // check that permission removals are reflected
+ pm.removeFromPrincipal(principal, "test/getallwithtypeprefix");
+ check_enumerator("test/", [
+ [subPrincipal, "test/getallwithtypeprefix", pm.PROMPT_ACTION],
+ ]);
+
+ pm.removeAll();
+ check_enumerator("test/", []);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_getPermissionObject.js b/extensions/permissions/test/unit/test_permmanager_getPermissionObject.js
new file mode 100644
index 0000000000..78ef9ab08a
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_getPermissionObject.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getPrincipalFromURI(aURI) {
+ let ssm = Services.scriptSecurityManager;
+ let uri = NetUtil.newURI(aURI);
+ return ssm.createContentPrincipal(uri, {});
+}
+
+function getSystemPrincipal() {
+ return Services.scriptSecurityManager.getSystemPrincipal();
+}
+
+function run_test() {
+ var pm = Services.perms;
+
+ Assert.equal(
+ null,
+ pm.getPermissionObject(getSystemPrincipal(), "test/pobject", false)
+ );
+
+ let principal = getPrincipalFromURI("http://example.com");
+ let subPrincipal = getPrincipalFromURI("http://sub.example.com");
+ let subSubPrincipal = getPrincipalFromURI("http://sub.sub.example.com");
+
+ Assert.equal(null, pm.getPermissionObject(principal, "test/pobject", false));
+ Assert.equal(null, pm.getPermissionObject(principal, "test/pobject", true));
+
+ pm.addFromPrincipal(principal, "test/pobject", pm.ALLOW_ACTION);
+ var rootPerm = pm.getPermissionObject(principal, "test/pobject", false);
+ Assert.ok(rootPerm != null);
+ Assert.equal(rootPerm.principal.origin, "http://example.com");
+ Assert.equal(rootPerm.type, "test/pobject");
+ Assert.equal(rootPerm.capability, pm.ALLOW_ACTION);
+ Assert.equal(rootPerm.expireType, pm.EXPIRE_NEVER);
+
+ Assert.ok(rootPerm != null);
+ Assert.equal(rootPerm.principal.origin, "http://example.com");
+
+ var subPerm = pm.getPermissionObject(subPrincipal, "test/pobject", true);
+ Assert.equal(null, subPerm);
+ subPerm = pm.getPermissionObject(subPrincipal, "test/pobject", false);
+ Assert.ok(subPerm != null);
+ Assert.equal(subPerm.principal.origin, "http://example.com");
+ Assert.equal(subPerm.type, "test/pobject");
+ Assert.equal(subPerm.capability, pm.ALLOW_ACTION);
+
+ subPerm = pm.getPermissionObject(subSubPrincipal, "test/pobject", true);
+ Assert.equal(null, subPerm);
+ subPerm = pm.getPermissionObject(subSubPrincipal, "test/pobject", false);
+ Assert.ok(subPerm != null);
+ Assert.equal(subPerm.principal.origin, "http://example.com");
+
+ pm.addFromPrincipal(
+ principal,
+ "test/pobject",
+ pm.DENY_ACTION,
+ pm.EXPIRE_SESSION
+ );
+
+ // make sure permission objects are not dynamic
+ Assert.equal(rootPerm.capability, pm.ALLOW_ACTION);
+
+ // but do update on change
+ rootPerm = pm.getPermissionObject(principal, "test/pobject", true);
+ Assert.equal(rootPerm.capability, pm.DENY_ACTION);
+ Assert.equal(rootPerm.expireType, pm.EXPIRE_SESSION);
+
+ subPerm = pm.getPermissionObject(subPrincipal, "test/pobject", false);
+ Assert.equal(subPerm.principal.origin, "http://example.com");
+ Assert.equal(subPerm.capability, pm.DENY_ACTION);
+ Assert.equal(subPerm.expireType, pm.EXPIRE_SESSION);
+
+ pm.addFromPrincipal(subPrincipal, "test/pobject", pm.PROMPT_ACTION);
+ rootPerm = pm.getPermissionObject(principal, "test/pobject", true);
+ Assert.equal(rootPerm.principal.origin, "http://example.com");
+ Assert.equal(rootPerm.capability, pm.DENY_ACTION);
+
+ subPerm = pm.getPermissionObject(subPrincipal, "test/pobject", true);
+ Assert.equal(subPerm.principal.origin, "http://sub.example.com");
+ Assert.equal(subPerm.capability, pm.PROMPT_ACTION);
+
+ subPerm = pm.getPermissionObject(subPrincipal, "test/pobject", false);
+ Assert.equal(subPerm.principal.origin, "http://sub.example.com");
+ Assert.equal(subPerm.capability, pm.PROMPT_ACTION);
+
+ subPerm = pm.getPermissionObject(subSubPrincipal, "test/pobject", true);
+ Assert.equal(null, subPerm);
+
+ subPerm = pm.getPermissionObject(subSubPrincipal, "test/pobject", false);
+ Assert.equal(subPerm.principal.origin, "http://sub.example.com");
+ Assert.equal(subPerm.capability, pm.PROMPT_ACTION);
+
+ pm.removeFromPrincipal(principal, "test/pobject");
+
+ rootPerm = pm.getPermissionObject(principal, "test/pobject", true);
+ Assert.equal(null, rootPerm);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_idn.js b/extensions/permissions/test/unit/test_permmanager_idn.js
new file mode 100644
index 0000000000..5719131245
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_idn.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getPrincipalFromDomain(aDomain) {
+ let ssm = Services.scriptSecurityManager;
+ let uri = NetUtil.newURI("http://" + aDomain);
+ return ssm.createContentPrincipal(uri, {});
+}
+
+function run_test() {
+ let pm = Services.perms;
+ let perm = "test-idn";
+
+ // We create three principal linked to IDN.
+ // One with just a domain, one with a subdomain and one with the TLD
+ // containing a UTF-8 character.
+ let mainDomainPrincipal = getPrincipalFromDomain("fôû.com");
+ let subDomainPrincipal = getPrincipalFromDomain("fôô.bàr.com");
+ let tldPrincipal = getPrincipalFromDomain("fôû.bàr.côm");
+
+ // We add those to the permission manager.
+ pm.addFromPrincipal(mainDomainPrincipal, perm, pm.ALLOW_ACTION, 0, 0);
+ pm.addFromPrincipal(subDomainPrincipal, perm, pm.ALLOW_ACTION, 0, 0);
+ pm.addFromPrincipal(tldPrincipal, perm, pm.ALLOW_ACTION, 0, 0);
+
+ // They should obviously be there now..
+ Assert.equal(
+ pm.testPermissionFromPrincipal(mainDomainPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subDomainPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(tldPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+
+ // We do the same thing with the puny-encoded versions of the IDN.
+ let punyMainDomainPrincipal = getPrincipalFromDomain("xn--f-xgav.com");
+ let punySubDomainPrincipal = getPrincipalFromDomain(
+ "xn--f-xgaa.xn--br-jia.com"
+ );
+ let punyTldPrincipal = getPrincipalFromDomain(
+ "xn--f-xgav.xn--br-jia.xn--cm-8ja"
+ );
+
+ // Those principals should have the permission granted too.
+ Assert.equal(
+ pm.testPermissionFromPrincipal(punyMainDomainPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(punySubDomainPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(punyTldPrincipal, perm),
+ pm.ALLOW_ACTION
+ );
+
+ // However, those two principals shouldn't be allowed because they are like
+ // the IDN but without the UT8-8 characters.
+ let witnessPrincipal = getPrincipalFromDomain("foo.com");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, perm),
+ pm.UNKNOWN_ACTION
+ );
+ witnessPrincipal = getPrincipalFromDomain("foo.bar.com");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, perm),
+ pm.UNKNOWN_ACTION
+ );
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_ipc.js b/extensions/permissions/test/unit/test_permmanager_ipc.js
new file mode 100644
index 0000000000..239cc5aeaf
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_ipc.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+add_task(async function test_permissions_sent_over_ipc_on_bloburl() {
+ const ssm = Services.scriptSecurityManager;
+ const pm = Services.perms;
+
+ // setup a profile.
+ do_get_profile();
+
+ async function assertExpectedContentPage(contentPage) {
+ const [processType, remoteType, principalSpec] = await page.spawn(
+ [],
+ async () => {
+ return [
+ Services.appinfo.processType,
+ Services.appinfo.remoteType,
+ this.content.document.nodePrincipal.spec,
+ ];
+ }
+ );
+
+ equal(
+ processType,
+ Services.appinfo.PROCESS_TYPE_CONTENT,
+ "Got a content process"
+ );
+ equal(remoteType, "file", "Got a file child process");
+ equal(principalSpec, principal.spec, "Got the expected document principal");
+ }
+
+ function getChildProcessID(contentPage) {
+ return contentPage.spawn([], () => Services.appinfo.processID);
+ }
+
+ async function assertHasAllowedPermission(contentPage, perm) {
+ const isPermissionAllowed = await contentPage.spawn(
+ [perm],
+ permName =>
+ Services.perms.getPermissionObject(
+ this.content.document.nodePrincipal,
+ permName,
+ true
+ )?.capability === Services.perms.ALLOW_ACTION
+ );
+ ok(isPermissionAllowed, `Permission "${perm}" allowed as expected`);
+ }
+
+ let file = do_get_file(".", true);
+ let fileURI = Services.io.newFileURI(file);
+ const principal = ssm.createContentPrincipal(fileURI, {});
+ info(`Add a test permission to the document principal: ${principal.spec}`);
+ pm.addFromPrincipal(principal, "test/perm", pm.ALLOW_ACTION);
+
+ info("Test expected permission is propagated into the child process");
+ let page = await ExtensionTestUtils.loadContentPage(fileURI.spec);
+ const childID1 = await getChildProcessID(page);
+ await assertExpectedContentPage(page);
+ await assertHasAllowedPermission(page, "test/perm");
+ await page.close();
+
+ // Ensure this blob url does not prevent permissions to be propagated
+ // to a new child process.
+ info("Create a blob url for a non http/https principal");
+ const blob = new Blob();
+ const blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Got a blob URL");
+
+ info("Test expected permission is still propagated");
+ page = await ExtensionTestUtils.loadContentPage(fileURI.spec);
+ const childID2 = await getChildProcessID(page);
+ await assertExpectedContentPage(page);
+ Assert.notEqual(childID1, childID2, "Got a new child process as expected");
+ await assertHasAllowedPermission(page, "test/perm");
+ await page.close();
+
+ URL.revokeObjectURL(blobURL);
+
+ page = await ExtensionTestUtils.loadContentPage(fileURI.spec);
+ const childID3 = await getChildProcessID(page);
+ await assertExpectedContentPage(page);
+ Assert.notEqual(childID2, childID3, "Got a new child process as expected");
+ await assertHasAllowedPermission(page, "test/perm");
+ await page.close();
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js b/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js
new file mode 100644
index 0000000000..decbce81a0
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+var DEBUG_TEST = false;
+
+function run_test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ // Setup a profile directory.
+ var dir = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ // Get the db file.
+ var file = dir.clone();
+ file.append("permissions.sqlite");
+
+ var storage = Services.storage;
+
+ // Create database.
+ var connection = storage.openDatabase(file);
+ // The file should now exist.
+ Assert.ok(file.exists());
+
+ connection.schemaVersion = 3;
+ connection.executeSimpleSQL("DROP TABLE moz_hosts");
+ connection.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ // Now we can inject garbadge in the database.
+ var garbadge = [
+ // Regular entry.
+ {
+ host: "42",
+ type: "0",
+ permission: 1,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // Special values in host (some being invalid).
+ {
+ host: "scheme:file",
+ type: "1",
+ permission: 0,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "192.168.0.1",
+ type: "2",
+ permission: 0,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "2001:0db8:0000:0000:0000:ff00:0042:8329",
+ type: "3",
+ permission: 0,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "::1",
+ type: "4",
+ permission: 0,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // Permission is UNKNOWN_ACTION.
+ {
+ host: "42",
+ type: "5",
+ permission: Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // Permission is out of range.
+ {
+ host: "42",
+ type: "6",
+ permission: 100,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "42",
+ type: "7",
+ permission: -100,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // ExpireType is out of range.
+ {
+ host: "42",
+ type: "8",
+ permission: 1,
+ expireType: -100,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "42",
+ type: "9",
+ permission: 1,
+ expireType: 100,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // ExpireTime is at 0 with ExpireType = Time.
+ {
+ host: "42",
+ type: "10",
+ permission: 1,
+ expireType: Ci.nsIPermissionManager.EXPIRE_TIME,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+
+ // ExpireTime has a value with ExpireType != Time
+ {
+ host: "42",
+ type: "11",
+ permission: 1,
+ expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
+ expireTime: 1000,
+ isInBrowserElement: 0,
+ },
+ {
+ host: "42",
+ type: "12",
+ permission: 1,
+ expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
+ expireTime: 1000,
+ isInBrowserElement: 0,
+ },
+
+ // ExpireTime is negative.
+ {
+ host: "42",
+ type: "13",
+ permission: 1,
+ expireType: Ci.nsIPermissionManager.EXPIRE_TIME,
+ expireTime: -1,
+ isInBrowserElement: 0,
+ },
+
+ // IsInBrowserElement is negative or higher than 1.
+ {
+ host: "42",
+ type: "15",
+ permission: 1,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: -1,
+ },
+ {
+ host: "42",
+ type: "16",
+ permission: 1,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 10,
+ },
+
+ // This insertion should be the last one. It is used to make sure we always
+ // load it regardless of the previous entries validities.
+ {
+ host: "example.org",
+ type: "test-load-invalid-entries",
+ permission: Ci.nsIPermissionManager.ALLOW_ACTION,
+ expireType: 0,
+ expireTime: 0,
+ isInBrowserElement: 0,
+ },
+ ];
+
+ for (var i = 0; i < garbadge.length; ++i) {
+ if (DEBUG_TEST) {
+ dump("\n value #" + i + "\n\n");
+ }
+ var data = garbadge[i];
+ connection.executeSimpleSQL(
+ "INSERT INTO moz_hosts " +
+ " (id, host, type, permission, expireType, expireTime, isInBrowserElement, appId) " +
+ "VALUES (" +
+ i +
+ ", '" +
+ data.host +
+ "', '" +
+ data.type +
+ "', " +
+ data.permission +
+ ", " +
+ data.expireType +
+ ", " +
+ data.expireTime +
+ ", " +
+ data.isInBrowserElement +
+ ", 0)"
+ );
+ }
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Let's do something in order to be sure the DB is read.
+ Assert.greater(pm.all.length, 0);
+
+ // The schema should be upgraded to 11, and a 'modificationTime' column should
+ // exist with all records having a value of 0.
+ Assert.equal(connection.schemaVersion, 12);
+
+ let select = connection.createStatement(
+ "SELECT modificationTime FROM moz_perms"
+ );
+ let numMigrated = 0;
+ while (select.executeStep()) {
+ let thisModTime = select.getInt64(0);
+ Assert.ok(
+ thisModTime > 0,
+ "new modifiedTime field is correct (but it's not 0!)"
+ );
+ numMigrated += 1;
+ }
+ // check we found at least 1 record that was migrated.
+ Assert.greater(
+ numMigrated,
+ 0,
+ "we found at least 1 record that was migrated"
+ );
+
+ // This permission should always be there.
+ let ssm = Services.scriptSecurityManager;
+ let uri = NetUtil.newURI("http://example.org");
+ let principal = ssm.createContentPrincipal(uri, {});
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, "test-load-invalid-entries"),
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_local_files.js b/extensions/permissions/test/unit/test_permmanager_local_files.js
new file mode 100644
index 0000000000..389eb77916
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_local_files.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that permissions work for file:// URIs (aka local files).
+
+function getPrincipalFromURIString(uriStr) {
+ let uri = NetUtil.newURI(uriStr);
+ return Services.scriptSecurityManager.createContentPrincipal(uri, {});
+}
+
+function run_test() {
+ let pm = Services.perms;
+
+ // If we add a permission to a file:// URI, the test should return true.
+ let principal = getPrincipalFromURIString("file:///foo/bar");
+ pm.addFromPrincipal(principal, "test/local-files", pm.ALLOW_ACTION, 0, 0);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, "test/local-files"),
+ pm.ALLOW_ACTION
+ );
+
+ // Another file:// URI should have the same permission.
+ let witnessPrincipal = getPrincipalFromURIString("file:///bar/foo");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Giving "file:///" a permission shouldn't give it to all file:// URIs.
+ let rootPrincipal = getPrincipalFromURIString("file:///");
+ pm.addFromPrincipal(rootPrincipal, "test/local-files", pm.ALLOW_ACTION, 0, 0);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Giving "file://" a permission shouldn't give it to all file:// URIs.
+ let schemeRootPrincipal = getPrincipalFromURIString("file://");
+ pm.addFromPrincipal(
+ schemeRootPrincipal,
+ "test/local-files",
+ pm.ALLOW_ACTION,
+ 0,
+ 0
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Giving 'node' a permission shouldn't give it to its 'children'.
+ let fileInDirPrincipal = getPrincipalFromURIString(
+ "file:///foo/bar/foobar.txt"
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(fileInDirPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Revert "file:///foo/bar" permission and check that it has been correctly taken into account.
+ pm.removeFromPrincipal(principal, "test/local-files");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(witnessPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(fileInDirPrincipal, "test/local-files"),
+ pm.UNKNOWN_ACTION
+ );
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_matches.js b/extensions/permissions/test/unit/test_permmanager_matches.js
new file mode 100644
index 0000000000..937a1ce750
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_matches.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var attrs;
+
+function matches_always(perm, principals) {
+ principals.forEach(principal => {
+ Assert.ok(
+ perm.matches(principal, true),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ Assert.ok(
+ perm.matches(principal, false),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ });
+}
+
+function matches_weak(perm, principals) {
+ principals.forEach(principal => {
+ Assert.ok(
+ !perm.matches(principal, true),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ Assert.ok(
+ perm.matches(principal, false),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ });
+}
+
+function matches_never(perm, principals) {
+ principals.forEach(principal => {
+ Assert.ok(
+ !perm.matches(principal, true),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ Assert.ok(
+ !perm.matches(principal, false),
+ "perm: " + perm.principal.origin + ", princ: " + principal.origin
+ );
+ });
+}
+
+function run_test() {
+ // initialize the permission manager service
+ let pm = Services.perms;
+
+ let secMan = Services.scriptSecurityManager;
+
+ // Add some permissions
+ let uri0 = NetUtil.newURI("http://google.com/search?q=foo#hashtag");
+ let uri1 = NetUtil.newURI("http://hangouts.google.com/subdir");
+ let uri2 = NetUtil.newURI("http://google.org/");
+ let uri3 = NetUtil.newURI("https://google.com/some/random/subdirectory");
+ let uri4 = NetUtil.newURI("https://hangouts.google.com/#!/hangout");
+ let uri5 = NetUtil.newURI("http://google.com:8096/");
+
+ let uri0_n = secMan.createContentPrincipal(uri0, {});
+ let uri1_n = secMan.createContentPrincipal(uri1, {});
+ let uri2_n = secMan.createContentPrincipal(uri2, {});
+ let uri3_n = secMan.createContentPrincipal(uri3, {});
+ let uri4_n = secMan.createContentPrincipal(uri4, {});
+ let uri5_n = secMan.createContentPrincipal(uri5, {});
+
+ attrs = { inIsolatedMozBrowser: true };
+ let uri0_y_ = secMan.createContentPrincipal(uri0, attrs);
+ let uri1_y_ = secMan.createContentPrincipal(uri1, attrs);
+ let uri2_y_ = secMan.createContentPrincipal(uri2, attrs);
+ let uri3_y_ = secMan.createContentPrincipal(uri3, attrs);
+ let uri4_y_ = secMan.createContentPrincipal(uri4, attrs);
+ let uri5_y_ = secMan.createContentPrincipal(uri5, attrs);
+
+ attrs = { userContextId: 1 };
+ let uri0_1 = secMan.createContentPrincipal(uri0, attrs);
+ let uri1_1 = secMan.createContentPrincipal(uri1, attrs);
+ let uri2_1 = secMan.createContentPrincipal(uri2, attrs);
+ let uri3_1 = secMan.createContentPrincipal(uri3, attrs);
+ let uri4_1 = secMan.createContentPrincipal(uri4, attrs);
+ let uri5_1 = secMan.createContentPrincipal(uri5, attrs);
+
+ attrs = { firstPartyDomain: "cnn.com" };
+ let uri0_cnn = secMan.createContentPrincipal(uri0, attrs);
+ let uri1_cnn = secMan.createContentPrincipal(uri1, attrs);
+ let uri2_cnn = secMan.createContentPrincipal(uri2, attrs);
+ let uri3_cnn = secMan.createContentPrincipal(uri3, attrs);
+ let uri4_cnn = secMan.createContentPrincipal(uri4, attrs);
+ let uri5_cnn = secMan.createContentPrincipal(uri5, attrs);
+
+ pm.addFromPrincipal(uri0_n, "test/matches", pm.ALLOW_ACTION);
+ let perm_n = pm.getPermissionObject(uri0_n, "test/matches", true);
+ pm.addFromPrincipal(uri0_y_, "test/matches", pm.ALLOW_ACTION);
+ let perm_y_ = pm.getPermissionObject(uri0_y_, "test/matches", true);
+ pm.addFromPrincipal(uri0_1, "test/matches", pm.ALLOW_ACTION);
+ let perm_1 = pm.getPermissionObject(uri0_n, "test/matches", true);
+ pm.addFromPrincipal(uri0_cnn, "test/matches", pm.ALLOW_ACTION);
+ let perm_cnn = pm.getPermissionObject(uri0_n, "test/matches", true);
+
+ matches_always(perm_n, [uri0_n, uri0_1]);
+ matches_weak(perm_n, [uri1_n, uri1_1]);
+ matches_never(perm_n, [
+ uri2_n,
+ uri3_n,
+ uri4_n,
+ uri5_n,
+ uri0_y_,
+ uri1_y_,
+ uri2_y_,
+ uri3_y_,
+ uri4_y_,
+ uri5_y_,
+ uri2_1,
+ uri3_1,
+ uri4_1,
+ uri5_1,
+ uri0_cnn,
+ uri1_cnn,
+ uri2_cnn,
+ uri3_cnn,
+ uri4_cnn,
+ uri5_cnn,
+ ]);
+
+ matches_always(perm_y_, [uri0_y_]);
+ matches_weak(perm_y_, [uri1_y_]);
+ matches_never(perm_y_, [
+ uri2_y_,
+ uri3_y_,
+ uri4_y_,
+ uri5_y_,
+ uri0_n,
+ uri1_n,
+ uri2_n,
+ uri3_n,
+ uri4_n,
+ uri5_n,
+ uri0_1,
+ uri1_1,
+ uri2_1,
+ uri3_1,
+ uri4_1,
+ uri5_1,
+ uri0_cnn,
+ uri1_cnn,
+ uri2_cnn,
+ uri3_cnn,
+ uri4_cnn,
+ uri5_cnn,
+ ]);
+
+ matches_always(perm_1, [uri0_n, uri0_1]);
+ matches_weak(perm_1, [uri1_n, uri1_1]);
+ matches_never(perm_1, [
+ uri2_n,
+ uri3_n,
+ uri4_n,
+ uri5_n,
+ uri0_y_,
+ uri1_y_,
+ uri2_y_,
+ uri3_y_,
+ uri4_y_,
+ uri5_y_,
+ uri2_1,
+ uri3_1,
+ uri4_1,
+ uri5_1,
+ uri0_cnn,
+ uri1_cnn,
+ uri2_cnn,
+ uri3_cnn,
+ uri4_cnn,
+ uri5_cnn,
+ ]);
+
+ matches_always(perm_cnn, [uri0_n, uri0_1]);
+ matches_weak(perm_cnn, [uri1_n, uri1_1]);
+ matches_never(perm_cnn, [
+ uri2_n,
+ uri3_n,
+ uri4_n,
+ uri5_n,
+ uri0_y_,
+ uri1_y_,
+ uri2_y_,
+ uri3_y_,
+ uri4_y_,
+ uri5_y_,
+ uri2_1,
+ uri3_1,
+ uri4_1,
+ uri5_1,
+ uri0_cnn,
+ uri1_cnn,
+ uri2_cnn,
+ uri3_cnn,
+ uri4_cnn,
+ uri5_cnn,
+ ]);
+
+ // Clean up!
+ pm.removeAll();
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_matchesuri.js b/extensions/permissions/test/unit/test_permmanager_matchesuri.js
new file mode 100644
index 0000000000..1218fbf9ca
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_matchesuri.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function matches_always(perm, uris) {
+ uris.forEach(uri => {
+ Assert.ok(
+ perm.matchesURI(uri, true),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ Assert.ok(
+ perm.matchesURI(uri, false),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ });
+}
+
+function matches_weak(perm, uris) {
+ uris.forEach(uri => {
+ Assert.ok(
+ !perm.matchesURI(uri, true),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ Assert.ok(
+ perm.matchesURI(uri, false),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ });
+}
+
+function matches_never(perm, uris) {
+ uris.forEach(uri => {
+ Assert.ok(
+ !perm.matchesURI(uri, true),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ Assert.ok(
+ !perm.matchesURI(uri, false),
+ "perm: " + perm.principal.origin + ", URI: " + uri.spec
+ );
+ });
+}
+
+function mk_permission(uri) {
+ let pm = Services.perms;
+
+ let secMan = Services.scriptSecurityManager;
+
+ // Get the permission from the principal!
+ let principal = secMan.createContentPrincipal(uri, {});
+
+ pm.addFromPrincipal(principal, "test/matchesuri", pm.ALLOW_ACTION);
+ let permission = pm.getPermissionObject(principal, "test/matchesuri", true);
+
+ return permission;
+}
+
+function run_test() {
+ // initialize the permission manager service
+ let pm = Services.perms;
+
+ let fileprefix = "file:///";
+ if (Services.appinfo.OS == "WINNT") {
+ // Windows rejects files if they don't have a drive. See Bug 1180870
+ fileprefix += "c:/";
+ }
+
+ // Add some permissions
+ let uri0 = NetUtil.newURI("http://google.com:9091/just/a/path");
+ let uri1 = NetUtil.newURI("http://hangouts.google.com:9091/some/path");
+ let uri2 = NetUtil.newURI("http://google.com:9091/");
+ let uri3 = NetUtil.newURI("http://google.org:9091/");
+ let uri4 = NetUtil.newURI("http://deeper.hangouts.google.com:9091/");
+ let uri5 = NetUtil.newURI("https://google.com/just/a/path");
+ let uri6 = NetUtil.newURI("https://hangouts.google.com");
+ let uri7 = NetUtil.newURI("https://google.com/");
+
+ let fileuri1 = NetUtil.newURI(fileprefix + "a/file/path");
+ let fileuri2 = NetUtil.newURI(fileprefix + "a/file/path/deeper");
+ let fileuri3 = NetUtil.newURI(fileprefix + "a/file/otherpath");
+
+ {
+ let perm = mk_permission(uri0);
+ matches_always(perm, [uri0, uri2]);
+ matches_weak(perm, [uri1, uri4]);
+ matches_never(perm, [uri3, uri5, uri6, uri7, fileuri1, fileuri2, fileuri3]);
+ }
+
+ {
+ let perm = mk_permission(uri1);
+ matches_always(perm, [uri1]);
+ matches_weak(perm, [uri4]);
+ matches_never(perm, [
+ uri0,
+ uri2,
+ uri3,
+ uri5,
+ uri6,
+ uri7,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(uri2);
+ matches_always(perm, [uri0, uri2]);
+ matches_weak(perm, [uri1, uri4]);
+ matches_never(perm, [uri3, uri5, uri6, uri7, fileuri1, fileuri2, fileuri3]);
+ }
+
+ {
+ let perm = mk_permission(uri3);
+ matches_always(perm, [uri3]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri1,
+ uri2,
+ uri4,
+ uri5,
+ uri6,
+ uri7,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(uri4);
+ matches_always(perm, [uri4]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri1,
+ uri2,
+ uri3,
+ uri5,
+ uri6,
+ uri7,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(uri5);
+ matches_always(perm, [uri5, uri7]);
+ matches_weak(perm, [uri6]);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(uri6);
+ matches_always(perm, [uri6]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ uri5,
+ uri7,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(uri7);
+ matches_always(perm, [uri5, uri7]);
+ matches_weak(perm, [uri6]);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ fileuri1,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(fileuri1);
+ matches_always(perm, [fileuri1]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ uri5,
+ uri6,
+ uri7,
+ fileuri2,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(fileuri2);
+ matches_always(perm, [fileuri2]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ uri5,
+ uri6,
+ uri7,
+ fileuri1,
+ fileuri3,
+ ]);
+ }
+
+ {
+ let perm = mk_permission(fileuri3);
+ matches_always(perm, [fileuri3]);
+ matches_weak(perm, []);
+ matches_never(perm, [
+ uri0,
+ uri1,
+ uri2,
+ uri3,
+ uri4,
+ uri5,
+ uri6,
+ uri7,
+ fileuri1,
+ fileuri2,
+ ]);
+ }
+
+ // Clean up!
+ pm.removeAll();
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_10-11.js b/extensions/permissions/test/unit/test_permmanager_migrate_10-11.js
new file mode 100644
index 0000000000..c0d30865ee
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_10-11.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 10;
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ insertOrigin(
+ "https://foo.com",
+ "storageAccessAPI^https://foo.com",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+ insertOrigin(
+ "http://foo.com",
+ "storageAccessAPI^https://bar.com^https://foo.com",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+ insertOrigin(
+ "http://foo.com",
+ "storageAccessAPI^https://bar.com^https://baz.com",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+ insertOrigin("http://foo.com^inBrowser=1", "A", 2, 0, 0, 0);
+
+ // CLose the db connection
+ stmt6Insert.finalize();
+ db.close();
+ db = null;
+
+ let expected = [
+ ["https://foo.com", "storageAccessAPI^https://foo.com", 2, 0, 0, 0],
+ ["http://foo.com", "storageAccessAPI^https://bar.com", 2, 0, 0, 0],
+ ["http://foo.com", "storageAccessAPI^https://bar.com", 2, 0, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 2, 0, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(Services.io.newURI("ftp://127.0.0.1:8080"));
+ await PlacesTestUtils.addVisits(Services.io.newURI("https://localhost:8080"));
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ let mozPermsCount = db.createStatement("SELECT count(*) FROM moz_perms");
+ try {
+ mozPermsCount.executeStep();
+ Assert.equal(mozPermsCount.getInt64(0), expected.length);
+ } finally {
+ mozPermsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_11-12.js b/extensions/permissions/test/unit/test_permmanager_migrate_11-12.js
new file mode 100644
index 0000000000..97170a9240
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_11-12.js
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 11;
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ insertOrigin("https://a.com", "3rdPartyStorage^https://b.com", 2, 0, 0, 0);
+ insertOrigin(
+ "https://www.a.com",
+ "3rdPartyStorage^https://www.c.com",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+ insertOrigin(
+ "https://localhost",
+ "3rdPartyStorage^http://www.c.com",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+
+ insertOrigin(
+ "https://www.b.co.uk",
+ "3rdPartyStorage^https://www.a.co.uk",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+
+ insertOrigin(
+ "https://sub.www.b.co.uk",
+ "3rdPartyStorage^https://sub.www.a.co.uk",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+
+ insertOrigin(
+ "https://example.b.co.uk",
+ "3rdPartyStorage^https://www.a.co.uk",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+
+ insertOrigin(
+ "https://[::1]",
+ "3rdPartyStorage^https://www.a.co.uk",
+ 2,
+ 0,
+ 0,
+ 0
+ );
+ // Close the db connection
+ stmt6Insert.finalize();
+ db.close();
+ db = null;
+ info(Services.perms.all);
+
+ let expected = [
+ ["https://a.com", "3rdPartyStorage^https://b.com", 2, 0, 0, 0],
+ ["https://a.com", "3rdPartyStorage^https://www.c.com", 2, 0, 0, 0],
+ ["https://localhost", "3rdPartyStorage^http://www.c.com", 2, 0, 0, 0],
+ ["https://b.co.uk", "3rdPartyStorage^https://www.a.co.uk", 2, 0, 0, 0],
+ ["https://b.co.uk", "3rdPartyStorage^https://sub.www.a.co.uk", 2, 0, 0, 0],
+ ["https://[::1]", "3rdPartyStorage^https://www.a.co.uk", 2, 0, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(Services.io.newURI("ftp://127.0.0.1:8080"));
+ await PlacesTestUtils.addVisits(Services.io.newURI("https://localhost:8080"));
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ info(Services.perms.all);
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+ info(expected);
+ info(found);
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ let mozPermsCount = db.createStatement("SELECT count(*) FROM moz_perms");
+ try {
+ mozPermsCount.executeStep();
+ Assert.equal(mozPermsCount.getInt64(0), expected.length);
+ } finally {
+ mozPermsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_4-7.js b/extensions/permissions/test/unit/test_permmanager_migrate_4-7.js
new file mode 100644
index 0000000000..857e0a462c
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_4-7.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 4;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 2000, true),
+ insertHost("sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("subber.sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("bar.ca", "A", 1, 0, 0, 0, 1000, true),
+ insertHost("localhost", "A", 1, 0, 0, 0, 0, false),
+ insertHost("127.0.0.1", "A", 1, 0, 0, 0, 0, false),
+ insertHost("192.0.2.235", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///some/path/to/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///another/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost(
+ "moz-nullprincipal:{8695105a-adbe-4e4e-8083-851faa5ca2d7}",
+ "A",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost(
+ "moz-nullprincipal:{12ahjksd-akjs-asd3-8393-asdu2189asdu}",
+ "B",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost("<file>", "A", 1, 0, 0, 0, 0, false),
+ insertHost("<file>", "B", 1, 0, 0, 0, 0, false),
+ ];
+
+ // CLose the db connection
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ // The http:// entries under foo.com won't be inserted, as there are history entries for foo.com,
+ // and http://foo.com or a subdomain are never visited.
+ // ["http://foo.com", "A", 1, 0, 0],
+ // ["http://foo.com^inBrowser=1", "A", 1, 0, 0],
+ //
+ // Because we search for port/scheme combinations under eTLD+1, we should not have http:// entries
+ // for subdomains of foo.com either
+ // ["http://sub.foo.com", "B", 1, 0, 0],
+ // ["http://subber.sub.foo.com", "B", 1, 0, 0],
+
+ ["https://foo.com", "A", 1, 0, 0],
+ ["https://foo.com", "C", 1, 0, 0],
+ ["https://foo.com^inBrowser=1", "A", 1, 0, 0],
+ ["https://sub.foo.com", "B", 1, 0, 0],
+ ["https://subber.sub.foo.com", "B", 1, 0, 0],
+
+ // bar.ca will have both http:// and https:// for all entries, because there are no associated history entries
+ ["http://bar.ca", "B", 1, 0, 0],
+ ["https://bar.ca", "B", 1, 0, 0],
+ ["http://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["https://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["file:///some/path/to/file.html", "A", 1, 0, 0],
+ ["file:///another/file.html", "A", 1, 0, 0],
+
+ // Because we put ftp://some.subdomain.of.foo.com:8000/some/subdirectory in the history, we should
+ // also have these entries
+ ["ftp://foo.com:8000", "A", 1, 0, 0],
+ ["ftp://foo.com:8000", "C", 1, 0, 0],
+ ["ftp://foo.com:8000^inBrowser=1", "A", 1, 0, 0],
+
+ // In addition, because we search for port/scheme combinations under eTLD+1, we should have the
+ // following entries
+ ["ftp://sub.foo.com:8000", "B", 1, 0, 0],
+ ["ftp://subber.sub.foo.com:8000", "B", 1, 0, 0],
+
+ // Make sure that we also support localhost, and IP addresses
+ ["http://localhost", "A", 1, 0, 0],
+ ["https://localhost", "A", 1, 0, 0],
+ ["http://127.0.0.1", "A", 1, 0, 0],
+ ["https://127.0.0.1", "A", 1, 0, 0],
+ ["http://192.0.2.235", "A", 1, 0, 0],
+ ["https://192.0.2.235", "A", 1, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ // The moz_hosts table should still exist but be empty
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_4-7_no_history.js b/extensions/permissions/test/unit/test_permmanager_migrate_4-7_no_history.js
new file mode 100644
index 0000000000..aa735e534a
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_4-7_no_history.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+/*
+ * Prevent the nsINavHistoryService from being avaliable for the migration
+ */
+
+var CONTRACT_ID = "@mozilla.org/browser/nav-history-service;1";
+var factory = {
+ createInstance() {
+ throw new Error("There is no history service");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
+};
+
+var newClassID = Services.uuid.generateUUID();
+
+var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+var oldClassID = registrar.contractIDToCID(CONTRACT_ID);
+// TODO: There was a var oldFactory = here causing linter errors as it
+// was unused. We should check if this function call is needed at all.
+Components.manager.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory);
+registrar.registerFactory(newClassID, "", CONTRACT_ID, factory);
+
+function cleanupFactory() {
+ registrar.unregisterFactory(newClassID, factory);
+ registrar.registerFactory(oldClassID, "", CONTRACT_ID, null);
+}
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+/*
+ * Done nsINavHistoryService code
+ */
+
+add_task(function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // Make sure that we can't resolve the nsINavHistoryService
+ try {
+ Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+ Assert.ok(false, "There shouldn't have been a nsINavHistoryService");
+ } catch (e) {
+ Assert.ok(true, "There wasn't a nsINavHistoryService");
+ }
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.ok(pm.all.length >= 0, "Permission manager not initialized?");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 4;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 2000, true),
+ insertHost("sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("subber.sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("bar.ca", "A", 1, 0, 0, 0, 1000, true),
+ insertHost("localhost", "A", 1, 0, 0, 0, 0, false),
+ insertHost("127.0.0.1", "A", 1, 0, 0, 0, 0, false),
+ insertHost("263.123.555.676", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///some/path/to/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///another/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost(
+ "moz-nullprincipal:{8695105a-adbe-4e4e-8083-851faa5ca2d7}",
+ "A",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost(
+ "moz-nullprincipal:{12ahjksd-akjs-asd3-8393-asdu2189asdu}",
+ "B",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost("<file>", "A", 1, 0, 0, 0, 0, false),
+ insertHost("<file>", "B", 1, 0, 0, 0, 0, false),
+ ];
+
+ // CLose the db connection
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ ["http://foo.com", "A", 1, 0, 0],
+ ["http://foo.com", "C", 1, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 1, 0, 0],
+ ["http://sub.foo.com", "B", 1, 0, 0],
+ ["http://subber.sub.foo.com", "B", 1, 0, 0],
+
+ ["https://foo.com", "A", 1, 0, 0],
+ ["https://foo.com", "C", 1, 0, 0],
+ ["https://foo.com^inBrowser=1", "A", 1, 0, 0],
+ ["https://sub.foo.com", "B", 1, 0, 0],
+ ["https://subber.sub.foo.com", "B", 1, 0, 0],
+
+ // bar.ca will have both http:// and https:// for all entries, because there are no associated history entries
+ ["http://bar.ca", "B", 1, 0, 0],
+ ["https://bar.ca", "B", 1, 0, 0],
+ ["http://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["https://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["file:///some/path/to/file.html", "A", 1, 0, 0],
+ ["file:///another/file.html", "A", 1, 0, 0],
+
+ // Make sure that we also support localhost, and IP addresses
+ ["http://localhost", "A", 1, 0, 0],
+ ["https://localhost", "A", 1, 0, 0],
+ ["http://127.0.0.1", "A", 1, 0, 0],
+ ["https://127.0.0.1", "A", 1, 0, 0],
+ ["http://263.123.555.676", "A", 1, 0, 0],
+ ["https://263.123.555.676", "A", 1, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ // The moz_hosts table should still exist but be empty
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ db.close();
+ }
+
+ cleanupFactory();
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_5-7a.js b/extensions/permissions/test/unit/test_permmanager_migrate_5-7a.js
new file mode 100644
index 0000000000..8ea03d5e16
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_5-7a.js
@@ -0,0 +1,365 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 5;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ /*
+ * V5 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt5Insert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ /*
+ * V4 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts_v4 (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts_v4 (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt5Insert.bindByName("id", thisId);
+ stmt5Insert.bindByName("origin", origin);
+ stmt5Insert.bindByName("type", type);
+ stmt5Insert.bindByName("permission", permission);
+ stmt5Insert.bindByName("expireType", expireType);
+ stmt5Insert.bindByName("expireTime", expireTime);
+ stmt5Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt5Insert.execute();
+ } finally {
+ stmt5Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+
+ let created5 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^inBrowser=1", "A", 2, 0, 0, 0),
+ ];
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 2000, true),
+ insertHost("sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("subber.sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("bar.ca", "A", 1, 0, 0, 0, 1000, true),
+ insertHost("localhost", "A", 1, 0, 0, 0, 0, false),
+ insertHost("127.0.0.1", "A", 1, 0, 0, 0, 0, false),
+ insertHost("192.0.2.235", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///some/path/to/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///another/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost(
+ "moz-nullprincipal:{8695105a-adbe-4e4e-8083-851faa5ca2d7}",
+ "A",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost(
+ "moz-nullprincipal:{12ahjksd-akjs-asd3-8393-asdu2189asdu}",
+ "B",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost("<file>", "A", 1, 0, 0, 0, 0, false),
+ insertHost("<file>", "B", 1, 0, 0, 0, 0, false),
+ ];
+
+ // CLose the db connection
+ stmt5Insert.finalize();
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ // The http:// entries under foo.com won't be inserted, as there are history entries for foo.com,
+ // and http://foo.com or a subdomain are never visited.
+ // ["http://foo.com", "A", 1, 0, 0],
+ // ["http://foo.com^inBrowser=1", "A", 1, 0, 0],
+ //
+ // Because we search for port/scheme combinations under eTLD+1, we should not have http:// entries
+ // for subdomains of foo.com either
+ // ["http://sub.foo.com", "B", 1, 0, 0],
+ // ["http://subber.sub.foo.com", "B", 1, 0, 0],
+
+ ["https://foo.com", "A", 1, 0, 0],
+ ["https://foo.com", "C", 1, 0, 0],
+ ["https://foo.com^inBrowser=1", "A", 1, 0, 0],
+ ["https://sub.foo.com", "B", 1, 0, 0],
+ ["https://subber.sub.foo.com", "B", 1, 0, 0],
+
+ // bar.ca will have both http:// and https:// for all entries, because there are no associated history entries
+ ["http://bar.ca", "B", 1, 0, 0],
+ ["https://bar.ca", "B", 1, 0, 0],
+ ["http://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["https://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["file:///some/path/to/file.html", "A", 1, 0, 0],
+ ["file:///another/file.html", "A", 1, 0, 0],
+
+ // Because we put ftp://some.subdomain.of.foo.com:8000/some/subdirectory in the history, we should
+ // also have these entries
+ ["ftp://foo.com:8000", "A", 1, 0, 0],
+ ["ftp://foo.com:8000", "C", 1, 0, 0],
+ ["ftp://foo.com:8000^inBrowser=1", "A", 1, 0, 0],
+
+ // In addition, because we search for port/scheme combinations under eTLD+1, we should have the
+ // following entries
+ ["ftp://sub.foo.com:8000", "B", 1, 0, 0],
+ ["ftp://subber.sub.foo.com:8000", "B", 1, 0, 0],
+
+ // Make sure that we also support localhost, and IP addresses
+ ["http://localhost", "A", 1, 0, 0],
+ ["https://localhost", "A", 1, 0, 0],
+ ["http://127.0.0.1", "A", 1, 0, 0],
+ ["https://127.0.0.1", "A", 1, 0, 0],
+ ["http://192.0.2.235", "A", 1, 0, 0],
+ ["https://192.0.2.235", "A", 1, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(db.tableExists("moz_perms_v6"));
+
+ // The moz_hosts table should still exist but be empty
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ // Check that the moz_perms_v6 table contains the backup of the entry we created
+ let mozPermsV6Stmt = db.createStatement(
+ "SELECT " +
+ "origin, type, permission, expireType, expireTime, modificationTime " +
+ "FROM moz_perms_v6 WHERE id = :id"
+ );
+ try {
+ // Check that the moz_hosts table still contains the correct values.
+ created5.forEach(it => {
+ mozPermsV6Stmt.reset();
+ mozPermsV6Stmt.bindByName("id", it.id);
+ mozPermsV6Stmt.executeStep();
+ Assert.equal(mozPermsV6Stmt.getUTF8String(0), it.origin);
+ Assert.equal(mozPermsV6Stmt.getUTF8String(1), it.type);
+ Assert.equal(mozPermsV6Stmt.getInt64(2), it.permission);
+ Assert.equal(mozPermsV6Stmt.getInt64(3), it.expireType);
+ Assert.equal(mozPermsV6Stmt.getInt64(4), it.expireTime);
+ Assert.equal(mozPermsV6Stmt.getInt64(5), it.modificationTime);
+ });
+ } finally {
+ mozPermsV6Stmt.finalize();
+ }
+
+ // Check that there are the right number of values
+ let mozPermsV6Count = db.createStatement(
+ "SELECT count(*) FROM moz_perms_v6"
+ );
+ try {
+ mozPermsV6Count.executeStep();
+ Assert.equal(mozPermsV6Count.getInt64(0), created5.length);
+ } finally {
+ mozPermsV6Count.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_5-7b.js b/extensions/permissions/test/unit/test_permmanager_migrate_5-7b.js
new file mode 100644
index 0000000000..8c330effa9
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_5-7b.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 5;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ /*
+ * V5 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt5Insert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ let id = 0;
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt5Insert.bindByName("id", thisId);
+ stmt5Insert.bindByName("origin", origin);
+ stmt5Insert.bindByName("type", type);
+ stmt5Insert.bindByName("permission", permission);
+ stmt5Insert.bindByName("expireType", expireType);
+ stmt5Insert.bindByName("expireTime", expireTime);
+ stmt5Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt5Insert.execute();
+ } finally {
+ stmt5Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ host: origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ let created5 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^appId=1000&inBrowser=1", "A", 2, 0, 0, 0),
+
+ insertOrigin("http://127.0.0.1", "B", 2, 0, 0, 0),
+ insertOrigin("http://localhost", "B", 2, 0, 0, 0),
+ ];
+
+ let created4 = []; // Didn't create any v4 entries, so the DB should be empty
+
+ // CLose the db connection
+ stmt5Insert.finalize();
+ db.close();
+ stmt5Insert = null;
+ db = null;
+
+ let expected = [
+ ["https://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 2, 0, 0, 0],
+
+ ["http://127.0.0.1", "B", 2, 0, 0, 0],
+ ["http://localhost", "B", 2, 0, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ let mozHostsStmt = db.createStatement(
+ "SELECT " +
+ "host, type, permission, expireType, expireTime, " +
+ "modificationTime, isInBrowserElement " +
+ "FROM moz_hosts WHERE id = :id"
+ );
+ try {
+ // Check that the moz_hosts table still contains the correct values.
+ created4.forEach(it => {
+ mozHostsStmt.reset();
+ mozHostsStmt.bindByName("id", it.id);
+ mozHostsStmt.executeStep();
+ Assert.equal(mozHostsStmt.getUTF8String(0), it.host);
+ Assert.equal(mozHostsStmt.getUTF8String(1), it.type);
+ Assert.equal(mozHostsStmt.getInt64(2), it.permission);
+ Assert.equal(mozHostsStmt.getInt64(3), it.expireType);
+ Assert.equal(mozHostsStmt.getInt64(4), it.expireTime);
+ Assert.equal(mozHostsStmt.getInt64(5), it.modificationTime);
+ Assert.equal(mozHostsStmt.getInt64(6), it.isInBrowserElement);
+ });
+ } finally {
+ mozHostsStmt.finalize();
+ }
+
+ // Check that there are the right number of values
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), created4.length);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_6-7a.js b/extensions/permissions/test/unit/test_permmanager_migrate_6-7a.js
new file mode 100644
index 0000000000..93b78e6478
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_6-7a.js
@@ -0,0 +1,366 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 6;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ /*
+ * V5 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_perms (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ /*
+ * V4 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+
+ let created6 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^inBrowser=1", "A", 2, 0, 0, 0),
+ ];
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 2000, true),
+ insertHost("sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("subber.sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("bar.ca", "A", 1, 0, 0, 0, 1000, true),
+ insertHost("localhost", "A", 1, 0, 0, 0, 0, false),
+ insertHost("127.0.0.1", "A", 1, 0, 0, 0, 0, false),
+ insertHost("192.0.2.235", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///some/path/to/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///another/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost(
+ "moz-nullprincipal:{8695105a-adbe-4e4e-8083-851faa5ca2d7}",
+ "A",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost(
+ "moz-nullprincipal:{12ahjksd-akjs-asd3-8393-asdu2189asdu}",
+ "B",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost("<file>", "A", 1, 0, 0, 0, 0, false),
+ insertHost("<file>", "B", 1, 0, 0, 0, 0, false),
+ ];
+
+ // CLose the db connection
+ stmt6Insert.finalize();
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ // The http:// entries under foo.com won't be inserted, as there are history entries for foo.com,
+ // and http://foo.com or a subdomain are never visited.
+ // ["http://foo.com", "A", 1, 0, 0],
+ // ["http://foo.com^inBrowser=1", "A", 1, 0, 0],
+ //
+ // Because we search for port/scheme combinations under eTLD+1, we should not have http:// entries
+ // for subdomains of foo.com either
+ // ["http://sub.foo.com", "B", 1, 0, 0],
+ // ["http://subber.sub.foo.com", "B", 1, 0, 0],
+
+ ["https://foo.com", "A", 1, 0, 0],
+ ["https://foo.com", "C", 1, 0, 0],
+ ["https://foo.com^inBrowser=1", "A", 1, 0, 0],
+ ["https://sub.foo.com", "B", 1, 0, 0],
+ ["https://subber.sub.foo.com", "B", 1, 0, 0],
+
+ // bar.ca will have both http:// and https:// for all entries, because there are no associated history entries
+ ["http://bar.ca", "B", 1, 0, 0],
+ ["https://bar.ca", "B", 1, 0, 0],
+ ["http://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["https://bar.ca^inBrowser=1", "A", 1, 0, 0],
+ ["file:///some/path/to/file.html", "A", 1, 0, 0],
+ ["file:///another/file.html", "A", 1, 0, 0],
+
+ // Because we put ftp://some.subdomain.of.foo.com:8000/some/subdirectory in the history, we should
+ // also have these entries
+ ["ftp://foo.com:8000", "A", 1, 0, 0],
+ ["ftp://foo.com:8000", "C", 1, 0, 0],
+ ["ftp://foo.com:8000^inBrowser=1", "A", 1, 0, 0],
+
+ // In addition, because we search for port/scheme combinations under eTLD+1, we should have the
+ // following entries
+ ["ftp://sub.foo.com:8000", "B", 1, 0, 0],
+ ["ftp://subber.sub.foo.com:8000", "B", 1, 0, 0],
+
+ // Make sure that we also support localhost, and IP addresses
+ ["http://localhost", "A", 1, 0, 0],
+ ["https://localhost", "A", 1, 0, 0],
+ ["http://127.0.0.1", "A", 1, 0, 0],
+ ["https://127.0.0.1", "A", 1, 0, 0],
+ ["http://192.0.2.235", "A", 1, 0, 0],
+ ["https://192.0.2.235", "A", 1, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(db.tableExists("moz_perms_v6"));
+
+ // The moz_hosts table should still exist but be empty
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ // Check that the moz_perms_v6 table contains the backup of the entry we created
+ let mozPermsV6Stmt = db.createStatement(
+ "SELECT " +
+ "origin, type, permission, expireType, expireTime, modificationTime " +
+ "FROM moz_perms_v6 WHERE id = :id"
+ );
+ try {
+ // Check that the moz_hosts table still contains the correct values.
+ created6.forEach(it => {
+ mozPermsV6Stmt.reset();
+ mozPermsV6Stmt.bindByName("id", it.id);
+ mozPermsV6Stmt.executeStep();
+ Assert.equal(mozPermsV6Stmt.getUTF8String(0), it.origin);
+ Assert.equal(mozPermsV6Stmt.getUTF8String(1), it.type);
+ Assert.equal(mozPermsV6Stmt.getInt64(2), it.permission);
+ Assert.equal(mozPermsV6Stmt.getInt64(3), it.expireType);
+ Assert.equal(mozPermsV6Stmt.getInt64(4), it.expireTime);
+ Assert.equal(mozPermsV6Stmt.getInt64(5), it.modificationTime);
+ });
+ } finally {
+ mozPermsV6Stmt.finalize();
+ }
+
+ // Check that there are the right number of values
+ let mozPermsV6Count = db.createStatement(
+ "SELECT count(*) FROM moz_perms_v6"
+ );
+ try {
+ mozPermsV6Count.executeStep();
+ Assert.equal(mozPermsV6Count.getInt64(0), created6.length);
+ } finally {
+ mozPermsV6Count.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_6-7b.js b/extensions/permissions/test/unit/test_permmanager_migrate_6-7b.js
new file mode 100644
index 0000000000..feed156d29
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_6-7b.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 6;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ /*
+ * V5 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_perms (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ let id = 0;
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ host: origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ let created6 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^appId=1000&inBrowser=1", "A", 2, 0, 0, 0),
+ ];
+
+ let created4 = []; // Didn't create any v4 entries, so the DB should be empty
+
+ // CLose the db connection
+ stmt6Insert.finalize();
+ db.close();
+ stmt6Insert = null;
+ db = null;
+
+ let expected = [
+ ["https://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 2, 0, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ let mozHostsStmt = db.createStatement(
+ "SELECT " +
+ "host, type, permission, expireType, expireTime, " +
+ "modificationTime, isInBrowserElement " +
+ "FROM moz_hosts WHERE id = :id"
+ );
+ try {
+ // Check that the moz_hosts table still contains the correct values.
+ created4.forEach(it => {
+ mozHostsStmt.reset();
+ mozHostsStmt.bindByName("id", it.id);
+ mozHostsStmt.executeStep();
+ Assert.equal(mozHostsStmt.getUTF8String(0), it.host);
+ Assert.equal(mozHostsStmt.getUTF8String(1), it.type);
+ Assert.equal(mozHostsStmt.getInt64(2), it.permission);
+ Assert.equal(mozHostsStmt.getInt64(3), it.expireType);
+ Assert.equal(mozHostsStmt.getInt64(4), it.expireTime);
+ Assert.equal(mozHostsStmt.getInt64(5), it.modificationTime);
+ Assert.equal(mozHostsStmt.getInt64(6), it.isInBrowserElement);
+ });
+ } finally {
+ mozHostsStmt.finalize();
+ }
+
+ // Check that there are the right number of values
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), created4.length);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_7-8.js b/extensions/permissions/test/unit/test_permmanager_migrate_7-8.js
new file mode 100644
index 0000000000..cd8b0f86cc
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_7-8.js
@@ -0,0 +1,328 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 7;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE moz_hosts");
+
+ /*
+ * V5 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_perms (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ /*
+ * V4 table
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ /*
+ * The v4 table is a backup
+ */
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts_is_backup (dummy INTEGER PRIMARY KEY)"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+ // eslint-disable-next-line no-unused-vars
+ let created7 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^inBrowser=1", "A", 2, 0, 0, 0),
+ insertOrigin("https://192.0.2.235", "A", 2, 0, 0),
+ ];
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "A", 1, 0, 0, 0, 2000, true),
+ insertHost("sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("subber.sub.foo.com", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 0, false),
+ insertHost("bar.ca", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("bar.ca", "A", 1, 0, 0, 0, 1000, true),
+ insertHost("localhost", "A", 1, 0, 0, 0, 0, false),
+ insertHost("127.0.0.1", "A", 1, 0, 0, 0, 0, false),
+ insertHost("192.0.2.235", "A", 1, 0, 0, 0, 0, false),
+ // Although ipv6 addresses are written with [] around the IP address,
+ // the .host property doesn't contain these []s, which means that we
+ // write it like this
+ insertHost("2001:db8::ff00:42:8329", "C", 1, 0, 0, 0, 0, false),
+ insertHost("file:///some/path/to/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost("file:///another/file.html", "A", 1, 0, 0, 0, 0, false),
+ insertHost(
+ "moz-nullprincipal:{8695105a-adbe-4e4e-8083-851faa5ca2d7}",
+ "A",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost(
+ "moz-nullprincipal:{12ahjksd-akjs-asd3-8393-asdu2189asdu}",
+ "B",
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ false
+ ),
+ insertHost("<file>", "A", 1, 0, 0, 0, 0, false),
+ insertHost("<file>", "B", 1, 0, 0, 0, 0, false),
+ ];
+
+ // CLose the db connection
+ stmt6Insert.finalize();
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ // We should have kept the previously migrated entries
+ ["https://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 2, 0, 0, 0],
+
+ // Make sure that we also support localhost, and IP addresses
+ ["https://localhost:8080", "A", 1, 0, 0],
+ ["ftp://127.0.0.1:8080", "A", 1, 0, 0],
+
+ ["http://[2001:db8::ff00:42:8329]", "C", 1, 0, 0],
+ ["https://[2001:db8::ff00:42:8329]", "C", 1, 0, 0],
+ ["http://192.0.2.235", "A", 1, 0, 0],
+
+ // There should only be one entry of this type in the database
+ ["https://192.0.2.235", "A", 2, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(Services.io.newURI("ftp://127.0.0.1:8080"));
+ await PlacesTestUtils.addVisits(Services.io.newURI("https://localhost:8080"));
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_hosts_is_backup"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ // The moz_hosts table should still exist but be empty
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 0);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ // Check that there are the right number of values in the permissions database
+ let mozPermsCount = db.createStatement("SELECT count(*) FROM moz_perms");
+ try {
+ mozPermsCount.executeStep();
+ Assert.equal(mozPermsCount.getInt64(0), expected.length);
+ } finally {
+ mozPermsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_migrate_9-10.js b/extensions/permissions/test/unit/test_permmanager_migrate_9-10.js
new file mode 100644
index 0000000000..02aa3bb467
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_migrate_9-10.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+var PERMISSIONS_FILE_NAME = "permissions.sqlite";
+
+function GetPermissionsFile(profile) {
+ let file = profile.clone();
+ file.append(PERMISSIONS_FILE_NAME);
+ return file;
+}
+
+add_task(async function test() {
+ // Create and set up the permissions database.
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ let profile = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.equal(pm.all.length, 0, "No cookies");
+
+ let db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ db.schemaVersion = 9;
+ db.executeSimpleSQL("DROP TABLE moz_perms");
+ db.executeSimpleSQL("DROP TABLE IF EXISTS moz_hosts");
+
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_perms (" +
+ " id INTEGER PRIMARY KEY" +
+ ",origin TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ")"
+ );
+
+ let stmt6Insert = db.createStatement(
+ "INSERT INTO moz_perms (" +
+ "id, origin, type, permission, expireType, expireTime, modificationTime" +
+ ") VALUES (" +
+ ":id, :origin, :type, :permission, :expireType, :expireTime, :modificationTime" +
+ ")"
+ );
+
+ db.executeSimpleSQL(
+ "CREATE TABLE moz_hosts (" +
+ " id INTEGER PRIMARY KEY" +
+ ",host TEXT" +
+ ",type TEXT" +
+ ",permission INTEGER" +
+ ",expireType INTEGER" +
+ ",expireTime INTEGER" +
+ ",modificationTime INTEGER" +
+ ",appId INTEGER" +
+ ",isInBrowserElement INTEGER" +
+ ")"
+ );
+
+ let stmtInsert = db.createStatement(
+ "INSERT INTO moz_hosts (" +
+ "id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement" +
+ ") VALUES (" +
+ ":id, :host, :type, :permission, :expireType, :expireTime, :modificationTime, :appId, :isInBrowserElement" +
+ ")"
+ );
+
+ let id = 0;
+
+ function insertOrigin(
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime
+ ) {
+ let thisId = id++;
+
+ stmt6Insert.bindByName("id", thisId);
+ stmt6Insert.bindByName("origin", origin);
+ stmt6Insert.bindByName("type", type);
+ stmt6Insert.bindByName("permission", permission);
+ stmt6Insert.bindByName("expireType", expireType);
+ stmt6Insert.bindByName("expireTime", expireTime);
+ stmt6Insert.bindByName("modificationTime", modificationTime);
+
+ try {
+ stmt6Insert.execute();
+ } finally {
+ stmt6Insert.reset();
+ }
+
+ return {
+ id: thisId,
+ origin,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ };
+ }
+
+ function insertHost(
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement
+ ) {
+ let thisId = id++;
+
+ stmtInsert.bindByName("id", thisId);
+ stmtInsert.bindByName("host", host);
+ stmtInsert.bindByName("type", type);
+ stmtInsert.bindByName("permission", permission);
+ stmtInsert.bindByName("expireType", expireType);
+ stmtInsert.bindByName("expireTime", expireTime);
+ stmtInsert.bindByName("modificationTime", modificationTime);
+ stmtInsert.bindByName("appId", appId);
+ stmtInsert.bindByName("isInBrowserElement", isInBrowserElement);
+
+ try {
+ stmtInsert.execute();
+ } finally {
+ stmtInsert.reset();
+ }
+
+ return {
+ id: thisId,
+ host,
+ type,
+ permission,
+ expireType,
+ expireTime,
+ modificationTime,
+ appId,
+ isInBrowserElement,
+ };
+ }
+ // eslint-disable-next-line no-unused-vars
+ let created7 = [
+ insertOrigin("https://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com", "A", 2, 0, 0, 0),
+ insertOrigin("http://foo.com^inBrowser=1", "A", 2, 0, 0, 0),
+ ];
+
+ // Add some rows to the database
+ // eslint-disable-next-line no-unused-vars
+ let created = [
+ insertHost("foo.com", "A", 1, 0, 0, 0, 0, false),
+ insertHost("foo.com", "B", 1, 0, 0, 0, 1000, false),
+ insertHost("foo.com", "C", 1, 0, 0, 0, 2000, true),
+ ];
+
+ // CLose the db connection
+ stmt6Insert.finalize();
+ stmtInsert.finalize();
+ db.close();
+ stmtInsert = null;
+ db = null;
+
+ let expected = [
+ ["https://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com", "A", 2, 0, 0, 0],
+ ["http://foo.com^inBrowser=1", "A", 2, 0, 0, 0],
+ ];
+
+ let found = expected.map(it => 0);
+
+ // Add some places to the places database
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("https://foo.com/some/other/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(
+ Services.io.newURI("ftp://some.subdomain.of.foo.com:8000/some/subdirectory")
+ );
+ await PlacesTestUtils.addVisits(Services.io.newURI("ftp://127.0.0.1:8080"));
+ await PlacesTestUtils.addVisits(Services.io.newURI("https://localhost:8080"));
+
+ // This will force the permission-manager to reload the data.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Force initialization of the PermissionManager
+ for (let permission of Services.perms.all) {
+ let isExpected = false;
+
+ expected.forEach((it, i) => {
+ if (
+ permission.principal.origin == it[0] &&
+ permission.type == it[1] &&
+ permission.capability == it[2] &&
+ permission.expireType == it[3] &&
+ permission.expireTime == it[4]
+ ) {
+ isExpected = true;
+ found[i]++;
+ }
+ });
+
+ Assert.ok(
+ isExpected,
+ "Permission " +
+ (isExpected ? "should" : "shouldn't") +
+ " be in permission database: " +
+ permission.principal.origin +
+ ", " +
+ permission.type +
+ ", " +
+ permission.capability +
+ ", " +
+ permission.expireType +
+ ", " +
+ permission.expireTime
+ );
+ }
+
+ found.forEach((count, i) => {
+ Assert.ok(
+ count == 1,
+ "Expected count = 1, got count = " +
+ count +
+ " for permission " +
+ expected[i]
+ );
+ });
+
+ // Check to make sure that all of the tables which we care about are present
+ {
+ db = Services.storage.openDatabase(GetPermissionsFile(profile));
+ Assert.ok(db.tableExists("moz_perms"));
+ Assert.ok(db.tableExists("moz_hosts"));
+ Assert.ok(!db.tableExists("moz_perms_v6"));
+
+ let mozHostsCount = db.createStatement("SELECT count(*) FROM moz_hosts");
+ try {
+ mozHostsCount.executeStep();
+ Assert.equal(mozHostsCount.getInt64(0), 3);
+ } finally {
+ mozHostsCount.finalize();
+ }
+
+ let mozPermsCount = db.createStatement("SELECT count(*) FROM moz_perms");
+ try {
+ mozPermsCount.executeStep();
+ Assert.equal(mozPermsCount.getInt64(0), expected.length);
+ } finally {
+ mozPermsCount.finalize();
+ }
+
+ db.close();
+ }
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_notifications.js b/extensions/permissions/test/unit/test_permmanager_notifications.js
new file mode 100644
index 0000000000..fb851aa04b
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_notifications.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the permissionmanager 'added', 'changed', 'deleted', and 'cleared'
+// notifications behave as expected.
+
+var test_generator = do_run_test();
+
+function run_test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ do_test_pending();
+ test_generator.next();
+}
+
+function* do_run_test() {
+ let pm = Services.perms;
+ let now = Number(Date.now());
+ let permType = "test/expiration-perm";
+ let ssm = Services.scriptSecurityManager;
+ let uri = NetUtil.newURI("http://example.com");
+ let principal = ssm.createContentPrincipal(uri, {});
+
+ let observer = new permission_observer(test_generator, now, permType);
+ Services.obs.addObserver(observer, "perm-changed");
+
+ // Add a permission, to test the 'add' notification. Note that we use
+ // do_execute_soon() so that we can use our generator to continue the test
+ // where we left off.
+ executeSoon(function () {
+ pm.addFromPrincipal(
+ principal,
+ permType,
+ pm.ALLOW_ACTION,
+ pm.EXPIRE_TIME,
+ now + 100000
+ );
+ });
+ yield;
+
+ // Alter a permission, to test the 'changed' notification.
+ executeSoon(function () {
+ pm.addFromPrincipal(
+ principal,
+ permType,
+ pm.ALLOW_ACTION,
+ pm.EXPIRE_TIME,
+ now + 200000
+ );
+ });
+ yield;
+
+ // Remove a permission, to test the 'deleted' notification.
+ executeSoon(function () {
+ pm.removeFromPrincipal(principal, permType);
+ });
+ yield;
+
+ // Clear permissions, to test the 'cleared' notification.
+ executeSoon(function () {
+ pm.removeAll();
+ });
+ yield;
+
+ Services.obs.removeObserver(observer, "perm-changed");
+ Assert.equal(observer.adds, 1);
+ Assert.equal(observer.changes, 1);
+ Assert.equal(observer.deletes, 1);
+ Assert.ok(observer.cleared);
+
+ do_finish_generator_test(test_generator);
+}
+
+function permission_observer(generator, now, type) {
+ // Set up our observer object.
+ this.generator = generator;
+ this.pm = Services.perms;
+ this.now = now;
+ this.type = type;
+ this.adds = 0;
+ this.changes = 0;
+ this.deletes = 0;
+ this.cleared = false;
+}
+
+permission_observer.prototype = {
+ observe(subject, topic, data) {
+ Assert.equal(topic, "perm-changed");
+
+ // "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.
+ if (data == "added") {
+ let perm = subject.QueryInterface(Ci.nsIPermission);
+ this.adds++;
+ switch (this.adds) {
+ case 1:
+ Assert.equal(this.type, perm.type);
+ Assert.equal(this.pm.EXPIRE_TIME, perm.expireType);
+ Assert.equal(this.now + 100000, perm.expireTime);
+ break;
+ default:
+ do_throw("too many add notifications posted.");
+ }
+ } else if (data == "changed") {
+ let perm = subject.QueryInterface(Ci.nsIPermission);
+ this.changes++;
+ switch (this.changes) {
+ case 1:
+ Assert.equal(this.type, perm.type);
+ Assert.equal(this.pm.EXPIRE_TIME, perm.expireType);
+ Assert.equal(this.now + 200000, perm.expireTime);
+ break;
+ default:
+ do_throw("too many change notifications posted.");
+ }
+ } else if (data == "deleted") {
+ let perm = subject.QueryInterface(Ci.nsIPermission);
+ this.deletes++;
+ switch (this.deletes) {
+ case 1:
+ Assert.equal(this.type, perm.type);
+ break;
+ default:
+ do_throw("too many delete notifications posted.");
+ }
+ } else if (data == "cleared") {
+ // only clear once: at the end
+ Assert.ok(!this.cleared);
+ Assert.equal(do_count_array(Services.perms.all), 0);
+ this.cleared = true;
+ } else {
+ do_throw("unexpected data '" + data + "'!");
+ }
+
+ // Continue the test.
+ do_run_generator(this.generator);
+ },
+};
diff --git a/extensions/permissions/test/unit/test_permmanager_oa_strip.js b/extensions/permissions/test/unit/test_permmanager_oa_strip.js
new file mode 100644
index 0000000000..d6c15bd807
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_oa_strip.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = Services.io.newURI("http://example.com");
+const TEST_PERMISSION = "test/oastrip";
+const TEST_PERMISSION2 = "test/oastrip2";
+const TEST_PERMISSION3 = "test/oastrip3";
+
+// List of permissions which are not isolated by private browsing or user context
+// as per array kStripOAPermissions in PermissionManager.cpp
+const STRIPPED_PERMS = ["cookie"];
+
+let principal = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_URI,
+ {}
+);
+let principalPrivateBrowsing =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_URI, {
+ privateBrowsingId: 1,
+ });
+let principalUserContext1 =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_URI, {
+ userContextId: 1,
+ });
+let principalUserContext2 =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_URI, {
+ userContextId: 2,
+ });
+
+function testOAIsolation(permIsolateUserContext, permIsolatePrivateBrowsing) {
+ info(
+ `testOAIsolation: permIsolateUserContext: ${permIsolateUserContext}; permIsolatePrivateBrowsing: ${permIsolatePrivateBrowsing}`
+ );
+
+ let pm = Services.perms;
+
+ Services.prefs.setBoolPref(
+ "permissions.isolateBy.userContext",
+ permIsolateUserContext
+ );
+ Services.prefs.setBoolPref(
+ "permissions.isolateBy.privateBrowsing",
+ permIsolatePrivateBrowsing
+ );
+
+ // Set test permission for normal browsing
+ pm.addFromPrincipal(principal, TEST_PERMISSION, pm.ALLOW_ACTION);
+
+ // Check normal browsing permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+ // normal browsing => user context 1
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext1, TEST_PERMISSION)
+ );
+ // normal browsing => user context 2
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext2, TEST_PERMISSION)
+ );
+ // normal browsing => private browsing
+ Assert.equal(
+ permIsolatePrivateBrowsing
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principalPrivateBrowsing, TEST_PERMISSION)
+ );
+
+ // Set permission for private browsing
+ pm.addFromPrincipal(
+ principalPrivateBrowsing,
+ TEST_PERMISSION2,
+ pm.DENY_ACTION
+ );
+
+ // Check private browsing permission
+ Assert.equal(
+ Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principalPrivateBrowsing, TEST_PERMISSION2)
+ );
+ // private browsing => normal browsing
+ Assert.equal(
+ permIsolatePrivateBrowsing
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION2)
+ );
+ // private browsing => user context 1
+ Assert.equal(
+ permIsolatePrivateBrowsing || permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext1, TEST_PERMISSION2)
+ );
+ // private browsing => user context 2
+ Assert.equal(
+ permIsolatePrivateBrowsing || permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext2, TEST_PERMISSION2)
+ );
+
+ // Set permission for user context 1
+ pm.addFromPrincipal(
+ principalUserContext1,
+ TEST_PERMISSION3,
+ pm.PROMPT_ACTION
+ );
+
+ // Check user context 1 permission
+ Assert.equal(
+ Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext1, TEST_PERMISSION3)
+ );
+
+ // user context 1 => normal browsing
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION3)
+ );
+ // user context 1 => user context 2
+ Assert.equal(
+ permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principalUserContext2, TEST_PERMISSION3)
+ );
+ // user context 1 => private browsing
+ Assert.equal(
+ permIsolatePrivateBrowsing || permIsolateUserContext
+ ? Ci.nsIPermissionManager.UNKNOWN_ACTION
+ : Ci.nsIPermissionManager.PROMPT_ACTION,
+ pm.testPermissionFromPrincipal(principalPrivateBrowsing, TEST_PERMISSION3)
+ );
+
+ pm.removeAll();
+
+ // Modifying an non-isolated/stripped permission should affect all browsing contexts,
+ // independently of permission isolation pref state
+ STRIPPED_PERMS.forEach(perm => {
+ info("Testing stripped permission " + perm);
+
+ // Add a permission for the normal window
+ pm.addFromPrincipal(principal, perm, pm.ALLOW_ACTION);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principalPrivateBrowsing, perm),
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principalUserContext1, perm),
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principalUserContext2, perm),
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+
+ // Remove the permission from private window
+ pm.removeFromPrincipal(principalPrivateBrowsing, perm);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, perm),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principalUserContext1, perm),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principalUserContext2, perm),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+
+ // Set a permission for a normal window and then override it by adding it to container 2 again
+ pm.addFromPrincipal(principal, perm, pm.PROMPT_ACTION);
+ pm.addFromPrincipal(principal, TEST_PERMISSION, pm.ALLOW_ACTION);
+ pm.addFromPrincipal(principalUserContext2, perm, pm.DENY_ACTION);
+
+ let principalPerms = pm.getAllForPrincipal(principalPrivateBrowsing, perm);
+
+ Assert.ok(
+ principalPerms.some(p => p.type == perm && p.capability == pm.DENY_ACTION)
+ );
+ if (permIsolatePrivateBrowsing) {
+ Assert.equal(principalPerms.length, 1);
+ Assert.ok(
+ principalPerms.some(
+ p => p.type == perm && p.capability == pm.DENY_ACTION
+ )
+ );
+ } else {
+ Assert.equal(principalPerms.length, 2);
+ Assert.ok(
+ principalPerms.some(
+ p => p.type == TEST_PERMISSION && p.capability == pm.ALLOW_ACTION
+ )
+ );
+ }
+ });
+
+ // Cleanup
+ pm.removeAll();
+}
+
+add_task(async function do_test() {
+ // Test all pref combinations and check if principals with different origin attributes
+ // are isolated.
+ testOAIsolation(true, true);
+ testOAIsolation(true, false);
+ testOAIsolation(false, true);
+ testOAIsolation(false, false);
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_remove_add_update.js b/extensions/permissions/test/unit/test_permmanager_remove_add_update.js
new file mode 100644
index 0000000000..6ec7b1f6e5
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_remove_add_update.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(principal, permissions) {
+ let perms = Services.perms.getAllForPrincipal(principal);
+ for (let [type, capability, expireType] of permissions) {
+ let perm = perms.shift();
+ Assert.ok(perm != null);
+ Assert.equal(perm.type, type);
+ Assert.equal(perm.capability, capability);
+ Assert.equal(perm.expireType, expireType);
+ }
+ Assert.ok(!perms.length);
+}
+
+add_task(async function test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+ // setup a profile directory
+ do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.ok(pm.all.length === 0);
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com"
+ );
+
+ info("From session to persistent");
+ pm.addFromPrincipal(
+ principal,
+ "test/foo",
+ pm.ALLOW_ACTION,
+ pm.EXPIRE_SESSION
+ );
+
+ check_enumerator(principal, [
+ ["test/foo", pm.ALLOW_ACTION, pm.EXPIRE_SESSION],
+ ]);
+
+ pm.addFromPrincipal(principal, "test/foo", pm.ALLOW_ACTION, pm.EXPIRE_NEVER);
+
+ check_enumerator(principal, [["test/foo", pm.ALLOW_ACTION, pm.EXPIRE_NEVER]]);
+
+ // Let's reload the DB.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ Assert.ok(pm.all.length === 1);
+ check_enumerator(principal, [["test/foo", pm.ALLOW_ACTION, pm.EXPIRE_NEVER]]);
+
+ info("From persistent to session");
+ pm.addFromPrincipal(
+ principal,
+ "test/foo",
+ pm.ALLOW_ACTION,
+ pm.EXPIRE_SESSION
+ );
+
+ check_enumerator(principal, [
+ ["test/foo", pm.ALLOW_ACTION, pm.EXPIRE_SESSION],
+ ]);
+
+ // Let's reload the DB.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+ Assert.ok(pm.all.length === 0);
+
+ info("From persistent to persistent");
+ pm.addFromPrincipal(principal, "test/foo", pm.ALLOW_ACTION, pm.EXPIRE_NEVER);
+ pm.addFromPrincipal(principal, "test/foo", pm.DENY_ACTION, pm.EXPIRE_NEVER);
+
+ check_enumerator(principal, [["test/foo", pm.DENY_ACTION, pm.EXPIRE_NEVER]]);
+
+ // Let's reload the DB.
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+ Assert.ok(pm.all.length === 1);
+ check_enumerator(principal, [["test/foo", pm.DENY_ACTION, pm.EXPIRE_NEVER]]);
+
+ info("Cleanup");
+ pm.removeAll();
+ check_enumerator(principal, []);
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_removeall.js b/extensions/permissions/test/unit/test_permmanager_removeall.js
new file mode 100644
index 0000000000..e6faea1369
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_removeall.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+ // setup a profile directory
+ var dir = do_get_profile();
+
+ // We need to execute a pm method to be sure that the DB is fully
+ // initialized.
+ var pm = Services.perms;
+ Assert.ok(pm.all.length === 0);
+
+ Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
+
+ // Let's force the completion of the DB reading.
+ Assert.ok(pm.all.length === 0);
+
+ // get the db file
+ var file = dir.clone();
+ file.append("permissions.sqlite");
+
+ Assert.ok(file.exists());
+
+ // corrupt the file
+ var ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(file, 0x02, 0o666, 0);
+ var conv = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(
+ Ci.nsIConverterOutputStream
+ );
+ conv.init(ostream, "UTF-8");
+ for (var i = 0; i < file.fileSize; ++i) {
+ conv.writeString("a");
+ }
+ conv.close();
+
+ // prepare an empty hostperm.1 file so that it can be used for importing
+ var hostperm = dir.clone();
+ hostperm.append("hostperm.1");
+ ostream.init(hostperm, 0x02 | 0x08, 0o666, 0);
+ ostream.close();
+
+ // remove all should not throw
+ pm.removeAll();
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_removebytype.js b/extensions/permissions/test/unit/test_permmanager_removebytype.js
new file mode 100644
index 0000000000..127e0de6ec
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_removebytype.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+ // initialize the permission manager service
+ let pm = Services.perms;
+
+ Assert.equal(pm.all.length, 0);
+
+ // add some permissions
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://amazon.com:8080"
+ );
+ let principal2 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://google.com:2048"
+ );
+ let principal3 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://google.com"
+ );
+
+ pm.addFromPrincipal(principal, "apple", 3);
+ pm.addFromPrincipal(principal, "pear", 1);
+ pm.addFromPrincipal(principal, "cucumber", 1);
+
+ pm.addFromPrincipal(principal2, "apple", 2);
+ pm.addFromPrincipal(principal2, "pear", 2);
+
+ pm.addFromPrincipal(principal3, "cucumber", 3);
+ pm.addFromPrincipal(principal3, "apple", 1);
+
+ Assert.equal(pm.all.length, 7);
+
+ pm.removeByType("apple");
+ Assert.equal(pm.all.length, 4);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 2);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 3);
+
+ pm.removeByType("cucumber");
+ Assert.equal(pm.all.length, 2);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 2);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 0);
+
+ pm.removeByType("pear");
+ Assert.equal(pm.all.length, 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 0);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_removebytypesince.js b/extensions/permissions/test/unit/test_permmanager_removebytypesince.js
new file mode 100644
index 0000000000..4c392501d7
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_removebytypesince.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+ // initialize the permission manager service
+ let pm = Services.perms;
+
+ Assert.equal(pm.all.length, 0);
+
+ // add some permissions
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://amazon.com:8080"
+ );
+ let principal2 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://google.com:2048"
+ );
+ let principal3 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://google.com"
+ );
+
+ pm.addFromPrincipal(principal, "apple", 3);
+ pm.addFromPrincipal(principal, "pear", 1);
+ pm.addFromPrincipal(principal, "cucumber", 1);
+
+ // sleep briefly, then record the time - we'll remove some permissions since then.
+ await new Promise(resolve => do_timeout(20, resolve));
+
+ let since = Date.now();
+
+ // *sob* - on Windows at least, the now recorded by PermissionManager.cpp
+ // might be a couple of ms *earlier* than what JS sees. So another sleep
+ // to ensure our |since| is greater than the time of the permissions we
+ // are now adding. Sadly this means we'll never be able to test when since
+ // exactly equals the modTime, but there you go...
+ await new Promise(resolve => do_timeout(20, resolve));
+
+ pm.addFromPrincipal(principal2, "apple", 2);
+ pm.addFromPrincipal(principal2, "pear", 2);
+
+ pm.addFromPrincipal(principal3, "cucumber", 3);
+ pm.addFromPrincipal(principal3, "apple", 1);
+
+ Assert.equal(pm.all.length, 7);
+
+ pm.removeByTypeSince("apple", since);
+
+ Assert.equal(pm.all.length, 5);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 2);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 3);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 3);
+
+ pm.removeByTypeSince("cucumber", since);
+ Assert.equal(pm.all.length, 4);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 2);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 3);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 0);
+
+ pm.removeByTypeSince("pear", since);
+ Assert.equal(pm.all.length, 3);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "pear"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "pear"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "apple"), 3);
+ Assert.equal(pm.testPermissionFromPrincipal(principal2, "apple"), 0);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "apple"), 0);
+
+ Assert.equal(pm.testPermissionFromPrincipal(principal, "cucumber"), 1);
+ Assert.equal(pm.testPermissionFromPrincipal(principal3, "cucumber"), 0);
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_removepermission.js b/extensions/permissions/test/unit/test_permmanager_removepermission.js
new file mode 100644
index 0000000000..50b2c3105b
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_removepermission.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ // initialize the permission manager service
+ let pm = Services.perms;
+
+ Assert.equal(pm.all.length, 0);
+
+ // add some permissions
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://amazon.com:8080"
+ );
+ let principal2 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://google.com:2048"
+ );
+
+ pm.addFromPrincipal(principal, "apple", 0);
+ pm.addFromPrincipal(principal, "apple", 3);
+ pm.addFromPrincipal(principal, "pear", 3);
+ pm.addFromPrincipal(principal, "pear", 1);
+ pm.addFromPrincipal(principal, "cucumber", 1);
+ pm.addFromPrincipal(principal, "cucumber", 1);
+ pm.addFromPrincipal(principal, "cucumber", 1);
+
+ pm.addFromPrincipal(principal2, "apple", 2);
+ pm.addFromPrincipal(principal2, "pear", 0);
+ pm.addFromPrincipal(principal2, "pear", 2);
+
+ // Make sure that removePermission doesn't remove more than one permission each time
+ Assert.equal(pm.all.length, 5);
+
+ remove_one_by_type("apple");
+ Assert.equal(pm.all.length, 4);
+
+ remove_one_by_type("apple");
+ Assert.equal(pm.all.length, 3);
+
+ remove_one_by_type("pear");
+ Assert.equal(pm.all.length, 2);
+
+ remove_one_by_type("cucumber");
+ Assert.equal(pm.all.length, 1);
+
+ remove_one_by_type("pear");
+ Assert.equal(pm.all.length, 0);
+
+ function remove_one_by_type(type) {
+ for (let perm of pm.all) {
+ if (perm.type == type) {
+ pm.removePermission(perm);
+ break;
+ }
+ }
+ }
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_removesince.js b/extensions/permissions/test/unit/test_permmanager_removesince.js
new file mode 100644
index 0000000000..c33d02b08b
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_removesince.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that removing permissions since a specified time behaves as expected.
+
+var test_generator = do_run_test();
+
+function run_test() {
+ do_test_pending();
+ test_generator.next();
+}
+
+function continue_test() {
+ do_run_generator(test_generator);
+}
+
+function* do_run_test() {
+ let pm = Services.perms;
+
+ // to help with testing edge-cases, we will arrange for .removeAllSince to
+ // remove *all* permissions from one principal and one permission from another.
+ let permURI1 = NetUtil.newURI("http://example.com");
+ let principal1 = Services.scriptSecurityManager.createContentPrincipal(
+ permURI1,
+ {}
+ );
+
+ let permURI2 = NetUtil.newURI("http://example.org");
+ let principal2 = Services.scriptSecurityManager.createContentPrincipal(
+ permURI2,
+ {}
+ );
+
+ // add a permission now - this isn't going to be removed.
+ pm.addFromPrincipal(principal1, "test/remove-since", 1);
+
+ // sleep briefly, then record the time - we'll remove all since then.
+ do_timeout(20, continue_test);
+ yield;
+
+ let since = Number(Date.now());
+
+ // *sob* - on Windows at least, the now recorded by PermissionManager.cpp
+ // might be a couple of ms *earlier* than what JS sees. So another sleep
+ // to ensure our |since| is greater than the time of the permissions we
+ // are now adding. Sadly this means we'll never be able to test when since
+ // exactly equals the modTime, but there you go...
+ do_timeout(20, continue_test);
+ yield;
+
+ // add another item - this second one should get nuked.
+ pm.addFromPrincipal(principal1, "test/remove-since-2", 1);
+
+ // add 2 items for the second principal - both will be removed.
+ pm.addFromPrincipal(principal2, "test/remove-since", 1);
+ pm.addFromPrincipal(principal2, "test/remove-since-2", 1);
+
+ // do the removal.
+ pm.removeAllSince(since);
+
+ // principal1 - the first one should remain.
+ Assert.equal(
+ 1,
+ pm.testPermissionFromPrincipal(principal1, "test/remove-since")
+ );
+ // but the second should have been removed.
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal1, "test/remove-since-2")
+ );
+
+ // principal2 - both should have been removed.
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal2, "test/remove-since")
+ );
+ Assert.equal(
+ 0,
+ pm.testPermissionFromPrincipal(principal2, "test/remove-since-2")
+ );
+
+ do_finish_generator_test(test_generator);
+}
diff --git a/extensions/permissions/test/unit/test_permmanager_site_scope.js b/extensions/permissions/test/unit/test_permmanager_site_scope.js
new file mode 100644
index 0000000000..2944fa2623
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_site_scope.js
@@ -0,0 +1,116 @@
+const TEST_SITE_URI = Services.io.newURI("http://example.com");
+const TEST_FQDN_1_URI = Services.io.newURI("http://test1.example.com");
+const TEST_FQDN_2_URI = Services.io.newURI("http://test2.example.com");
+const TEST_OTHER_URI = Services.io.newURI("http://example.net");
+const TEST_PERMISSION = "3rdPartyStorage^https://example.org";
+
+add_task(async function do_test() {
+ let pm = Services.perms;
+
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_SITE_URI,
+ {}
+ );
+
+ let subdomain1Principal =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_FQDN_1_URI, {});
+
+ let subdomain2Principal =
+ Services.scriptSecurityManager.createContentPrincipal(TEST_FQDN_2_URI, {});
+
+ let otherPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ TEST_OTHER_URI,
+ {}
+ );
+
+ // Set test permission for site
+ pm.addFromPrincipal(principal, TEST_PERMISSION, pm.ALLOW_ACTION);
+
+ // Check normal site permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+
+ // Check subdomain permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(subdomain1Principal, TEST_PERMISSION)
+ );
+
+ // Check other site permission
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(otherPrincipal, TEST_PERMISSION)
+ );
+
+ // Remove the permission from the site
+ pm.removeFromPrincipal(principal, TEST_PERMISSION);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subdomain1Principal, TEST_PERMISSION),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+
+ // Set test permission for subdomain
+ pm.addFromPrincipal(subdomain1Principal, TEST_PERMISSION, pm.ALLOW_ACTION);
+
+ // Check normal site permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)
+ );
+
+ // Check subdomain permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(subdomain1Principal, TEST_PERMISSION)
+ );
+
+ // Check other subdomain permission
+ Assert.equal(
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ pm.testPermissionFromPrincipal(subdomain2Principal, TEST_PERMISSION)
+ );
+
+ // Check other site permission
+ Assert.equal(
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ pm.testPermissionFromPrincipal(otherPrincipal, TEST_PERMISSION)
+ );
+
+ // Check that subdomains include the site-scoped in the getAllForPrincipal
+ let sitePerms = pm.getAllForPrincipal(principal, TEST_PERMISSION);
+ let subdomain1Perms = pm.getAllForPrincipal(
+ subdomain1Principal,
+ TEST_PERMISSION
+ );
+ let subdomain2Perms = pm.getAllForPrincipal(
+ subdomain2Principal,
+ TEST_PERMISSION
+ );
+ let otherSitePerms = pm.getAllForPrincipal(otherPrincipal, TEST_PERMISSION);
+
+ Assert.equal(sitePerms.length, 1);
+ Assert.equal(subdomain1Perms.length, 1);
+ Assert.equal(subdomain2Perms.length, 1);
+ Assert.equal(otherSitePerms.length, 0);
+
+ // Remove the permission from the subdomain
+ pm.removeFromPrincipal(subdomain1Principal, TEST_PERMISSION);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(principal, TEST_PERMISSION),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subdomain1Principal, TEST_PERMISSION),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subdomain2Principal, TEST_PERMISSION),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION
+ );
+});
diff --git a/extensions/permissions/test/unit/test_permmanager_subdomains.js b/extensions/permissions/test/unit/test_permmanager_subdomains.js
new file mode 100644
index 0000000000..4d30372332
--- /dev/null
+++ b/extensions/permissions/test/unit/test_permmanager_subdomains.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getPrincipalFromURI(aURI) {
+ let ssm = Services.scriptSecurityManager;
+ let uri = NetUtil.newURI(aURI);
+ return ssm.createContentPrincipal(uri, {});
+}
+
+function run_test() {
+ var pm = Services.perms;
+
+ // Adds a permission to a sub-domain. Checks if it is working.
+ let sub1Principal = getPrincipalFromURI("http://sub1.example.com");
+ pm.addFromPrincipal(sub1Principal, "test/subdomains", pm.ALLOW_ACTION, 0, 0);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub1Principal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+
+ // A sub-sub-domain should get the permission.
+ let subsubPrincipal = getPrincipalFromURI("http://sub.sub1.example.com");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subsubPrincipal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+
+ // Another sub-domain shouldn't get the permission.
+ let sub2Principal = getPrincipalFromURI("http://sub2.example.com");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub2Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Remove current permissions.
+ pm.removeFromPrincipal(sub1Principal, "test/subdomains");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub1Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // Adding the permission to the main domain. Checks if it is working.
+ let mainPrincipal = getPrincipalFromURI("http://example.com");
+ pm.addFromPrincipal(mainPrincipal, "test/subdomains", pm.ALLOW_ACTION, 0, 0);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(mainPrincipal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+
+ // All sub-domains should have the permission now.
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub1Principal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub2Principal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subsubPrincipal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+
+ // Remove current permissions.
+ pm.removeFromPrincipal(mainPrincipal, "test/subdomains");
+ Assert.equal(
+ pm.testPermissionFromPrincipal(mainPrincipal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub1Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub2Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subsubPrincipal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+
+ // A sanity check that the previous implementation wasn't passing...
+ let crazyPrincipal = getPrincipalFromURI("http://com");
+ pm.addFromPrincipal(crazyPrincipal, "test/subdomains", pm.ALLOW_ACTION, 0, 0);
+ Assert.equal(
+ pm.testPermissionFromPrincipal(crazyPrincipal, "test/subdomains"),
+ pm.ALLOW_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(mainPrincipal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub1Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(sub2Principal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+ Assert.equal(
+ pm.testPermissionFromPrincipal(subsubPrincipal, "test/subdomains"),
+ pm.UNKNOWN_ACTION
+ );
+}
diff --git a/extensions/permissions/test/unit/xpcshell.ini b/extensions/permissions/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..294d7c47a3
--- /dev/null
+++ b/extensions/permissions/test/unit/xpcshell.ini
@@ -0,0 +1,60 @@
+[DEFAULT]
+head = head.js
+
+[test_permmanager_default_pref.js]
+[test_permmanager_defaults.js]
+[test_permmanager_expiration.js]
+skip-if =
+ win10_2004 # Bug 1718292
+ win10_2009 # Bug 1718292
+ win11_2009 # Bug 1797751
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[test_permmanager_getAllByTypes.js]
+[test_permmanager_getAllByTypeSince.js]
+[test_permmanager_getAllForPrincipal.js]
+[test_permmanager_getAllWithTypePrefix.js]
+[test_permmanager_getPermissionObject.js]
+[test_permmanager_notifications.js]
+[test_permmanager_removeall.js]
+[test_permmanager_removebytype.js]
+[test_permmanager_removebytypesince.js]
+[test_permmanager_removesince.js]
+[test_permmanager_load_invalid_entries.js]
+skip-if = debug == true
+[test_permmanager_idn.js]
+[test_permmanager_subdomains.js]
+[test_permmanager_local_files.js]
+[test_permmanager_cleardata.js]
+[test_permmanager_removepermission.js]
+[test_permmanager_matchesuri.js]
+[test_permmanager_matches.js]
+[test_permmanager_migrate_4-7.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_5-7a.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_5-7b.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_6-7a.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_6-7b.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_4-7_no_history.js]
+skip-if =
+ apple_silicon || toolkit == 'android' # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[test_permmanager_migrate_7-8.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_9-10.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_10-11.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_migrate_11-12.js]
+skip-if = toolkit == 'android' # Android doesn't use places
+[test_permmanager_oa_strip.js]
+[test_permmanager_site_scope.js]
+[test_permmanager_remove_add_update.js]
+skip-if = win10_2004 && bits == 64 # Bug 1718292
+[test_permmanager_ipc.js]
+# This test is meant to run on a multi process mode
+# and with file urls loaded in their own child process.
+skip-if = !e10s
+firefox-appdir = browser