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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"serviceMode",
"cookiebanners.service.mode",
Ci.nsICookieBannerService.MODE_DISABLED
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"serviceModePBM",
"cookiebanners.service.mode.privateBrowsing",
Ci.nsICookieBannerService.MODE_DISABLED
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"maxTriesPerSiteAndSession",
"cookiebanners.bannerClicking.maxTriesPerSiteAndSession",
3
);
ChromeUtils.defineLazyGetter(lazy, "CookieBannerL10n", () => {
return new Localization([
"branding/brand.ftl",
"toolkit/global/cookieBannerHandling.ftl",
]);
});
export class CookieBannerParent extends JSWindowActorParent {
/**
* Get the browser associated with this window which is the top level embedder
* element. Returns null if the top embedder isn't a browser.
*/
get #browserElement() {
let topBC = this.browsingContext.top;
// Not all embedders are browsers.
if (topBC.embedderElementType != "browser") {
return null;
}
return topBC.embedderElement;
}
get #isTopLevel() {
return !this.manager.browsingContext.parent;
}
#isPrivateBrowsingCached;
get #isPrivateBrowsing() {
if (this.#isPrivateBrowsingCached !== undefined) {
return this.#isPrivateBrowsingCached;
}
let browser = this.#browserElement;
if (!browser) {
return false;
}
this.#isPrivateBrowsingCached =
lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);
return this.#isPrivateBrowsingCached;
}
/**
* Dispatches a custom "cookiebannerhandled" event on the chrome window.
*/
#notifyCookieBannerState(eventType) {
let chromeWin = this.browsingContext.topChromeWindow;
if (!chromeWin) {
return;
}
let windowUtils = chromeWin.windowUtils;
if (!windowUtils) {
return;
}
let event = new CustomEvent(eventType, {
bubbles: true,
cancelable: false,
detail: {
windowContext: this.manager,
},
});
windowUtils.dispatchEventToChromeOnly(chromeWin, event);
}
/**
* Logs a warning to the web console that a cookie banner has been handled.
*/
async #logCookieBannerHandledToWebConsole() {
let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
Ci.nsIScriptError
);
let [message] = await lazy.CookieBannerL10n.formatMessages([
{ id: "cookie-banner-handled-webconsole" },
]);
if (!this.manager?.innerWindowId) {
return;
}
consoleMsg.initWithWindowID(
message.value,
this.manager.documentURI?.spec,
0,
0,
Ci.nsIScriptError.warningFlag,
"cookiebannerhandling",
this.manager?.innerWindowId
);
Services.console.logMessage(consoleMsg);
}
async receiveMessage(message) {
if (message.name == "CookieBanner::Test-FinishClicking") {
Services.obs.notifyObservers(
null,
"cookie-banner-test-clicking-finish",
this.manager.documentPrincipal?.baseDomain
);
return undefined;
}
// Forwards cookie banner detected signals to frontend consumers.
if (message.name == "CookieBanner::DetectedBanner") {
this.#notifyCookieBannerState("cookiebannerdetected");
return undefined;
}
// Forwards cookie banner handled signals to frontend consumers.
if (message.name == "CookieBanner::HandledBanner") {
this.#notifyCookieBannerState("cookiebannerhandled");
this.#logCookieBannerHandledToWebConsole();
return undefined;
}
let domain = this.manager.documentPrincipal?.baseDomain;
if (message.name == "CookieBanner::MarkSiteExecuted") {
if (!domain) {
return undefined;
}
Services.cookieBanners.markSiteExecuted(
domain,
this.#isTopLevel,
this.#isPrivateBrowsing
);
return undefined;
}
if (message.name != "CookieBanner::GetClickRules") {
return undefined;
}
// TODO: Bug 1790688: consider moving this logic to the cookie banner service.
let mode;
if (this.#isPrivateBrowsing) {
mode = lazy.serviceModePBM;
} else {
mode = lazy.serviceMode;
}
// Check if we have a site preference of the top-level URI. If so, it
// takes precedence over the pref setting.
let topBrowsingContext = this.manager.browsingContext.top;
let topURI = topBrowsingContext.currentWindowGlobal?.documentURI;
// We don't need to check the domain preference if the cookie banner
// handling was disabled by pref.
if (mode != Ci.nsICookieBannerService.MODE_DISABLED && topURI) {
try {
let perDomainMode = Services.cookieBanners.getDomainPref(
topURI,
this.#isPrivateBrowsing
);
if (perDomainMode != Ci.nsICookieBannerService.MODE_UNSET) {
mode = perDomainMode;
}
} catch (e) {
// getPerSitePref could throw with NS_ERROR_NOT_AVAILABLE if the service
// is disabled. We will fallback to global pref setting if any errors
// occur.
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
console.error("The cookie banner handling service is not available");
} else {
console.error("Fail on getting domain pref:", e);
}
}
}
// Check if we previously executed banner clicking for the site. If the pref
// instructs to always execute banner clicking, we will set it to
// false.
let hasExecuted = false;
if (lazy.maxTriesPerSiteAndSession > 0) {
hasExecuted = Services.cookieBanners.shouldStopBannerClickingForSite(
domain,
this.#isTopLevel,
this.#isPrivateBrowsing
);
}
// If we have previously executed banner clicking or the service is disabled
// for current context (normal or private browsing), return empty array.
if (hasExecuted || mode == Ci.nsICookieBannerService.MODE_DISABLED) {
return { rules: [], hasExecuted };
}
if (!domain) {
return { rules: [], hasExecuted };
}
let rules = Services.cookieBanners.getClickRulesForDomain(
domain,
this.#isTopLevel
);
if (!rules.length) {
return { rules: [], hasExecuted };
}
// Determine whether we can fall back to opt-in rules. This includes the
// detect-only mode where don't interact with the banner.
let modeAllowsOptIn =
mode == Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT;
rules = rules.map(rule => {
let target = rule.optOut;
if (modeAllowsOptIn && !target) {
target = rule.optIn;
}
return {
hide: rule.hide ?? rule.presence,
presence: rule.presence,
skipPresenceVisibilityCheck: rule.skipPresenceVisibilityCheck,
target,
};
});
return { rules, hasExecuted };
}
}