diff options
Diffstat (limited to '')
50 files changed, 11312 insertions, 0 deletions
diff --git a/extensions/permissions/Permission.cpp b/extensions/permissions/Permission.cpp new file mode 100644 index 0000000000..72ed1de82f --- /dev/null +++ b/extensions/permissions/Permission.cpp @@ -0,0 +1,138 @@ +/* -*- 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" + +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(); + if (!StaticPrefs::permissions_isolateBy_userContext()) { + attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID); + } + + 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); + + *aMatches = false; + + nsCOMPtr<nsIPrincipal> principal = + Permission::ClonePrincipalForPermission(aPrincipal); + if (!principal) { + *aMatches = false; + return NS_OK; + } + + return MatchesPrincipalForPermission(principal, aExactHost, aMatches); +} + +NS_IMETHODIMP +Permission::MatchesPrincipalForPermission(nsIPrincipal* aPrincipal, + bool aExactHost, bool* 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..098d8386e8 --- /dev/null +++ b/extensions/permissions/PermissionDelegateHandler.cpp @@ -0,0 +1,399 @@ +/* -*- 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 + {"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}, + {"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) { + 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)); + NS_ENSURE_TRUE_VOID(info); + 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..f5b7e096c7 --- /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 = 11; + + 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); + + /* + * 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..9553e3c179 --- /dev/null +++ b/extensions/permissions/PermissionDelegateIPCUtils.h @@ -0,0 +1,41 @@ +/* -*- 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(Message* aMsg, const paramType& aParam) { + for (auto& permission : aParam.mPermissions) { + WriteParam(aMsg, permission); + } + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + for (auto& permission : aResult->mPermissions) { + if (!ReadParam(aMsg, aIter, &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..bc69579ecc --- /dev/null +++ b/extensions/permissions/PermissionManager.cpp @@ -0,0 +1,3669 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/AbstractThread.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ContentPrincipal.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/ExpandedPrincipal.h" +#include "mozilla/net/NeckoMessageUtils.h" +#include "mozilla/Permission.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_permissions.h" +#include "mozilla/Telemetry.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozStorageCID.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsCRT.h" +#include "nsEffectiveTLDService.h" +#include "nsIConsoleService.h" +#include "nsIUserIdleService.h" +#include "nsIInputStream.h" +#include "nsINavHistoryService.h" +#include "nsIObserverService.h" +#include "nsIPrefBranch.h" +#include "nsIPrincipal.h" +#include "nsIURIMutator.h" +#include "nsIWritablePropertyBag2.h" +#include "nsReadLine.h" +#include "nsToolkitCompsCID.h" + +using namespace mozilla::dom; + +namespace mozilla { + +#define PERMISSIONS_FILE_NAME "permissions.sqlite" +#define HOSTS_SCHEMA_VERSION 11 + +// Default permissions are read from a URL - this is the preference we read +// to find that URL. If not set, don't use any default permissions. +constexpr char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl"; + +constexpr char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION; + +// A special value for a permission ID that indicates the ID was loaded as +// a default value. These will never be written to the database, but may +// be overridden with an explicit permission (including UNKNOWN_ACTION) +constexpr int64_t cIDPermissionIsDefault = -1; + +static StaticRefPtr<PermissionManager> gPermissionManager; + +#define ENSURE_NOT_CHILD_PROCESS_(onError) \ + PR_BEGIN_MACRO \ + if (IsChildProcess()) { \ + NS_ERROR("Cannot perform action in content process!"); \ + onError \ + } \ + PR_END_MACRO + +#define ENSURE_NOT_CHILD_PROCESS \ + ENSURE_NOT_CHILD_PROCESS_({ return NS_ERROR_NOT_AVAILABLE; }) + +#define ENSURE_NOT_CHILD_PROCESS_NORET ENSURE_NOT_CHILD_PROCESS_(;) + +#define EXPIRY_NOW PR_Now() / 1000 + +//////////////////////////////////////////////////////////////////////////////// + +namespace { + +bool IsChildProcess() { return XRE_IsContentProcess(); } + +void LogToConsole(const nsAString& aMsg) { + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + + nsAutoString msg(aMsg); + console->LogStringMessage(msg.get()); +} + +// NOTE: an empty string can be passed as aType - if it is this function will +// return "false" unconditionally. +bool HasDefaultPref(const nsACString& aType) { + // A list of permissions that can have a fallback default permission + // set under the permissions.default.* pref. + static const nsLiteralCString kPermissionsWithDefaults[] = { + "camera"_ns, "microphone"_ns, "geo"_ns, "desktop-notification"_ns, + "shortcuts"_ns}; + + if (!aType.IsEmpty()) { + for (const auto& perm : kPermissionsWithDefaults) { + if (perm.Equals(aType)) { + return true; + } + } + } + + return false; +} + +// These permissions are special permissions which must be transmitted to the +// content process before documents with their principals have loaded within +// that process. +// +// Permissions which are in this list are considered to have a "" permission +// key, even if their principal would not normally have that key. +static const nsLiteralCString kPreloadPermissions[] = { + // This permission is preloaded to support properly blocking service worker + // interception when a user has disabled storage for a specific site. Once + // service worker interception moves to the parent process this should be + // removed. See bug 1428130. + "cookie"_ns}; + +// Certain permissions should never be persisted to disk under GeckoView; it's +// the responsibility of the app to manage storing these beyond the scope of +// a single session. +#ifdef ANDROID +static const nsLiteralCString kGeckoViewRestrictedPermissions[] = { + "MediaManagerVideo"_ns, "geolocation"_ns, + "desktop-notification"_ns, "persistent-storage"_ns, + "trackingprotection"_ns, "trackingprotection-pb"_ns}; +#endif + +// NOTE: nullptr can be passed as aType - if it is this function will return +// "false" unconditionally. +bool IsPreloadPermission(const nsACString& aType) { + if (!aType.IsEmpty()) { + for (const auto& perm : kPreloadPermissions) { + if (perm.Equals(aType)) { + return true; + } + } + } + + return false; +} + +// Array of permission types which should not be isolated by origin attributes, +// for user context and private browsing. +// Keep this array in sync with 'STRIPPED_PERMS' in +// 'test_permmanager_oa_strip.js' +// Currently only preloaded permissions are supported. +// This is because perms are sent to the content process in bulk by perm key. +// Non-preloaded, but OA stripped permissions would not be accessible by sites +// in private browsing / non-default user context. +static constexpr std::array<nsLiteralCString, 1> kStripOAPermissions = { + {"cookie"_ns}}; + +bool IsOAForceStripPermission(const nsACString& aType) { + if (aType.IsEmpty()) { + return false; + } + for (const auto& perm : kStripOAPermissions) { + if (perm.Equals(aType)) { + return true; + } + } + return false; +} + +/** + * Strip origin attributes depending on pref state + * @param aForceStrip If true, strips user context and private browsing id, + * ignoring stripping prefs. + * @param aOriginAttributes object to strip. + */ +void MaybeStripOAs(bool aForceStrip, OriginAttributes& aOriginAttributes) { + uint32_t flags = 0; + + if (aForceStrip || !StaticPrefs::permissions_isolateBy_privateBrowsing()) { + flags |= OriginAttributes::STRIP_PRIVATE_BROWSING_ID; + } + + if (aForceStrip || !StaticPrefs::permissions_isolateBy_userContext()) { + flags |= OriginAttributes::STRIP_USER_CONTEXT_ID; + } + + if (flags != 0) { + aOriginAttributes.StripAttributes(flags); + } +} + +void OriginAppendOASuffix(OriginAttributes aOriginAttributes, + bool aForceStripOA, nsACString& aOrigin) { + MaybeStripOAs(aForceStripOA, aOriginAttributes); + + nsAutoCString oaSuffix; + aOriginAttributes.CreateSuffix(oaSuffix); + aOrigin.Append(oaSuffix); +} + +nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA, + nsACString& aOrigin) { + nsresult rv = aPrincipal->GetOriginNoSuffix(aOrigin); + // The principal may belong to the about:blank content viewer, so this can be + // expected to fail. + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString suffix; + rv = aPrincipal->GetOriginSuffix(suffix); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(suffix)) { + return NS_ERROR_FAILURE; + } + + OriginAppendOASuffix(attrs, aForceStripOA, aOrigin); + + return NS_OK; +} + +nsresult GetOriginFromURIAndOA(nsIURI* aURI, + const OriginAttributes* aOriginAttributes, + bool aForceStripOA, nsACString& aOrigin) { + nsAutoCString origin(aOrigin); + nsresult rv = ContentPrincipal::GenerateOriginNoSuffixFromURI(aURI, origin); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAppendOASuffix(*aOriginAttributes, aForceStripOA, origin); + + aOrigin = origin; + + return NS_OK; +} + +nsresult GetPrincipalFromOrigin(const nsACString& aOrigin, bool aForceStripOA, + nsIPrincipal** aPrincipal) { + nsAutoCString originNoSuffix; + OriginAttributes attrs; + if (!attrs.PopulateFromOrigin(aOrigin, originNoSuffix)) { + return NS_ERROR_FAILURE; + } + + MaybeStripOAs(aForceStripOA, attrs); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + principal.forget(aPrincipal); + return NS_OK; +} + +nsresult GetPrincipal(nsIURI* aURI, bool aIsInIsolatedMozBrowserElement, + nsIPrincipal** aPrincipal) { + OriginAttributes attrs(aIsInIsolatedMozBrowserElement); + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aURI, attrs); + NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); + + principal.forget(aPrincipal); + return NS_OK; +} + +nsresult GetPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal) { + OriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aURI, attrs); + NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); + + principal.forget(aPrincipal); + return NS_OK; +} + +nsCString GetNextSubDomainForHost(const nsACString& aHost) { + nsCString subDomain; + nsresult rv = + nsEffectiveTLDService::GetInstance()->GetNextSubDomain(aHost, subDomain); + // We can fail if there is no more subdomain or if the host can't have a + // subdomain. + if (NS_FAILED(rv)) { + return ""_ns; + } + + return subDomain; +} + +// This function produces a nsIURI which is identical to the current +// nsIURI, except that it has one less subdomain segment. It returns +// `nullptr` if there are no more segments to remove. +already_AddRefed<nsIURI> GetNextSubDomainURI(nsIURI* aURI) { + nsAutoCString host; + nsresult rv = aURI->GetHost(host); + if (NS_FAILED(rv)) { + return nullptr; + } + + nsCString domain = GetNextSubDomainForHost(host); + if (domain.IsEmpty()) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + rv = NS_MutateURI(aURI).SetHost(domain).Finalize(uri); + if (NS_FAILED(rv) || !uri) { + return nullptr; + } + + return uri.forget(); +} + +nsresult UpgradeHostToOriginAndInsert( + const nsACString& aHost, const nsCString& aType, uint32_t aPermission, + uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime, + bool aIsInIsolatedMozBrowserElement, + std::function<nsresult(const nsACString& aOrigin, const nsCString& aType, + uint32_t aPermission, uint32_t aExpireType, + int64_t aExpireTime, int64_t aModificationTime)>&& + aCallback) { + if (aHost.EqualsLiteral("<file>")) { + // We no longer support the magic host <file> + NS_WARNING( + "The magic host <file> is no longer supported. " + "It is being removed from the permissions database."); + return NS_OK; + } + + // First, we check to see if the host is a valid URI. If it is, it can be + // imported directly + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aHost); + if (NS_SUCCEEDED(rv)) { + // It was previously possible to insert useless entries to your permissions + // database for URIs which have a null principal. This acts as a cleanup, + // getting rid of these useless database entries + if (uri->SchemeIs("moz-nullprincipal")) { + NS_WARNING("A moz-nullprincipal: permission is being discarded."); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal; + rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement, + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString origin; + rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType), + origin); + NS_ENSURE_SUCCESS(rv, rv); + + aCallback(origin, aType, aPermission, aExpireType, aExpireTime, + aModificationTime); + return NS_OK; + } + + // The user may use this host at non-standard ports or protocols, we can use + // their history to guess what ports and protocols we want to add permissions + // for. We find every URI which they have visited with this host (or a + // subdomain of this host), and try to add it as a principal. + bool foundHistory = false; + + nsCOMPtr<nsINavHistoryService> histSrv = + do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); + + if (histSrv) { + nsCOMPtr<nsINavHistoryQuery> histQuery; + rv = histSrv->GetNewQuery(getter_AddRefs(histQuery)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the eTLD+1 of the domain + nsAutoCString eTLD1; + rv = nsEffectiveTLDService::GetInstance()->GetBaseDomainFromHost(aHost, 0, + eTLD1); + + if (NS_FAILED(rv)) { + // If the lookup on the tldService for the base domain for the host + // failed, that means that we just want to directly use the host as the + // host name for the lookup. + eTLD1 = aHost; + } + + // We want to only find history items for this particular eTLD+1, and + // subdomains + rv = histQuery->SetDomain(eTLD1); + NS_ENSURE_SUCCESS(rv, rv); + + rv = histQuery->SetDomainIsHost(false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts; + rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts)); + NS_ENSURE_SUCCESS(rv, rv); + + // We want to get the URIs for every item in the user's history with the + // given host + rv = + histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_URI); + NS_ENSURE_SUCCESS(rv, rv); + + // We only search history, because searching both bookmarks and history + // is not supported, and history tends to be more comprehensive. + rv = histQueryOpts->SetQueryType( + nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY); + NS_ENSURE_SUCCESS(rv, rv); + + // We include hidden URIs (such as those visited via iFrames) as they may + // have permissions too + rv = histQueryOpts->SetIncludeHidden(true); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryResult> histResult; + rv = histSrv->ExecuteQuery(histQuery, histQueryOpts, + getter_AddRefs(histResult)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryContainerResultNode> histResultContainer; + rv = histResult->GetRoot(getter_AddRefs(histResultContainer)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = histResultContainer->SetContainerOpen(true); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t childCount = 0; + rv = histResultContainer->GetChildCount(&childCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsTHashtable<nsCStringHashKey> insertedOrigins; + for (uint32_t i = 0; i < childCount; i++) { + nsCOMPtr<nsINavHistoryResultNode> child; + histResultContainer->GetChild(i, getter_AddRefs(child)); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + uint32_t type; + rv = child->GetType(&type); + if (NS_WARN_IF(NS_FAILED(rv)) || + type != nsINavHistoryResultNode::RESULT_TYPE_URI) { + NS_WARNING( + "Unexpected non-RESULT_TYPE_URI node in " + "UpgradeHostToOriginAndInsert()"); + continue; + } + + nsAutoCString uriSpec; + rv = child->GetUri(uriSpec); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + // Use the provided host - this URI may be for a subdomain, rather than + // the host we care about. + rv = NS_MutateURI(uri).SetHost(aHost).Finalize(uri); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + // We now have a URI which we can make a nsIPrincipal out of + nsCOMPtr<nsIPrincipal> principal; + rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement, + getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + nsAutoCString origin; + rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType), + origin); + if (NS_WARN_IF(NS_FAILED(rv))) continue; + + // Ensure that we don't insert the same origin repeatedly + if (insertedOrigins.Contains(origin)) { + continue; + } + + foundHistory = true; + rv = aCallback(origin, aType, aPermission, aExpireType, aExpireTime, + aModificationTime); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Insert failed"); + insertedOrigins.PutEntry(origin); + } + + rv = histResultContainer->SetContainerOpen(false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If we didn't find any origins for this host in the poermissions database, + // we can insert the default http:// and https:// permissions into the + // database. This has a relatively high likelihood of applying the permission + // to the correct origin. + if (!foundHistory) { + nsAutoCString hostSegment; + nsCOMPtr<nsIPrincipal> principal; + nsAutoCString origin; + + // If this is an ipv6 URI, we need to surround it in '[', ']' before trying + // to parse it as a URI. + if (aHost.FindChar(':') != -1) { + hostSegment.AssignLiteral("["); + hostSegment.Append(aHost); + hostSegment.AppendLiteral("]"); + } else { + hostSegment.Assign(aHost); + } + + // http:// URI default + rv = NS_NewURI(getter_AddRefs(uri), "http://"_ns + hostSegment); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement, + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType), + origin); + NS_ENSURE_SUCCESS(rv, rv); + + aCallback(origin, aType, aPermission, aExpireType, aExpireTime, + aModificationTime); + + // https:// URI default + rv = NS_NewURI(getter_AddRefs(uri), "https://"_ns + hostSegment); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetPrincipal(uri, aIsInIsolatedMozBrowserElement, + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetOriginFromPrincipal(principal, IsOAForceStripPermission(aType), + origin); + NS_ENSURE_SUCCESS(rv, rv); + + aCallback(origin, aType, aPermission, aExpireType, aExpireTime, + aModificationTime); + } + + return NS_OK; +} + +bool IsExpandedPrincipal(nsIPrincipal* aPrincipal) { + nsCOMPtr<nsIExpandedPrincipal> ep = do_QueryInterface(aPrincipal); + return !!ep; +} + +// We only want to persist permissions which don't have session or policy +// expiration. +bool IsPersistentExpire(uint32_t aExpire, const nsACString& aType) { + bool res = (aExpire != nsIPermissionManager::EXPIRE_SESSION && + aExpire != nsIPermissionManager::EXPIRE_POLICY); +#ifdef ANDROID + for (const auto& perm : kGeckoViewRestrictedPermissions) { + res = res && !perm.Equals(aType); + } +#endif + return res; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +PermissionManager::PermissionKey* +PermissionManager::PermissionKey::CreateFromPrincipal(nsIPrincipal* aPrincipal, + bool aForceStripOA, + nsresult& aResult) { + nsAutoCString origin; + aResult = GetOriginFromPrincipal(aPrincipal, aForceStripOA, origin); + if (NS_WARN_IF(NS_FAILED(aResult))) { + return nullptr; + } + + return new PermissionKey(origin); +} + +PermissionManager::PermissionKey* +PermissionManager::PermissionKey::CreateFromURIAndOriginAttributes( + nsIURI* aURI, const OriginAttributes* aOriginAttributes, bool aForceStripOA, + nsresult& aResult) { + nsAutoCString origin; + aResult = + GetOriginFromURIAndOA(aURI, aOriginAttributes, aForceStripOA, origin); + if (NS_WARN_IF(NS_FAILED(aResult))) { + return nullptr; + } + + return new PermissionKey(origin); +} + +PermissionManager::PermissionKey* +PermissionManager::PermissionKey::CreateFromURI(nsIURI* aURI, + nsresult& aResult) { + nsAutoCString origin; + aResult = ContentPrincipal::GenerateOriginNoSuffixFromURI(aURI, origin); + if (NS_WARN_IF(NS_FAILED(aResult))) { + return nullptr; + } + + return new PermissionKey(origin); +} + +/* static */ +void PermissionManager::Startup() { + nsCOMPtr<nsIPermissionManager> permManager = + do_GetService("@mozilla.org/permissionmanager;1"); +} + +//////////////////////////////////////////////////////////////////////////////// +// PermissionManager Implementation + +NS_IMPL_ISUPPORTS(PermissionManager, nsIPermissionManager, nsIObserver, + nsISupportsWeakReference, nsIAsyncShutdownBlocker) + +PermissionManager::PermissionManager() + : mMonitor("PermissionManager::mMonitor"), + mState(eInitializing), + mMemoryOnlyDB(false), + mBlockerAdded(false), + mLargestID(0) {} + +PermissionManager::~PermissionManager() { + // NOTE: Make sure to reject each of the promises in mPermissionKeyPromiseMap + // before destroying. + for (auto iter = mPermissionKeyPromiseMap.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data()) { + iter.Data()->Reject(NS_ERROR_FAILURE, __func__); + } + } + mPermissionKeyPromiseMap.Clear(); + + if (mThread) { + mThread->Shutdown(); + mThread = nullptr; + } +} + +// static +already_AddRefed<nsIPermissionManager> PermissionManager::GetXPCOMSingleton() { + if (gPermissionManager) { + return do_AddRef(gPermissionManager); + } + + // Create a new singleton PermissionManager. + // We AddRef only once since XPCOM has rules about the ordering of module + // teardowns - by the time our module destructor is called, it's too late to + // Release our members, since GC cycles have already been completed and + // would result in serious leaks. + // See bug 209571. + auto permManager = MakeRefPtr<PermissionManager>(); + if (NS_SUCCEEDED(permManager->Init())) { + gPermissionManager = permManager.get(); + return permManager.forget(); + } + + return nullptr; +} + +// static +PermissionManager* PermissionManager::GetInstance() { + if (!gPermissionManager) { + // Hand off the creation of the permission manager to GetXPCOMSingleton. + nsCOMPtr<nsIPermissionManager> permManager = GetXPCOMSingleton(); + } + + return gPermissionManager; +} + +nsresult PermissionManager::Init() { + // If the 'permissions.memory_only' pref is set to true, then don't write any + // permission settings to disk, but keep them in a memory-only database. + mMemoryOnlyDB = Preferences::GetBool("permissions.memory_only", false); + + nsresult rv; + nsCOMPtr<nsIPrefService> prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefService->GetBranch("permissions.default.", + getter_AddRefs(mDefaultPrefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsChildProcess()) { + // Stop here; we don't need the DB in the child process. Instead we will be + // sent permissions as we need them by our parent process. + mState = eReady; + + // We use ClearOnShutdown on the content process only because on the parent + // process we need to block the shutdown for the final closeDB() call. + ClearOnShutdown(&gPermissionManager); + return NS_OK; + } + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "profile-before-change", true); + observerService->AddObserver(this, "profile-do-change", true); + observerService->AddObserver(this, "testonly-reload-permissions-from-disk", + true); + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIAsyncShutdownClient> asc = GetShutdownPhase(); + if (asc) { + nsAutoString blockerName; + MOZ_ALWAYS_SUCCEEDS(GetName(blockerName)); + + // This method can fail during some xpcshell-tests. + nsresult rv = + asc->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), + __LINE__, blockerName); + Unused << NS_WARN_IF(NS_FAILED(rv)); + if (NS_SUCCEEDED(rv)) { + mBlockerAdded = true; + } + } + + if (!mBlockerAdded) { + ClearOnShutdown(&gPermissionManager); + } + } + + AddIdleDailyMaintenanceJob(); + + MOZ_ASSERT(!mThread); + NS_ENSURE_SUCCESS(NS_NewNamedThread("Permission", getter_AddRefs(mThread)), + NS_ERROR_FAILURE); + + PRThread* prThread; + MOZ_ALWAYS_SUCCEEDS(mThread->GetPRThread(&prThread)); + MOZ_ASSERT(prThread); + + mThreadBoundData.Transfer(prThread); + + InitDB(false); + + return NS_OK; +} + +nsresult PermissionManager::OpenDatabase(nsIFile* aPermissionsFile) { + MOZ_ASSERT(!NS_IsMainThread()); + auto data = mThreadBoundData.Access(); + + nsresult rv; + nsCOMPtr<mozIStorageService> storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + if (!storage) { + return NS_ERROR_UNEXPECTED; + } + // cache a connection to the hosts database + if (mMemoryOnlyDB) { + rv = + storage->OpenSpecialDatabase(kMozStorageMemoryStorageKey, VoidCString(), + getter_AddRefs(data->mDBConn)); + } else { + rv = storage->OpenDatabase(aPermissionsFile, getter_AddRefs(data->mDBConn)); + } + return rv; +} + +void PermissionManager::InitDB(bool aRemoveFile) { + mState = eInitializing; + + { + MonitorAutoLock lock(mMonitor); + mReadEntries.Clear(); + } + + auto readyIfFailed = MakeScopeExit([&]() { + // ignore failure here, since it's non-fatal (we can run fine without + // persistent storage - e.g. if there's no profile). + // XXX should we tell the user about this? + mState = eReady; + }); + + if (!mPermissionsFile) { + nsresult rv = NS_GetSpecialDirectory(NS_APP_PERMISSION_PARENT_DIR, + getter_AddRefs(mPermissionsFile)); + if (NS_FAILED(rv)) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mPermissionsFile)); + if (NS_FAILED(rv)) { + return; + } + } + + rv = + mPermissionsFile->AppendNative(nsLiteralCString(PERMISSIONS_FILE_NAME)); + NS_ENSURE_SUCCESS_VOID(rv); + } + + nsCOMPtr<nsIInputStream> defaultsInputStream = GetDefaultsInputStream(); + + RefPtr<PermissionManager> self = this; + mThread->Dispatch(NS_NewRunnableFunction( + "PermissionManager::InitDB", [self, aRemoveFile, defaultsInputStream] { + nsresult rv = self->TryInitDB(aRemoveFile, defaultsInputStream); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + // This extra runnable calls EnsureReadCompleted to finialize the + // initialization. If there is something blocked by the monitor, it will + // be NOP. + NS_DispatchToMainThread( + NS_NewRunnableFunction("PermissionManager::InitDB-MainThread", + [self] { self->EnsureReadCompleted(); })); + + self->mMonitor.Notify(); + })); + + readyIfFailed.release(); +} + +nsresult PermissionManager::TryInitDB(bool aRemoveFile, + nsIInputStream* aDefaultsInputStream) { + MOZ_ASSERT(!NS_IsMainThread()); + + MonitorAutoLock lock(mMonitor); + + auto raii = MakeScopeExit([&]() { + if (aDefaultsInputStream) { + aDefaultsInputStream->Close(); + } + + mState = eDBInitialized; + }); + + auto data = mThreadBoundData.Access(); + + auto raiiFailure = MakeScopeExit([&]() { + if (data->mDBConn) { + DebugOnly<nsresult> rv = data->mDBConn->Close(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + data->mDBConn = nullptr; + } + }); + + nsresult rv; + + if (aRemoveFile) { + bool exists = false; + rv = mPermissionsFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (exists) { + rv = mPermissionsFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + rv = OpenDatabase(mPermissionsFile); + if (rv == NS_ERROR_FILE_CORRUPTED) { + LogToConsole(u"permissions.sqlite is corrupted! Try again!"_ns); + + // Add telemetry probe + Telemetry::Accumulate(Telemetry::PERMISSIONS_SQL_CORRUPTED, 1); + + // delete corrupted permissions.sqlite and try again + rv = mPermissionsFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + LogToConsole(u"Corrupted permissions.sqlite has been removed."_ns); + + rv = OpenDatabase(mPermissionsFile); + NS_ENSURE_SUCCESS(rv, rv); + LogToConsole(u"OpenDatabase to permissions.sqlite is successful!"_ns); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool ready; + data->mDBConn->GetConnectionReady(&ready); + if (!ready) { + LogToConsole(nsLiteralString( + u"Fail to get connection to permissions.sqlite! Try again!")); + + // delete and try again + rv = mPermissionsFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + LogToConsole(u"Defective permissions.sqlite has been removed."_ns); + + // Add telemetry probe + Telemetry::Accumulate(Telemetry::DEFECTIVE_PERMISSIONS_SQL_REMOVED, 1); + + rv = OpenDatabase(mPermissionsFile); + NS_ENSURE_SUCCESS(rv, rv); + LogToConsole(u"OpenDatabase to permissions.sqlite is successful!"_ns); + + data->mDBConn->GetConnectionReady(&ready); + if (!ready) return NS_ERROR_UNEXPECTED; + } + + bool tableExists = false; + data->mDBConn->TableExists("moz_perms"_ns, &tableExists); + if (!tableExists) { + data->mDBConn->TableExists("moz_hosts"_ns, &tableExists); + } + if (!tableExists) { + rv = CreateTable(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // table already exists; check the schema version before reading + int32_t dbSchemaVersion; + rv = data->mDBConn->GetSchemaVersion(&dbSchemaVersion); + NS_ENSURE_SUCCESS(rv, rv); + + switch (dbSchemaVersion) { + // upgrading. + // every time you increment the database schema, you need to + // implement the upgrading code from the previous version to the + // new one. fall through to current version + + case 1: { + // previous non-expiry version of database. Upgrade it by adding + // the expiration columns + rv = data->mDBConn->ExecuteSimpleSQL( + "ALTER TABLE moz_hosts ADD expireType INTEGER"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->ExecuteSimpleSQL( + "ALTER TABLE moz_hosts ADD expireTime INTEGER"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // TODO: we want to make default version as version 2 in order to + // fix bug 784875. + case 0: + case 2: { + // Add appId/isInBrowserElement fields. + rv = data->mDBConn->ExecuteSimpleSQL( + "ALTER TABLE moz_hosts ADD appId INTEGER"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_hosts ADD isInBrowserElement INTEGER")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->SetSchemaVersion(3); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // Version 3->4 is the creation of the modificationTime field. + case 3: { + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_hosts ADD modificationTime INTEGER")); + NS_ENSURE_SUCCESS(rv, rv); + + // We leave the modificationTime at zero for all existing records; + // using now() would mean, eg, that doing "remove all from the + // last hour" within the first hour after migration would remove + // all permissions. + + rv = data->mDBConn->SetSchemaVersion(4); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // In version 5, host appId, and isInBrowserElement were merged into + // a single origin entry + // + // In version 6, the tables were renamed for backwards compatability + // reasons with version 4 and earlier. + // + // In version 7, a bug in the migration used for version 4->5 was + // discovered which could have triggered data-loss. Because of that, + // all users with a version 4, 5, or 6 database will be re-migrated + // from the backup database. (bug 1186034). This migration bug is + // not present after bug 1185340, and the re-migration ensures that + // all users have the fix. + case 5: + // This branch could also be reached via dbSchemaVersion == 3, in + // which case we want to fall through to the dbSchemaVersion == 4 + // case. The easiest way to do that is to perform this extra check + // here to make sure that we didn't get here via a fallthrough + // from v3 + if (dbSchemaVersion == 5) { + // In version 5, the backup database is named moz_hosts_v4. We + // perform the version 5->6 migration to get the tables to have + // consistent naming conventions. + + // Version 5->6 is the renaming of moz_hosts to moz_perms, and + // moz_hosts_v4 to moz_hosts (bug 1185343) + // + // In version 5, we performed the modifications to the + // permissions database in place, this meant that if you + // upgraded to a version which used V5, and then downgraded to a + // version which used v4 or earlier, the fallback path would + // drop the table, and your permissions data would be lost. This + // migration undoes that mistake, by restoring the old moz_hosts + // table (if it was present), and instead using the new table + // moz_perms for the new permissions schema. + // + // NOTE: If you downgrade, store new permissions, and then + // upgrade again, these new permissions won't be migrated or + // reflected in the updated database. This migration only occurs + // once, as if moz_perms exists, it will skip creating it. In + // addition, permissions added after the migration will not be + // visible in previous versions of firefox. + + bool permsTableExists = false; + data->mDBConn->TableExists("moz_perms"_ns, &permsTableExists); + if (!permsTableExists) { + // Move the upgraded database to moz_perms + rv = data->mDBConn->ExecuteSimpleSQL( + "ALTER TABLE moz_hosts RENAME TO moz_perms"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_WARNING( + "moz_hosts was not renamed to moz_perms, " + "as a moz_perms table already exists"); + + // In the situation where a moz_perms table already exists, + // but the schema is lower than 6, a migration has already + // previously occured to V6, but a downgrade has caused the + // moz_hosts table to be dropped. This should only occur in + // the case of a downgrade to a V5 database, which was only + // present in a few day's nightlies. As that version was + // likely used only on a temporary basis, we assume that the + // database from the previous V6 has the permissions which the + // user actually wants to use. We have to get rid of moz_hosts + // such that moz_hosts_v4 can be moved into its place if it + // exists. + rv = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_hosts"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + +#ifdef DEBUG + // The moz_hosts table shouldn't exist anymore + bool hostsTableExists = false; + data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists); + MOZ_ASSERT(!hostsTableExists); +#endif + + // Rename moz_hosts_v4 back to it's original location, if it + // exists + bool v4TableExists = false; + data->mDBConn->TableExists("moz_hosts_v4"_ns, &v4TableExists); + if (v4TableExists) { + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_hosts_v4 RENAME TO moz_hosts")); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = data->mDBConn->SetSchemaVersion(6); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // At this point, the version 5 table has been migrated to a version + // 6 table We are guaranteed to have at least one of moz_hosts and + // moz_perms. If we have moz_hosts, we will migrate moz_hosts into + // moz_perms (even if we already have a moz_perms, as we need a + // re-migration due to bug 1186034). + // + // After this migration, we are guaranteed to have both a moz_hosts + // (for backwards compatability), and a moz_perms table. The + // moz_hosts table will have a v4 schema, and the moz_perms table + // will have a v6 schema. + case 4: + case 6: { + bool hostsTableExists = false; + data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists); + if (hostsTableExists) { + // Both versions 4 and 6 have a version 4 formatted hosts table + // named moz_hosts. We can migrate this table to our version 7 + // table moz_perms. If moz_perms is present, then we can use it + // as a basis for comparison. + + rv = data->mDBConn->BeginTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + bool tableExists = false; + data->mDBConn->TableExists("moz_hosts_new"_ns, &tableExists); + if (tableExists) { + NS_WARNING( + "The temporary database moz_hosts_new already exists, " + "dropping " + "it."); + rv = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_hosts_new"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = data->mDBConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_hosts_new (" + " id INTEGER PRIMARY KEY" + ",origin TEXT" + ",type TEXT" + ",permission INTEGER" + ",expireType INTEGER" + ",expireTime INTEGER" + ",modificationTime INTEGER" + ")")); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageStatement> stmt; + rv = data->mDBConn->CreateStatement( + nsLiteralCString( + "SELECT host, type, permission, expireType, " + "expireTime, " + "modificationTime, isInBrowserElement FROM moz_hosts"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t id = 0; + bool hasResult; + + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + MigrationEntry entry; + + // Read in the old row + rv = stmt->GetUTF8String(0, entry.mHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + rv = stmt->GetUTF8String(1, entry.mType); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + entry.mId = id++; + entry.mPermission = stmt->AsInt32(2); + entry.mExpireType = stmt->AsInt32(3); + entry.mExpireTime = stmt->AsInt64(4); + entry.mModificationTime = stmt->AsInt64(5); + entry.mIsInBrowserElement = static_cast<bool>(stmt->AsInt32(6)); + + mMigrationEntries.AppendElement(entry); + } + + // We don't drop the moz_hosts table such that it is available + // for backwards-compatability and for future migrations in case + // of migration errors in the current code. Create a marker + // empty table which will indicate that the moz_hosts table is + // intended to act as a backup. If this table is not present, + // then the moz_hosts table was created as a random empty table. + rv = data->mDBConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_hosts_is_backup (dummy " + "INTEGER PRIMARY KEY)")); + NS_ENSURE_SUCCESS(rv, rv); + + bool permsTableExists = false; + data->mDBConn->TableExists("moz_perms"_ns, &permsTableExists); + if (permsTableExists) { + // The user already had a moz_perms table, and we are + // performing a re-migration. We count the rows in the old + // table for telemetry, and then back up their old database as + // moz_perms_v6 + + nsCOMPtr<mozIStorageStatement> countStmt; + rv = data->mDBConn->CreateStatement( + "SELECT COUNT(*) FROM moz_perms"_ns, getter_AddRefs(countStmt)); + bool hasResult = false; + if (NS_FAILED(rv) || + NS_FAILED(countStmt->ExecuteStep(&hasResult)) || !hasResult) { + NS_WARNING("Could not count the rows in moz_perms"); + } + + // Back up the old moz_perms database as moz_perms_v6 before + // we move the new table into its position + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_perms RENAME TO moz_perms_v6")); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_hosts_new RENAME TO moz_perms")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->CommitTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // We don't have a moz_hosts table, so we create one for + // downgrading purposes. This table is empty. + rv = data->mDBConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_hosts (" + " id INTEGER PRIMARY KEY" + ",host TEXT" + ",type TEXT" + ",permission INTEGER" + ",expireType INTEGER" + ",expireTime INTEGER" + ",modificationTime INTEGER" + ",appId INTEGER" + ",isInBrowserElement INTEGER" + ")")); + NS_ENSURE_SUCCESS(rv, rv); + + // We are guaranteed to have a moz_perms table at this point. + } + +#ifdef DEBUG + { + // At this point, both the moz_hosts and moz_perms tables should + // exist + bool hostsTableExists = false; + bool permsTableExists = false; + data->mDBConn->TableExists("moz_hosts"_ns, &hostsTableExists); + data->mDBConn->TableExists("moz_perms"_ns, &permsTableExists); + MOZ_ASSERT(hostsTableExists && permsTableExists); + } +#endif + + rv = data->mDBConn->SetSchemaVersion(7); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // The version 7-8 migration is the re-migration of localhost and + // ip-address entries due to errors in the previous version 7 + // migration which caused localhost and ip-address entries to be + // incorrectly discarded. The version 7 migration logic has been + // corrected, and thus this logic only needs to execute if the user + // is currently on version 7. + case 7: { + // This migration will be relatively expensive as we need to + // perform database lookups for each origin which we want to + // insert. Fortunately, it shouldn't be too expensive as we only + // want to insert a small number of entries created for localhost + // or IP addresses. + + // We only want to perform the re-migration if moz_hosts is a + // backup + bool hostsIsBackupExists = false; + data->mDBConn->TableExists("moz_hosts_is_backup"_ns, + &hostsIsBackupExists); + + // Only perform this migration if the original schema version was + // 7, and the moz_hosts table is a backup. + if (dbSchemaVersion == 7 && hostsIsBackupExists) { + nsCOMPtr<mozIStorageStatement> stmt; + rv = data->mDBConn->CreateStatement( + nsLiteralCString( + "SELECT host, type, permission, expireType, " + "expireTime, " + "modificationTime, isInBrowserElement FROM moz_hosts"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageStatement> idStmt; + rv = data->mDBConn->CreateStatement( + "SELECT MAX(id) FROM moz_hosts"_ns, getter_AddRefs(idStmt)); + + int64_t id = 0; + bool hasResult = false; + if (NS_SUCCEEDED(rv) && + NS_SUCCEEDED(idStmt->ExecuteStep(&hasResult)) && hasResult) { + id = idStmt->AsInt32(0) + 1; + } + + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + MigrationEntry entry; + + // Read in the old row + rv = stmt->GetUTF8String(0, entry.mHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString eTLD1; + rv = nsEffectiveTLDService::GetInstance()->GetBaseDomainFromHost( + entry.mHost, 0, eTLD1); + if (NS_SUCCEEDED(rv)) { + // We only care about entries which the tldService can't + // handle + continue; + } + + rv = stmt->GetUTF8String(1, entry.mType); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + entry.mId = id++; + entry.mPermission = stmt->AsInt32(2); + entry.mExpireType = stmt->AsInt32(3); + entry.mExpireTime = stmt->AsInt64(4); + entry.mModificationTime = stmt->AsInt64(5); + entry.mIsInBrowserElement = static_cast<bool>(stmt->AsInt32(6)); + + mMigrationEntries.AppendElement(entry); + } + } + + // Even if we didn't perform the migration, we want to bump the + // schema version to 8. + rv = data->mDBConn->SetSchemaVersion(8); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // The version 8-9 migration removes the unnecessary backup + // moz-hosts database contents. as the data no longer needs to be + // migrated + case 8: { + // We only want to clear out the old table if it is a backup. If + // it isn't a backup, we don't need to touch it. + bool hostsIsBackupExists = false; + data->mDBConn->TableExists("moz_hosts_is_backup"_ns, + &hostsIsBackupExists); + if (hostsIsBackupExists) { + // Delete everything from the backup, we want to keep around the + // table so that you can still downgrade and not break things, + // but we don't need to keep the rows around. + rv = data->mDBConn->ExecuteSimpleSQL("DELETE FROM moz_hosts"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // The table is no longer a backup, so get rid of it. + rv = data->mDBConn->ExecuteSimpleSQL( + "DROP TABLE moz_hosts_is_backup"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = data->mDBConn->SetSchemaVersion(9); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + case 9: { + rv = data->mDBConn->SetSchemaVersion(10); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + case 10: { + // Filter out the rows with storage access API permissions with a + // granted origin, and remove the granted origin part from the + // permission type. + rv = data->mDBConn->ExecuteSimpleSQL(nsLiteralCString( + "UPDATE moz_perms " + "SET type=SUBSTR(type, 0, INSTR(SUBSTR(type, INSTR(type, " + "'^') + " + "1), '^') + INSTR(type, '^')) " + "WHERE INSTR(SUBSTR(type, INSTR(type, '^') + 1), '^') AND " + "SUBSTR(type, 0, 18) == \"storageAccessAPI^\";")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + } + + // fall through to the next upgrade + [[fallthrough]]; + + // current version. + case HOSTS_SCHEMA_VERSION: + break; + + // downgrading. + // if columns have been added to the table, we can still use the + // ones we understand safely. if columns have been deleted or + // altered, just blow away the table and start from scratch! if you + // change the way a column is interpreted, make sure you also change + // its name so this check will catch it. + default: { + // check if all the expected columns exist + nsCOMPtr<mozIStorageStatement> stmt; + rv = data->mDBConn->CreateStatement( + nsLiteralCString("SELECT origin, type, permission, " + "expireType, expireTime, " + "modificationTime FROM moz_perms"), + getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) break; + + // our columns aren't there - drop the table! + rv = data->mDBConn->ExecuteSimpleSQL("DROP TABLE moz_perms"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateTable(); + NS_ENSURE_SUCCESS(rv, rv); + } break; + } + } + + // cache frequently used statements (for insertion, deletion, and + // updating) + rv = data->mDBConn->CreateStatement( + nsLiteralCString("INSERT INTO moz_perms " + "(id, origin, type, permission, expireType, " + "expireTime, modificationTime) " + "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), + getter_AddRefs(data->mStmtInsert)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->CreateStatement(nsLiteralCString("DELETE FROM moz_perms " + "WHERE id = ?1"), + getter_AddRefs(data->mStmtDelete)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = data->mDBConn->CreateStatement( + nsLiteralCString("UPDATE moz_perms " + "SET permission = ?2, expireType= ?3, expireTime = " + "?4, modificationTime = ?5 WHERE id = ?1"), + getter_AddRefs(data->mStmtUpdate)); + NS_ENSURE_SUCCESS(rv, rv); + + // Always import default permissions. + ConsumeDefaultsInputStream(aDefaultsInputStream, lock); + + // check whether to import or just read in the db + if (tableExists) { + rv = Read(lock); + NS_ENSURE_SUCCESS(rv, rv); + } + + raiiFailure.release(); + + return NS_OK; +} + +void PermissionManager::AddIdleDailyMaintenanceJob() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(observerService); + + nsresult rv = + observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, false); + NS_ENSURE_SUCCESS_VOID(rv); +} + +void PermissionManager::RemoveIdleDailyMaintenanceJob() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(observerService); + + nsresult rv = + observerService->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY); + NS_ENSURE_SUCCESS_VOID(rv); +} + +void PermissionManager::PerformIdleDailyMaintenance() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<PermissionManager> self = this; + mThread->Dispatch(NS_NewRunnableFunction( + "PermissionManager::PerformIdleDailyMaintenance", [self] { + auto data = self->mThreadBoundData.Access(); + + if (self->mState == eClosed || !data->mDBConn) { + return; + } + + nsCOMPtr<mozIStorageStatement> stmtDeleteExpired; + nsresult rv = data->mDBConn->CreateStatement( + nsLiteralCString("DELETE FROM moz_perms WHERE expireType = " + "?1 AND expireTime <= ?2"), + getter_AddRefs(stmtDeleteExpired)); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = stmtDeleteExpired->BindInt32ByIndex( + 0, nsIPermissionManager::EXPIRE_TIME); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = stmtDeleteExpired->BindInt64ByIndex(1, EXPIRY_NOW); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = stmtDeleteExpired->Execute(); + NS_ENSURE_SUCCESS_VOID(rv); + })); +} + +// sets the schema version and creates the moz_perms table. +nsresult PermissionManager::CreateTable() { + MOZ_ASSERT(!NS_IsMainThread()); + auto data = mThreadBoundData.Access(); + + // set the schema version, before creating the table + nsresult rv = data->mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION); + if (NS_FAILED(rv)) return rv; + + // create the table + // SQL also lives in automation.py.in. If you change this SQL change that + // one too + rv = data->mDBConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_perms (" + " id INTEGER PRIMARY KEY" + ",origin TEXT" + ",type TEXT" + ",permission INTEGER" + ",expireType INTEGER" + ",expireTime INTEGER" + ",modificationTime INTEGER" + ")")); + if (NS_FAILED(rv)) return rv; + + // We also create a legacy V4 table, for backwards compatability, + // and to ensure that downgrades don't trigger a schema version change. + return data->mDBConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_hosts (" + " id INTEGER PRIMARY KEY" + ",host TEXT" + ",type TEXT" + ",permission INTEGER" + ",expireType INTEGER" + ",expireTime INTEGER" + ",modificationTime INTEGER" + ",isInBrowserElement INTEGER" + ")")); +} + +// Returns whether the given combination of expire type and expire time are +// expired. Note that EXPIRE_SESSION only honors expireTime if it is nonzero. +bool PermissionManager::HasExpired(uint32_t aExpireType, int64_t aExpireTime) { + return (aExpireType == nsIPermissionManager::EXPIRE_TIME || + (aExpireType == nsIPermissionManager::EXPIRE_SESSION && + aExpireTime != 0)) && + aExpireTime <= EXPIRY_NOW; +} + +NS_IMETHODIMP +PermissionManager::AddFromPrincipal(nsIPrincipal* aPrincipal, + const nsACString& aType, + uint32_t aPermission, uint32_t aExpireType, + int64_t aExpireTime) { + ENSURE_NOT_CHILD_PROCESS; + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_TRUE(aExpireType == nsIPermissionManager::EXPIRE_NEVER || + aExpireType == nsIPermissionManager::EXPIRE_TIME || + aExpireType == nsIPermissionManager::EXPIRE_SESSION || + aExpireType == nsIPermissionManager::EXPIRE_POLICY, + NS_ERROR_INVALID_ARG); + + // Skip addition if the permission is already expired. + if (HasExpired(aExpireType, aExpireTime)) { + return NS_OK; + } + + // We don't add the system principal because it actually has no URI and we + // always allow action for them. + if (aPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + + // Null principals can't meaningfully have persisted permissions attached to + // them, so we don't allow adding permissions for them. + if (aPrincipal->GetIsNullPrincipal()) { + return NS_OK; + } + + // Permissions may not be added to expanded principals. + if (IsExpandedPrincipal(aPrincipal)) { + return NS_ERROR_INVALID_ARG; + } + + // A modificationTime of zero will cause AddInternal to use now(). + int64_t modificationTime = 0; + + return AddInternal(aPrincipal, aType, aPermission, 0, aExpireType, + aExpireTime, modificationTime, eNotify, eWriteToDB); +} + +nsresult PermissionManager::AddInternal( + nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aPermission, + int64_t aID, uint32_t aExpireType, int64_t aExpireTime, + int64_t aModificationTime, NotifyOperationType aNotifyOperation, + DBOperationType aDBOperation, const bool aIgnoreSessionPermissions, + const nsACString* aOriginString) { + MOZ_ASSERT(NS_IsMainThread()); + + EnsureReadCompleted(); + + nsresult rv = NS_OK; + nsAutoCString origin; + // Only attempt to compute the origin string when it is going to be needed + // later on in the function. + if (!IsChildProcess() || + (aDBOperation == eWriteToDB && IsPersistentExpire(aExpireType, aType))) { + if (aOriginString) { + // Use the origin string provided by the caller. + origin = *aOriginString; + } else { + // Compute it from the principal provided. + rv = GetOriginFromPrincipal(aPrincipal, IsOAForceStripPermission(aType), + origin); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // For private browsing only store permissions for the session + if (aExpireType != EXPIRE_SESSION) { + uint32_t privateBrowsingId = + nsScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + nsresult rv = aPrincipal->GetPrivateBrowsingId(&privateBrowsingId); + if (NS_SUCCEEDED(rv) && + privateBrowsingId != + nsScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + aExpireType = EXPIRE_SESSION; + } + } + + // Let's send the new permission to the content process only if it has to be + // notified. + if (!IsChildProcess() && aNotifyOperation == eNotify) { + IPC::Permission permission(origin, aType, aPermission, aExpireType, + aExpireTime); + + nsAutoCString permissionKey; + GetKeyForPermission(aPrincipal, aType, permissionKey); + + nsTArray<ContentParent*> cplist; + ContentParent::GetAll(cplist); + for (uint32_t i = 0; i < cplist.Length(); ++i) { + ContentParent* cp = cplist[i]; + if (cp->NeedsPermissionsUpdate(permissionKey)) + Unused << cp->SendAddPermission(permission); + } + } + + MOZ_ASSERT(PermissionAvailable(aPrincipal, aType)); + + // look up the type index + int32_t typeIndex = GetTypeIndex(aType, true); + NS_ENSURE_TRUE(typeIndex != -1, NS_ERROR_OUT_OF_MEMORY); + + // When an entry already exists, PutEntry will return that, instead + // of adding a new one + RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal( + aPrincipal, IsOAForceStripPermission(aType), rv); + if (!key) { + MOZ_ASSERT(NS_FAILED(rv)); + return rv; + } + + PermissionHashKey* entry = mPermissionTable.PutEntry(key); + if (!entry) return NS_ERROR_FAILURE; + if (!entry->GetKey()) { + mPermissionTable.RemoveEntry(entry); + return NS_ERROR_OUT_OF_MEMORY; + } + + // figure out the transaction type, and get any existing permission value + OperationType op; + int32_t index = entry->GetPermissionIndex(typeIndex); + if (index == -1) { + if (aPermission == nsIPermissionManager::UNKNOWN_ACTION) + op = eOperationNone; + else + op = eOperationAdding; + + } else { + PermissionEntry oldPermissionEntry = entry->GetPermissions()[index]; + + // remove the permission if the permission is UNKNOWN, update the + // permission if its value or expire type have changed OR if the time has + // changed and the expire type is time, otherwise, don't modify. There's + // no need to modify a permission that doesn't expire with time when the + // only thing changed is the expire time. + if (aPermission == oldPermissionEntry.mPermission && + aExpireType == oldPermissionEntry.mExpireType && + (aExpireType == nsIPermissionManager::EXPIRE_NEVER || + aExpireTime == oldPermissionEntry.mExpireTime)) + op = eOperationNone; + else if (oldPermissionEntry.mID == cIDPermissionIsDefault) + // The existing permission is one added as a default and the new + // permission doesn't exactly match so we are replacing the default. This + // is true even if the new permission is UNKNOWN_ACTION (which means a + // "logical remove" of the default) + op = eOperationReplacingDefault; + else if (aID == cIDPermissionIsDefault) + // We are adding a default permission but a "real" permission already + // exists. This almost-certainly means we just did a removeAllSince and + // are re-importing defaults - so we can ignore this. + op = eOperationNone; + else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION) + op = eOperationRemoving; + else + op = eOperationChanging; + } + + // child processes should *always* be passed a modificationTime of zero. + MOZ_ASSERT(!IsChildProcess() || aModificationTime == 0); + + // do the work for adding, deleting, or changing a permission: + // update the in-memory list, write to the db, and notify consumers. + int64_t id; + if (aModificationTime == 0) { + aModificationTime = EXPIRY_NOW; + } + + switch (op) { + case eOperationNone: { + // nothing to do + return NS_OK; + } + + case eOperationAdding: { + if (aDBOperation == eWriteToDB) { + // we'll be writing to the database - generate a known unique id + id = ++mLargestID; + } else { + // we're reading from the database - use the id already assigned + id = aID; + } + + entry->GetPermissions().AppendElement( + PermissionEntry(id, typeIndex, aPermission, aExpireType, aExpireTime, + aModificationTime)); + + if (aDBOperation == eWriteToDB && + IsPersistentExpire(aExpireType, aType)) { + UpdateDB(op, id, origin, aType, aPermission, aExpireType, aExpireTime, + aModificationTime); + } + + if (aNotifyOperation == eNotify) { + NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], + aPermission, aExpireType, aExpireTime, + aModificationTime, u"added"); + } + + break; + } + + case eOperationRemoving: { + PermissionEntry oldPermissionEntry = entry->GetPermissions()[index]; + id = oldPermissionEntry.mID; + + // If the type we want to remove is EXPIRE_POLICY, we need to reject + // attempts to change the permission. + if (entry->GetPermissions()[index].mExpireType == EXPIRE_POLICY) { + NS_WARNING("Attempting to remove EXPIRE_POLICY permission"); + break; + } + + entry->GetPermissions().RemoveElementAt(index); + + if (aDBOperation == eWriteToDB) + // We care only about the id here so we pass dummy values for all other + // parameters. + UpdateDB(op, id, ""_ns, ""_ns, 0, nsIPermissionManager::EXPIRE_NEVER, 0, + 0); + + if (aNotifyOperation == eNotify) { + NotifyObserversWithPermission( + aPrincipal, mTypeArray[typeIndex], oldPermissionEntry.mPermission, + oldPermissionEntry.mExpireType, oldPermissionEntry.mExpireTime, + oldPermissionEntry.mModificationTime, u"deleted"); + } + + // If there are no more permissions stored for that entry, clear it. + if (entry->GetPermissions().IsEmpty()) { + mPermissionTable.RemoveEntry(entry); + } + + break; + } + + case eOperationChanging: { + id = entry->GetPermissions()[index].mID; + + // If the existing type is EXPIRE_POLICY, we need to reject attempts to + // change the permission. + if (entry->GetPermissions()[index].mExpireType == EXPIRE_POLICY) { + NS_WARNING("Attempting to modify EXPIRE_POLICY permission"); + break; + } + + PermissionEntry oldPermissionEntry = entry->GetPermissions()[index]; + + // If the new expireType is EXPIRE_SESSION, then we have to keep a + // copy of the previous permission/expireType values. This cached value + // will be used when restoring the permissions of an app. + if (entry->GetPermissions()[index].mExpireType != + nsIPermissionManager::EXPIRE_SESSION && + aExpireType == nsIPermissionManager::EXPIRE_SESSION) { + entry->GetPermissions()[index].mNonSessionPermission = + entry->GetPermissions()[index].mPermission; + entry->GetPermissions()[index].mNonSessionExpireType = + entry->GetPermissions()[index].mExpireType; + entry->GetPermissions()[index].mNonSessionExpireTime = + entry->GetPermissions()[index].mExpireTime; + } else if (aExpireType != nsIPermissionManager::EXPIRE_SESSION) { + entry->GetPermissions()[index].mNonSessionPermission = aPermission; + entry->GetPermissions()[index].mNonSessionExpireType = aExpireType; + entry->GetPermissions()[index].mNonSessionExpireTime = aExpireTime; + } + + entry->GetPermissions()[index].mPermission = aPermission; + entry->GetPermissions()[index].mExpireType = aExpireType; + entry->GetPermissions()[index].mExpireTime = aExpireTime; + entry->GetPermissions()[index].mModificationTime = aModificationTime; + + if (aDBOperation == eWriteToDB) { + bool newIsPersistentExpire = IsPersistentExpire(aExpireType, aType); + bool oldIsPersistentExpire = + IsPersistentExpire(oldPermissionEntry.mExpireType, aType); + + if (!newIsPersistentExpire && oldIsPersistentExpire) { + // Maybe we have to remove the previous permission if that was + // persistent. + UpdateDB(eOperationRemoving, id, ""_ns, ""_ns, 0, + nsIPermissionManager::EXPIRE_NEVER, 0, 0); + } else if (newIsPersistentExpire && !oldIsPersistentExpire) { + // It could also be that the previous permission was session-only but + // this needs to be written into the DB. In this case, we have to run + // an Adding operation. + UpdateDB(eOperationAdding, id, origin, aType, aPermission, + aExpireType, aExpireTime, aModificationTime); + } else if (newIsPersistentExpire) { + // This is the a simple update. We care only about the id, the + // permission and expireType/expireTime/modificationTime here. We pass + // dummy values for all other parameters. + UpdateDB(op, id, ""_ns, ""_ns, aPermission, aExpireType, aExpireTime, + aModificationTime); + } + } + + if (aNotifyOperation == eNotify) { + NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], + aPermission, aExpireType, aExpireTime, + aModificationTime, u"changed"); + } + + break; + } + case eOperationReplacingDefault: { + // this is handling the case when we have an existing permission + // entry that was created as a "default" (and thus isn't in the DB) with + // an explicit permission (that may include UNKNOWN_ACTION.) + // Note we will *not* get here if we are replacing an already replaced + // default value - that is handled as eOperationChanging. + + // So this is a hybrid of eOperationAdding (as we are writing a new entry + // to the DB) and eOperationChanging (as we are replacing the in-memory + // repr and sending a "changed" notification). + + // We want a new ID even if not writing to the DB, so the modified entry + // in memory doesn't have the magic cIDPermissionIsDefault value. + id = ++mLargestID; + + // The default permission being replaced can't have session expiry or + // policy expiry. + NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != + nsIPermissionManager::EXPIRE_SESSION, + NS_ERROR_UNEXPECTED); + NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != + nsIPermissionManager::EXPIRE_POLICY, + NS_ERROR_UNEXPECTED); + // We don't support the new entry having any expiry - supporting that + // would make things far more complex and none of the permissions we set + // as a default support that. + NS_ENSURE_TRUE(aExpireType == EXPIRE_NEVER, NS_ERROR_UNEXPECTED); + + // update the existing entry in memory. + entry->GetPermissions()[index].mID = id; + entry->GetPermissions()[index].mPermission = aPermission; + entry->GetPermissions()[index].mExpireType = aExpireType; + entry->GetPermissions()[index].mExpireTime = aExpireTime; + entry->GetPermissions()[index].mModificationTime = aModificationTime; + + // If requested, create the entry in the DB. + if (aDBOperation == eWriteToDB && + IsPersistentExpire(aExpireType, aType)) { + UpdateDB(eOperationAdding, id, origin, aType, aPermission, aExpireType, + aExpireTime, aModificationTime); + } + + if (aNotifyOperation == eNotify) { + NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], + aPermission, aExpireType, aExpireTime, + aModificationTime, u"changed"); + } + + } break; + } + + return NS_OK; +} + +NS_IMETHODIMP +PermissionManager::RemoveFromPrincipal(nsIPrincipal* aPrincipal, + const nsACString& aType) { + ENSURE_NOT_CHILD_PROCESS; + NS_ENSURE_ARG_POINTER(aPrincipal); + + // System principals are never added to the database, no need to remove them. + if (aPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + + // Permissions may not be added to expanded principals. + if (IsExpandedPrincipal(aPrincipal)) { + return NS_ERROR_INVALID_ARG; + } + + // AddInternal() handles removal, just let it do the work + return AddInternal(aPrincipal, aType, nsIPermissionManager::UNKNOWN_ACTION, 0, + nsIPermissionManager::EXPIRE_NEVER, 0, 0, eNotify, + eWriteToDB); +} + +NS_IMETHODIMP +PermissionManager::RemovePermission(nsIPermission* aPerm) { + if (!aPerm) { + return NS_OK; + } + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = aPerm->GetPrincipal(getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString type; + rv = aPerm->GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + // Permissions are uniquely identified by their principal and type. + // We remove the permission using these two pieces of data. + return RemoveFromPrincipal(principal, type); +} + +NS_IMETHODIMP +PermissionManager::RemoveAll() { + ENSURE_NOT_CHILD_PROCESS; + return RemoveAllInternal(true); +} + +NS_IMETHODIMP +PermissionManager::RemoveAllSince(int64_t aSince) { + ENSURE_NOT_CHILD_PROCESS; + return RemoveAllModifiedSince(aSince); +} + +template <class T> +nsresult PermissionManager::RemovePermissionEntries(T aCondition) { + EnsureReadCompleted(); + + Vector<Tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10> array; + for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { + PermissionHashKey* entry = iter.Get(); + for (const auto& permEntry : entry->GetPermissions()) { + if (!aCondition(permEntry)) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = GetPrincipalFromOrigin( + entry->GetKey()->mOrigin, + IsOAForceStripPermission(mTypeArray[permEntry.mType]), + getter_AddRefs(principal)); + if (NS_FAILED(rv)) { + continue; + } + + if (!array.emplaceBack(principal, mTypeArray[permEntry.mType], + entry->GetKey()->mOrigin)) { + continue; + } + } + } + + for (auto& i : array) { + // AddInternal handles removal, so let it do the work... + AddInternal(Get<0>(i), Get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0, + nsIPermissionManager::EXPIRE_NEVER, 0, 0, + PermissionManager::eNotify, PermissionManager::eWriteToDB, + false, &Get<2>(i)); + } + + // now re-import any defaults as they may now be required if we just deleted + // an override. + ImportLatestDefaults(); + return NS_OK; +} + +NS_IMETHODIMP +PermissionManager::RemoveByType(const nsACString& aType) { + ENSURE_NOT_CHILD_PROCESS; + + int32_t typeIndex = GetTypeIndex(aType, false); + // If type == -1, the type isn't known, + // so just return NS_OK + if (typeIndex == -1) { + return NS_OK; + } + + return RemovePermissionEntries( + [typeIndex](const PermissionEntry& aPermEntry) { + return static_cast<uint32_t>(typeIndex) == aPermEntry.mType; + }); +} + +NS_IMETHODIMP +PermissionManager::RemoveByTypeSince(const nsACString& aType, + int64_t aModificationTime) { + ENSURE_NOT_CHILD_PROCESS; + + int32_t typeIndex = GetTypeIndex(aType, false); + // If type == -1, the type isn't known, + // so just return NS_OK + if (typeIndex == -1) { + return NS_OK; + } + + return RemovePermissionEntries( + [typeIndex, aModificationTime](const PermissionEntry& aPermEntry) { + return uint32_t(typeIndex) == aPermEntry.mType && + aModificationTime <= aPermEntry.mModificationTime; + }); +} + +void PermissionManager::CloseDB(CloseDBNextOp aNextOp) { + EnsureReadCompleted(); + + mState = eClosed; + + nsCOMPtr<nsIInputStream> defaultsInputStream; + if (aNextOp == eRebuldOnSuccess) { + defaultsInputStream = GetDefaultsInputStream(); + } + + RefPtr<PermissionManager> self = this; + mThread->Dispatch(NS_NewRunnableFunction( + "PermissionManager::CloseDB", [self, aNextOp, defaultsInputStream] { + auto data = self->mThreadBoundData.Access(); + // Null the statements, this will finalize them. + data->mStmtInsert = nullptr; + data->mStmtDelete = nullptr; + data->mStmtUpdate = nullptr; + if (data->mDBConn) { + DebugOnly<nsresult> rv = data->mDBConn->Close(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + data->mDBConn = nullptr; + + if (aNextOp == eRebuldOnSuccess) { + self->TryInitDB(true, defaultsInputStream); + } + } + + if (aNextOp == eShutdown) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "PermissionManager::MaybeCompleteShutdown", + [self] { self->MaybeCompleteShutdown(); })); + } + })); +} + +nsresult PermissionManager::RemoveAllFromIPC() { + MOZ_ASSERT(IsChildProcess()); + + // Remove from memory and notify immediately. Since the in-memory + // database is authoritative, we do not need confirmation from the + // on-disk database to notify observers. + RemoveAllFromMemory(); + + return NS_OK; +} + +nsresult PermissionManager::RemoveAllInternal(bool aNotifyObservers) { + ENSURE_NOT_CHILD_PROCESS; + + EnsureReadCompleted(); + + // Let's broadcast the removeAll() to any content process. + nsTArray<ContentParent*> parents; + ContentParent::GetAll(parents); + for (ContentParent* parent : parents) { + Unused << parent->SendRemoveAllPermissions(); + } + + // Remove from memory and notify immediately. Since the in-memory + // database is authoritative, we do not need confirmation from the + // on-disk database to notify observers. + RemoveAllFromMemory(); + + // Re-import the defaults + ImportLatestDefaults(); + + if (aNotifyObservers) { + NotifyObservers(nullptr, u"cleared"); + } + + RefPtr<PermissionManager> self = this; + mThread->Dispatch( + NS_NewRunnableFunction("PermissionManager::RemoveAllInternal", [self] { + auto data = self->mThreadBoundData.Access(); + + if (self->mState == eClosed || !data->mDBConn) { + return; + } + + // clear the db + nsresult rv = + data->mDBConn->ExecuteSimpleSQL("DELETE FROM moz_perms"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "PermissionManager::RemoveAllInternal-Failure", + [self] { self->CloseDB(eRebuldOnSuccess); })); + } + })); + + return NS_OK; +} + +NS_IMETHODIMP +PermissionManager::TestExactPermissionFromPrincipal(nsIPrincipal* aPrincipal, + const nsACString& aType, + uint32_t* aPermission) { + return CommonTestPermission(aPrincipal, -1, aType, aPermission, + nsIPermissionManager::UNKNOWN_ACTION, false, true, + true); +} + +NS_IMETHODIMP +PermissionManager::TestExactPermanentPermission(nsIPrincipal* aPrincipal, + const nsACString& aType, + uint32_t* aPermission) { + return CommonTestPermission(aPrincipal, -1, aType, aPermission, + nsIPermissionManager::UNKNOWN_ACTION, false, true, + false); +} + +nsresult PermissionManager::LegacyTestPermissionFromURI( + nsIURI* aURI, const OriginAttributes* aOriginAttributes, + const nsACString& aType, uint32_t* aPermission) { + return CommonTestPermission(aURI, aOriginAttributes, -1, aType, aPermission, + nsIPermissionManager::UNKNOWN_ACTION, false, + false, true); +} + +NS_IMETHODIMP +PermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal, + const nsACString& aType, + uint32_t* aPermission) { + return CommonTestPermission(aPrincipal, -1, aType, aPermission, + nsIPermissionManager::UNKNOWN_ACTION, false, + false, true); +} + +NS_IMETHODIMP +PermissionManager::GetPermissionObject(nsIPrincipal* aPrincipal, + const nsACString& aType, + bool aExactHostMatch, + nsIPermission** aResult) { + NS_ENSURE_ARG_POINTER(aPrincipal); + *aResult = nullptr; + + EnsureReadCompleted(); + + if (aPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + + // Querying the permission object of an nsEP is non-sensical. + if (IsExpandedPrincipal(aPrincipal)) { + return NS_ERROR_INVALID_ARG; + } + + MOZ_ASSERT(PermissionAvailable(aPrincipal, aType)); + + int32_t typeIndex = GetTypeIndex(aType, false); + // If type == -1, the type isn't known, + // so just return NS_OK + if (typeIndex == -1) return NS_OK; + + PermissionHashKey* entry = + GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch); + if (!entry) { + return NS_OK; + } + + // We don't call GetPermission(typeIndex) because that returns a fake + // UNKNOWN_ACTION entry if there is no match. + int32_t idx = entry->GetPermissionIndex(typeIndex); + if (-1 == idx) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, + IsOAForceStripPermission(aType), + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + PermissionEntry& perm = entry->GetPermissions()[idx]; + nsCOMPtr<nsIPermission> r = Permission::Create( + principal, mTypeArray[perm.mType], perm.mPermission, perm.mExpireType, + perm.mExpireTime, perm.mModificationTime); + if (NS_WARN_IF(!r)) { + return NS_ERROR_FAILURE; + } + r.forget(aResult); + return NS_OK; +} + +nsresult PermissionManager::CommonTestPermissionInternal( + nsIPrincipal* aPrincipal, nsIURI* aURI, + const OriginAttributes* aOriginAttributes, int32_t aTypeIndex, + const nsACString& aType, uint32_t* aPermission, bool aExactHostMatch, + bool aIncludingSession) { + MOZ_ASSERT(aPrincipal || aURI); + NS_ENSURE_ARG_POINTER(aPrincipal || aURI); + MOZ_ASSERT_IF(aPrincipal, !aURI && !aOriginAttributes); + MOZ_ASSERT_IF(aURI || aOriginAttributes, !aPrincipal); + + EnsureReadCompleted(); + +#ifdef DEBUG + { + nsCOMPtr<nsIPrincipal> prin = aPrincipal; + if (!prin) { + if (aURI) { + prin = BasePrincipal::CreateContentPrincipal(aURI, OriginAttributes()); + } + } + MOZ_ASSERT(prin); + MOZ_ASSERT(PermissionAvailable(prin, aType)); + } +#endif + + PermissionHashKey* entry = + aPrincipal ? GetPermissionHashKey(aPrincipal, aTypeIndex, aExactHostMatch) + : GetPermissionHashKey(aURI, aOriginAttributes, aTypeIndex, + aExactHostMatch); + if (!entry || (!aIncludingSession && + entry->GetPermission(aTypeIndex).mNonSessionExpireType == + nsIPermissionManager::EXPIRE_SESSION)) { + return NS_OK; + } + + *aPermission = aIncludingSession + ? entry->GetPermission(aTypeIndex).mPermission + : entry->GetPermission(aTypeIndex).mNonSessionPermission; + + return NS_OK; +} + +// Helper function to filter permissions using a condition function. +template <class T> +nsresult PermissionManager::GetPermissionEntries( + T aCondition, nsTArray<RefPtr<nsIPermission>>& aResult) { + aResult.Clear(); + if (XRE_IsContentProcess()) { + NS_WARNING( + "Iterating over all permissions is not available in the " + "content process, as not all permissions may be available."); + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureReadCompleted(); + + for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { + PermissionHashKey* entry = iter.Get(); + for (const auto& permEntry : entry->GetPermissions()) { + // Given how "default" permissions work and the possibility of them being + // overridden with UNKNOWN_ACTION, we might see this value here - but we + // do *not* want to return them via the enumerator. + if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + continue; + } + + // If the permission is expired, skip it. We're not deleting it here + // because we're iterating over a lot of permissions. + // It will be removed as part of the daily maintenance later. + if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) { + continue; + } + + if (!aCondition(permEntry)) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = GetPrincipalFromOrigin( + entry->GetKey()->mOrigin, + IsOAForceStripPermission(mTypeArray[permEntry.mType]), + getter_AddRefs(principal)); + if (NS_FAILED(rv)) { + continue; + } + + RefPtr<nsIPermission> permission = Permission::Create( + principal, mTypeArray[permEntry.mType], permEntry.mPermission, + permEntry.mExpireType, permEntry.mExpireTime, + permEntry.mModificationTime); + if (NS_WARN_IF(!permission)) { + continue; + } + aResult.AppendElement(std::move(permission)); + } + } + + return NS_OK; +} + +NS_IMETHODIMP PermissionManager::GetAll( + nsTArray<RefPtr<nsIPermission>>& aResult) { + return GetPermissionEntries( + [](const PermissionEntry& aPermEntry) { return true; }, aResult); +} + +NS_IMETHODIMP PermissionManager::GetAllByTypeSince( + const nsACString& aPrefix, int64_t aSince, + nsTArray<RefPtr<nsIPermission>>& aResult) { + return GetPermissionEntries( + [&](const PermissionEntry& aPermEntry) { + return mTypeArray[aPermEntry.mType].Equals(aPrefix) && + aSince <= aPermEntry.mModificationTime; + }, + aResult); +} + +NS_IMETHODIMP PermissionManager::GetAllWithTypePrefix( + const nsACString& aPrefix, nsTArray<RefPtr<nsIPermission>>& aResult) { + return GetPermissionEntries( + [&](const PermissionEntry& aPermEntry) { + return StringBeginsWith(mTypeArray[aPermEntry.mType], aPrefix); + }, + aResult); +} + +NS_IMETHODIMP +PermissionManager::GetAllForPrincipal( + nsIPrincipal* aPrincipal, nsTArray<RefPtr<nsIPermission>>& aResult) { + aResult.Clear(); + EnsureReadCompleted(); + + MOZ_ASSERT(PermissionAvailable(aPrincipal, ""_ns)); + + nsresult rv; + RefPtr<PermissionKey> key = + PermissionKey::CreateFromPrincipal(aPrincipal, false, rv); + if (!key) { + MOZ_ASSERT(NS_FAILED(rv)); + return rv; + } + PermissionHashKey* entry = mPermissionTable.GetEntry(key); + + nsTArray<PermissionEntry> strippedPerms; + rv = GetStripPermsForPrincipal(aPrincipal, strippedPerms); + if (NS_FAILED(rv)) { + return rv; + } + + if (entry) { + for (const auto& permEntry : entry->GetPermissions()) { + // Only return custom permissions + if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + continue; + } + + // If the permission is expired, skip it. We're not deleting it here + // because we're iterating over a lot of permissions. + // It will be removed as part of the daily maintenance later. + if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) { + continue; + } + + // Stripped principal permissions overwrite regular ones + // For each permission check if there is a stripped permission we should + // use instead + PermissionEntry perm = permEntry; + nsTArray<PermissionEntry>::index_type index = 0; + for (const auto& strippedPerm : strippedPerms) { + if (strippedPerm.mType == permEntry.mType) { + perm = strippedPerm; + strippedPerms.RemoveElementAt(index); + break; + } + index++; + } + + RefPtr<nsIPermission> permission = Permission::Create( + aPrincipal, mTypeArray[perm.mType], perm.mPermission, + perm.mExpireType, perm.mExpireTime, perm.mModificationTime); + if (NS_WARN_IF(!permission)) { + continue; + } + aResult.AppendElement(permission); + } + } + + for (const auto& perm : strippedPerms) { + RefPtr<nsIPermission> permission = Permission::Create( + aPrincipal, mTypeArray[perm.mType], perm.mPermission, perm.mExpireType, + perm.mExpireTime, perm.mModificationTime); + if (NS_WARN_IF(!permission)) { + continue; + } + aResult.AppendElement(permission); + } + + return NS_OK; +} + +NS_IMETHODIMP PermissionManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* someData) { + ENSURE_NOT_CHILD_PROCESS; + + if (!nsCRT::strcmp(aTopic, "profile-before-change")) { + if (!mBlockerAdded) { + // The profile is about to change and the shutdown blocker has not been + // added yet (we are probably in a xpcshell-test). + RemoveIdleDailyMaintenanceJob(); + RemoveAllFromMemory(); + CloseDB(eNone); + } + } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) { + // the profile has already changed; init the db from the new location + InitDB(false); + } else if (!nsCRT::strcmp(aTopic, "testonly-reload-permissions-from-disk")) { + // Testing mechanism to reload all permissions from disk. Because the + // permission manager automatically initializes itself at startup, tests + // that directly manipulate the permissions database need some way to reload + // the database for their changes to have any effect. This mechanism was + // introduced when moving the permissions manager from on-demand startup to + // always being initialized. This is not guarded by a pref because it's not + // dangerous to reload permissions from disk, just bad for performance. + RemoveAllFromMemory(); + CloseDB(eNone); + InitDB(false); + } else if (!nsCRT::strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) { + PerformIdleDailyMaintenance(); + } + + return NS_OK; +} + +nsresult PermissionManager::RemoveAllModifiedSince(int64_t aModificationTime) { + ENSURE_NOT_CHILD_PROCESS; + + return RemovePermissionEntries( + [aModificationTime](const PermissionEntry& aPermEntry) { + return aModificationTime <= aPermEntry.mModificationTime; + }); +} + +NS_IMETHODIMP +PermissionManager::RemovePermissionsWithAttributes(const nsAString& aPattern) { + ENSURE_NOT_CHILD_PROCESS; + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + return RemovePermissionsWithAttributes(pattern); +} + +nsresult PermissionManager::RemovePermissionsWithAttributes( + OriginAttributesPattern& aPattern) { + EnsureReadCompleted(); + + Vector<Tuple<nsCOMPtr<nsIPrincipal>, nsCString, nsCString>, 10> permissions; + for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { + PermissionHashKey* entry = iter.Get(); + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, false, + getter_AddRefs(principal)); + if (NS_FAILED(rv)) { + continue; + } + + if (!aPattern.Matches(principal->OriginAttributesRef())) { + continue; + } + + for (const auto& permEntry : entry->GetPermissions()) { + if (!permissions.emplaceBack(principal, mTypeArray[permEntry.mType], + entry->GetKey()->mOrigin)) { + continue; + } + } + } + + for (auto& i : permissions) { + AddInternal(Get<0>(i), Get<1>(i), nsIPermissionManager::UNKNOWN_ACTION, 0, + nsIPermissionManager::EXPIRE_NEVER, 0, 0, + PermissionManager::eNotify, PermissionManager::eWriteToDB, + false, &Get<2>(i)); + } + + return NS_OK; +} + +nsresult PermissionManager::GetStripPermsForPrincipal( + nsIPrincipal* aPrincipal, nsTArray<PermissionEntry>& aResult) { + aResult.Clear(); + aResult.SetCapacity(kStripOAPermissions.size()); + + // No special strip permissions + if (kStripOAPermissions.empty()) { + return NS_OK; + } + + nsresult rv; + // Create a key for the principal, but strip any origin attributes + RefPtr<PermissionKey> key = + PermissionKey::CreateFromPrincipal(aPrincipal, true, rv); + if (!key) { + MOZ_ASSERT(NS_FAILED(rv)); + return rv; + } + + PermissionHashKey* hashKey = mPermissionTable.GetEntry(key); + if (!hashKey) { + return NS_OK; + } + + for (const auto& permType : kStripOAPermissions) { + int32_t index = GetTypeIndex(permType, false); + if (index == -1) { + continue; + } + PermissionEntry perm = hashKey->GetPermission(index); + if (perm.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + continue; + } + aResult.AppendElement(perm); + } + + return NS_OK; +} + +int32_t PermissionManager::GetTypeIndex(const nsACString& aType, bool aAdd) { + for (uint32_t i = 0; i < mTypeArray.length(); ++i) { + if (mTypeArray[i].Equals(aType)) { + return i; + } + } + + if (!aAdd) { + // Not found, but that is ok - we were just looking. + return -1; + } + + // This type was not registered before. + // append it to the array, without copy-constructing the string + if (!mTypeArray.emplaceBack(aType)) { + return -1; + } + + return mTypeArray.length() - 1; +} + +PermissionManager::PermissionHashKey* PermissionManager::GetPermissionHashKey( + nsIPrincipal* aPrincipal, uint32_t aType, bool aExactHostMatch) { + EnsureReadCompleted(); + + MOZ_ASSERT(PermissionAvailable(aPrincipal, mTypeArray[aType])); + + nsresult rv; + RefPtr<PermissionKey> key = PermissionKey::CreateFromPrincipal( + aPrincipal, IsOAForceStripPermission(mTypeArray[aType]), rv); + if (!key) { + return nullptr; + } + + PermissionHashKey* entry = mPermissionTable.GetEntry(key); + + if (entry) { + PermissionEntry permEntry = entry->GetPermission(aType); + + // if the entry is expired, remove and keep looking for others. + if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) { + entry = nullptr; + RemoveFromPrincipal(aPrincipal, mTypeArray[aType]); + } else if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + entry = nullptr; + } + } + + if (entry) { + return entry; + } + + // If aExactHostMatch wasn't true, we can check if the base domain has a + // permission entry. + if (!aExactHostMatch) { + nsCOMPtr<nsIPrincipal> principal = aPrincipal->GetNextSubDomainPrincipal(); + if (principal) { + return GetPermissionHashKey(principal, aType, aExactHostMatch); + } + } + + // No entry, really... + return nullptr; +} + +PermissionManager::PermissionHashKey* PermissionManager::GetPermissionHashKey( + nsIURI* aURI, const OriginAttributes* aOriginAttributes, uint32_t aType, + bool aExactHostMatch) { + MOZ_ASSERT(aURI); + +#ifdef DEBUG + { + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = NS_OK; + if (aURI) { + rv = GetPrincipal(aURI, getter_AddRefs(principal)); + } + MOZ_ASSERT_IF(NS_SUCCEEDED(rv), + PermissionAvailable(principal, mTypeArray[aType])); + } +#endif + + nsresult rv; + RefPtr<PermissionKey> key; + + if (aOriginAttributes) { + key = PermissionKey::CreateFromURIAndOriginAttributes( + aURI, aOriginAttributes, IsOAForceStripPermission(mTypeArray[aType]), + rv); + } else { + key = PermissionKey::CreateFromURI(aURI, rv); + } + + if (!key) { + return nullptr; + } + + PermissionHashKey* entry = mPermissionTable.GetEntry(key); + + if (entry) { + PermissionEntry permEntry = entry->GetPermission(aType); + + // if the entry is expired, remove and keep looking for others. + if (HasExpired(permEntry.mExpireType, permEntry.mExpireTime)) { + entry = nullptr; + // If we need to remove a permission we mint a principal. This is a bit + // inefficient, but hopefully this code path isn't super common. + nsCOMPtr<nsIPrincipal> principal; + if (aURI) { + nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + } + RemoveFromPrincipal(principal, mTypeArray[aType]); + } else if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + entry = nullptr; + } + } + + if (entry) { + return entry; + } + + // If aExactHostMatch wasn't true, we can check if the base domain has a + // permission entry. + if (!aExactHostMatch) { + nsCOMPtr<nsIURI> uri; + if (aURI) { + uri = GetNextSubDomainURI(aURI); + } + if (uri) { + return GetPermissionHashKey(uri, aOriginAttributes, aType, + aExactHostMatch); + } + } + + // No entry, really... + return nullptr; +} + +nsresult PermissionManager::RemoveAllFromMemory() { + mLargestID = 0; + mTypeArray.clear(); + mPermissionTable.Clear(); + + return NS_OK; +} + +// wrapper function for mangling (host,type,perm,expireType,expireTime) +// set into an nsIPermission. +void PermissionManager::NotifyObserversWithPermission( + nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t aPermission, + uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime, + const char16_t* aData) { + nsCOMPtr<nsIPermission> permission = + Permission::Create(aPrincipal, aType, aPermission, aExpireType, + aExpireTime, aModificationTime); + if (permission) NotifyObservers(permission, aData); +} + +// notify observers that the permission list changed. there are four possible +// values for aData: +// "deleted" means a permission was deleted. aPermission is the deleted +// permission. "added" means a permission was added. aPermission is the added +// permission. "changed" means a permission was altered. aPermission is the new +// permission. "cleared" means the entire permission list was cleared. +// aPermission is null. +void PermissionManager::NotifyObservers(nsIPermission* aPermission, + const char16_t* aData) { + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService) + observerService->NotifyObservers(aPermission, kPermissionChangeNotification, + aData); +} + +nsresult PermissionManager::Read(const MonitorAutoLock& aProofOfLock) { + ENSURE_NOT_CHILD_PROCESS; + + MOZ_ASSERT(!NS_IsMainThread()); + auto data = mThreadBoundData.Access(); + + nsresult rv; + bool hasResult; + nsCOMPtr<mozIStorageStatement> stmt; + + // Let's retrieve the last used ID. + rv = data->mDBConn->CreateStatement( + nsLiteralCString("SELECT MAX(id) FROM moz_perms"), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + int64_t id = stmt->AsInt64(0); + mLargestID = id; + } + + rv = data->mDBConn->CreateStatement( + nsLiteralCString( + "SELECT id, origin, type, permission, expireType, " + "expireTime, modificationTime " + "FROM moz_perms WHERE expireType != ?1 OR expireTime > ?2"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByIndex(0, nsIPermissionManager::EXPIRE_TIME); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByIndex(1, EXPIRY_NOW); + NS_ENSURE_SUCCESS(rv, rv); + + bool readError = false; + + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + ReadEntry entry; + + // explicitly set our entry id counter for use in AddInternal(), + // and keep track of the largest id so we know where to pick up. + entry.mId = stmt->AsInt64(0); + MOZ_ASSERT(entry.mId <= mLargestID); + + rv = stmt->GetUTF8String(1, entry.mOrigin); + if (NS_FAILED(rv)) { + readError = true; + continue; + } + + rv = stmt->GetUTF8String(2, entry.mType); + if (NS_FAILED(rv)) { + readError = true; + continue; + } + + entry.mPermission = stmt->AsInt32(3); + entry.mExpireType = stmt->AsInt32(4); + + // convert into int64_t values (milliseconds) + entry.mExpireTime = stmt->AsInt64(5); + entry.mModificationTime = stmt->AsInt64(6); + + entry.mFromMigration = false; + + mReadEntries.AppendElement(entry); + } + + if (readError) { + NS_ERROR("Error occured while reading the permissions database!"); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void PermissionManager::CompleteMigrations() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eReady); + + nsresult rv; + + nsTArray<MigrationEntry> entries; + { + MonitorAutoLock lock(mMonitor); + entries = std::move(mMigrationEntries); + } + + for (const MigrationEntry& entry : entries) { + rv = UpgradeHostToOriginAndInsert( + entry.mHost, entry.mType, entry.mPermission, entry.mExpireType, + entry.mExpireTime, entry.mModificationTime, entry.mIsInBrowserElement, + [&](const nsACString& aOrigin, const nsCString& aType, + uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, + int64_t aModificationTime) { + MaybeAddReadEntryFromMigration(aOrigin, aType, aPermission, + aExpireType, aExpireTime, + aModificationTime, entry.mId); + return NS_OK; + }); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } +} + +void PermissionManager::CompleteRead() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eReady); + + nsresult rv; + + nsTArray<ReadEntry> entries; + { + MonitorAutoLock lock(mMonitor); + entries = std::move(mReadEntries); + } + + for (const ReadEntry& entry : entries) { + nsCOMPtr<nsIPrincipal> principal; + rv = GetPrincipalFromOrigin(entry.mOrigin, + IsOAForceStripPermission(entry.mType), + getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + DBOperationType op = entry.mFromMigration ? eWriteToDB : eNoDBOperation; + + rv = AddInternal(principal, entry.mType, entry.mPermission, entry.mId, + entry.mExpireType, entry.mExpireTime, + entry.mModificationTime, eDontNotify, op, false, + &entry.mOrigin); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } +} + +void PermissionManager::MaybeAddReadEntryFromMigration( + const nsACString& aOrigin, const nsCString& aType, uint32_t aPermission, + uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime, + int64_t aId) { + MonitorAutoLock lock(mMonitor); + + // We convert a migration to a ReadEntry only if we don't have an existing + // ReadEntry for the same origin + type. + for (const ReadEntry& entry : mReadEntries) { + if (entry.mOrigin == aOrigin && entry.mType == aType) { + return; + } + } + + ReadEntry entry; + entry.mId = aId; + entry.mOrigin = aOrigin; + entry.mType = aType; + entry.mPermission = aPermission; + entry.mExpireType = aExpireType; + entry.mExpireTime = aExpireTime; + entry.mModificationTime = aModificationTime; + entry.mFromMigration = true; + + mReadEntries.AppendElement(entry); +} + +void PermissionManager::UpdateDB(OperationType aOp, int64_t aID, + const nsACString& aOrigin, + const nsACString& aType, uint32_t aPermission, + uint32_t aExpireType, int64_t aExpireTime, + int64_t aModificationTime) { + ENSURE_NOT_CHILD_PROCESS_NORET; + + MOZ_ASSERT(NS_IsMainThread()); + EnsureReadCompleted(); + + nsCString origin(aOrigin); + nsCString type(aType); + + RefPtr<PermissionManager> self = this; + mThread->Dispatch(NS_NewRunnableFunction( + "PermissionManager::UpdateDB", + [self, aOp, aID, origin, type, aPermission, aExpireType, aExpireTime, + aModificationTime] { + nsresult rv; + + auto data = self->mThreadBoundData.Access(); + + if (self->mState == eClosed || !data->mDBConn) { + // no statement is ok - just means we don't have a profile + return; + } + + mozIStorageStatement* stmt = nullptr; + switch (aOp) { + case eOperationAdding: { + stmt = data->mStmtInsert; + + rv = stmt->BindInt64ByIndex(0, aID); + if (NS_FAILED(rv)) break; + + rv = stmt->BindUTF8StringByIndex(1, origin); + if (NS_FAILED(rv)) break; + + rv = stmt->BindUTF8StringByIndex(2, type); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt32ByIndex(3, aPermission); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt32ByIndex(4, aExpireType); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt64ByIndex(5, aExpireTime); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt64ByIndex(6, aModificationTime); + break; + } + + case eOperationRemoving: { + stmt = data->mStmtDelete; + rv = stmt->BindInt64ByIndex(0, aID); + break; + } + + case eOperationChanging: { + stmt = data->mStmtUpdate; + + rv = stmt->BindInt64ByIndex(0, aID); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt32ByIndex(1, aPermission); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt32ByIndex(2, aExpireType); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt64ByIndex(3, aExpireTime); + if (NS_FAILED(rv)) break; + + rv = stmt->BindInt64ByIndex(4, aModificationTime); + break; + } + + default: { + MOZ_ASSERT_UNREACHABLE("need a valid operation in UpdateDB()!"); + rv = NS_ERROR_UNEXPECTED; + break; + } + } + + if (NS_FAILED(rv)) { + NS_WARNING("db change failed!"); + return; + } + + rv = stmt->Execute(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + })); +} + +bool PermissionManager::GetPermissionsFromOriginOrKey( + const nsACString& aOrigin, const nsACString& aKey, + nsTArray<IPC::Permission>& aPerms) { + EnsureReadCompleted(); + + aPerms.Clear(); + if (NS_WARN_IF(XRE_IsContentProcess())) { + return false; + } + + for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { + PermissionHashKey* entry = iter.Get(); + + nsAutoCString permissionKey; + if (aOrigin.IsEmpty()) { + // We can't check for individual OA strip perms here. + // Don't force strip origin attributes. + GetKeyForOrigin(entry->GetKey()->mOrigin, false, permissionKey); + + // If the keys don't match, and we aren't getting the default "" key, then + // we can exit early. We have to keep looking if we're getting the default + // key, as we may see a preload permission which should be transmitted. + if (aKey != permissionKey && !aKey.IsEmpty()) { + continue; + } + } else if (aOrigin != entry->GetKey()->mOrigin) { + // If the origins don't match, then we can exit early. We have to keep + // looking if we're getting the default origin, as we may see a preload + // permission which should be transmitted. + continue; + } + + for (const auto& permEntry : entry->GetPermissions()) { + // Given how "default" permissions work and the possibility of them + // being overridden with UNKNOWN_ACTION, we might see this value here - + // but we do not want to send it to the content process. + if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + continue; + } + + bool isPreload = IsPreloadPermission(mTypeArray[permEntry.mType]); + bool shouldAppend; + if (aOrigin.IsEmpty()) { + shouldAppend = (isPreload && aKey.IsEmpty()) || + (!isPreload && aKey == permissionKey); + } else { + shouldAppend = (!isPreload && aOrigin == entry->GetKey()->mOrigin); + } + if (shouldAppend) { + aPerms.AppendElement( + IPC::Permission(entry->GetKey()->mOrigin, + mTypeArray[permEntry.mType], permEntry.mPermission, + permEntry.mExpireType, permEntry.mExpireTime)); + } + } + } + + return true; +} + +void PermissionManager::SetPermissionsWithKey( + const nsACString& aPermissionKey, nsTArray<IPC::Permission>& aPerms) { + if (NS_WARN_IF(XRE_IsParentProcess())) { + return; + } + + RefPtr<GenericNonExclusivePromise::Private> promise; + bool foundKey = + mPermissionKeyPromiseMap.Get(aPermissionKey, getter_AddRefs(promise)); + if (promise) { + MOZ_ASSERT(foundKey); + // NOTE: This will resolve asynchronously, so we can mark it as resolved + // now, and be confident that we will have filled in the database before any + // callbacks run. + promise->Resolve(true, __func__); + } else if (foundKey) { + // NOTE: We shouldn't be sent two InitializePermissionsWithKey for the same + // key, but it's possible. + return; + } + mPermissionKeyPromiseMap.Put(aPermissionKey, + RefPtr<GenericNonExclusivePromise::Private>{}); + + // Add the permissions locally to our process + for (IPC::Permission& perm : aPerms) { + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = + GetPrincipalFromOrigin(perm.origin, IsOAForceStripPermission(perm.type), + getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + +#ifdef DEBUG + nsAutoCString permissionKey; + GetKeyForPermission(principal, perm.type, permissionKey); + MOZ_ASSERT(permissionKey == aPermissionKey, + "The permission keys which were sent over should match!"); +#endif + + // The child process doesn't care about modification times - it neither + // reads nor writes, nor removes them based on the date - so 0 (which + // will end up as now()) is fine. + uint64_t modificationTime = 0; + AddInternal(principal, perm.type, perm.capability, 0, perm.expireType, + perm.expireTime, modificationTime, eNotify, eNoDBOperation, + true /* ignoreSessionPermissions */); + } +} + +/* static */ +void PermissionManager::GetKeyForOrigin(const nsACString& aOrigin, + bool aForceStripOA, nsACString& aKey) { + aKey.Truncate(); + + // We only key origins for http, https, and ftp URIs. All origins begin with + // the URL which they apply to, which means that they should begin with their + // scheme in the case where they are one of these interesting URIs. We don't + // want to actually parse the URL here however, because this can be called on + // hot paths. + if (!StringBeginsWith(aOrigin, "http:"_ns) && + !StringBeginsWith(aOrigin, "https:"_ns) && + !StringBeginsWith(aOrigin, "ftp:"_ns)) { + return; + } + + // We need to look at the originAttributes if they are present, to make sure + // to remove any which we don't want. We put the rest of the origin, not + // including the attributes, into the key. + OriginAttributes attrs; + if (!attrs.PopulateFromOrigin(aOrigin, aKey)) { + aKey.Truncate(); + return; + } + + MaybeStripOAs(aForceStripOA, attrs); + +#ifdef DEBUG + // Parse the origin string into a principal, and extract some useful + // information from it for assertions. + nsCOMPtr<nsIPrincipal> dbgPrincipal; + MOZ_ALWAYS_SUCCEEDS(GetPrincipalFromOrigin(aOrigin, aForceStripOA, + getter_AddRefs(dbgPrincipal))); + MOZ_ASSERT(dbgPrincipal->SchemeIs("http") || + dbgPrincipal->SchemeIs("https") || dbgPrincipal->SchemeIs("ftp")); + MOZ_ASSERT(dbgPrincipal->OriginAttributesRef() == attrs); +#endif + + // Append the stripped suffix to the output origin key. + nsAutoCString suffix; + attrs.CreateSuffix(suffix); + aKey.Append(suffix); +} + +/* static */ +void PermissionManager::GetKeyForPrincipal(nsIPrincipal* aPrincipal, + bool aForceStripOA, + nsACString& aKey) { + nsAutoCString origin; + nsresult rv = aPrincipal->GetOrigin(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + aKey.Truncate(); + return; + } + GetKeyForOrigin(origin, aForceStripOA, aKey); +} + +/* static */ +void PermissionManager::GetKeyForPermission(nsIPrincipal* aPrincipal, + const nsACString& aType, + nsACString& aKey) { + // Preload permissions have the "" key. + if (IsPreloadPermission(aType)) { + aKey.Truncate(); + return; + } + + GetKeyForPrincipal(aPrincipal, IsOAForceStripPermission(aType), aKey); +} + +/* static */ +nsTArray<std::pair<nsCString, nsCString>> +PermissionManager::GetAllKeysForPrincipal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + nsTArray<std::pair<nsCString, nsCString>> pairs; + nsCOMPtr<nsIPrincipal> prin = aPrincipal; + while (prin) { + // Add the pair to the list + std::pair<nsCString, nsCString>* pair = + pairs.AppendElement(std::make_pair(""_ns, ""_ns)); + // We can't check for individual OA strip perms here. + // Don't force strip origin attributes. + GetKeyForPrincipal(prin, false, pair->first); + + Unused << GetOriginFromPrincipal(prin, false, pair->second); + prin = prin->GetNextSubDomainPrincipal(); + // Get the next subdomain principal and loop back around. + } + + MOZ_ASSERT(pairs.Length() >= 1, + "Every principal should have at least one pair item."); + return pairs; +} + +NS_IMETHODIMP +PermissionManager::BroadcastPermissionsForPrincipalToAllContentProcesses( + nsIPrincipal* aPrincipal) { + nsTArray<ContentParent*> cps; + ContentParent::GetAll(cps); + for (ContentParent* cp : cps) { + nsresult rv = cp->TransmitPermissionsForPrincipal(aPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +bool PermissionManager::PermissionAvailable(nsIPrincipal* aPrincipal, + const nsACString& aType) { + EnsureReadCompleted(); + + if (XRE_IsContentProcess()) { + nsAutoCString permissionKey; + // NOTE: GetKeyForPermission accepts a null aType. + GetKeyForPermission(aPrincipal, aType, permissionKey); + + // If we have a pending promise for the permission key in question, we don't + // have the permission available, so report a warning and return false. + RefPtr<GenericNonExclusivePromise::Private> promise; + if (!mPermissionKeyPromiseMap.Get(permissionKey, getter_AddRefs(promise)) || + promise) { + // Emit a useful diagnostic warning with the permissionKey for the process + // which hasn't received permissions yet. + NS_WARNING(nsPrintfCString("This content process hasn't received the " + "permissions for %s yet", + permissionKey.get()) + .get()); + return false; + } + } + return true; +} + +void PermissionManager::WhenPermissionsAvailable(nsIPrincipal* aPrincipal, + nsIRunnable* aRunnable) { + MOZ_ASSERT(aRunnable); + + if (!XRE_IsContentProcess()) { + aRunnable->Run(); + return; + } + + nsTArray<RefPtr<GenericNonExclusivePromise>> promises; + for (auto& pair : GetAllKeysForPrincipal(aPrincipal)) { + RefPtr<GenericNonExclusivePromise::Private> promise; + if (!mPermissionKeyPromiseMap.Get(pair.first, getter_AddRefs(promise))) { + // In this case we have found a permission which isn't available in the + // content process and hasn't been requested yet. We need to create a new + // promise, and send the request to the parent (if we have not already + // done so). + promise = new GenericNonExclusivePromise::Private(__func__); + mPermissionKeyPromiseMap.Put(pair.first, RefPtr{promise}); + } + + if (promise) { + promises.AppendElement(std::move(promise)); + } + } + + // If all of our permissions are available, immediately run the runnable. This + // avoids any extra overhead during fetch interception which is performance + // sensitive. + if (promises.IsEmpty()) { + aRunnable->Run(); + return; + } + + auto* thread = AbstractThread::MainThread(); + + RefPtr<nsIRunnable> runnable = aRunnable; + GenericNonExclusivePromise::All(thread, promises) + ->Then( + thread, __func__, [runnable]() { runnable->Run(); }, + []() { + NS_WARNING( + "PermissionManager permission promise rejected. We're " + "probably shutting down."); + }); +} + +void PermissionManager::EnsureReadCompleted() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == eInitializing) { + MonitorAutoLock lock(mMonitor); + + while (mState == eInitializing) { + mMonitor.Wait(); + } + } + + switch (mState) { + case eInitializing: + MOZ_CRASH("This state is impossible!"); + + case eDBInitialized: + mState = eReady; + + CompleteMigrations(); + ImportLatestDefaults(); + CompleteRead(); + + [[fallthrough]]; + + case eReady: + [[fallthrough]]; + + case eClosed: + return; + + default: + MOZ_CRASH("Invalid state"); + } +} + +already_AddRefed<nsIInputStream> PermissionManager::GetDefaultsInputStream() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString defaultsURL; + Preferences::GetCString(kDefaultsUrlPrefName, defaultsURL); + if (defaultsURL.IsEmpty()) { // == Don't use built-in permissions. + return nullptr; + } + + nsCOMPtr<nsIURI> defaultsURI; + nsresult rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), defaultsURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIInputStream> inputStream; + rv = channel->Open(getter_AddRefs(inputStream)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return inputStream.forget(); +} + +void PermissionManager::ConsumeDefaultsInputStream( + nsIInputStream* aInputStream, const MonitorAutoLock& aProofOfLock) { + MOZ_ASSERT(!NS_IsMainThread()); + + constexpr char kMatchTypeHost[] = "host"; + constexpr char kMatchTypeOrigin[] = "origin"; + + mDefaultEntries.Clear(); + + if (!aInputStream) { + return; + } + + nsresult rv; + + /* format is: + * matchtype \t type \t permission \t host + * Only "host" is supported for matchtype + * type is a string that identifies the type of permission (e.g. "cookie") + * permission is an integer between 1 and 15 + */ + + // Ideally we'd do this with nsILineInputString, but this is called with an + // nsIInputStream that comes from a resource:// URI, which doesn't support + // that interface. So NS_ReadLine to the rescue... + nsLineBuffer<char> lineBuffer; + nsCString line; + bool isMore = true; + do { + rv = NS_ReadLine(aInputStream, &lineBuffer, line, &isMore); + NS_ENSURE_SUCCESS_VOID(rv); + + if (line.IsEmpty() || line.First() == '#') { + continue; + } + + nsTArray<nsCString> lineArray; + + // Split the line at tabs + ParseString(line, '\t', lineArray); + + if (lineArray.Length() != 4) { + continue; + } + + nsresult error = NS_OK; + uint32_t permission = lineArray[2].ToInteger(&error); + if (NS_FAILED(error)) { + continue; + } + + DefaultEntry::Op op; + + if (lineArray[0].EqualsLiteral(kMatchTypeHost)) { + op = DefaultEntry::eImportMatchTypeHost; + } else if (lineArray[0].EqualsLiteral(kMatchTypeOrigin)) { + op = DefaultEntry::eImportMatchTypeOrigin; + } else { + continue; + } + + DefaultEntry* entry = mDefaultEntries.AppendElement(); + MOZ_ASSERT(entry); + + entry->mOp = op; + entry->mPermission = permission; + entry->mHostOrOrigin = lineArray[3]; + entry->mType = lineArray[1]; + } while (isMore); +} + +// ImportLatestDefaults will import the latest default cookies read during the +// last DB initialization. +nsresult PermissionManager::ImportLatestDefaults() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == eReady); + + nsresult rv; + + MonitorAutoLock lock(mMonitor); + + for (const DefaultEntry& entry : mDefaultEntries) { + if (entry.mOp == DefaultEntry::eImportMatchTypeHost) { + // the import file format doesn't handle modification times, so we use + // 0, which AddInternal will convert to now() + int64_t modificationTime = 0; + + rv = UpgradeHostToOriginAndInsert( + entry.mHostOrOrigin, entry.mType, entry.mPermission, + nsIPermissionManager::EXPIRE_NEVER, 0, modificationTime, false, + [&](const nsACString& aOrigin, const nsCString& aType, + uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, + int64_t aModificationTime) { + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = + GetPrincipalFromOrigin(aOrigin, IsOAForceStripPermission(aType), + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + return AddInternal( + principal, aType, aPermission, cIDPermissionIsDefault, + aExpireType, aExpireTime, aModificationTime, + PermissionManager::eDontNotify, + PermissionManager::eNoDBOperation, false, &aOrigin); + }); + + if (NS_FAILED(rv)) { + NS_WARNING("There was a problem importing a host permission"); + } + continue; + } + + MOZ_ASSERT(entry.mOp == DefaultEntry::eImportMatchTypeOrigin); + + nsCOMPtr<nsIPrincipal> principal; + rv = GetPrincipalFromOrigin(entry.mHostOrOrigin, + IsOAForceStripPermission(entry.mType), + getter_AddRefs(principal)); + if (NS_FAILED(rv)) { + NS_WARNING("Couldn't import an origin permission - malformed origin"); + continue; + } + + // the import file format doesn't handle modification times, so we use + // 0, which AddInternal will convert to now() + int64_t modificationTime = 0; + + rv = AddInternal(principal, entry.mType, entry.mPermission, + cIDPermissionIsDefault, nsIPermissionManager::EXPIRE_NEVER, + 0, modificationTime, eDontNotify, eNoDBOperation); + if (NS_FAILED(rv)) { + NS_WARNING("There was a problem importing an origin permission"); + } + } + + return NS_OK; +} + +/** + * Perform the early steps of a permission check and determine whether we need + * to call CommonTestPermissionInternal() for the actual permission check. + * + * @param aPrincipal optional principal argument to check the permission for, + * can be nullptr if we aren't performing a principal-based + * check. + * @param aTypeIndex if the caller isn't sure what the index of the permission + * type to check for is in the mTypeArray member variable, + * it should pass -1, otherwise this would be the index of + * the type inside mTypeArray. This would only be something + * other than -1 in recursive invocations of this function. + * @param aType the permission type to test. + * @param aPermission out argument which will be a permission type that we + * will return from this function once the function is + * done. + * @param aDefaultPermission the default permission to be used if we can't + * determine the result of the permission check. + * @param aDefaultPermissionIsValid whether the previous argument contains a + * valid value. + * @param aExactHostMatch whether to look for the exact host name or also for + * subdomains that can have the same permission. + * @param aIncludingSession whether to include session permissions when + * testing for the permission. + */ +PermissionManager::TestPreparationResult +PermissionManager::CommonPrepareToTestPermission( + nsIPrincipal* aPrincipal, int32_t aTypeIndex, const nsACString& aType, + uint32_t* aPermission, uint32_t aDefaultPermission, + bool aDefaultPermissionIsValid, bool aExactHostMatch, + bool aIncludingSession) { + auto* basePrin = BasePrincipal::Cast(aPrincipal); + if (basePrin && basePrin->IsSystemPrincipal()) { + *aPermission = ALLOW_ACTION; + return AsVariant(NS_OK); + } + + EnsureReadCompleted(); + + // For some permissions, query the default from a pref. We want to avoid + // doing this for all permissions so that permissions can opt into having + // the pref lookup overhead on each call. + int32_t defaultPermission = + aDefaultPermissionIsValid ? aDefaultPermission : UNKNOWN_ACTION; + if (!aDefaultPermissionIsValid && HasDefaultPref(aType)) { + Unused << mDefaultPrefBranch->GetIntPref(PromiseFlatCString(aType).get(), + &defaultPermission); + } + + // Set the default. + *aPermission = defaultPermission; + + int32_t typeIndex = + aTypeIndex == -1 ? GetTypeIndex(aType, false) : aTypeIndex; + + // For expanded principals, we want to iterate over the allowlist and see + // if the permission is granted for any of them. + if (basePrin && basePrin->Is<ExpandedPrincipal>()) { + auto ep = basePrin->As<ExpandedPrincipal>(); + for (auto& prin : ep->AllowList()) { + uint32_t perm; + nsresult rv = + CommonTestPermission(prin, typeIndex, aType, &perm, defaultPermission, + true, aExactHostMatch, aIncludingSession); + if (NS_WARN_IF(NS_FAILED(rv))) { + return AsVariant(rv); + } + + if (perm == nsIPermissionManager::ALLOW_ACTION) { + *aPermission = perm; + return AsVariant(NS_OK); + } + if (perm == nsIPermissionManager::PROMPT_ACTION) { + // Store it, but keep going to see if we can do better. + *aPermission = perm; + } + } + + return AsVariant(NS_OK); + } + + // If type == -1, the type isn't known, just signal that we are done. + if (typeIndex == -1) { + return AsVariant(NS_OK); + } + + return AsVariant(typeIndex); +} + +// If aTypeIndex is passed -1, we try to inder the type index from aType. +nsresult PermissionManager::CommonTestPermission( + nsIPrincipal* aPrincipal, int32_t aTypeIndex, const nsACString& aType, + uint32_t* aPermission, uint32_t aDefaultPermission, + bool aDefaultPermissionIsValid, bool aExactHostMatch, + bool aIncludingSession) { + auto preparationResult = CommonPrepareToTestPermission( + aPrincipal, aTypeIndex, aType, aPermission, aDefaultPermission, + aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession); + if (preparationResult.is<nsresult>()) { + return preparationResult.as<nsresult>(); + } + + return CommonTestPermissionInternal( + aPrincipal, nullptr, nullptr, preparationResult.as<int32_t>(), aType, + aPermission, aExactHostMatch, aIncludingSession); +} + +// If aTypeIndex is passed -1, we try to inder the type index from aType. +nsresult PermissionManager::CommonTestPermission( + nsIURI* aURI, int32_t aTypeIndex, const nsACString& aType, + uint32_t* aPermission, uint32_t aDefaultPermission, + bool aDefaultPermissionIsValid, bool aExactHostMatch, + bool aIncludingSession) { + auto preparationResult = CommonPrepareToTestPermission( + nullptr, aTypeIndex, aType, aPermission, aDefaultPermission, + aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession); + if (preparationResult.is<nsresult>()) { + return preparationResult.as<nsresult>(); + } + + return CommonTestPermissionInternal( + nullptr, aURI, nullptr, preparationResult.as<int32_t>(), aType, + aPermission, aExactHostMatch, aIncludingSession); +} + +nsresult PermissionManager::CommonTestPermission( + nsIURI* aURI, const OriginAttributes* aOriginAttributes, int32_t aTypeIndex, + const nsACString& aType, uint32_t* aPermission, uint32_t aDefaultPermission, + bool aDefaultPermissionIsValid, bool aExactHostMatch, + bool aIncludingSession) { + auto preparationResult = CommonPrepareToTestPermission( + nullptr, aTypeIndex, aType, aPermission, aDefaultPermission, + aDefaultPermissionIsValid, aExactHostMatch, aIncludingSession); + if (preparationResult.is<nsresult>()) { + return preparationResult.as<nsresult>(); + } + + return CommonTestPermissionInternal( + nullptr, aURI, aOriginAttributes, preparationResult.as<int32_t>(), aType, + aPermission, aExactHostMatch, aIncludingSession); +} + +nsresult PermissionManager::TestPermissionWithoutDefaultsFromPrincipal( + nsIPrincipal* aPrincipal, const nsACString& aType, uint32_t* aPermission) { + MOZ_ASSERT(!HasDefaultPref(aType)); + + return CommonTestPermission(aPrincipal, -1, aType, aPermission, + nsIPermissionManager::UNKNOWN_ACTION, true, false, + true); +} + +void PermissionManager::MaybeCompleteShutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIAsyncShutdownClient> asc = GetShutdownPhase(); + MOZ_ASSERT(asc); + + DebugOnly<nsresult> rv = asc->RemoveBlocker(this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// Async shutdown blocker methods + +NS_IMETHODIMP PermissionManager::GetName(nsAString& aName) { + aName = u"PermissionManager: Flushing data"_ns; + return NS_OK; +} + +NS_IMETHODIMP PermissionManager::BlockShutdown( + nsIAsyncShutdownClient* aClient) { + RemoveIdleDailyMaintenanceJob(); + RemoveAllFromMemory(); + CloseDB(eShutdown); + + gPermissionManager = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +PermissionManager::GetState(nsIPropertyBag** aBagOut) { + nsCOMPtr<nsIWritablePropertyBag2> propertyBag = + do_CreateInstance("@mozilla.org/hash-property-bag;1"); + + nsresult rv = propertyBag->SetPropertyAsInt32(u"state"_ns, mState); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + propertyBag.forget(aBagOut); + + return NS_OK; +} + +nsCOMPtr<nsIAsyncShutdownClient> PermissionManager::GetShutdownPhase() const { + nsresult rv; + nsCOMPtr<nsIAsyncShutdownService> svc = + do_GetService("@mozilla.org/async-shutdown-service;1", &rv); + if (NS_FAILED(rv)) { + return nullptr; + } + + nsCOMPtr<nsIAsyncShutdownClient> client; + rv = svc->GetProfileBeforeChange(getter_AddRefs(client)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return client; +} + +} // namespace mozilla diff --git a/extensions/permissions/PermissionManager.h b/extensions/permissions/PermissionManager.h new file mode 100644 index 0000000000..6748e16c0a --- /dev/null +++ b/extensions/permissions/PermissionManager.h @@ -0,0 +1,651 @@ +/* -*- 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 "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/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, + 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 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); + + /** + * Initialize the permission-manager service. + * The permission manager is always initialized at startup because when it + * was lazy-initialized on demand, it was possible for it to be created + * once shutdown had begun, resulting in the manager failing to correctly + * shutdown because it missed its shutdown observer notification. + */ + static void Startup(); + + 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 aKey A string which will be filled with the permission + * key. + */ + static void GetKeyForPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA, + 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 aKey A string which will be filled with the permission + * key. + */ + static void GetKeyForOrigin(const nsACString& aOrigin, bool aForceStripOA, + 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 void 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); + + private: + ~PermissionManager(); + + /** + * 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. + */ + nsresult GetStripPermsForPrincipal(nsIPrincipal* aPrincipal, + 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); + + // 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); + + 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> GetShutdownPhase() 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; + + 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; + + bool mBlockerAdded; + + 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..b73d114a3a --- /dev/null +++ b/extensions/permissions/components.conf @@ -0,0 +1,24 @@ +# -*- 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 = [ + { + '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..57ed3309bc --- /dev/null +++ b/extensions/permissions/moz.build @@ -0,0 +1,42 @@ +# -*- 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.jsm", +] + +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" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +with Files("**"): + BUG_COMPONENT = ("Core", "Permission Manager") diff --git a/extensions/permissions/test/.eslintrc.js b/extensions/permissions/test/.eslintrc.js new file mode 100644 index 0000000000..735f687ed1 --- /dev/null +++ b/extensions/permissions/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test", "plugin:mozilla/browser-test"], +}; diff --git a/extensions/permissions/test/PermissionTestUtils.jsm b/extensions/permissions/test/PermissionTestUtils.jsm new file mode 100644 index 0000000000..1968d1697a --- /dev/null +++ b/extensions/permissions/test/PermissionTestUtils.jsm @@ -0,0 +1,110 @@ +/* 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. + */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +let pm = Services.perms; + +let secMan = Services.scriptSecurityManager; + +const EXPORTED_SYMBOLS = ["PermissionTestUtils"]; + +/** + * 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." + ); +} + +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..332e8741dc --- /dev/null +++ b/extensions/permissions/test/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] + +[browser_permmgr_sync.js] +# The browser_permmgr_sync test tests e10s specific behavior, and runs code +# paths which would hit the debug only assertion in +# PermissionManager::PermissionKey::CreateFromPrincipal. Because of this, it +# is only run in e10s opt builds. +skip-if = debug || !e10s +[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..d4ada33394 --- /dev/null +++ b/extensions/permissions/test/browser_permmgr_sync.js @@ -0,0 +1,449 @@ +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..d689994d10 --- /dev/null +++ b/extensions/permissions/test/browser_permmgr_viewsrc.js @@ -0,0 +1,27 @@ +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( + "http://example.com" + ); + Services.perms.addFromPrincipal( + principal, + "viewsourceTestingPerm", + Services.perms.ALLOW_ACTION + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "view-source:http://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..7571234256 --- /dev/null +++ b/extensions/permissions/test/unit/head.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +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..b2bae241f0 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_default_pref.js @@ -0,0 +1,75 @@ +/* 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..0c0ab76d9d --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_defaults.js @@ -0,0 +1,435 @@ +/* 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 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 principal6 = 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) + ); + + // 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) + ); + // 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(principal6, 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(principal6, 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 principals with userContextId share permissions depending on pref state + Assert.equal( + permIsolateUserContext + ? Ci.nsIPermissionManager.UNKNOWN_ACTION + : Ci.nsIPermissionManager.ALLOW_ACTION, + pm.testPermissionFromPrincipal(principal6, 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 share permissions depending on pref state + Assert.equal( + permIsolateUserContext + ? Ci.nsIPermissionManager.UNKNOWN_ACTION + : Ci.nsIPermissionManager.DENY_ACTION, + pm.testPermissionFromPrincipal(principal6, 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 share permissions depending on pref state + Assert.equal( + permIsolateUserContext + ? Ci.nsIPermissionManager.UNKNOWN_ACTION + : Ci.nsIPermissionManager.PROMPT_ACTION, + pm.testPermissionFromPrincipal(principal6, 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..743935f906 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_expiration.js @@ -0,0 +1,173 @@ +/* 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); + + 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..0d328d0a12 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_getAllByTypeSince.js @@ -0,0 +1,77 @@ +/* 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_getAllForPrincipal.js b/extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js new file mode 100644 index 0000000000..b8cfe26549 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_getAllForPrincipal.js @@ -0,0 +1,70 @@ +/* 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..b6a99fbdbe --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_getAllWithTypePrefix.js @@ -0,0 +1,82 @@ +/* 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_load_invalid_entries.js b/extensions/permissions/test/unit/test_permmanager_load_invalid_entries.js new file mode 100644 index 0000000000..a339549c83 --- /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, 11); + + 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..07835ef2b1 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_10-11.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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_4-7.js b/extensions/permissions/test/unit/test_permmanager_migrate_4-7.js new file mode 100644 index 0000000000..cde06ee255 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_4-7.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..4013e0a5e2 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_4-7_no_history.js @@ -0,0 +1,280 @@ +/* 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"); + }, + lockFactory() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), +}; + +var newClassID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID(); + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID = registrar.contractIDToCID(CONTRACT_ID); +var oldFactory = 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"); + } + + 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..adf4fbf521 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_5-7a.js @@ -0,0 +1,367 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..c662711506 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_5-7b.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..2e0b78bf8b --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_6-7a.js @@ -0,0 +1,368 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..7bf8dc6706 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_6-7b.js @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..809fcfb36c --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_7-8.js @@ -0,0 +1,330 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..96edd69dfe --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_migrate_9-10.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +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..ee357ac44d --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_notifications.js @@ -0,0 +1,143 @@ +/* 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 continue_test() { + do_run_generator(test_generator); +} + +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..7c791928cd --- /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..815468e0e8 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_remove_add_update.js @@ -0,0 +1,83 @@ +/* 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..12a05c2902 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_removebytype.js @@ -0,0 +1,73 @@ +/* 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..294ce410c2 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_removebytypesince.js @@ -0,0 +1,86 @@ +/* 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..6f454d8f35 --- /dev/null +++ b/extensions/permissions/test/unit/test_permmanager_removepermission.js @@ -0,0 +1,56 @@ +/* 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_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..f9a5d3c111 --- /dev/null +++ b/extensions/permissions/test/unit/xpcshell.ini @@ -0,0 +1,36 @@ +[DEFAULT] +head = head.js +skip-if = toolkit == 'android' + +[test_permmanager_default_pref.js] +[test_permmanager_defaults.js] +[test_permmanager_expiration.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] +[test_permmanager_migrate_5-7a.js] +[test_permmanager_migrate_5-7b.js] +[test_permmanager_migrate_6-7a.js] +[test_permmanager_migrate_6-7b.js] +[test_permmanager_migrate_4-7_no_history.js] +[test_permmanager_migrate_7-8.js] +[test_permmanager_migrate_9-10.js] +[test_permmanager_migrate_10-11.js] +[test_permmanager_oa_strip.js] +[test_permmanager_remove_add_update.js] |