/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/Attributes.h" #include "mozilla/DebugOnly.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/BasePrincipal.h" #include "mozilla/Services.h" #include "mozilla/unused.h" #include "nsPermissionManager.h" #include "nsPermission.h" #include "nsCRT.h" #include "nsNetUtil.h" #include "nsCOMArray.h" #include "nsArrayEnumerator.h" #include "nsTArray.h" #include "nsReadableUtils.h" #include "nsILineInputStream.h" #include "nsAppDirectoryServiceDefs.h" #include "nsDirectoryServiceDefs.h" #include "prprf.h" #include "mozilla/storage.h" #include "mozilla/Attributes.h" #include "nsXULAppAPI.h" #include "nsIPrincipal.h" #include "nsContentUtils.h" #include "nsIScriptSecurityManager.h" #include "nsIAppsService.h" #include "mozIApplication.h" #include "nsIEffectiveTLDService.h" #include "nsPIDOMWindow.h" #include "nsIDocument.h" #include "mozilla/net/NeckoMessageUtils.h" #include "mozilla/Preferences.h" #include "nsReadLine.h" #include "mozilla/Telemetry.h" #include "nsIConsoleService.h" #include "nsINavHistoryService.h" #include "nsToolkitCompsCID.h" #include "nsIObserverService.h" static nsPermissionManager *gPermissionManager = nullptr; using mozilla::dom::ContentParent; using mozilla::dom::ContentChild; using mozilla::Unused; // ha! static bool IsChildProcess() { return XRE_IsContentProcess(); } /** * @returns The child process object, or if we are not in the child * process, nullptr. */ static ContentChild* ChildProcess() { if (IsChildProcess()) { ContentChild* cpc = ContentChild::GetSingleton(); if (!cpc) NS_RUNTIMEABORT("Content Process is nullptr!"); return cpc; } return nullptr; } static 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()); } #define ENSURE_NOT_CHILD_PROCESS_(onError) \ PR_BEGIN_MACRO \ if (IsChildProcess()) { \ NS_ERROR("Cannot perform action in content process!"); \ onError \ } \ PR_END_MACRO #define ENSURE_NOT_CHILD_PROCESS \ ENSURE_NOT_CHILD_PROCESS_({ return NS_ERROR_NOT_AVAILABLE; }) #define ENSURE_NOT_CHILD_PROCESS_NORET \ ENSURE_NOT_CHILD_PROCESS_(;) //////////////////////////////////////////////////////////////////////////////// namespace { nsresult GetPrincipalFromOrigin(const nsACString& aOrigin, nsIPrincipal** aPrincipal) { nsAutoCString originNoSuffix; mozilla::PrincipalOriginAttributes attrs; if (!attrs.PopulateFromOrigin(aOrigin, originNoSuffix)) { return NS_ERROR_FAILURE; } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr principal = mozilla::BasePrincipal::CreateCodebasePrincipal(uri, attrs); principal.forget(aPrincipal); return NS_OK; } nsresult GetPrincipal(nsIURI* aURI, uint32_t aAppId, bool aIsInBrowserElement, nsIPrincipal** aPrincipal) { // TODO: Bug 1165267 - Use OriginAttributes for nsCookieService mozilla::PrincipalOriginAttributes attrs(aAppId, aIsInBrowserElement); nsCOMPtr principal = mozilla::BasePrincipal::CreateCodebasePrincipal(aURI, attrs); NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); principal.forget(aPrincipal); return NS_OK; } nsresult GetPrincipal(nsIURI* aURI, nsIPrincipal** aPrincipal) { mozilla::PrincipalOriginAttributes attrs; nsCOMPtr principal = mozilla::BasePrincipal::CreateCodebasePrincipal(aURI, attrs); NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); principal.forget(aPrincipal); return NS_OK; } nsCString GetNextSubDomainForHost(const nsACString& aHost) { nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (!tldService) { NS_ERROR("Should have a tld service!"); return EmptyCString(); } nsCString subDomain; nsresult rv = tldService->GetNextSubDomain(aHost, subDomain); // We can fail if there is no more subdomain or if the host can't have a // subdomain. if (NS_FAILED(rv)) { return EmptyCString(); } return subDomain; } class ClearOriginDataObserver final : public nsIObserver { ~ClearOriginDataObserver() {} public: NS_DECL_ISUPPORTS // nsIObserver implementation. NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) override { MOZ_ASSERT(!nsCRT::strcmp(aTopic, "clear-origin-data")); nsCOMPtr permManager = do_GetService("@mozilla.org/permissionmanager;1"); return permManager->RemovePermissionsWithAttributes(nsDependentString(aData)); } }; NS_IMPL_ISUPPORTS(ClearOriginDataObserver, nsIObserver) class MOZ_STACK_CLASS UpgradeHostToOriginHelper { public: virtual nsresult Insert(const nsACString& aOrigin, const nsAFlatCString& aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) = 0; }; class MOZ_STACK_CLASS UpgradeHostToOriginDBMigration final : public UpgradeHostToOriginHelper { public: UpgradeHostToOriginDBMigration(mozIStorageConnection* aDBConn, int64_t* aID) : mDBConn(aDBConn) , mID(aID) { mDBConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO moz_hosts_new " "(id, origin, type, permission, expireType, expireTime, modificationTime) " "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmt)); } nsresult Insert(const nsACString& aOrigin, const nsAFlatCString& aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) final { nsresult rv = mStmt->BindInt64ByIndex(0, *mID); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindUTF8StringByIndex(1, aOrigin); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindUTF8StringByIndex(2, aType); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt32ByIndex(3, aPermission); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt32ByIndex(4, aExpireType); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt64ByIndex(5, aExpireTime); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt64ByIndex(6, aModificationTime); NS_ENSURE_SUCCESS(rv, rv); // Increment the working identifier, as we are about to use this one (*mID)++; rv = mStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } private: nsCOMPtr mStmt; nsCOMPtr mDBConn; int64_t* mID; }; class MOZ_STACK_CLASS UpgradeHostToOriginHostfileImport final : public UpgradeHostToOriginHelper { public: UpgradeHostToOriginHostfileImport(nsPermissionManager* aPm, nsPermissionManager::DBOperationType aOperation, int64_t aID) : mPm(aPm) , mOperation(aOperation) , mID(aID) {} nsresult Insert(const nsACString& aOrigin, const nsAFlatCString& aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) final { nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(aOrigin, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return mPm->AddInternal(principal, aType, aPermission, mID, aExpireType, aExpireTime, aModificationTime, nsPermissionManager::eDontNotify, mOperation); } private: RefPtr mPm; nsPermissionManager::DBOperationType mOperation; int64_t mID; }; class MOZ_STACK_CLASS UpgradeIPHostToOriginDB final : public UpgradeHostToOriginHelper { public: UpgradeIPHostToOriginDB(mozIStorageConnection* aDBConn, int64_t* aID) : mDBConn(aDBConn) , mID(aID) { mDBConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO moz_perms" "(id, origin, type, permission, expireType, expireTime, modificationTime) " "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmt)); mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id FROM moz_perms WHERE origin = ?1 AND type = ?2"), getter_AddRefs(mLookupStmt)); } nsresult Insert(const nsACString& aOrigin, const nsAFlatCString& aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) final { // Every time the migration code wants to insert an origin into // the database we need to check to see if someone has already // created a permissions entry for that permission. If they have, // we don't want to insert a duplicate row. // // We can afford to do this lookup unconditionally and not perform // caching, as a origin type pair should only be attempted to be // inserted once. nsresult rv = mLookupStmt->Reset(); NS_ENSURE_SUCCESS(rv, rv); rv = mLookupStmt->BindUTF8StringByIndex(0, aOrigin); NS_ENSURE_SUCCESS(rv, rv); rv = mLookupStmt->BindUTF8StringByIndex(1, aType); NS_ENSURE_SUCCESS(rv, rv); // Check if we already have the row in the database, if we do, then // we don't want to be inserting it again. bool moreStmts = false; if (NS_FAILED(mLookupStmt->ExecuteStep(&moreStmts)) || moreStmts) { mLookupStmt->Reset(); NS_WARNING("A permissions entry was going to be re-migrated, " "but was already found in the permissions database."); return NS_OK; } // Actually insert the statement into the database. rv = mStmt->BindInt64ByIndex(0, *mID); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindUTF8StringByIndex(1, aOrigin); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindUTF8StringByIndex(2, aType); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt32ByIndex(3, aPermission); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt32ByIndex(4, aExpireType); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt64ByIndex(5, aExpireTime); NS_ENSURE_SUCCESS(rv, rv); rv = mStmt->BindInt64ByIndex(6, aModificationTime); NS_ENSURE_SUCCESS(rv, rv); // Increment the working identifier, as we are about to use this one (*mID)++; rv = mStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } private: nsCOMPtr mStmt; nsCOMPtr mLookupStmt; nsCOMPtr mDBConn; int64_t* mID; }; nsresult UpgradeHostToOriginAndInsert(const nsACString& aHost, const nsAFlatCString& aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime, uint32_t aAppId, bool aIsInBrowserElement, UpgradeHostToOriginHelper* aHelper) { 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 bool nullpScheme = false; if (NS_SUCCEEDED(uri->SchemeIs("moz-nullprincipal", &nullpScheme)) && nullpScheme) { NS_WARNING("A moz-nullprincipal: permission is being discarded."); return NS_OK; } nsCOMPtr principal; rv = GetPrincipal(uri, aAppId, aIsInBrowserElement, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString origin; rv = principal->GetOrigin(origin); NS_ENSURE_SUCCESS(rv, rv); return aHelper->Insert(origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); return NS_OK; } // The user may use this host at non-standard ports or protocols, we can use their history // to guess what ports and protocols we want to add permissions for. // We find every URI which they have visited with this host (or a subdomain of this host), // and try to add it as a principal. bool foundHistory = false; nsCOMPtr 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; nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); MOZ_ASSERT(tldService); // We should always have a tldService if (tldService) { rv = tldService->GetBaseDomainFromHost(aHost, 0, eTLD1); } if (!tldService || NS_FAILED(rv)) { // If the lookup on the tldService for the base domain for the host failed, // that means that we just want to directly use the host as the host name // for the lookup. eTLD1 = aHost; } // We want to only find history items for this particular eTLD+1, and subdomains rv = histQuery->SetDomain(eTLD1); NS_ENSURE_SUCCESS(rv, rv); rv = histQuery->SetDomainIsHost(false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 = uri->SetHost(aHost); if (NS_WARN_IF(NS_FAILED(rv))) continue; // We now have a URI which we can make a nsIPrincipal out of nsCOMPtr principal; rv = GetPrincipal(uri, aAppId, aIsInBrowserElement, getter_AddRefs(principal)); if (NS_WARN_IF(NS_FAILED(rv))) continue; nsAutoCString origin; rv = principal->GetOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) continue; // Ensure that we don't insert the same origin repeatedly if (insertedOrigins.Contains(origin)) { continue; } foundHistory = true; rv = aHelper->Insert(origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); NS_WARN_IF(NS_WARN_IF(NS_FAILED(rv))); 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 liklihood 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.Assign("["); hostSegment.Append(aHost); hostSegment.Append("]"); } else { hostSegment.Assign(aHost); } // http:// URI default rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("http://") + hostSegment); NS_ENSURE_SUCCESS(rv, rv); rv = GetPrincipal(uri, aAppId, aIsInBrowserElement, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); rv = principal->GetOrigin(origin); NS_ENSURE_SUCCESS(rv, rv); aHelper->Insert(origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); // https:// URI default rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + hostSegment); NS_ENSURE_SUCCESS(rv, rv); rv = GetPrincipal(uri, aAppId, aIsInBrowserElement, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); rv = principal->GetOrigin(origin); NS_ENSURE_SUCCESS(rv, rv); aHelper->Insert(origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); } return NS_OK; } static bool IsExpandedPrincipal(nsIPrincipal* aPrincipal) { nsCOMPtr ep = do_QueryInterface(aPrincipal); return !!ep; } } // namespace //////////////////////////////////////////////////////////////////////////////// nsPermissionManager::PermissionKey::PermissionKey(nsIPrincipal* aPrincipal) { MOZ_ALWAYS_TRUE(NS_SUCCEEDED(aPrincipal->GetOrigin(mOrigin))); } /** * Simple callback used by |AsyncClose| to trigger a treatment once * the database is closed. * * Note: Beware that, if you hold onto a |CloseDatabaseListener| from a * |nsPermissionManager|, this will create a cycle. * * Note: Once the callback has been called this DeleteFromMozHostListener cannot * be reused. */ class CloseDatabaseListener final : public mozIStorageCompletionCallback { ~CloseDatabaseListener() {} public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGECOMPLETIONCALLBACK /** * @param aManager The owning manager. * @param aRebuildOnSuccess If |true|, reinitialize the database once * it has been closed. Otherwise, do nothing such. */ CloseDatabaseListener(nsPermissionManager* aManager, bool aRebuildOnSuccess); protected: RefPtr mManager; bool mRebuildOnSuccess; }; NS_IMPL_ISUPPORTS(CloseDatabaseListener, mozIStorageCompletionCallback) CloseDatabaseListener::CloseDatabaseListener(nsPermissionManager* aManager, bool aRebuildOnSuccess) : mManager(aManager) , mRebuildOnSuccess(aRebuildOnSuccess) { } NS_IMETHODIMP CloseDatabaseListener::Complete(nsresult, nsISupports*) { // Help breaking cycles RefPtr manager = mManager.forget(); if (mRebuildOnSuccess && !manager->mIsShuttingDown) { return manager->InitDB(true); } return NS_OK; } /** * Simple callback used by |RemoveAllInternal| to trigger closing * the database and reinitializing it. * * Note: Beware that, if you hold onto a |DeleteFromMozHostListener| from a * |nsPermissionManager|, this will create a cycle. * * Note: Once the callback has been called this DeleteFromMozHostListener cannot * be reused. */ class DeleteFromMozHostListener final : public mozIStorageStatementCallback { ~DeleteFromMozHostListener() {} public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGESTATEMENTCALLBACK /** * @param aManager The owning manager. */ explicit DeleteFromMozHostListener(nsPermissionManager* aManager); protected: RefPtr mManager; }; NS_IMPL_ISUPPORTS(DeleteFromMozHostListener, mozIStorageStatementCallback) DeleteFromMozHostListener:: DeleteFromMozHostListener(nsPermissionManager* aManager) : mManager(aManager) { } NS_IMETHODIMP DeleteFromMozHostListener::HandleResult(mozIStorageResultSet *) { MOZ_CRASH("Should not get any results"); } NS_IMETHODIMP DeleteFromMozHostListener::HandleError(mozIStorageError *) { // Errors are handled in |HandleCompletion| return NS_OK; } NS_IMETHODIMP DeleteFromMozHostListener::HandleCompletion(uint16_t aReason) { // Help breaking cycles RefPtr manager = mManager.forget(); if (aReason == REASON_ERROR) { manager->CloseDB(true); } return NS_OK; } /* static */ void nsPermissionManager::ClearOriginDataObserverInit() { nsCOMPtr observerService = mozilla::services::GetObserverService(); observerService->AddObserver(new ClearOriginDataObserver(), "clear-origin-data", /* holdsWeak= */ false); } //////////////////////////////////////////////////////////////////////////////// // nsPermissionManager Implementation #define PERMISSIONS_FILE_NAME "permissions.sqlite" #define HOSTS_SCHEMA_VERSION 9 #define HOSTPERM_FILE_NAME "hostperm.1" // Default permissions are read from a URL - this is the preference we read // to find that URL. If not set, don't use any default permissions. static const char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl"; static const char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION; NS_IMPL_ISUPPORTS(nsPermissionManager, nsIPermissionManager, nsIObserver, nsISupportsWeakReference) nsPermissionManager::nsPermissionManager() : mMemoryOnlyDB(false) , mLargestID(0) , mIsShuttingDown(false) { } nsPermissionManager::~nsPermissionManager() { RemoveAllFromMemory(); gPermissionManager = nullptr; } // static nsIPermissionManager* nsPermissionManager::GetXPCOMSingleton() { if (gPermissionManager) { NS_ADDREF(gPermissionManager); return gPermissionManager; } // Create a new singleton nsPermissionManager. // We AddRef only once since XPCOM has rules about the ordering of module // teardowns - by the time our module destructor is called, it's too late to // Release our members, since GC cycles have already been completed and // would result in serious leaks. // See bug 209571. gPermissionManager = new nsPermissionManager(); if (gPermissionManager) { NS_ADDREF(gPermissionManager); if (NS_FAILED(gPermissionManager->Init())) { NS_RELEASE(gPermissionManager); } } return gPermissionManager; } nsresult nsPermissionManager::Init() { // If the 'permissions.memory_only' pref is set to true, then don't write any // permission settings to disk, but keep them in a memory-only database. mMemoryOnlyDB = mozilla::Preferences::GetBool("permissions.memory_only", false); if (IsChildProcess()) { // Stop here; we don't need the DB in the child process return FetchPermissions(); } nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->AddObserver(this, "profile-before-change", true); observerService->AddObserver(this, "profile-do-change", true); } // ignore failure here, since it's non-fatal (we can run fine without // persistent storage - e.g. if there's no profile). // XXX should we tell the user about this? InitDB(false); return NS_OK; } NS_IMETHODIMP nsPermissionManager::RefreshPermission() { NS_ENSURE_TRUE(IsChildProcess(), NS_ERROR_FAILURE); nsresult rv = RemoveAllFromMemory(); NS_ENSURE_SUCCESS(rv, rv); rv = FetchPermissions(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsPermissionManager::OpenDatabase(nsIFile* aPermissionsFile) { 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("memory", getter_AddRefs(mDBConn)); } else { rv = storage->OpenDatabase(aPermissionsFile, getter_AddRefs(mDBConn)); } return rv; } nsresult nsPermissionManager::InitDB(bool aRemoveFile) { nsCOMPtr permissionsFile; nsresult rv = NS_GetSpecialDirectory(NS_APP_PERMISSION_PARENT_DIR, getter_AddRefs(permissionsFile)); if (NS_FAILED(rv)) { rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(permissionsFile)); } NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); rv = permissionsFile->AppendNative(NS_LITERAL_CSTRING(PERMISSIONS_FILE_NAME)); NS_ENSURE_SUCCESS(rv, rv); if (aRemoveFile) { bool exists = false; rv = permissionsFile->Exists(&exists); NS_ENSURE_SUCCESS(rv, rv); if (exists) { rv = permissionsFile->Remove(false); NS_ENSURE_SUCCESS(rv, rv); } } rv = OpenDatabase(permissionsFile); if (rv == NS_ERROR_FILE_CORRUPTED) { LogToConsole(NS_LITERAL_STRING("permissions.sqlite is corrupted! Try again!")); // Add telemetry probe mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERMISSIONS_SQL_CORRUPTED, 1); // delete corrupted permissions.sqlite and try again rv = permissionsFile->Remove(false); NS_ENSURE_SUCCESS(rv, rv); LogToConsole(NS_LITERAL_STRING("Corrupted permissions.sqlite has been removed.")); rv = OpenDatabase(permissionsFile); NS_ENSURE_SUCCESS(rv, rv); LogToConsole(NS_LITERAL_STRING("OpenDatabase to permissions.sqlite is successful!")); } else if (NS_FAILED(rv)) { return rv; } bool ready; mDBConn->GetConnectionReady(&ready); if (!ready) { LogToConsole(NS_LITERAL_STRING("Fail to get connection to permissions.sqlite! Try again!")); // delete and try again rv = permissionsFile->Remove(false); NS_ENSURE_SUCCESS(rv, rv); LogToConsole(NS_LITERAL_STRING("Defective permissions.sqlite has been removed.")); // Add telemetry probe mozilla::Telemetry::Accumulate(mozilla::Telemetry::DEFECTIVE_PERMISSIONS_SQL_REMOVED, 1); rv = OpenDatabase(permissionsFile); NS_ENSURE_SUCCESS(rv, rv); LogToConsole(NS_LITERAL_STRING("OpenDatabase to permissions.sqlite is successful!")); mDBConn->GetConnectionReady(&ready); if (!ready) return NS_ERROR_UNEXPECTED; } bool tableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &tableExists); if (!tableExists) { mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &tableExists); } if (!tableExists) { rv = CreateTable(); NS_ENSURE_SUCCESS(rv, rv); } else { // table already exists; check the schema version before reading int32_t dbSchemaVersion; rv = mDBConn->GetSchemaVersion(&dbSchemaVersion); NS_ENSURE_SUCCESS(rv, rv); switch (dbSchemaVersion) { // upgrading. // every time you increment the database schema, you need to implement // the upgrading code from the previous version to the new one. // fall through to current version case 1: { // previous non-expiry version of database. Upgrade it by adding the // expiration columns rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD expireType INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD expireTime INTEGER")); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // TODO: we want to make default version as version 2 in order to fix bug 784875. case 0: case 2: { // Add appId/isInBrowserElement fields. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD appId INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD isInBrowserElement INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->SetSchemaVersion(3); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // Version 3->4 is the creation of the modificationTime field. case 3: { rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD modificationTime INTEGER")); NS_ENSURE_SUCCESS(rv, rv); // We leave the modificationTime at zero for all existing records; using // now() would mean, eg, that doing "remove all from the last hour" // within the first hour after migration would remove all permissions. rv = mDBConn->SetSchemaVersion(4); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // In version 5, host appId, and isInBrowserElement were merged into a // single origin entry // // In version 6, the tables were renamed for backwards compatability reasons // with version 4 and earlier. // // In version 7, a bug in the migration used for version 4->5 was discovered // which could have triggered data-loss. Because of that, all users with a // version 4, 5, or 6 database will be re-migrated from the backup database. // (bug 1186034). This migration bug is not present after bug 1185340, and the // re-migration ensures that all users have the fix. case 5: // This branch could also be reached via dbSchemaVersion == 3, in which case // we want to fall through to the dbSchemaVersion == 4 case. // The easiest way to do that is to perform this extra check here to make // sure that we didn't get here via a fallthrough from v3 if (dbSchemaVersion == 5) { // In version 5, the backup database is named moz_hosts_v4. We perform // the version 5->6 migration to get the tables to have consistent // naming conventions. // Version 5->6 is the renaming of moz_hosts to moz_perms, and // moz_hosts_v4 to moz_hosts (bug 1185343) // // In version 5, we performed the modifications to the permissions // database in place, this meant that if you upgraded to a version which // used V5, and then downgraded to a version which used v4 or earlier, // the fallback path would drop the table, and your permissions data // would be lost. This migration undoes that mistake, by restoring the // old moz_hosts table (if it was present), and instead using the new // table moz_perms for the new permissions schema. // // NOTE: If you downgrade, store new permissions, and then upgrade // again, these new permissions won't be migrated or reflected in the // updated database. This migration only occurs once, as if moz_perms // exists, it will skip creating it. In addition, permissions added // after the migration will not be visible in previous versions of // firefox. bool permsTableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists); if (!permsTableExists) { // Move the upgraded database to moz_perms rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts RENAME TO moz_perms")); NS_ENSURE_SUCCESS(rv, rv); } else { NS_WARNING("moz_hosts was not renamed to moz_perms, " "as a moz_perms table already exists"); // In the situation where a moz_perms table already exists, but the // schema is lower than 6, a migration has already previously occured // to V6, but a downgrade has caused the moz_hosts table to be // dropped. This should only occur in the case of a downgrade to a V5 // database, which was only present in a few day's nightlies. As that // version was likely used only on a temporary basis, we assume that // the database from the previous V6 has the permissions which the // user actually wants to use. We have to get rid of moz_hosts such // that moz_hosts_v4 can be moved into its place if it exists. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts")); NS_ENSURE_SUCCESS(rv, rv); } #ifdef DEBUG // The moz_hosts table shouldn't exist anymore bool hostsTableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists); MOZ_ASSERT(!hostsTableExists); #endif // Rename moz_hosts_v4 back to it's original location, if it exists bool v4TableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_v4"), &v4TableExists); if (v4TableExists) { rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts_v4 RENAME TO moz_hosts")); NS_ENSURE_SUCCESS(rv, rv); } rv = mDBConn->SetSchemaVersion(6); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // At this point, the version 5 table has been migrated to a version 6 table // We are guaranteed to have at least one of moz_hosts and moz_perms. If // we have moz_hosts, we will migrate moz_hosts into moz_perms (even if // we already have a moz_perms, as we need a re-migration due to bug 1186034). // // After this migration, we are guaranteed to have both a moz_hosts (for backwards // compatability), and a moz_perms table. The moz_hosts table will have a v4 schema, // and the moz_perms table will have a v6 schema. case 4: case 6: { bool hostsTableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists); if (hostsTableExists) { bool migrationError = false; // Both versions 4 and 6 have a version 4 formatted hosts table named // moz_hosts. We can migrate this table to our version 7 table moz_perms. // If moz_perms is present, then we can use it as a basis for comparison. rv = mDBConn->BeginTransaction(); NS_ENSURE_SUCCESS(rv, rv); bool tableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_new"), &tableExists); if (tableExists) { NS_WARNING("The temporary database moz_hosts_new already exists, dropping it."); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts_new")); NS_ENSURE_SUCCESS(rv, rv); } rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_hosts_new (" " id INTEGER PRIMARY KEY" ",origin TEXT" ",type TEXT" ",permission INTEGER" ",expireType INTEGER" ",expireTime INTEGER" ",modificationTime INTEGER" ")")); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr stmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT host, type, permission, expireType, expireTime, " "modificationTime, appId, isInBrowserElement FROM moz_hosts"), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); int64_t id = 0; nsAutoCString host, type; uint32_t permission; uint32_t expireType; int64_t expireTime; int64_t modificationTime; uint32_t appId; bool isInBrowserElement; bool hasResult; while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { // Read in the old row rv = stmt->GetUTF8String(0, host); if (NS_WARN_IF(NS_FAILED(rv))) { migrationError = true; continue; } rv = stmt->GetUTF8String(1, type); if (NS_WARN_IF(NS_FAILED(rv))) { migrationError = true; continue; } permission = stmt->AsInt32(2); expireType = stmt->AsInt32(3); expireTime = stmt->AsInt64(4); modificationTime = stmt->AsInt64(5); if (NS_WARN_IF(stmt->AsInt64(6) < 0)) { migrationError = true; continue; } appId = static_cast(stmt->AsInt64(6)); isInBrowserElement = static_cast(stmt->AsInt32(7)); // Perform the meat of the migration by deferring to the // UpgradeHostToOriginAndInsert function. UpgradeHostToOriginDBMigration upHelper(mDBConn, &id); rv = UpgradeHostToOriginAndInsert(host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement, &upHelper); if (NS_FAILED(rv)) { NS_WARNING("Unexpected failure when upgrading migrating permission " "from host to origin"); migrationError = true; } } // We don't drop the moz_hosts table such that it is avaliable for // backwards-compatability and for future migrations in case of // migration errors in the current code. // Create a marker empty table which will indicate that the moz_hosts // table is intended to act as a backup. If this table is not present, // then the moz_hosts table was created as a random empty table. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_hosts_is_backup (dummy INTEGER PRIMARY KEY)")); NS_ENSURE_SUCCESS(rv, rv); bool permsTableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists); if (permsTableExists) { // The user already had a moz_perms table, and we are performing a // re-migration. We count the rows in the old table for telemetry, // and then back up their old database as moz_perms_v6 nsCOMPtr countStmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT COUNT(*) FROM moz_perms"), getter_AddRefs(countStmt)); bool hasResult = false; if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(countStmt->ExecuteStep(&hasResult)) && hasResult) { int32_t permsCount = countStmt->AsInt32(0); // The id variable contains the number of rows inserted into the // moz_hosts_new table (as one ID was used per entry) uint32_t telemetryValue; if (permsCount > id) { telemetryValue = 3; // NEW > OLD } else if (permsCount == id) { telemetryValue = 2; // NEW == OLD } else if (permsCount == 0) { telemetryValue = 0; // NEW = 0 } else { telemetryValue = 1; // NEW < OLD } // Report the telemetry value to telemetry mozilla::Telemetry::Accumulate( mozilla::Telemetry::PERMISSIONS_REMIGRATION_COMPARISON, telemetryValue); } else { NS_WARNING("Could not count the rows in moz_perms"); } // Back up the old moz_perms database as moz_perms_v6 before we // move the new table into its position rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_perms RENAME TO moz_perms_v6")); NS_ENSURE_SUCCESS(rv, rv); } rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts_new RENAME TO moz_perms")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CommitTransaction(); NS_ENSURE_SUCCESS(rv, rv); mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERMISSIONS_MIGRATION_7_ERROR, NS_WARN_IF(migrationError)); } else { // We don't have a moz_hosts table, so we create one for downgrading purposes. // This table is empty. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_hosts (" " id INTEGER PRIMARY KEY" ",host TEXT" ",type TEXT" ",permission INTEGER" ",expireType INTEGER" ",expireTime INTEGER" ",modificationTime INTEGER" ",appId INTEGER" ",isInBrowserElement INTEGER" ")")); NS_ENSURE_SUCCESS(rv, rv); // We are guaranteed to have a moz_perms table at this point. } #ifdef DEBUG { // At this point, both the moz_hosts and moz_perms tables should exist bool hostsTableExists = false; bool permsTableExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &hostsTableExists); mDBConn->TableExists(NS_LITERAL_CSTRING("moz_perms"), &permsTableExists); MOZ_ASSERT(hostsTableExists && permsTableExists); } #endif rv = mDBConn->SetSchemaVersion(7); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // The version 7-8 migration is the re-migration of localhost and ip-address // entries due to errors in the previous version 7 migration which caused // localhost and ip-address entries to be incorrectly discarded. // The version 7 migration logic has been corrected, and thus this logic only // needs to execute if the user is currently on version 7. case 7: { // This migration will be relatively expensive as we need to perform // database lookups for each origin which we want to insert. Fortunately, // it shouldn't be too expensive as we only want to insert a small number // of entries created for localhost or IP addresses. // We only want to perform the re-migration if moz_hosts is a backup bool hostsIsBackupExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_is_backup"), &hostsIsBackupExists); // Only perform this migration if the original schema version was 7, and // the moz_hosts table is a backup. if (dbSchemaVersion == 7 && hostsIsBackupExists) { nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); MOZ_ASSERT(tldService); // We should always have a tldService nsCOMPtr stmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT host, type, permission, expireType, expireTime, " "modificationTime, appId, isInBrowserElement FROM moz_hosts"), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr idStmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT MAX(id) FROM moz_hosts"), getter_AddRefs(idStmt)); int64_t id = 0; bool hasResult = false; if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(idStmt->ExecuteStep(&hasResult)) && hasResult) { id = idStmt->AsInt32(0) + 1; } nsAutoCString host, type; uint32_t permission; uint32_t expireType; int64_t expireTime; int64_t modificationTime; uint32_t appId; bool isInBrowserElement; while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { // Read in the old row rv = stmt->GetUTF8String(0, host); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsAutoCString eTLD1; rv = tldService->GetBaseDomainFromHost(host, 0, eTLD1); if (NS_SUCCEEDED(rv)) { // We only care about entries which the tldService can't handle continue; } rv = stmt->GetUTF8String(1, type); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } permission = stmt->AsInt32(2); expireType = stmt->AsInt32(3); expireTime = stmt->AsInt64(4); modificationTime = stmt->AsInt64(5); if (NS_WARN_IF(stmt->AsInt64(6) < 0)) { continue; } appId = static_cast(stmt->AsInt64(6)); isInBrowserElement = static_cast(stmt->AsInt32(7)); // Perform the meat of the migration by deferring to the // UpgradeHostToOriginAndInsert function. UpgradeIPHostToOriginDB upHelper(mDBConn, &id); rv = UpgradeHostToOriginAndInsert(host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement, &upHelper); if (NS_FAILED(rv)) { NS_WARNING("Unexpected failure when upgrading migrating permission " "from host to origin"); } } } // Even if we didn't perform the migration, we want to bump the schema // version to 8. rv = mDBConn->SetSchemaVersion(8); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // The version 8-9 migration removes the unnecessary backup moz-hosts database contents. // as the data no longer needs to be migrated case 8: { // We only want to clear out the old table if it is a backup. If it isn't a backup, // we don't need to touch it. bool hostsIsBackupExists = false; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_hosts_is_backup"), &hostsIsBackupExists); if (hostsIsBackupExists) { // Delete everything from the backup, we want to keep around the table so that // you can still downgrade and not break things, but we don't need to keep the // rows around. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_hosts")); NS_ENSURE_SUCCESS(rv, rv); // The table is no longer a backup, so get rid of it. rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts_is_backup")); NS_ENSURE_SUCCESS(rv, rv); } rv = mDBConn->SetSchemaVersion(9); NS_ENSURE_SUCCESS(rv, rv); } // fall through to the next upgrade MOZ_FALLTHROUGH; // current version. case HOSTS_SCHEMA_VERSION: break; // downgrading. // if columns have been added to the table, we can still use the ones we // understand safely. if columns have been deleted or altered, just // blow away the table and start from scratch! if you change the way // a column is interpreted, make sure you also change its name so this // check will catch it. default: { // check if all the expected columns exist nsCOMPtr stmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT origin, type, permission, expireType, expireTime, " "modificationTime FROM moz_perms"), getter_AddRefs(stmt)); if (NS_SUCCEEDED(rv)) break; // our columns aren't there - drop the table! rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_perms")); NS_ENSURE_SUCCESS(rv, rv); rv = CreateTable(); NS_ENSURE_SUCCESS(rv, rv); } break; } } // cache frequently used statements (for insertion, deletion, and updating) rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "INSERT INTO moz_perms " "(id, origin, type, permission, expireType, expireTime, modificationTime) " "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"), getter_AddRefs(mStmtInsert)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_perms " "WHERE id = ?1"), getter_AddRefs(mStmtDelete)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_perms " "SET permission = ?2, expireType= ?3, expireTime = ?4, modificationTime = ?5 WHERE id = ?1"), getter_AddRefs(mStmtUpdate)); NS_ENSURE_SUCCESS(rv, rv); // Always import default permissions. ImportDefaults(); // check whether to import or just read in the db if (tableExists) return Read(); return Import(); } // sets the schema version and creates the moz_perms table. nsresult nsPermissionManager::CreateTable() { // set the schema version, before creating the table nsresult rv = mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION); if (NS_FAILED(rv)) return rv; // create the table // SQL also lives in automation.py.in. If you change this SQL change that // one too rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_perms (" " id INTEGER PRIMARY KEY" ",origin TEXT" ",type TEXT" ",permission INTEGER" ",expireType INTEGER" ",expireTime INTEGER" ",modificationTime INTEGER" ")")); if (NS_FAILED(rv)) return rv; // We also create a legacy V4 table, for backwards compatability, // and to ensure that downgrades don't trigger a schema version change. return mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_hosts (" " id INTEGER PRIMARY KEY" ",host TEXT" ",type TEXT" ",permission INTEGER" ",expireType INTEGER" ",expireTime INTEGER" ",modificationTime INTEGER" ",appId INTEGER" ",isInBrowserElement INTEGER" ")")); } NS_IMETHODIMP nsPermissionManager::Add(nsIURI *aURI, const char *aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime) { NS_ENSURE_ARG_POINTER(aURI); nsCOMPtr principal; nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return AddFromPrincipal(principal, aType, aPermission, aExpireType, aExpireTime); } NS_IMETHODIMP nsPermissionManager::AddFromPrincipal(nsIPrincipal* aPrincipal, const char* aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime) { ENSURE_NOT_CHILD_PROCESS; NS_ENSURE_ARG_POINTER(aPrincipal); NS_ENSURE_ARG_POINTER(aType); NS_ENSURE_TRUE(aExpireType == nsIPermissionManager::EXPIRE_NEVER || aExpireType == nsIPermissionManager::EXPIRE_TIME || aExpireType == nsIPermissionManager::EXPIRE_SESSION, NS_ERROR_INVALID_ARG); // Skip addition if the permission is already expired. Note that EXPIRE_SESSION only // honors expireTime if it is nonzero. if ((aExpireType == nsIPermissionManager::EXPIRE_TIME || (aExpireType == nsIPermissionManager::EXPIRE_SESSION && aExpireTime != 0)) && aExpireTime <= (PR_Now() / 1000)) { return NS_OK; } // We don't add the system principal because it actually has no URI and we // always allow action for them. if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { return NS_OK; } // Null principals can't meaningfully have persisted permissions attached to // them, so we don't allow adding permissions for them. bool isNullPrincipal; nsresult rv = aPrincipal->GetIsNullPrincipal(&isNullPrincipal); NS_ENSURE_SUCCESS(rv, rv); if (isNullPrincipal) { return NS_OK; } // Permissions may not be added to expanded principals. if (IsExpandedPrincipal(aPrincipal)) { return NS_ERROR_INVALID_ARG; } // A modificationTime of zero will cause AddInternal to use now(). int64_t modificationTime = 0; return AddInternal(aPrincipal, nsDependentCString(aType), aPermission, 0, aExpireType, aExpireTime, modificationTime, eNotify, eWriteToDB); } nsresult nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal, const nsAFlatCString &aType, uint32_t aPermission, int64_t aID, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime, NotifyOperationType aNotifyOperation, DBOperationType aDBOperation, const bool aIgnoreSessionPermissions) { nsAutoCString origin; nsresult rv = aPrincipal->GetOrigin(origin); NS_ENSURE_SUCCESS(rv, rv); if (!IsChildProcess()) { IPC::Permission permission(origin, aType, aPermission, aExpireType, aExpireTime); nsTArray cplist; ContentParent::GetAll(cplist); for (uint32_t i = 0; i < cplist.Length(); ++i) { ContentParent* cp = cplist[i]; // On platforms where we use a preallocated template process we don't // want to notify this process about session specific permissions so // new tabs or apps created on it won't inherit the session permissions. if (cp->IsPreallocated() && aExpireType == nsIPermissionManager::EXPIRE_SESSION) continue; if (cp->NeedsPermissionsUpdate()) Unused << cp->SendAddPermission(permission); } } // look up the type index int32_t typeIndex = GetTypeIndex(aType.get(), true); NS_ENSURE_TRUE(typeIndex != -1, NS_ERROR_OUT_OF_MEMORY); // When an entry already exists, PutEntry will return that, instead // of adding a new one RefPtr key = new PermissionKey(aPrincipal); PermissionHashKey* entry = mPermissionTable.PutEntry(key); if (!entry) return NS_ERROR_FAILURE; if (!entry->GetKey()) { mPermissionTable.RawRemoveEntry(entry); return NS_ERROR_OUT_OF_MEMORY; } // figure out the transaction type, and get any existing permission value OperationType op; int32_t index = entry->GetPermissionIndex(typeIndex); if (index == -1) { if (aPermission == nsIPermissionManager::UNKNOWN_ACTION) op = eOperationNone; else op = eOperationAdding; } else { PermissionEntry oldPermissionEntry = entry->GetPermissions()[index]; // remove the permission if the permission is UNKNOWN, update the // permission if its value or expire type have changed OR if the time has // changed and the expire type is time, otherwise, don't modify. There's // no need to modify a permission that doesn't expire with time when the // only thing changed is the expire time. if (aPermission == oldPermissionEntry.mPermission && aExpireType == oldPermissionEntry.mExpireType && (aExpireType == nsIPermissionManager::EXPIRE_NEVER || aExpireTime == oldPermissionEntry.mExpireTime)) op = eOperationNone; else if (oldPermissionEntry.mID == cIDPermissionIsDefault) // The existing permission is one added as a default and the new permission // doesn't exactly match so we are replacing the default. This is true // even if the new permission is UNKNOWN_ACTION (which means a "logical // remove" of the default) op = eOperationReplacingDefault; else if (aID == cIDPermissionIsDefault) // We are adding a default permission but a "real" permission already // exists. This almost-certainly means we just did a removeAllSince and // are re-importing defaults - so we can ignore this. op = eOperationNone; else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION) op = eOperationRemoving; else op = eOperationChanging; } // child processes should *always* be passed a modificationTime of zero. MOZ_ASSERT(!IsChildProcess() || aModificationTime == 0); // do the work for adding, deleting, or changing a permission: // update the in-memory list, write to the db, and notify consumers. int64_t id; if (aModificationTime == 0) { aModificationTime = PR_Now() / 1000; } switch (op) { case eOperationNone: { // nothing to do return NS_OK; } case eOperationAdding: { if (aDBOperation == eWriteToDB) { // we'll be writing to the database - generate a known unique id id = ++mLargestID; } else { // we're reading from the database - use the id already assigned id = aID; } // When we do the initial addition of the permissions we don't want to // inherit session specific permissions from other tabs or apps // so we ignore them and set the permission to PROMPT_ACTION if it was // previously allowed or denied by the user. if (aIgnoreSessionPermissions && aExpireType == nsIPermissionManager::EXPIRE_SESSION) { aPermission = nsIPermissionManager::PROMPT_ACTION; aExpireType = nsIPermissionManager::EXPIRE_NEVER; } entry->GetPermissions().AppendElement(PermissionEntry(id, typeIndex, aPermission, aExpireType, aExpireTime, aModificationTime)); if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION) { UpdateDB(op, mStmtInsert, id, origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); } if (aNotifyOperation == eNotify) { NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], aPermission, aExpireType, aExpireTime, MOZ_UTF16("added")); } break; } case eOperationRemoving: { PermissionEntry oldPermissionEntry = entry->GetPermissions()[index]; id = oldPermissionEntry.mID; 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, mStmtDelete, id, EmptyCString(), EmptyCString(), 0, nsIPermissionManager::EXPIRE_NEVER, 0, 0); if (aNotifyOperation == eNotify) { NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], oldPermissionEntry.mPermission, oldPermissionEntry.mExpireType, oldPermissionEntry.mExpireTime, MOZ_UTF16("deleted")); } // If there are no more permissions stored for that entry, clear it. if (entry->GetPermissions().IsEmpty()) { mPermissionTable.RawRemoveEntry(entry); } break; } case eOperationChanging: { id = entry->GetPermissions()[index].mID; // If the new expireType is EXPIRE_SESSION, then we have to keep a // copy of the previous permission/expireType values. This cached value will be // used when restoring the permissions of an app. if (entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION && aExpireType == nsIPermissionManager::EXPIRE_SESSION) { entry->GetPermissions()[index].mNonSessionPermission = entry->GetPermissions()[index].mPermission; entry->GetPermissions()[index].mNonSessionExpireType = entry->GetPermissions()[index].mExpireType; entry->GetPermissions()[index].mNonSessionExpireTime = entry->GetPermissions()[index].mExpireTime; } else if (aExpireType != nsIPermissionManager::EXPIRE_SESSION) { entry->GetPermissions()[index].mNonSessionPermission = aPermission; entry->GetPermissions()[index].mNonSessionExpireType = aExpireType; entry->GetPermissions()[index].mNonSessionExpireTime = aExpireTime; } entry->GetPermissions()[index].mPermission = aPermission; entry->GetPermissions()[index].mExpireType = aExpireType; entry->GetPermissions()[index].mExpireTime = aExpireTime; entry->GetPermissions()[index].mModificationTime = aModificationTime; if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION) // We care only about the id, the permission and expireType/expireTime/modificationTime here. // We pass dummy values for all other parameters. UpdateDB(op, mStmtUpdate, id, EmptyCString(), EmptyCString(), aPermission, aExpireType, aExpireTime, aModificationTime); if (aNotifyOperation == eNotify) { NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], aPermission, aExpireType, aExpireTime, MOZ_UTF16("changed")); } break; } case eOperationReplacingDefault: { // this is handling the case when we have an existing permission // entry that was created as a "default" (and thus isn't in the DB) with // an explicit permission (that may include UNKNOWN_ACTION.) // Note we will *not* get here if we are replacing an already replaced // default value - that is handled as eOperationChanging. // So this is a hybrid of eOperationAdding (as we are writing a new entry // to the DB) and eOperationChanging (as we are replacing the in-memory // repr and sending a "changed" notification). // We want a new ID even if not writing to the DB, so the modified entry // in memory doesn't have the magic cIDPermissionIsDefault value. id = ++mLargestID; // The default permission being replaced can't have session expiry. NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION, NS_ERROR_UNEXPECTED); // We don't support the new entry having any expiry - supporting that would // make things far more complex and none of the permissions we set as a // default support that. NS_ENSURE_TRUE(aExpireType == EXPIRE_NEVER, NS_ERROR_UNEXPECTED); // update the existing entry in memory. entry->GetPermissions()[index].mID = id; entry->GetPermissions()[index].mPermission = aPermission; entry->GetPermissions()[index].mExpireType = aExpireType; entry->GetPermissions()[index].mExpireTime = aExpireTime; entry->GetPermissions()[index].mModificationTime = aModificationTime; // If requested, create the entry in the DB. if (aDBOperation == eWriteToDB) { uint32_t appId; rv = aPrincipal->GetAppId(&appId); NS_ENSURE_SUCCESS(rv, rv); bool isInBrowserElement; rv = aPrincipal->GetIsInBrowserElement(&isInBrowserElement); NS_ENSURE_SUCCESS(rv, rv); UpdateDB(eOperationAdding, mStmtInsert, id, origin, aType, aPermission, aExpireType, aExpireTime, aModificationTime); } if (aNotifyOperation == eNotify) { NotifyObserversWithPermission(aPrincipal, mTypeArray[typeIndex], aPermission, aExpireType, aExpireTime, MOZ_UTF16("changed")); } } break; } return NS_OK; } NS_IMETHODIMP nsPermissionManager::Remove(nsIURI* aURI, const char* aType) { NS_ENSURE_ARG_POINTER(aURI); nsCOMPtr principal; nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return RemoveFromPrincipal(principal, aType); } NS_IMETHODIMP nsPermissionManager::RemoveFromPrincipal(nsIPrincipal* aPrincipal, const char* aType) { ENSURE_NOT_CHILD_PROCESS; NS_ENSURE_ARG_POINTER(aPrincipal); NS_ENSURE_ARG_POINTER(aType); // System principals are never added to the database, no need to remove them. if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { return NS_OK; } // Permissions may not be added to expanded principals. if (IsExpandedPrincipal(aPrincipal)) { return NS_ERROR_INVALID_ARG; } // AddInternal() handles removal, just let it do the work return AddInternal(aPrincipal, nsDependentCString(aType), nsIPermissionManager::UNKNOWN_ACTION, 0, nsIPermissionManager::EXPIRE_NEVER, 0, 0, eNotify, eWriteToDB); } NS_IMETHODIMP nsPermissionManager::RemovePermission(nsIPermission* aPerm) { 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.get()); } NS_IMETHODIMP nsPermissionManager::RemoveAll() { ENSURE_NOT_CHILD_PROCESS; return RemoveAllInternal(true); } NS_IMETHODIMP nsPermissionManager::RemoveAllSince(int64_t aSince) { ENSURE_NOT_CHILD_PROCESS; return RemoveAllModifiedSince(aSince); } void nsPermissionManager::CloseDB(bool aRebuildOnSuccess) { // Null the statements, this will finalize them. mStmtInsert = nullptr; mStmtDelete = nullptr; mStmtUpdate = nullptr; if (mDBConn) { mozIStorageCompletionCallback* cb = new CloseDatabaseListener(this, aRebuildOnSuccess); mozilla::DebugOnly rv = mDBConn->AsyncClose(cb); MOZ_ASSERT(NS_SUCCEEDED(rv)); mDBConn = nullptr; // Avoid race conditions } } nsresult nsPermissionManager::RemoveAllInternal(bool aNotifyObservers) { // Remove from memory and notify immediately. Since the in-memory // database is authoritative, we do not need confirmation from the // on-disk database to notify observers. RemoveAllFromMemory(); // Re-import the defaults ImportDefaults(); if (aNotifyObservers) { NotifyObservers(nullptr, MOZ_UTF16("cleared")); } // clear the db if (mDBConn) { nsCOMPtr removeStmt; nsresult rv = mDBConn-> CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_perms" ), getter_AddRefs(removeStmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); if (!removeStmt) { return NS_ERROR_UNEXPECTED; } nsCOMPtr pending; mozIStorageStatementCallback* cb = new DeleteFromMozHostListener(this); rv = removeStmt->ExecuteAsync(cb, getter_AddRefs(pending)); MOZ_ASSERT(NS_SUCCEEDED(rv)); return rv; } return NS_OK; } NS_IMETHODIMP nsPermissionManager::TestExactPermission(nsIURI *aURI, const char *aType, uint32_t *aPermission) { nsCOMPtr principal; nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return TestExactPermissionFromPrincipal(principal, aType, aPermission); } NS_IMETHODIMP nsPermissionManager::TestExactPermissionFromPrincipal(nsIPrincipal* aPrincipal, const char* aType, uint32_t* aPermission) { return CommonTestPermission(aPrincipal, aType, aPermission, true, true); } NS_IMETHODIMP nsPermissionManager::TestExactPermanentPermission(nsIPrincipal* aPrincipal, const char* aType, uint32_t* aPermission) { return CommonTestPermission(aPrincipal, aType, aPermission, true, false); } NS_IMETHODIMP nsPermissionManager::TestPermission(nsIURI *aURI, const char *aType, uint32_t *aPermission) { nsCOMPtr principal; nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return TestPermissionFromPrincipal(principal, aType, aPermission); } NS_IMETHODIMP nsPermissionManager::TestPermissionFromWindow(nsIDOMWindow* aWindow, const char* aType, uint32_t* aPermission) { nsCOMPtr window = do_QueryInterface(aWindow); NS_ENSURE_TRUE(window, NS_NOINTERFACE); nsPIDOMWindow* innerWindow = window->IsInnerWindow() ? window.get() : window->GetCurrentInnerWindow(); // Get the document for security check nsCOMPtr document = innerWindow->GetExtantDoc(); NS_ENSURE_TRUE(document, NS_NOINTERFACE); nsCOMPtr principal = document->NodePrincipal(); return TestPermissionFromPrincipal(principal, aType, aPermission); } NS_IMETHODIMP nsPermissionManager::TestPermissionFromPrincipal(nsIPrincipal* aPrincipal, const char* aType, uint32_t* aPermission) { return CommonTestPermission(aPrincipal, aType, aPermission, false, true); } NS_IMETHODIMP nsPermissionManager::GetPermissionObject(nsIPrincipal* aPrincipal, const char* aType, bool aExactHostMatch, nsIPermission** aResult) { NS_ENSURE_ARG_POINTER(aPrincipal); NS_ENSURE_ARG_POINTER(aType); *aResult = nullptr; if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { return NS_OK; } // Querying the permission object of an nsEP is non-sensical. if (IsExpandedPrincipal(aPrincipal)) { return NS_ERROR_INVALID_ARG; } 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, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); PermissionEntry& perm = entry->GetPermissions()[idx]; nsCOMPtr r = new nsPermission(principal, mTypeArray.ElementAt(perm.mType), perm.mPermission, perm.mExpireType, perm.mExpireTime); r.forget(aResult); return NS_OK; } nsresult nsPermissionManager::CommonTestPermission(nsIPrincipal* aPrincipal, const char *aType, uint32_t *aPermission, bool aExactHostMatch, bool aIncludingSession) { NS_ENSURE_ARG_POINTER(aPrincipal); NS_ENSURE_ARG_POINTER(aType); if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { *aPermission = nsIPermissionManager::ALLOW_ACTION; return NS_OK; } // Set the default. *aPermission = nsIPermissionManager::UNKNOWN_ACTION; // For expanded principals, we want to iterate over the whitelist and see // if the permission is granted for any of them. nsCOMPtr ep = do_QueryInterface(aPrincipal); if (ep) { nsTArray>* whitelist; nsresult rv = ep->GetWhiteList(&whitelist); NS_ENSURE_SUCCESS(rv, rv); for (size_t i = 0; i < whitelist->Length(); ++i) { uint32_t perm; rv = CommonTestPermission(whitelist->ElementAt(i), aType, &perm, aExactHostMatch, aIncludingSession); NS_ENSURE_SUCCESS(rv, rv); if (perm == nsIPermissionManager::ALLOW_ACTION) { *aPermission = perm; return NS_OK; } else if (perm == nsIPermissionManager::PROMPT_ACTION) { // Store it, but keep going to see if we can do better. *aPermission = perm; } } return NS_OK; } 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 || (!aIncludingSession && entry->GetPermission(typeIndex).mNonSessionExpireType == nsIPermissionManager::EXPIRE_SESSION)) { return NS_OK; } *aPermission = aIncludingSession ? entry->GetPermission(typeIndex).mPermission : entry->GetPermission(typeIndex).mNonSessionPermission; return NS_OK; } // Returns PermissionHashKey for a given { host, appId, isInBrowserElement } tuple. // This is not simply using PermissionKey because we will walk-up domains in // case of |host| contains sub-domains. // Returns null if nothing found. // Also accepts host on the format "". This will perform an exact match // lookup as the string doesn't contain any dots. nsPermissionManager::PermissionHashKey* nsPermissionManager::GetPermissionHashKey(nsIPrincipal* aPrincipal, uint32_t aType, bool aExactHostMatch) { PermissionHashKey* entry = nullptr; RefPtr key = new PermissionKey(aPrincipal); entry = mPermissionTable.GetEntry(key); if (entry) { PermissionEntry permEntry = entry->GetPermission(aType); // if the entry is expired, remove and keep looking for others. // Note that EXPIRE_SESSION only honors expireTime if it is nonzero. if ((permEntry.mExpireType == nsIPermissionManager::EXPIRE_TIME || (permEntry.mExpireType == nsIPermissionManager::EXPIRE_SESSION && permEntry.mExpireTime != 0)) && permEntry.mExpireTime <= (PR_Now() / 1000)) { entry = nullptr; RemoveFromPrincipal(aPrincipal, mTypeArray[aType].get()); } else if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { entry = nullptr; } } if (entry) { return entry; } // If aExactHostMatch wasn't true, we can check if the base domain has a permission entry. if (!aExactHostMatch) { nsCOMPtr uri; nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri)); if (NS_FAILED(rv)) { return nullptr; } nsAutoCString host; rv = uri->GetHost(host); if (NS_FAILED(rv)) { return nullptr; } nsCString domain = GetNextSubDomainForHost(host); if (domain.IsEmpty()) { return nullptr; } // Create a new principal which is identical to the current one, but with the new host nsCOMPtr newURI; rv = uri->Clone(getter_AddRefs(newURI)); if (NS_FAILED(rv)) { return nullptr; } rv = newURI->SetHost(domain); if (NS_FAILED(rv)) { return nullptr; } // Copy the attributes over mozilla::PrincipalOriginAttributes attrs = mozilla::BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); nsCOMPtr principal = mozilla::BasePrincipal::CreateCodebasePrincipal(newURI, attrs); return GetPermissionHashKey(principal, aType, aExactHostMatch); } // No entry, really... return nullptr; } NS_IMETHODIMP nsPermissionManager::GetEnumerator(nsISimpleEnumerator **aEnum) { // roll an nsCOMArray of all our permissions, then hand out an enumerator nsCOMArray array; for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { PermissionHashKey* entry = iter.Get(); for (const auto& permEntry : entry->GetPermissions()) { // Given how "default" permissions work and the possibility of them being // overridden with UNKNOWN_ACTION, we might see this value here - but we // do *not* want to return them via the enumerator. if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { continue; } nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, getter_AddRefs(principal)); if (NS_FAILED(rv)) { continue; } array.AppendObject( new nsPermission(principal, mTypeArray.ElementAt(permEntry.mType), permEntry.mPermission, permEntry.mExpireType, permEntry.mExpireTime)); } } return NS_NewArrayEnumerator(aEnum, array); } NS_IMETHODIMP nsPermissionManager::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *someData) { ENSURE_NOT_CHILD_PROCESS; if (!nsCRT::strcmp(aTopic, "profile-before-change")) { // The profile is about to change, // or is going away because the application is shutting down. mIsShuttingDown = true; RemoveAllFromMemory(); CloseDB(false); } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) { // the profile has already changed; init the db from the new location InitDB(false); } return NS_OK; } nsresult nsPermissionManager::RemoveAllModifiedSince(int64_t aModificationTime) { ENSURE_NOT_CHILD_PROCESS; nsCOMArray array; for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { PermissionHashKey* entry = iter.Get(); for (const auto& permEntry : entry->GetPermissions()) { if (aModificationTime > permEntry.mModificationTime) { continue; } nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, getter_AddRefs(principal)); if (NS_FAILED(rv)) { continue; } array.AppendObject( new nsPermission(principal, mTypeArray.ElementAt(permEntry.mType), permEntry.mPermission, permEntry.mExpireType, permEntry.mExpireTime)); } } for (int32_t i = 0; i principal; nsAutoCString type; nsresult rv = array[i]->GetPrincipal(getter_AddRefs(principal)); if (NS_FAILED(rv)) { NS_ERROR("GetPrincipal() failed!"); continue; } rv = array[i]->GetType(type); if (NS_FAILED(rv)) { NS_ERROR("GetType() failed!"); continue; } // AddInternal handles removal, so let it do the work... AddInternal( principal, type, nsIPermissionManager::UNKNOWN_ACTION, 0, nsIPermissionManager::EXPIRE_NEVER, 0, 0, nsPermissionManager::eNotify, nsPermissionManager::eWriteToDB); } // now re-import any defaults as they may now be required if we just deleted // an override. ImportDefaults(); return NS_OK; } NS_IMETHODIMP nsPermissionManager::RemovePermissionsWithAttributes(const nsAString& aPattern) { ENSURE_NOT_CHILD_PROCESS; mozilla::OriginAttributesPattern pattern; if (!pattern.Init(aPattern)) { return NS_ERROR_INVALID_ARG; } return RemovePermissionsWithAttributes(pattern); } nsresult nsPermissionManager::RemovePermissionsWithAttributes(mozilla::OriginAttributesPattern& aPattern) { nsCOMArray permissions; for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { PermissionHashKey* entry = iter.Get(); nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(entry->GetKey()->mOrigin, getter_AddRefs(principal)); if (NS_FAILED(rv)) { continue; } if (!aPattern.Matches(mozilla::BasePrincipal::Cast(principal)->OriginAttributesRef())) { continue; } for (const auto& permEntry : entry->GetPermissions()) { permissions.AppendObject( new nsPermission(principal, mTypeArray.ElementAt(permEntry.mType), permEntry.mPermission, permEntry.mExpireType, permEntry.mExpireTime)); } } for (int32_t i = 0; i < permissions.Count(); ++i) { nsCOMPtr principal; nsAutoCString type; permissions[i]->GetPrincipal(getter_AddRefs(principal)); permissions[i]->GetType(type); AddInternal(principal, type, nsIPermissionManager::UNKNOWN_ACTION, 0, nsIPermissionManager::EXPIRE_NEVER, 0, 0, nsPermissionManager::eNotify, nsPermissionManager::eWriteToDB); } return NS_OK; } nsresult nsPermissionManager::RemoveExpiredPermissionsForApp(uint32_t aAppId) { ENSURE_NOT_CHILD_PROCESS; if (aAppId == nsIScriptSecurityManager::NO_APP_ID) { return NS_OK; } for (auto iter = mPermissionTable.Iter(); !iter.Done(); iter.Next()) { PermissionHashKey* entry = iter.Get(); nsCOMPtr principal; GetPrincipalFromOrigin(entry->GetKey()->mOrigin, getter_AddRefs(principal)); uint32_t appId; principal->GetAppId(&appId); if (appId != aAppId) { continue; } for (uint32_t i = 0; i < entry->GetPermissions().Length(); ++i) { PermissionEntry& permEntry = entry->GetPermissions()[i]; if (permEntry.mExpireType != nsIPermissionManager::EXPIRE_SESSION) { continue; } if (permEntry.mNonSessionExpireType == nsIPermissionManager::EXPIRE_SESSION) { PermissionEntry oldPermEntry = entry->GetPermissions()[i]; entry->GetPermissions().RemoveElementAt(i); NotifyObserversWithPermission(principal, mTypeArray.ElementAt(oldPermEntry.mType), oldPermEntry.mPermission, oldPermEntry.mExpireType, oldPermEntry.mExpireTime, MOZ_UTF16("deleted")); --i; continue; } permEntry.mPermission = permEntry.mNonSessionPermission; permEntry.mExpireType = permEntry.mNonSessionExpireType; permEntry.mExpireTime = permEntry.mNonSessionExpireTime; NotifyObserversWithPermission(principal, mTypeArray.ElementAt(permEntry.mType), permEntry.mPermission, permEntry.mExpireType, permEntry.mExpireTime, MOZ_UTF16("changed")); } } return NS_OK; } //***************************************************************************** //*** nsPermissionManager private methods //***************************************************************************** nsresult nsPermissionManager::RemoveAllFromMemory() { mLargestID = 0; mTypeArray.Clear(); mPermissionTable.Clear(); return NS_OK; } // Returns -1 on failure int32_t nsPermissionManager::GetTypeIndex(const char *aType, bool aAdd) { for (uint32_t i = 0; i < mTypeArray.Length(); ++i) if (mTypeArray[i].Equals(aType)) return i; if (!aAdd) { // Not found, but that is ok - we were just looking. return -1; } // This type was not registered before. // append it to the array, without copy-constructing the string nsCString *elem = mTypeArray.AppendElement(); if (!elem) return -1; elem->Assign(aType); return mTypeArray.Length() - 1; } // wrapper function for mangling (host,type,perm,expireType,expireTime) // set into an nsIPermission. void nsPermissionManager::NotifyObserversWithPermission(nsIPrincipal* aPrincipal, const nsCString &aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, const char16_t *aData) { nsCOMPtr permission = new nsPermission(aPrincipal, aType, aPermission, aExpireType, aExpireTime); if (permission) NotifyObservers(permission, aData); } // notify observers that the permission list changed. there are four possible // values for aData: // "deleted" means a permission was deleted. aPermission is the deleted permission. // "added" means a permission was added. aPermission is the added permission. // "changed" means a permission was altered. aPermission is the new permission. // "cleared" means the entire permission list was cleared. aPermission is null. void nsPermissionManager::NotifyObservers(nsIPermission *aPermission, const char16_t *aData) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) observerService->NotifyObservers(aPermission, kPermissionChangeNotification, aData); } nsresult nsPermissionManager::Read() { ENSURE_NOT_CHILD_PROCESS; nsresult rv; // delete expired permissions before we read in the db { // this deletion has its own scope so the write lock is released when done. nsCOMPtr stmtDeleteExpired; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_perms WHERE expireType = ?1 AND expireTime <= ?2"), getter_AddRefs(stmtDeleteExpired)); NS_ENSURE_SUCCESS(rv, rv); rv = stmtDeleteExpired->BindInt32ByIndex(0, nsIPermissionManager::EXPIRE_TIME); NS_ENSURE_SUCCESS(rv, rv); rv = stmtDeleteExpired->BindInt64ByIndex(1, PR_Now() / 1000); NS_ENSURE_SUCCESS(rv, rv); bool hasResult; rv = stmtDeleteExpired->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, rv); } nsCOMPtr stmt; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id, origin, type, permission, expireType, expireTime, modificationTime " "FROM moz_perms"), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); int64_t id; nsAutoCString origin, type; uint32_t permission; uint32_t expireType; int64_t expireTime; int64_t modificationTime; bool hasResult; bool readError = false; while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { // explicitly set our entry id counter for use in AddInternal(), // and keep track of the largest id so we know where to pick up. id = stmt->AsInt64(0); if (id > mLargestID) mLargestID = id; rv = stmt->GetUTF8String(1, origin); if (NS_FAILED(rv)) { readError = true; continue; } rv = stmt->GetUTF8String(2, type); if (NS_FAILED(rv)) { readError = true; continue; } permission = stmt->AsInt32(3); expireType = stmt->AsInt32(4); // convert into int64_t values (milliseconds) expireTime = stmt->AsInt64(5); modificationTime = stmt->AsInt64(6); nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(origin, getter_AddRefs(principal)); if (NS_FAILED(rv)) { readError = true; continue; } rv = AddInternal(principal, type, permission, id, expireType, expireTime, modificationTime, eDontNotify, eNoDBOperation); if (NS_FAILED(rv)) { readError = true; continue; } } if (readError) { NS_ERROR("Error occured while reading the permissions database!"); return NS_ERROR_FAILURE; } return NS_OK; } static const char kMatchTypeHost[] = "host"; static const char kMatchTypeOrigin[] = "origin"; // Import() will read a file from the profile directory and add them to the // database before deleting the file - ie, this is a one-shot operation that // will not succeed on subsequent runs as the file imported from is removed. nsresult nsPermissionManager::Import() { nsresult rv; nsCOMPtr permissionsFile; rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(permissionsFile)); if (NS_FAILED(rv)) return rv; rv = permissionsFile->AppendNative(NS_LITERAL_CSTRING(HOSTPERM_FILE_NAME)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr fileInputStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), permissionsFile); NS_ENSURE_SUCCESS(rv, rv); rv = _DoImport(fileInputStream, mDBConn); NS_ENSURE_SUCCESS(rv, rv); // we successfully imported and wrote to the DB - delete the old file. permissionsFile->Remove(false); return NS_OK; } // ImportDefaults will read a URL with default permissions and add them to the // in-memory copy of permissions. The database is *not* written to. nsresult nsPermissionManager::ImportDefaults() { nsCString defaultsURL = mozilla::Preferences::GetCString(kDefaultsUrlPrefName); if (defaultsURL.IsEmpty()) { // == Don't use built-in permissions. return NS_OK; } nsCOMPtr defaultsURI; nsresult rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr channel; rv = NS_NewChannel(getter_AddRefs(channel), defaultsURI, nsContentUtils::GetSystemPrincipal(), nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr inputStream; rv = channel->Open2(getter_AddRefs(inputStream)); NS_ENSURE_SUCCESS(rv, rv); rv = _DoImport(inputStream, nullptr); inputStream->Close(); return rv; } // _DoImport reads the specified stream and adds the parsed elements. If // |conn| is passed, the imported data will be written to the database, but if // |conn| is null the data will be added only to the in-memory copy of the // database. nsresult nsPermissionManager::_DoImport(nsIInputStream *inputStream, mozIStorageConnection *conn) { ENSURE_NOT_CHILD_PROCESS; nsresult rv; // start a transaction on the storage db, to optimize insertions. // transaction will automically commit on completion // (note the transaction is a no-op if a null connection is passed) mozStorageTransaction transaction(conn, true); // The DB operation - we only try and write if a connection was passed. DBOperationType operation = conn ? eWriteToDB : eNoDBOperation; // and if no DB connection was passed we assume this is a "default" permission, // so use the special ID which indicates this. int64_t id = conn ? 0 : cIDPermissionIsDefault; /* format is: * matchtype \t type \t permission \t host * Only "host" is supported for matchtype * type is a string that identifies the type of permission (e.g. "cookie") * permission is an integer between 1 and 15 */ // Ideally we'd do this with nsILineInputString, but this is called with an // nsIInputStream that comes from a resource:// URI, which doesn't support // that interface. So NS_ReadLine to the rescue... nsLineBuffer lineBuffer; nsCString line; bool isMore = true; do { rv = NS_ReadLine(inputStream, &lineBuffer, line, &isMore); NS_ENSURE_SUCCESS(rv, rv); if (line.IsEmpty() || line.First() == '#') { continue; } nsTArray lineArray; // Split the line at tabs ParseString(line, '\t', lineArray); if (lineArray[0].EqualsLiteral(kMatchTypeHost) && lineArray.Length() == 4) { nsresult error = NS_OK; uint32_t permission = lineArray[2].ToInteger(&error); if (NS_FAILED(error)) continue; // the import file format doesn't handle modification times, so we use // 0, which AddInternal will convert to now() int64_t modificationTime = 0; UpgradeHostToOriginHostfileImport upHelper(this, operation, id); error = UpgradeHostToOriginAndInsert(lineArray[3], lineArray[1], permission, nsIPermissionManager::EXPIRE_NEVER, 0, modificationTime, nsIScriptSecurityManager::NO_APP_ID, false, &upHelper); if (NS_FAILED(error)) { NS_WARNING("There was a problem importing a host permission"); } } else if (lineArray[0].EqualsLiteral(kMatchTypeOrigin) && lineArray.Length() == 4) { nsresult error = NS_OK; uint32_t permission = lineArray[2].ToInteger(&error); if (NS_FAILED(error)) continue; nsCOMPtr principal; error = GetPrincipalFromOrigin(lineArray[3], getter_AddRefs(principal)); if (NS_FAILED(error)) { NS_WARNING("Couldn't import an origin permission - malformed origin"); continue; } // the import file format doesn't handle modification times, so we use // 0, which AddInternal will convert to now() int64_t modificationTime = 0; error = AddInternal(principal, lineArray[1], permission, id, nsIPermissionManager::EXPIRE_NEVER, 0, modificationTime, eDontNotify, operation); if (NS_FAILED(error)) { NS_WARNING("There was a problem importing an origin permission"); } } } while (isMore); return NS_OK; } void nsPermissionManager::UpdateDB(OperationType aOp, mozIStorageAsyncStatement* aStmt, int64_t aID, const nsACString &aOrigin, const nsACString &aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime, int64_t aModificationTime) { ENSURE_NOT_CHILD_PROCESS_NORET; nsresult rv; // no statement is ok - just means we don't have a profile if (!aStmt) return; switch (aOp) { case eOperationAdding: { rv = aStmt->BindInt64ByIndex(0, aID); if (NS_FAILED(rv)) break; rv = aStmt->BindUTF8StringByIndex(1, aOrigin); if (NS_FAILED(rv)) break; rv = aStmt->BindUTF8StringByIndex(2, aType); if (NS_FAILED(rv)) break; rv = aStmt->BindInt32ByIndex(3, aPermission); if (NS_FAILED(rv)) break; rv = aStmt->BindInt32ByIndex(4, aExpireType); if (NS_FAILED(rv)) break; rv = aStmt->BindInt64ByIndex(5, aExpireTime); if (NS_FAILED(rv)) break; rv = aStmt->BindInt64ByIndex(6, aModificationTime); break; } case eOperationRemoving: { rv = aStmt->BindInt64ByIndex(0, aID); break; } case eOperationChanging: { rv = aStmt->BindInt64ByIndex(0, aID); if (NS_FAILED(rv)) break; rv = aStmt->BindInt32ByIndex(1, aPermission); if (NS_FAILED(rv)) break; rv = aStmt->BindInt32ByIndex(2, aExpireType); if (NS_FAILED(rv)) break; rv = aStmt->BindInt64ByIndex(3, aExpireTime); if (NS_FAILED(rv)) break; rv = aStmt->BindInt64ByIndex(4, aModificationTime); break; } default: { NS_NOTREACHED("need a valid operation in UpdateDB()!"); rv = NS_ERROR_UNEXPECTED; break; } } if (NS_FAILED(rv)) { NS_WARNING("db change failed!"); return; } nsCOMPtr pending; rv = aStmt->ExecuteAsync(nullptr, getter_AddRefs(pending)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } NS_IMETHODIMP nsPermissionManager::AddrefAppId(uint32_t aAppId) { if (aAppId == nsIScriptSecurityManager::NO_APP_ID) { return NS_OK; } bool found = false; for (uint32_t i = 0; i < mAppIdRefcounts.Length(); ++i) { if (mAppIdRefcounts[i].mAppId == aAppId) { ++mAppIdRefcounts[i].mCounter; found = true; break; } } if (!found) { ApplicationCounter app = { aAppId, 1 }; mAppIdRefcounts.AppendElement(app); } return NS_OK; } NS_IMETHODIMP nsPermissionManager::ReleaseAppId(uint32_t aAppId) { // An app has been released, maybe we have to reset its session. if (aAppId == nsIScriptSecurityManager::NO_APP_ID) { return NS_OK; } for (uint32_t i = 0; i < mAppIdRefcounts.Length(); ++i) { if (mAppIdRefcounts[i].mAppId == aAppId) { --mAppIdRefcounts[i].mCounter; if (!mAppIdRefcounts[i].mCounter) { mAppIdRefcounts.RemoveElementAt(i); return RemoveExpiredPermissionsForApp(aAppId); } break; } } return NS_OK; } NS_IMETHODIMP nsPermissionManager::UpdateExpireTime(nsIPrincipal* aPrincipal, const char* aType, bool aExactHostMatch, uint64_t aSessionExpireTime, uint64_t aPersistentExpireTime) { NS_ENSURE_ARG_POINTER(aPrincipal); NS_ENSURE_ARG_POINTER(aType); uint64_t nowms = PR_Now() / 1000; if (aSessionExpireTime < nowms || aPersistentExpireTime < nowms) { return NS_ERROR_INVALID_ARG; } if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { return NS_OK; } // Setting the expire time of an nsEP is non-sensical. if (IsExpandedPrincipal(aPrincipal)) { return NS_ERROR_INVALID_ARG; } int32_t typeIndex = GetTypeIndex(aType, false); // If type == -1, the type isn't known, // so just return NS_OK if (typeIndex == -1) return NS_OK; PermissionHashKey* entry = GetPermissionHashKey(aPrincipal, typeIndex, aExactHostMatch); if (!entry) { return NS_OK; } int32_t idx = entry->GetPermissionIndex(typeIndex); if (-1 == idx) { return NS_OK; } PermissionEntry& perm = entry->GetPermissions()[idx]; if (perm.mExpireType == EXPIRE_TIME) { perm.mExpireTime = aPersistentExpireTime; } else if (perm.mExpireType == EXPIRE_SESSION && perm.mExpireTime != 0) { perm.mExpireTime = aSessionExpireTime; } return NS_OK; } nsresult nsPermissionManager::FetchPermissions() { MOZ_ASSERT(IsChildProcess(), "FetchPermissions can only be invoked in child process"); // Get the permissions from the parent process InfallibleTArray perms; ChildProcess()->SendReadPermissions(&perms); for (uint32_t i = 0; i < perms.Length(); i++) { const IPC::Permission &perm = perms[i]; nsCOMPtr principal; nsresult rv = GetPrincipalFromOrigin(perm.origin, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); // The child process doesn't care about modification times - it neither // reads nor writes, nor removes them based on the date - so 0 (which // will end up as now()) is fine. uint64_t modificationTime = 0; AddInternal(principal, perm.type, perm.capability, 0, perm.expireType, perm.expireTime, modificationTime, eNotify, eNoDBOperation, true /* ignoreSessionPermissions */); } return NS_OK; }