/* 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 "CookieBannerDomainPrefService.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/DebugOnly.h" #include "mozilla/Logging.h" #include "mozilla/RefPtr.h" #include "mozilla/Services.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPtr.h" #include "nsIContentPrefService2.h" #include "nsICookieBannerService.h" #include "nsIObserverService.h" #include "nsServiceManagerUtils.h" #include "nsVariant.h" #define COOKIE_BANNER_CONTENT_PREF_NAME u"cookiebanner"_ns #define COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE u"cookiebannerprivate"_ns namespace mozilla { NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService, nsIAsyncShutdownBlocker, nsIObserver) NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService::DomainPrefData, nsISupports) LazyLogModule gCookieBannerPerSitePrefLog("CookieBannerDomainPref"); static StaticRefPtr sCookieBannerDomainPrefService; namespace { // A helper function to get the profile-before-change shutdown barrier. nsCOMPtr GetShutdownBarrier() { nsCOMPtr svc = services::GetAsyncShutdownService(); NS_ENSURE_TRUE(svc, nullptr); nsCOMPtr barrier; nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier)); NS_ENSURE_SUCCESS(rv, nullptr); return barrier; } } // anonymous namespace /* static */ already_AddRefed CookieBannerDomainPrefService::GetOrCreate() { if (!sCookieBannerDomainPrefService) { sCookieBannerDomainPrefService = new CookieBannerDomainPrefService(); RunOnShutdown([] { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Debug, ("RunOnShutdown.")); sCookieBannerDomainPrefService->Shutdown(); sCookieBannerDomainPrefService = nullptr; }); } return do_AddRef(sCookieBannerDomainPrefService); } void CookieBannerDomainPrefService::Init() { // Make sure we won't init again. if (mIsInitialized) { return; } nsCOMPtr contentPrefService = do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); if (!contentPrefService) { return; } nsCOMPtr obs = services::GetObserverService(); if (!obs) { return; } // Register the observer to watch private browsing session ends. We will clean // the private domain prefs when this happens. nsresult rv = obs->AddObserver(this, "last-pb-context-exited", false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to add observer for 'last-pb-context-exited'."); auto initCallback = MakeRefPtr(this, false); // Populate the content pref for cookie banner domain preferences. rv = contentPrefService->GetByName(COOKIE_BANNER_CONTENT_PREF_NAME, nullptr, initCallback); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to get all content prefs during init."); auto initPrivateCallback = MakeRefPtr(this, true); // Populate the content pref for the private browsing. rv = contentPrefService->GetByName(COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE, nullptr, initPrivateCallback); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to get all content prefs during init."); rv = AddShutdownBlocker(); NS_ENSURE_SUCCESS_VOID(rv); mIsInitialized = true; } void CookieBannerDomainPrefService::Shutdown() { // Bail out early if the service never gets initialized. if (!mIsInitialized) { return; } nsCOMPtr obs = services::GetObserverService(); if (!obs) { return; } DebugOnly rv = obs->RemoveObserver(this, "last-pb-context-exited"); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to remove observer for 'last-pb-context-exited'."); } Maybe CookieBannerDomainPrefService::GetPref( const nsACString& aDomain, bool aIsPrivate) { bool isContentPrefLoaded = aIsPrivate ? mIsPrivateContentPrefLoaded : mIsContentPrefLoaded; // We return nothing if the first reading of the content pref is not completed // yet. Note that, we won't be able to get the domain pref for early loads. // But, we think this is acceptable because the cookie banners on the early // load tabs would have interacted before when the user disabled the banner // handling. So, there should be consent cookies in place to prevent banner // showing. In this case, our cookie injection and banner clicking won't do // anything. if (!isContentPrefLoaded) { return Nothing(); } Maybe> data = aIsPrivate ? mPrefsPrivate.MaybeGet(aDomain) : mPrefs.MaybeGet(aDomain); if (!data) { return Nothing(); } return Some(data.ref()->mMode); } nsresult CookieBannerDomainPrefService::SetPref( const nsACString& aDomain, nsICookieBannerService::Modes aMode, bool aIsPrivate, bool aPersistInPrivateBrowsing) { MOZ_ASSERT(NS_IsMainThread()); // Don't do anything if we are shutting down. if (NS_WARN_IF(mIsShuttingDown)) { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning, ("Attempt to set a domain pref while shutting down.")); return NS_OK; } EnsureInitCompleted(aIsPrivate); // Create the domain pref data. The data is always persistent for normal // windows. For private windows, the data is only persistent if requested. auto domainPrefData = MakeRefPtr( aMode, aIsPrivate ? aPersistInPrivateBrowsing : true); bool wasPersistentInPrivate = false; // Update the in-memory domain preference map. if (aIsPrivate) { Maybe> data = mPrefsPrivate.MaybeGet(aDomain); wasPersistentInPrivate = data ? data.ref()->mIsPersistent : false; Unused << mPrefsPrivate.InsertOrUpdate(aDomain, domainPrefData); } else { Unused << mPrefs.InsertOrUpdate(aDomain, domainPrefData); } // For private windows, the domain prefs will only be stored in memory. // Unless, this function is instructed to persist setting for private // browsing. To make the disk state consistent with the memory state, we need // to clear the domain pref in the disk when we no longer need to persist the // domain pref for the domain in PBM. if (!aPersistInPrivateBrowsing && aIsPrivate) { // Clear the domain pref in disk if it was persistent. if (wasPersistentInPrivate) { return RemoveContentPrefForDomain(aDomain, true); } return NS_OK; } // Set the preference to the content pref service. nsCOMPtr contentPrefService = do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); RefPtr variant = new nsVariant(); nsresult rv = variant->SetAsUint8(aMode); NS_ENSURE_SUCCESS(rv, rv); auto callback = MakeRefPtr(this); mWritingCount++; // Store the domain preference to the content pref service. rv = contentPrefService->Set(NS_ConvertUTF8toUTF16(aDomain), aIsPrivate ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE : COOKIE_BANNER_CONTENT_PREF_NAME, variant, nullptr, callback); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to set cookie banner domain pref."); return rv; } nsresult CookieBannerDomainPrefService::RemovePref(const nsACString& aDomain, bool aIsPrivate) { MOZ_ASSERT(NS_IsMainThread()); // Don't do anything if we are shutting down. if (NS_WARN_IF(mIsShuttingDown)) { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning, ("Attempt to remove a domain pref while shutting down.")); return NS_OK; } EnsureInitCompleted(aIsPrivate); // Clear in-memory domain pref. if (aIsPrivate) { mPrefsPrivate.Remove(aDomain); } else { mPrefs.Remove(aDomain); } return RemoveContentPrefForDomain(aDomain, aIsPrivate); } nsresult CookieBannerDomainPrefService::RemoveAll(bool aIsPrivate) { MOZ_ASSERT(NS_IsMainThread()); // Don't do anything if we are shutting down. if (NS_WARN_IF(mIsShuttingDown)) { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning, ("Attempt to remove all domain prefs while shutting down.")); return NS_OK; } EnsureInitCompleted(aIsPrivate); // Clear in-memory domain pref. if (aIsPrivate) { mPrefsPrivate.Clear(); } else { mPrefs.Clear(); } nsCOMPtr contentPrefService = do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); auto callback = MakeRefPtr(this); mWritingCount++; // Remove all the domain preferences. nsresult rv = contentPrefService->RemoveByName( aIsPrivate ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE : COOKIE_BANNER_CONTENT_PREF_NAME, nullptr, callback); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to remove all cookie banner domain prefs."); return rv; } void CookieBannerDomainPrefService::EnsureInitCompleted(bool aIsPrivate) { bool& isContentPrefLoaded = aIsPrivate ? mIsPrivateContentPrefLoaded : mIsContentPrefLoaded; if (isContentPrefLoaded) { return; } // Wait until the service is fully initialized. SpinEventLoopUntil("CookieBannerDomainPrefService::EnsureUpdateComplete"_ns, [&] { return isContentPrefLoaded; }); } nsresult CookieBannerDomainPrefService::AddShutdownBlocker() { MOZ_ASSERT(!mIsShuttingDown); nsCOMPtr barrier = GetShutdownBarrier(); NS_ENSURE_TRUE(barrier, NS_ERROR_FAILURE); return GetShutdownBarrier()->AddBlocker( this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u"CookieBannerDomainPrefService: shutdown"_ns); } nsresult CookieBannerDomainPrefService::RemoveShutdownBlocker() { MOZ_ASSERT(mIsShuttingDown); nsCOMPtr barrier = GetShutdownBarrier(); NS_ENSURE_TRUE(barrier, NS_ERROR_FAILURE); return GetShutdownBarrier()->RemoveBlocker(this); } nsresult CookieBannerDomainPrefService::RemoveContentPrefForDomain( const nsACString& aDomain, bool aIsPrivate) { nsCOMPtr contentPrefService = do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); auto callback = MakeRefPtr(this); mWritingCount++; // Remove the domain preference from the content pref service. nsresult rv = contentPrefService->RemoveByDomainAndName( NS_ConvertUTF8toUTF16(aDomain), aIsPrivate ? COOKIE_BANNER_CONTENT_PREF_NAME_PRIVATE : COOKIE_BANNER_CONTENT_PREF_NAME, nullptr, callback); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Fail to remove cookie banner domain pref."); return rv; } NS_IMPL_ISUPPORTS(CookieBannerDomainPrefService::BaseContentPrefCallback, nsIContentPrefCallback2) NS_IMETHODIMP CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleResult( nsIContentPref* aPref) { NS_ENSURE_ARG_POINTER(aPref); MOZ_ASSERT(mService); nsAutoString domain; nsresult rv = aPref->GetDomain(domain); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr value; rv = aPref->GetValue(getter_AddRefs(value)); NS_ENSURE_SUCCESS(rv, rv); if (!value) { return NS_OK; } uint8_t data; rv = value->GetAsUint8(&data); NS_ENSURE_SUCCESS(rv, rv); // Create the domain pref data and indicate it's persistent. auto domainPrefData = MakeRefPtr(nsICookieBannerService::Modes(data), true); if (mIsPrivate) { Unused << mService->mPrefsPrivate.InsertOrUpdate( NS_ConvertUTF16toUTF8(domain), domainPrefData); } else { Unused << mService->mPrefs.InsertOrUpdate(NS_ConvertUTF16toUTF8(domain), domainPrefData); } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleCompletion( uint16_t aReason) { MOZ_ASSERT(mService); if (mIsPrivate) { mService->mIsPrivateContentPrefLoaded = true; } else { mService->mIsContentPrefLoaded = true; } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::InitialLoadContentPrefCallback::HandleError( nsresult error) { // We don't need to do anything here because HandleCompletion is always // called. if (NS_WARN_IF(NS_FAILED(error))) { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning, ("Fail to get content pref during initiation.")); } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::WriteContentPrefCallback::HandleResult( nsIContentPref* aPref) { return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::WriteContentPrefCallback::HandleCompletion( uint16_t aReason) { MOZ_ASSERT(mService); MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mService->mWritingCount > 0); mService->mWritingCount--; // Remove the shutdown blocker after we complete writing to content pref. if (mService->mIsShuttingDown && mService->mWritingCount == 0) { mService->RemoveShutdownBlocker(); } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::WriteContentPrefCallback::HandleError( nsresult error) { if (NS_WARN_IF(NS_FAILED(error))) { MOZ_LOG(gCookieBannerPerSitePrefLog, LogLevel::Warning, ("Fail to write content pref.")); return NS_OK; } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::Observe(nsISupports* /*aSubject*/, const char* aTopic, const char16_t* /*aData*/) { if (strcmp(aTopic, "last-pb-context-exited") != 0) { MOZ_ASSERT_UNREACHABLE("unexpected topic"); return NS_ERROR_UNEXPECTED; } // Clear the private browsing domain prefs that are not persistent when we // observe the private browsing session has ended. mPrefsPrivate.RemoveIf([](const auto& iter) { const RefPtr& data = iter.Data(); return !data->mIsPersistent; }); return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::GetName(nsAString& aName) { aName.AssignLiteral( "CookieBannerDomainPrefService: write content pref before " "profile-before-change."); return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::BlockShutdown(nsIAsyncShutdownClient*) { MOZ_ASSERT(NS_IsMainThread()); mIsShuttingDown = true; // If we are not writing the content pref, we can remove the shutdown blocker // directly. if (mWritingCount == 0) { RemoveShutdownBlocker(); } return NS_OK; } NS_IMETHODIMP CookieBannerDomainPrefService::GetState(nsIPropertyBag**) { return NS_OK; } } // namespace mozilla