diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/cookiebanners/CookieBannerDomainPrefService.cpp | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/toolkit/components/cookiebanners/CookieBannerDomainPrefService.cpp b/toolkit/components/cookiebanners/CookieBannerDomainPrefService.cpp new file mode 100644 index 0000000000..4b4912f1a1 --- /dev/null +++ b/toolkit/components/cookiebanners/CookieBannerDomainPrefService.cpp @@ -0,0 +1,484 @@ +/* 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<CookieBannerDomainPrefService> + sCookieBannerDomainPrefService; + +namespace { + +// A helper function to get the profile-before-change shutdown barrier. + +nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier() { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + NS_ENSURE_TRUE(svc, nullptr); + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return barrier; +} + +} // anonymous namespace + +/* static */ +already_AddRefed<CookieBannerDomainPrefService> +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<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + + if (!contentPrefService) { + return; + } + + nsCOMPtr<nsIObserverService> 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<InitialLoadContentPrefCallback>(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<InitialLoadContentPrefCallback>(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<nsIObserverService> obs = services::GetObserverService(); + + if (!obs) { + return; + } + + DebugOnly<nsresult> rv = obs->RemoveObserver(this, "last-pb-context-exited"); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Fail to remove observer for 'last-pb-context-exited'."); +} + +Maybe<nsICookieBannerService::Modes> 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<RefPtr<DomainPrefData>> 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<DomainPrefData>( + aMode, aIsPrivate ? aPersistInPrivateBrowsing : true); + bool wasPersistentInPrivate = false; + + // Update the in-memory domain preference map. + if (aIsPrivate) { + Maybe<RefPtr<DomainPrefData>> 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<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); + + RefPtr<nsVariant> variant = new nsVariant(); + nsresult rv = variant->SetAsUint8(aMode); + NS_ENSURE_SUCCESS(rv, rv); + + auto callback = MakeRefPtr<WriteContentPrefCallback>(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<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); + + auto callback = MakeRefPtr<WriteContentPrefCallback>(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<nsIAsyncShutdownClient> 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<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + NS_ENSURE_TRUE(barrier, NS_ERROR_FAILURE); + + return GetShutdownBarrier()->RemoveBlocker(this); +} + +nsresult CookieBannerDomainPrefService::RemoveContentPrefForDomain( + const nsACString& aDomain, bool aIsPrivate) { + nsCOMPtr<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(contentPrefService, NS_ERROR_FAILURE); + + auto callback = MakeRefPtr<WriteContentPrefCallback>(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<nsIVariant> 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<DomainPrefData>(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<DomainPrefData>& 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 |