Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "nsCookieBannerService.h"
#include "CookieBannerDomainPrefService.h"
#include "ErrorList.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/EventQueue.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/Logging.h"
#include "mozilla/StaticPrefs_cookiebanners.h"
#include "nsCOMPtr.h"
#include "nsCookieBannerRule.h"
#include "nsCookieInjector.h"
#include "nsCRT.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsIClickRule.h"
#include "nsICookieBannerListService.h"
#include "nsICookieBannerRule.h"
#include "nsICookie.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#include "nsStringFwd.h"
#include "nsThreadUtils.h"
#include "Cookie.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(nsCookieBannerService, nsICookieBannerService, nsIObserver)
LazyLogModule gCookieBannerLog("nsCookieBannerService");
static const char kCookieBannerServiceModePref[] = "cookiebanners.service.mode";
static const char kCookieBannerServiceModePBMPref[] =
"cookiebanners.service.mode.privateBrowsing";
static StaticRefPtr<nsCookieBannerService> sCookieBannerServiceSingleton;
namespace {
// A helper function that converts service modes to strings.
nsCString ConvertModeToStringForTelemetry(uint32_t aModes) {
switch (aModes) {
case nsICookieBannerService::MODE_DISABLED:
return "disabled"_ns;
case nsICookieBannerService::MODE_REJECT:
return "reject"_ns;
case nsICookieBannerService::MODE_REJECT_OR_ACCEPT:
return "reject_or_accept"_ns;
default:
// Fall back to return "invalid" if we got any unsupported service
// mode. Note this this also includes MODE_UNSET.
return "invalid"_ns;
}
}
} // anonymous namespace
// static
already_AddRefed<nsCookieBannerService> nsCookieBannerService::GetSingleton() {
if (!sCookieBannerServiceSingleton) {
sCookieBannerServiceSingleton = new nsCookieBannerService();
RunOnShutdown([] {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("RunOnShutdown. Mode: %d. Mode PBM: %d.",
StaticPrefs::cookiebanners_service_mode(),
StaticPrefs::cookiebanners_service_mode_privateBrowsing()));
// Unregister pref listeners.
DebugOnly<nsresult> rv = Preferences::UnregisterCallback(
&nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Unregistering kCookieBannerServiceModePref callback failed");
rv = Preferences::UnregisterCallback(&nsCookieBannerService::OnPrefChange,
kCookieBannerServiceModePBMPref);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Unregistering kCookieBannerServiceModePBMPref callback failed");
rv = sCookieBannerServiceSingleton->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Shutdown failed.");
sCookieBannerServiceSingleton = nullptr;
});
}
return do_AddRef(sCookieBannerServiceSingleton);
}
// static
void nsCookieBannerService::OnPrefChange(const char* aPref, void* aData) {
RefPtr<nsCookieBannerService> service = GetSingleton();
// If the feature is enabled for normal or private browsing, init the service.
if (StaticPrefs::cookiebanners_service_mode() !=
nsICookieBannerService::MODE_DISABLED ||
StaticPrefs::cookiebanners_service_mode_privateBrowsing() !=
nsICookieBannerService::MODE_DISABLED) {
MOZ_LOG(
gCookieBannerLog, LogLevel::Info,
("Initializing nsCookieBannerService after pref change. %s", aPref));
DebugOnly<nsresult> rv = service->Init();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Init failed");
return;
}
MOZ_LOG(gCookieBannerLog, LogLevel::Info,
("Disabling nsCookieBannerService after pref change. %s", aPref));
DebugOnly<nsresult> rv = service->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsCookieBannerService::Shutdown failed");
}
NS_IMETHODIMP
nsCookieBannerService::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
// Report the daily telemetry for the cookie banner service on "idle-daily".
if (nsCRT::strcmp(aTopic, "idle-daily") == 0) {
DailyReportTelemetry();
return NS_OK;
}
// Initializing the cookie banner service on startup on
// "profile-after-change".
if (nsCRT::strcmp(aTopic, "profile-after-change") == 0) {
nsresult rv = Preferences::RegisterCallback(
&nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePBMPref);
NS_ENSURE_SUCCESS(rv, rv);
return Preferences::RegisterCallbackAndCall(
&nsCookieBannerService::OnPrefChange, kCookieBannerServiceModePref);
}
// Clear the executed data for private session when the last private browsing
// session exits.
if (nsCRT::strcmp(aTopic, "last-pb-context-exited") == 0) {
return RemoveAllExecutedRecords(true);
}
return NS_OK;
}
nsresult nsCookieBannerService::Init() {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Mode: %d. Mode PBM: %d.", __FUNCTION__,
StaticPrefs::cookiebanners_service_mode(),
StaticPrefs::cookiebanners_service_mode_privateBrowsing()));
// Check if already initialized.
if (mIsInitialized) {
return NS_OK;
}
// Initialize the service which fetches cookie banner rules.
mListService = do_GetService(NS_COOKIEBANNERLISTSERVICE_CONTRACTID);
NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
mDomainPrefService = CookieBannerDomainPrefService::GetOrCreate();
NS_ENSURE_TRUE(mDomainPrefService, NS_ERROR_FAILURE);
// Setting mIsInitialized before importing rules, because the list service
// needs to call nsCookieBannerService methods that would throw if not
// marked initialized.
mIsInitialized = true;
// Import initial rule-set, domain preference and enable rule syncing. Uses
// NS_DispatchToCurrentThreadQueue with idle priority to avoid early
// main-thread IO caused by the list service accessing RemoteSettings.
nsresult rv = NS_DispatchToCurrentThreadQueue(
NS_NewRunnableFunction("CookieBannerListService init startup",
[&] {
if (!mIsInitialized) {
return;
}
nsresult rv = mListService->Init();
NS_ENSURE_SUCCESS_VOID(rv);
mDomainPrefService->Init();
}),
EventQueuePriority::Idle);
NS_ENSURE_SUCCESS(rv, rv);
// Initialize the cookie injector.
RefPtr<nsCookieInjector> injector = nsCookieInjector::GetSingleton();
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
return obsSvc->AddObserver(this, "last-pb-context-exited", false);
}
nsresult nsCookieBannerService::Shutdown() {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Mode: %d. Mode PBM: %d.", __FUNCTION__,
StaticPrefs::cookiebanners_service_mode(),
StaticPrefs::cookiebanners_service_mode_privateBrowsing()));
// Check if already shutdown.
if (!mIsInitialized) {
return NS_OK;
}
// Shut down the list service which will stop updating mRules.
nsresult rv = mListService->Shutdown();
NS_ENSURE_SUCCESS(rv, rv);
// Clear all stored cookie banner rules. They will be imported again on Init.
mRules.Clear();
// Clear executed records for normal and private browsing.
rv = RemoveAllExecutedRecords(false);
NS_ENSURE_SUCCESS(rv, rv);
rv = RemoveAllExecutedRecords(true);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
rv = obsSvc->RemoveObserver(this, "last-pb-context-exited");
NS_ENSURE_SUCCESS(rv, rv);
mIsInitialized = false;
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::GetIsEnabled(bool* aResult) {
NS_ENSURE_ARG_POINTER(aResult);
*aResult = mIsInitialized;
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::GetRules(nsTArray<RefPtr<nsICookieBannerRule>>& aRules) {
aRules.Clear();
// Service is disabled, throw with empty array.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
// Append global rules if enabled. We don't have to deduplicate here because
// global rules are stored by ID and every ID maps to exactly one rule.
if (StaticPrefs::cookiebanners_service_enableGlobalRules()) {
AppendToArray(aRules, mGlobalRules.Values());
}
// Append domain-keyed rules.
// Since multiple domains can map to the same rule in mRules we need to
// deduplicate using a set before returning a rules array.
nsTHashSet<nsRefPtrHashKey<nsICookieBannerRule>> rulesSet;
for (const nsCOMPtr<nsICookieBannerRule>& rule : mRules.Values()) {
rulesSet.Insert(rule);
}
AppendToArray(aRules, rulesSet);
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::ResetRules(const bool doImport) {
// Service is disabled, throw.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
mRules.Clear();
mGlobalRules.Clear();
if (doImport) {
NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
nsresult rv = mListService->ImportAllRules();
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult nsCookieBannerService::GetRuleForDomain(const nsACString& aDomain,
nsICookieBannerRule** aRule) {
NS_ENSURE_ARG_POINTER(aRule);
*aRule = nullptr;
// Service is disabled, throw with null.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsICookieBannerRule> rule = mRules.Get(aDomain);
if (rule) {
rule.forget(aRule);
}
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::GetCookiesForURI(
nsIURI* aURI, const bool aIsPrivateBrowsing,
nsTArray<RefPtr<nsICookieRule>>& aCookies) {
NS_ENSURE_ARG_POINTER(aURI);
aCookies.Clear();
// We only need URI spec for logging, avoid getting it otherwise.
if (MOZ_LOG_TEST(gCookieBannerLog, LogLevel::Debug)) {
nsAutoCString spec;
nsresult rv = aURI->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. aURI: %s. aIsPrivateBrowsing: %d", __FUNCTION__, spec.get(),
aIsPrivateBrowsing));
}
// Service is disabled, throw with empty array.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
// Check which cookie banner service mode applies for this request. This
// depends on whether the browser is in private browsing or normal browsing
// mode.
uint32_t mode;
if (aIsPrivateBrowsing) {
mode = StaticPrefs::cookiebanners_service_mode_privateBrowsing();
} else {
mode = StaticPrefs::cookiebanners_service_mode();
}
MOZ_LOG(
gCookieBannerLog, LogLevel::Debug,
("%s. Found nsICookieBannerRule. Computed mode: %d", __FUNCTION__, mode));
// We don't need to check the domain preference if the cookie banner handling
// service is disabled by pref.
if (mode != nsICookieBannerService::MODE_DISABLED &&
!StaticPrefs::cookiebanners_service_detectOnly()) {
// Get the domain preference for the uri, the domain preference takes
// precedence over the pref setting. Note that the domain preference is
// supposed to stored only for top level URIs.
nsICookieBannerService::Modes domainPref;
nsresult rv = GetDomainPref(aURI, aIsPrivateBrowsing, &domainPref);
NS_ENSURE_SUCCESS(rv, rv);
if (domainPref != nsICookieBannerService::MODE_UNSET) {
mode = domainPref;
}
}
// Service is disabled for current context (normal, private browsing or domain
// preference), return empty array. Same for detect-only mode where no cookies
// should be injected.
if (mode == nsICookieBannerService::MODE_DISABLED ||
StaticPrefs::cookiebanners_service_detectOnly()) {
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Returning empty array. Got MODE_DISABLED for "
"aIsPrivateBrowsing: %d.",
__FUNCTION__, aIsPrivateBrowsing));
return NS_OK;
}
nsresult rv;
// Compute the baseDomain from aURI.
nsCOMPtr<nsIEffectiveTLDService> eTLDService(
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
NS_ENSURE_SUCCESS(rv, rv);
nsCString baseDomain;
rv = eTLDService->GetBaseDomain(aURI, 0, baseDomain);
NS_ENSURE_SUCCESS(rv, rv);
return GetCookieRulesForDomainInternal(
baseDomain, static_cast<nsICookieBannerService::Modes>(mode), true,
aCookies);
}
NS_IMETHODIMP
nsCookieBannerService::GetClickRulesForDomain(
const nsACString& aDomain, const bool aIsTopLevel,
nsTArray<RefPtr<nsIClickRule>>& aRules) {
return GetClickRulesForDomainInternal(aDomain, aIsTopLevel, aRules);
}
nsresult nsCookieBannerService::GetClickRulesForDomainInternal(
const nsACString& aDomain, const bool aIsTopLevel,
nsTArray<RefPtr<nsIClickRule>>& aRules) {
aRules.Clear();
// Service is disabled, throw with empty rule.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsICookieBannerRule> ruleForDomain;
nsresult rv = GetRuleForDomain(aDomain, getter_AddRefs(ruleForDomain));
NS_ENSURE_SUCCESS(rv, rv);
bool useGlobalSubFrameRules =
StaticPrefs::cookiebanners_service_enableGlobalRules_subFrames();
// Extract click rule from an nsICookieBannerRule and if found append it to
// the array returned.
auto appendClickRule = [&](const nsCOMPtr<nsICookieBannerRule>& bannerRule,
bool isGlobal) {
nsCOMPtr<nsIClickRule> clickRule;
rv = bannerRule->GetClickRule(getter_AddRefs(clickRule));
NS_ENSURE_SUCCESS(rv, rv);
if (!clickRule) {
return NS_OK;
}
// Evaluate the rule's runContext field and skip it if the caller's context
// doesn't match. See nsIClickRule::RunContext for possible values.
nsIClickRule::RunContext runContext;
rv = clickRule->GetRunContext(&runContext);
NS_ENSURE_SUCCESS(rv, rv);
bool runContextMatchesRule =
(runContext == nsIClickRule::RUN_ALL) ||
(runContext == nsIClickRule::RUN_TOP && aIsTopLevel) ||
(runContext == nsIClickRule::RUN_CHILD && !aIsTopLevel);
if (!runContextMatchesRule) {
return NS_OK;
}
// If global sub-frame rules are disabled skip adding them.
if (!useGlobalSubFrameRules && isGlobal && !aIsTopLevel) {
if (MOZ_LOG_TEST(gCookieBannerLog, LogLevel::Debug)) {
nsAutoCString ruleId;
rv = bannerRule->GetId(ruleId);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_LOG(gCookieBannerLog, LogLevel::Debug,
("%s. Skip adding global sub-frame rule: %s.", __FUNCTION__,
ruleId.get()));
}
return NS_OK;
}
aRules.AppendElement(clickRule);
return NS_OK;
};
// If there is a domain-specific rule it takes precedence over the global
// rules.
if (ruleForDomain) {
return appendClickRule(ruleForDomain, false);
}
if (!StaticPrefs::cookiebanners_service_enableGlobalRules()) {
// Global rules are disabled, skip adding them.
return NS_OK;
}
// Append all global click rules.
for (nsICookieBannerRule* globalRule : mGlobalRules.Values()) {
rv = appendClickRule(globalRule, true);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::InsertRule(nsICookieBannerRule* aRule) {
NS_ENSURE_ARG_POINTER(aRule);
// Service is disabled, throw.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCookieBannerRule::LogRule(gCookieBannerLog, "InsertRule:", aRule,
LogLevel::Debug);
nsTArray<nsCString> domains;
nsresult rv = aRule->GetDomains(domains);
NS_ENSURE_SUCCESS(rv, rv);
// Global rules are stored in a separate map mGlobalRules.
// They are identified by having an empty domains array.
// They are keyed by the unique ID field.
if (domains.IsEmpty()) {
nsAutoCString id;
rv = aRule->GetId(id);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(!id.IsEmpty(), NS_ERROR_FAILURE);
// Global rules must not have cookies. We shouldn't set cookies for every
// site without indication that they handle banners. Click rules are
// different, because they have a "presence" indicator and only click if it
// is reasonable to do so.
rv = aRule->ClearCookies();
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsICookieBannerRule> result =
mGlobalRules.InsertOrUpdate(id, aRule);
NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
return NS_OK;
}
// Multiple domains can be mapped to the same rule.
for (auto& domain : domains) {
nsCOMPtr<nsICookieBannerRule> result = mRules.InsertOrUpdate(domain, aRule);
NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
}
return NS_OK;
}
NS_IMETHODIMP
nsCookieBannerService::RemoveRule(nsICookieBannerRule* aRule) {
NS_ENSURE_ARG_POINTER(aRule);
// Service is disabled, throw.
if (!mIsInitialized) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCookieBannerRule::LogRule(gCookieBannerLog, "RemoveRule:", aRule,
LogLevel::Debug);
nsTArray<nsCString> domains;
nsresult rv = aRule->GetDomains(domains);