/* -*- 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 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 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 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 uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(uri, attrs); principal.forget(aPrincipal); return NS_OK; } nsresult GetPrincipal(nsIURI* aURI, bool aIsInIsolatedMozBrowserElement, nsIPrincipal** aPrincipal) { OriginAttributes attrs(aIsInIsolatedMozBrowserElement); nsCOMPtr 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 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 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 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&& aCallback) { if (aHost.EqualsLiteral("")) { // We no longer support the magic host NS_WARNING( "The magic host 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 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 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 histSrv = do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); if (histSrv) { nsCOMPtr 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 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 histResult; rv = histSrv->ExecuteQuery(histQuery, histQueryOpts, getter_AddRefs(histResult)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 insertedOrigins; for (uint32_t i = 0; i < childCount; i++) { nsCOMPtr 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 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 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 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 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 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 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(); 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 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 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 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 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 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 defaultsInputStream = GetDefaultsInputStream(); RefPtr 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 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 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(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 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 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 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(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 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 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 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 self = this; mThread->Dispatch(NS_NewRunnableFunction( "PermissionManager::PerformIdleDailyMaintenance", [self] { auto data = self->mThreadBoundData.Access(); if (self->mState == eClosed || !data->mDBConn) { return; } nsCOMPtr 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 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 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 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 nsresult PermissionManager::RemovePermissionEntries(T aCondition) { EnsureReadCompleted(); Vector, 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 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(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 defaultsInputStream; if (aNextOp == eRebuldOnSuccess) { defaultsInputStream = GetDefaultsInputStream(); } RefPtr 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 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 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 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 principal; nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, IsOAForceStripPermission(aType), getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); PermissionEntry& perm = entry->GetPermissions()[idx]; nsCOMPtr 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 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 nsresult PermissionManager::GetPermissionEntries( T aCondition, nsTArray>& 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 principal; nsresult rv = GetPrincipalFromOrigin( entry->GetKey()->mOrigin, IsOAForceStripPermission(mTypeArray[permEntry.mType]), getter_AddRefs(principal)); if (NS_FAILED(rv)) { continue; } RefPtr 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>& aResult) { return GetPermissionEntries( [](const PermissionEntry& aPermEntry) { return true; }, aResult); } NS_IMETHODIMP PermissionManager::GetAllByTypeSince( const nsACString& aPrefix, int64_t aSince, nsTArray>& aResult) { return GetPermissionEntries( [&](const PermissionEntry& aPermEntry) { return mTypeArray[aPermEntry.mType].Equals(aPrefix) && aSince <= aPermEntry.mModificationTime; }, aResult); } NS_IMETHODIMP PermissionManager::GetAllWithTypePrefix( const nsACString& aPrefix, nsTArray>& aResult) { return GetPermissionEntries( [&](const PermissionEntry& aPermEntry) { return StringBeginsWith(mTypeArray[aPermEntry.mType], aPrefix); }, aResult); } NS_IMETHODIMP PermissionManager::GetAllForPrincipal( nsIPrincipal* aPrincipal, nsTArray>& aResult) { aResult.Clear(); EnsureReadCompleted(); MOZ_ASSERT(PermissionAvailable(aPrincipal, ""_ns)); nsresult rv; RefPtr key = PermissionKey::CreateFromPrincipal(aPrincipal, false, rv); if (!key) { MOZ_ASSERT(NS_FAILED(rv)); return rv; } PermissionHashKey* entry = mPermissionTable.GetEntry(key); nsTArray 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::index_type index = 0; for (const auto& strippedPerm : strippedPerms) { if (strippedPerm.mType == permEntry.mType) { perm = strippedPerm; strippedPerms.RemoveElementAt(index); break; } index++; } RefPtr 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 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, nsCString, nsCString>, 10> permissions; for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { PermissionHashKey* entry = iter.Get(); nsCOMPtr 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& 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 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 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 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 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 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 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 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 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 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 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 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 entries; { MonitorAutoLock lock(mMonitor); entries = std::move(mReadEntries); } for (const ReadEntry& entry : entries) { nsCOMPtr 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 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& 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& aPerms) { if (NS_WARN_IF(XRE_IsParentProcess())) { return; } RefPtr 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{}); // Add the permissions locally to our process for (IPC::Permission& perm : aPerms) { nsCOMPtr 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 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> PermissionManager::GetAllKeysForPrincipal(nsIPrincipal* aPrincipal) { MOZ_ASSERT(aPrincipal); nsTArray> pairs; nsCOMPtr prin = aPrincipal; while (prin) { // Add the pair to the list std::pair* 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 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 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> promises; for (auto& pair : GetAllKeysForPrincipal(aPrincipal)) { RefPtr 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 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 PermissionManager::GetDefaultsInputStream() { MOZ_ASSERT(NS_IsMainThread()); nsAutoCString defaultsURL; Preferences::GetCString(kDefaultsUrlPrefName, defaultsURL); if (defaultsURL.IsEmpty()) { // == Don't use built-in permissions. return nullptr; } nsCOMPtr defaultsURI; nsresult rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL); NS_ENSURE_SUCCESS(rv, nullptr); nsCOMPtr 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 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 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 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 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 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()) { auto ep = basePrin->As(); 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()) { return preparationResult.as(); } return CommonTestPermissionInternal( aPrincipal, nullptr, nullptr, preparationResult.as(), 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()) { return preparationResult.as(); } return CommonTestPermissionInternal( nullptr, aURI, nullptr, preparationResult.as(), 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()) { return preparationResult.as(); } return CommonTestPermissionInternal( nullptr, aURI, aOriginAttributes, preparationResult.as(), 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 asc = GetShutdownPhase(); MOZ_ASSERT(asc); DebugOnly 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 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 PermissionManager::GetShutdownPhase() const { nsresult rv; nsCOMPtr svc = do_GetService("@mozilla.org/async-shutdown-service;1", &rv); if (NS_FAILED(rv)) { return nullptr; } nsCOMPtr client; rv = svc->GetProfileBeforeChange(getter_AddRefs(client)); MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); return client; } } // namespace mozilla