389 lines
12 KiB
C++
389 lines
12 KiB
C++
/* 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 "nsCookieBannerTelemetryService.h"
|
|
|
|
#include "cookieBanner.pb.h"
|
|
|
|
#include "mozilla/Base64.h"
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/glean/CookiebannersMetrics.h"
|
|
#include "mozilla/OriginAttributes.h"
|
|
#include "mozilla/StaticPtr.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "Cookie.h"
|
|
#include "nsCRT.h"
|
|
#include "nsError.h"
|
|
#include "nsICookie.h"
|
|
#include "nsICookieManager.h"
|
|
#include "nsICookieNotification.h"
|
|
#include "nsTHashSet.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsIScriptSecurityManager.h"
|
|
#include "nsISearchService.h"
|
|
#include "nsServiceManagerUtils.h"
|
|
#include "nsStringFwd.h"
|
|
#include "nsTArray.h"
|
|
|
|
namespace mozilla {
|
|
|
|
NS_IMPL_ISUPPORTS(nsCookieBannerTelemetryService,
|
|
nsICookieBannerTelemetryService, nsIObserver)
|
|
|
|
static LazyLogModule gCookieBannerTelemetryLog(
|
|
"nsCookieBannerTelemetryService");
|
|
|
|
static StaticRefPtr<nsCookieBannerTelemetryService>
|
|
sCookieBannerTelemetryServiceSingleton;
|
|
|
|
// A hash set used to tell whether a base domain is a Google domain.
|
|
static StaticAutoPtr<nsTHashSet<nsCString>> sGoogleDomainsSet;
|
|
|
|
namespace {
|
|
|
|
// A helper function that decodes Google's SOCS cookie and returns the GDPR
|
|
// choice and the region. The choice and be either "Accept", "Reject", or
|
|
// "Custom".
|
|
nsresult DecodeSOCSGoogleCookie(const nsACString& aCookie, nsACString& aChoice,
|
|
nsACString& aRegion) {
|
|
aChoice.Truncate();
|
|
aRegion.Truncate();
|
|
|
|
FallibleTArray<uint8_t> decoded;
|
|
nsresult rv =
|
|
Base64URLDecode(aCookie, Base64URLDecodePaddingPolicy::Ignore, decoded);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
std::string buf(reinterpret_cast<const char*>(decoded.Elements()),
|
|
decoded.Length());
|
|
cookieBanner::GoogleSOCSCookie socs;
|
|
|
|
if (!socs.ParseFromString(buf)) {
|
|
return NS_ERROR_CANNOT_CONVERT_DATA;
|
|
}
|
|
|
|
aRegion.Assign(socs.data().region().c_str(), socs.data().region().length());
|
|
|
|
// The first field represents the gdpr choice, 1 means "Reject" and 2 means
|
|
// either "Accept" or "Custom". We need to check a following field to decide.
|
|
if (socs.gdpr_choice() == 1) {
|
|
aChoice.AssignLiteral("Reject");
|
|
return NS_OK;
|
|
}
|
|
|
|
// The platform field represents where does this GDPR consent is selected. The
|
|
// value can be either "gws_*" or "boq_identityfrontenduiserver_*". If the
|
|
// field value starts with "gws_", it means the consent is from the Google
|
|
// search page. Otherwise, it's from the consent.google.com, which is used for
|
|
// the custom setting.
|
|
std::string prefix = "gws_";
|
|
|
|
if (socs.data().platform().compare(0, prefix.length(), prefix) == 0) {
|
|
aChoice.AssignLiteral("Accept");
|
|
return NS_OK;
|
|
}
|
|
|
|
aChoice.AssignLiteral("Custom");
|
|
return NS_OK;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// static
|
|
already_AddRefed<nsCookieBannerTelemetryService>
|
|
nsCookieBannerTelemetryService::GetSingleton() {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("GetSingleton."));
|
|
|
|
if (!sCookieBannerTelemetryServiceSingleton) {
|
|
sCookieBannerTelemetryServiceSingleton =
|
|
new nsCookieBannerTelemetryService();
|
|
|
|
RunOnShutdown([] {
|
|
DebugOnly<nsresult> rv =
|
|
sCookieBannerTelemetryServiceSingleton->Shutdown();
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"nsCookieBannerTelemetryService::Shutdown failed.");
|
|
|
|
sCookieBannerTelemetryServiceSingleton = nullptr;
|
|
});
|
|
}
|
|
|
|
return do_AddRef(sCookieBannerTelemetryServiceSingleton);
|
|
}
|
|
|
|
nsresult nsCookieBannerTelemetryService::Init() {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Init."));
|
|
if (mIsInitialized) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mIsInitialized = true;
|
|
|
|
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
|
|
NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
|
|
|
|
nsresult rv = obsSvc->AddObserver(this, "browser-search-service", false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->AddObserver(this, "idle-daily", false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->AddObserver(this, "cookie-changed", false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->AddObserver(this, "private-cookie-changed", false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult nsCookieBannerTelemetryService::Shutdown() {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Shutdown."));
|
|
|
|
if (!mIsInitialized) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mIsInitialized = false;
|
|
|
|
sGoogleDomainsSet = nullptr;
|
|
|
|
nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
|
|
NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
|
|
|
|
nsresult rv = obsSvc->RemoveObserver(this, "browser-search-service");
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->RemoveObserver(this, "idle-daily");
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->RemoveObserver(this, "cookie-changed");
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = obsSvc->RemoveObserver(this, "private-cookie-changed");
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsCookieBannerTelemetryService::Observe(nsISupports* aSubject,
|
|
const char* aTopic,
|
|
const char16_t* aData) {
|
|
if (nsCRT::strcmp(aTopic, "profile-after-change") == 0) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
|
|
("Observe profile-after-change"));
|
|
|
|
return Init();
|
|
}
|
|
|
|
if (nsCRT::strcmp(aTopic, "idle-daily") == 0) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("idle-daily"));
|
|
|
|
return MaybeReportGoogleGDPRChoiceTelemetry();
|
|
}
|
|
|
|
if (nsCRT::strcmp(aTopic, "browser-search-service") == 0 &&
|
|
nsDependentString(aData).EqualsLiteral("init-complete")) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
|
|
("Observe browser-search-service::init-complete."));
|
|
mIsSearchServiceInitialized = true;
|
|
|
|
return MaybeReportGoogleGDPRChoiceTelemetry();
|
|
}
|
|
|
|
if (nsCRT::strcmp(aTopic, "cookie-changed") == 0 ||
|
|
nsCRT::strcmp(aTopic, "private-cookie-changed") == 0) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug, ("Observe %s", aTopic));
|
|
|
|
nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject);
|
|
NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE);
|
|
|
|
if (notification->GetAction() != nsICookieNotification::COOKIE_ADDED &&
|
|
notification->GetAction() != nsICookieNotification::COOKIE_CHANGED) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsICookie> cookie;
|
|
nsresult rv = notification->GetCookie(getter_AddRefs(cookie));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsAutoCString name;
|
|
rv = cookie->GetName(name);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Bail out early if this is not a SOCS cookie.
|
|
if (!name.EqualsLiteral("SOCS")) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// A "SOCS" cookie is added. We record the GDPR choice as well as an event
|
|
// telemetry for recording a choice has been made at the moment.
|
|
return MaybeReportGoogleGDPRChoiceTelemetry(cookie, true);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult nsCookieBannerTelemetryService::MaybeReportGoogleGDPRChoiceTelemetry(
|
|
nsICookie* aCookie, bool aReportEvent) {
|
|
MOZ_ASSERT(mIsInitialized);
|
|
nsresult rv;
|
|
|
|
// Don't report the telemetry if the search service is not yet initialized.
|
|
if (!mIsSearchServiceInitialized) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
|
|
("Search Service is not yet initialized."));
|
|
return NS_OK;
|
|
}
|
|
|
|
// We only collect Google GDPR choice if Google is the default search engine.
|
|
nsCOMPtr<nsISearchService> searchService(
|
|
do_GetService("@mozilla.org/browser/search-service;1", &rv));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsISearchEngine> engine;
|
|
rv = searchService->GetDefaultEngine(getter_AddRefs(engine));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Bail out early if no default search engine is available. This could happen
|
|
// if no search engine is shipped with the Gecko.
|
|
if (!engine) {
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
|
|
("No default search engine is available."));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoString id;
|
|
rv = engine->GetId(id);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Bail out early if the default search engine is not Google.
|
|
if (!id.EqualsLiteral("google")) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Build up the Google domain set from the alternative Google search domains
|
|
if (!sGoogleDomainsSet) {
|
|
sGoogleDomainsSet = new nsTHashSet<nsCString>();
|
|
|
|
nsTArray<nsCString> googleDomains;
|
|
rv = searchService->GetAlternateDomains("www.google.com"_ns, googleDomains);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
for (const auto& domain : googleDomains) {
|
|
// We need to trim down the preceding "www" to get the host because the
|
|
// alternate domains provided by search service always have leading "www".
|
|
// However, the "SOCS" cookie is always set for all subdomains, e.g.
|
|
// ".google.com". Therefore, we need to trim down "www" to match the host
|
|
// of the cookie.
|
|
NS_ENSURE_TRUE(domain.Length() > 3, NS_ERROR_FAILURE);
|
|
sGoogleDomainsSet->Insert(Substring(domain, 3, domain.Length() - 3));
|
|
}
|
|
}
|
|
|
|
nsTArray<RefPtr<nsICookie>> cookies;
|
|
if (aCookie) {
|
|
const auto& attrs = aCookie->AsCookie().OriginAttributesRef();
|
|
|
|
// We only report cookies for the default originAttributes or private
|
|
// browsing mode.
|
|
if (attrs.IsPrivateBrowsing() || attrs == OriginAttributes()) {
|
|
cookies.AppendElement(RefPtr<nsICookie>(aCookie));
|
|
}
|
|
} else {
|
|
// If no cookie is given, we will iterate all cookies under Google Search
|
|
// domains.
|
|
nsCOMPtr<nsICookieManager> cookieManager =
|
|
do_GetService("@mozilla.org/cookiemanager;1");
|
|
if (!cookieManager) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
for (const auto& domain : *sGoogleDomainsSet) {
|
|
// Getting Google Search cookies only for normal windows. We will exclude
|
|
// cookies of non-default originAttributes, like cookies from containers.
|
|
//
|
|
// Note that we need to trim the leading "." from the domain here.
|
|
nsTArray<RefPtr<nsICookie>> googleCookies;
|
|
rv = cookieManager->GetCookiesWithOriginAttributes(
|
|
u"{ \"privateBrowsingId\": 0, \"userContextId\": 0 }"_ns,
|
|
Substring(domain, 1, domain.Length() - 1), false, googleCookies);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
cookies.AppendElements(googleCookies);
|
|
}
|
|
}
|
|
|
|
for (const auto& cookie : cookies) {
|
|
nsAutoCString name;
|
|
rv = cookie->GetName(name);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (!name.EqualsLiteral("SOCS")) {
|
|
continue;
|
|
}
|
|
|
|
nsAutoCString host;
|
|
rv = cookie->GetHost(host);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (!sGoogleDomainsSet->Contains(host)) {
|
|
continue;
|
|
}
|
|
|
|
nsAutoCString value;
|
|
rv = cookie->GetValue(value);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsAutoCString choice;
|
|
nsAutoCString region;
|
|
rv = DecodeSOCSGoogleCookie(value, choice, region);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
bool isPrivateBrowsing =
|
|
cookie->AsCookie().OriginAttributesRef().IsPrivateBrowsing();
|
|
|
|
MOZ_LOG(gCookieBannerTelemetryLog, LogLevel::Debug,
|
|
("Record the Google GDPR choice %s on the host %s in region %s for "
|
|
"the %s window",
|
|
choice.get(), host.get(), region.get(),
|
|
isPrivateBrowsing ? "private" : "normal"));
|
|
|
|
// We rely on the dynamical labelled string which can send most 16 different
|
|
// labels per report. In average cases, 16 labels is sufficient because we
|
|
// expect a user might have only 1 or 2 "SOCS" cookies on different Google
|
|
// Search domains.
|
|
//
|
|
// Note that we only report event telemetry for private browsing windows
|
|
// because the private session is ephemeral. People can change GDPR
|
|
// choice across different private sessions, so it's hard to collect the
|
|
// state correctly.
|
|
if (!isPrivateBrowsing) {
|
|
glean::cookie_banners::google_gdpr_choice_cookie.Get(host).Set(choice);
|
|
}
|
|
|
|
if (aReportEvent) {
|
|
if (isPrivateBrowsing) {
|
|
glean::cookie_banners::GoogleGdprChoiceCookieEventPbmExtra extra = {
|
|
.choice = Some(choice),
|
|
};
|
|
glean::cookie_banners::google_gdpr_choice_cookie_event_pbm.Record(
|
|
Some(extra));
|
|
} else {
|
|
glean::cookie_banners::GoogleGdprChoiceCookieEventExtra extra = {
|
|
.choice = Some(choice),
|
|
.region = Some(region),
|
|
.searchDomain = Some(host),
|
|
};
|
|
glean::cookie_banners::google_gdpr_choice_cookie_event.Record(
|
|
Some(extra));
|
|
}
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace mozilla
|