diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/cookiebanners/CookieBannerChild.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cookiebanners/CookieBannerChild.sys.mjs')
-rw-r--r-- | toolkit/components/cookiebanners/CookieBannerChild.sys.mjs | 864 |
1 files changed, 864 insertions, 0 deletions
diff --git a/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs b/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs new file mode 100644 index 0000000000..4d8b481504 --- /dev/null +++ b/toolkit/components/cookiebanners/CookieBannerChild.sys.mjs @@ -0,0 +1,864 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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, { + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setInterval: "resource://gre/modules/Timer.sys.mjs", + clearInterval: "resource://gre/modules/Timer.sys.mjs", + 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, + "prefDetectOnly", + "cookiebanners.service.detectOnly", + false +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "bannerClickingEnabled", + "cookiebanners.bannerClicking.enabled", + false +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "cleanupTimeoutAfterLoad", + "cookiebanners.bannerClicking.timeoutAfterLoad" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "cleanupTimeoutAfterDOMContentLoaded", + "cookiebanners.bannerClicking.timeoutAfterDOMContentLoaded" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "pollingInterval", + "cookiebanners.bannerClicking.pollingInterval", + 500 +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "testing", + "cookiebanners.bannerClicking.testing", + false +); + +ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { + return console.createInstance({ + prefix: "CookieBannerChild", + maxLogLevelPref: "cookiebanners.bannerClicking.logLevel", + }); +}); + +export class CookieBannerChild extends JSWindowActorChild { + // Caches the enabled state to ensure we only compute it once for the lifetime + // of the actor. Particularly the private browsing check can be expensive. + #isEnabledCached = null; + #isTopLevel; + #clickRules; + #observerCleanUp; + #observerCleanUpTimer; + // Indicates whether the page "load" event occurred. + #didLoad = false; + // Indicates whether we are using global rules to handle the banner. + #isUsingGlobalRules = false; + + // Used to keep track of click telemetry for the current window. + #telemetryStatus = { + currentStage: null, + success: false, + successStage: null, + failReason: null, + bannerVisibilityFail: false, + querySelectorCount: 0, + querySelectorTimeMS: 0, + bannerDetectedAfterCookieInjection: false, + detectedCMP: [], + }; + // For measuring the cookie banner handling duration. + #gleanBannerHandlingTimer = null; + // Indicates whether we should stop running the cookie banner handling + // mechanism because it has been previously executed for the site. So, we can + // cool down the cookie banner handing to improve performance. + #isCooledDownInSession = false; + + handleEvent(event) { + if (!this.#isEnabled) { + // Automated tests may still expect the test message to be sent. + this.#maybeSendTestMessage(); + return; + } + + switch (event.type) { + case "DOMContentLoaded": + this.#onDOMContentLoaded(); + break; + case "load": + this.#onLoad(); + break; + default: + lazy.logConsole.warn(`Unexpected event ${event.type}.`, event); + } + } + + get #isPrivateBrowsing() { + return lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow); + } + + /** + * Whether the feature is enabled based on pref state. + * @type {boolean} true if feature is enabled, false otherwise. + */ + get #isEnabled() { + if (this.#isEnabledCached != null) { + return this.#isEnabledCached; + } + + let checkIsEnabled = () => { + if (!lazy.bannerClickingEnabled) { + return false; + } + if (this.#isPrivateBrowsing) { + return lazy.serviceModePBM != Ci.nsICookieBannerService.MODE_DISABLED; + } + return lazy.serviceMode != Ci.nsICookieBannerService.MODE_DISABLED; + }; + + this.#isEnabledCached = checkIsEnabled(); + return this.#isEnabledCached; + } + + /** + * Whether the feature is enabled in detect-only-mode where cookie banner + * detection events are dispatched, but banners aren't handled. + * @type {boolean} true if feature mode is enabled, false otherwise. + */ + get #isDetectOnly() { + // We can't be in detect-only-mode if fully disabled. + if (!this.#isEnabled) { + return false; + } + return lazy.prefDetectOnly; + } + + /** + * @returns {boolean} Whether we handled a banner for the current load by + * injecting cookies. + */ + get #hasInjectedCookieForCookieBannerHandling() { + return this.docShell?.currentDocumentChannel?.loadInfo + ?.hasInjectedCookieForCookieBannerHandling; + } + + /** + * Checks whether we handled a banner for this site by injecting cookies and + * dispatches events. + * @returns {boolean} Whether we handled the banner and dispatched events. + */ + #dispatchEventsForBannerHandledByInjection() { + if ( + !this.#hasInjectedCookieForCookieBannerHandling || + this.#isCooledDownInSession + ) { + return false; + } + // Strictly speaking we don't actively detect a banner when we handle it by + // cookie injection. We still dispatch "cookiebannerdetected" in this case + // for consistency. + this.sendAsyncMessage("CookieBanner::DetectedBanner"); + this.sendAsyncMessage("CookieBanner::HandledBanner"); + return true; + } + + /** + * Handler for DOMContentLoaded events which is the entry point for cookie + * banner handling. + */ + async #onDOMContentLoaded() { + lazy.logConsole.debug("onDOMContentLoaded", { didLoad: this.#didLoad }); + this.#isTopLevel = this.browsingContext == this.browsingContext?.top; + this.#didLoad = false; + this.#telemetryStatus.currentStage = "dom_content_loaded"; + + let principal = this.document?.nodePrincipal; + + // We only apply banner auto-clicking if the document has a content + // principal. + if (!principal?.isContentPrincipal) { + return; + } + + // We don't need to do auto-clicking if it's not a http/https page. + if (!principal.schemeIs("http") && !principal.schemeIs("https")) { + return; + } + + lazy.logConsole.debug("Send message to get rule", { + baseDomain: principal.baseDomain, + isTopLevel: this.#isTopLevel, + }); + let rules; + + try { + let data = await this.sendQuery("CookieBanner::GetClickRules", {}); + + rules = data.rules; + // Set we are cooling down for this session if the cookie banner handling + // has been executed previously. + this.#isCooledDownInSession = data.hasExecuted; + } catch (e) { + lazy.logConsole.warn("Failed to get click rule from parent.", e); + return; + } + + lazy.logConsole.debug("Got rules:", rules); + // We can stop here if we don't have a rule. + if (!rules.length) { + // If the cookie injector has handled the banner and there are no click + // rules we still need to dispatch a "cookiebannerhandled" event. + let dispatchedEvents = this.#dispatchEventsForBannerHandledByInjection(); + // Record telemetry about handling the banner via cookie injection. + // Note: The success state recorded here may be invalid if the given + // cookie fails to handle the banner. Since we don't have a presence + // detector for this rule we can't determine whether the banner is still + // showing or not. + if (dispatchedEvents) { + this.#telemetryStatus.failReason = null; + this.#telemetryStatus.success = true; + this.#telemetryStatus.successStage = "cookie_injected"; + } + + this.#maybeSendTestMessage(); + return; + } + + this.#clickRules = rules; + + // Check if we are using global rules. If we are using a site rule, there + // will be one rule has its isGlobalRule property set to false. Otherwise, + // we are using global rules if every rule has this property set to true. + this.#isUsingGlobalRules = rules.every(rule => rule.isGlobalRule); + + if (!this.#isDetectOnly) { + // Start a timer to measure how long it takes for the banner to appear and + // be handled. + this.#gleanBannerHandlingTimer = this.#isUsingGlobalRules + ? Glean.cookieBannersCmp.handleDuration.start() + : Glean.cookieBannersClick.handleDuration.start(); + } + + let { bannerHandled, bannerDetected, matchedRules } = + await this.handleCookieBanner(); + + // Send a message to mark that the cookie banner handling has been executed. + this.sendAsyncMessage("CookieBanner::MarkSiteExecuted"); + + let dispatchedEventsForCookieInjection = + this.#dispatchEventsForBannerHandledByInjection(); + if (dispatchedEventsForCookieInjection) { + if (bannerDetected) { + // Record the failure that the banner is still present with cookies + // injected. + this.#telemetryStatus.bannerDetectedAfterCookieInjection = true; + } else { + // A cookie injection followed by not detecting the banner via querySelector + // is a success state. Record that in telemetry. + // Note: The success state reported may be invalid in edge cases where both + // the cookie injection and the banner detection via query selector fails. + this.#telemetryStatus.success = true; + this.#telemetryStatus.successStage = "cookie_injected"; + } + } + + // 1. Detected event. + if (bannerDetected) { + lazy.logConsole.info("Detected cookie banner.", { + url: this.document?.location.href, + }); + // Avoid dispatching a duplicate "cookiebannerdetected" event. + if (!dispatchedEventsForCookieInjection) { + this.sendAsyncMessage("CookieBanner::DetectedBanner"); + } + } + + // 2. Handled event. + if (bannerHandled) { + lazy.logConsole.info("Handled cookie banner.", { + url: this.document?.location.href, + matchedRules, + }); + + // Stop the timer to record how long it took to handle the banner. + lazy.logConsole.debug( + "Telemetry timer: stop and accumulate", + this.#gleanBannerHandlingTimer + ); + + if (this.#isUsingGlobalRules) { + Glean.cookieBannersCmp.handleDuration.stopAndAccumulate( + this.#gleanBannerHandlingTimer + ); + } else { + Glean.cookieBannersClick.handleDuration.stopAndAccumulate( + this.#gleanBannerHandlingTimer + ); + } + + // Avoid dispatching a duplicate "cookiebannerhandled" event. + if (!dispatchedEventsForCookieInjection) { + this.sendAsyncMessage("CookieBanner::HandledBanner"); + } + } else if (!this.#isDetectOnly) { + // Cancel the timer we didn't handle the banner. + if (this.#isUsingGlobalRules) { + Glean.cookieBannersCmp.handleDuration.cancel( + this.#gleanBannerHandlingTimer + ); + } else { + Glean.cookieBannersClick.handleDuration.cancel( + this.#gleanBannerHandlingTimer + ); + } + } + + this.#maybeSendTestMessage(); + } + + /** + * Handler for "load" events. Used as a signal to stop observing the DOM for + * cookie banners after a timeout. + */ + #onLoad() { + this.#didLoad = true; + + // Exit early if we are not handling banners for this site. + if (!this.#clickRules?.length) { + return; + } + + lazy.logConsole.debug("Observed 'load' event", { + href: this.document?.location.href, + hasActiveObserver: !!this.#observerCleanUp, + observerCleanupTimer: this.#observerCleanUpTimer, + }); + + // Update stage for click telemetry. + if (!this.#telemetryStatus.success) { + this.#telemetryStatus.currentStage = "mutation_post_load"; + } + + // On load reset the timer for cleanup. + this.#startOrResetCleanupTimer(); + } + + /** + * We limit how long we observe cookie banner mutations for performance + * reasons. If not present initially on DOMContentLoaded, cookie banners are + * expected to show up during or shortly after page load. + * This method starts a cleanup timeout which duration depends on the current + * load stage (DOMContentLoaded, or load). When called, if a timeout is + * already running, it is cancelled and a new timeout is scheduled. + */ + #startOrResetCleanupTimer() { + // Cancel any already running timeout so we can schedule a new one. + if (this.#observerCleanUpTimer) { + lazy.logConsole.debug( + "#startOrResetCleanupTimer: Cancelling existing cleanup timeout", + { + didLoad: this.#didLoad, + } + ); + lazy.clearTimeout(this.#observerCleanUpTimer); + } + + let durationMS = this.#didLoad + ? lazy.cleanupTimeoutAfterLoad + : lazy.cleanupTimeoutAfterDOMContentLoaded; + lazy.logConsole.debug( + "#startOrResetCleanupTimer: Starting cleanup timeout", + { + durationMS, + didLoad: this.#didLoad, + hasObserverCleanup: !!this.#observerCleanUp, + } + ); + + this.#observerCleanUpTimer = this.contentWindow?.setTimeout(() => { + lazy.logConsole.debug( + "#startOrResetCleanupTimer: Cleanup timeout triggered", + { + durationMS, + didLoad: this.#didLoad, + hasObserverCleanup: !!this.#observerCleanUp, + } + ); + this.#observerCleanUpTimer = null; + this.#observerCleanUp?.(); + }, durationMS); + } + + didDestroy() { + this.#reportTelemetry(); + + // Clean up the observer and timer if needed. + this.#observerCleanUp?.(); + } + + #reportTelemetry() { + // Nothing to report, banner handling didn't run. + if ( + this.#telemetryStatus.successStage == null && + this.#telemetryStatus.failReason == null + ) { + lazy.logConsole.debug( + "Skip telemetry", + this.#telemetryStatus, + this.#clickRules + ); + return; + } + + let { + success, + successStage, + currentStage, + failReason, + bannerDetectedAfterCookieInjection, + detectedCMP, + } = this.#telemetryStatus; + + // Check if we got interrupted during an observe. + if (this.#observerCleanUp && !success) { + failReason = "actor_destroyed"; + } + + let status, reason; + if (success) { + status = "success"; + reason = successStage; + } else { + status = "fail"; + reason = failReason; + } + + // Select the target result telemetry. + let resultTelemetry = this.#isUsingGlobalRules + ? Glean.cookieBannersCmp.result + : Glean.cookieBannersClick.result; + + // Increment general success or failure counter. + resultTelemetry[status].add(1); + // Increment reason counters. + if (reason) { + resultTelemetry[`${status}_${reason}`].add(1); + } else { + lazy.logConsole.debug( + "Could not determine success / fail reason for telemetry." + ); + } + + lazy.logConsole.debug("Submitted clickResult telemetry", status, reason, { + success, + successStage, + currentStage, + failReason, + }); + + let { querySelectorCount, querySelectorTimeMS } = this.#telemetryStatus; + + // Glean needs an integer. + let querySelectorTimeUS = Math.round(querySelectorTimeMS * 1000); + + if (this.#isTopLevel) { + Glean.cookieBannersClick.querySelectorRunCountPerWindowTopLevel.accumulateSamples( + [querySelectorCount] + ); + Glean.cookieBannersClick.querySelectorRunDurationPerWindowTopLevel.accumulateSamples( + [querySelectorTimeUS] + ); + } else { + Glean.cookieBannersClick.querySelectorRunCountPerWindowFrame.accumulateSamples( + [querySelectorCount] + ); + Glean.cookieBannersClick.querySelectorRunDurationPerWindowFrame.accumulateSamples( + [querySelectorTimeUS] + ); + } + + lazy.logConsole.debug("Submitted querySelector telemetry", { + isTopLevel: this.#isTopLevel, + querySelectorCount, + querySelectorTimeUS, + querySelectorTimeMS, + }); + + if (bannerDetectedAfterCookieInjection) { + Glean.cookieBanners.cookieInjectionFail.add(1); + } + + lazy.logConsole.debug("Submitted cookieInjectionFail telemetry", { + bannerDetectedAfterCookieInjection, + }); + + if (detectedCMP.length) { + detectedCMP.forEach(id => { + Glean.cookieBannersCmp.detectedCmp[id].add(1); + }); + } + + lazy.logConsole.debug("Submitted detectedCMP telemetry", { + detectedCMP, + }); + + // Record whether the banner was handled by a global rule or a site rule. + if (success && reason != "cookie_injected") { + Glean.cookieBannersCmp.ratioHandledByCmpRule.addToDenominator(1); + if (this.#isUsingGlobalRules) { + Glean.cookieBannersCmp.ratioHandledByCmpRule.addToNumerator(1); + } + + lazy.logConsole.debug("Submitted handled ratio telemetry", { + isUsingGlobalRules: this.#isUsingGlobalRules, + }); + } + } + + /** + * The function to perform the core logic of handing the cookie banner. It + * will detect the banner and click the banner button whenever possible + * according to the given click rules. + * If the service mode pref is set to detect only mode we will only attempt to + * find the cookie banner element and return early. + * + * @returns A promise which resolves when it finishes auto clicking. + */ + async handleCookieBanner() { + lazy.logConsole.debug("handleCookieBanner", this.document?.location.href); + + // Start timer to clean up detection code (polling and mutation observers). + this.#startOrResetCleanupTimer(); + + // First, we detect if the banner is shown on the page + let rules = await this.#detectBanner(); + + if (!rules.length) { + // The banner was never shown. + this.#telemetryStatus.success = false; + if (this.#telemetryStatus.bannerVisibilityFail) { + this.#telemetryStatus.failReason = "banner_not_visible"; + } else { + this.#telemetryStatus.failReason = "banner_not_found"; + } + + return { bannerHandled: false, bannerDetected: false }; + } + + // Record every detected CMP. Note that our detection mechanism return every + // rule if the presence detector matches. So, we could have multiple CMPs + // if the page contains elements match presence detector of them. + if (this.#isUsingGlobalRules) { + rules.forEach(rule => { + this.#telemetryStatus.detectedCMP.push(rule.id); + }); + } + + // No rule with valid button to click. This can happen if we're in + // MODE_REJECT and there are only opt-in buttons available. + // This also applies when detect-only mode is enabled. We only want to + // dispatch events matching the current service mode. + if (rules.every(rule => rule.target == null)) { + this.#telemetryStatus.success = false; + this.#telemetryStatus.failReason = "no_rule_for_mode"; + return { bannerHandled: false, bannerDetected: false }; + } + + // If the cookie banner prefs only enable detection but not handling we're done here. + if (this.#isDetectOnly) { + return { bannerHandled: false, bannerDetected: true }; + } + + let successClick = false; + successClick = await this.#clickTarget(rules); + + if (successClick) { + // For telemetry, Keep track of in which stage we successfully handled the banner. + this.#telemetryStatus.successStage = this.#telemetryStatus.currentStage; + } else { + this.#telemetryStatus.failReason = "button_not_found"; + this.#telemetryStatus.successStage = null; + } + this.#telemetryStatus.success = successClick; + + return { + bannerHandled: successClick, + bannerDetected: true, + matchedRules: rules, + }; + } + + /** + * The helper function to observe the changes on the document with a timeout. + * It will call the check function when it observes mutations on the document + * body. Once the check function returns a truthy value, it will resolve with + * that value. Otherwise, it will resolve with null on timeout. + * + * @param {function} [checkFn] - The check function. + * @returns {Promise} - A promise which resolves with the return value of the + * check function or null if the function times out. + */ + #promiseObserve(checkFn) { + if (this.#observerCleanUp) { + throw new Error( + "The promiseObserve is called before previous one resolves." + ); + } + lazy.logConsole.debug("#promiseObserve", { didLoad: this.#didLoad }); + + return new Promise(resolve => { + let win = this.contentWindow; + // Marks whether a mutation on the site has been observed since we last + // ran checkFn. + let sawMutation = false; + + // IDs for interval for checkFn polling. + let pollIntervalId = null; + + // Keep track of DOM changes via MutationObserver. We only run query + // selectors again if the DOM updated since our last check. + let observer = new win.MutationObserver(() => { + sawMutation = true; + }); + observer.observe(win.document.body, { + attributes: true, + subtree: true, + childList: true, + }); + + // Start polling checkFn. + let intervalFn = () => { + // Nothing changed since last run, skip running checkFn. + if (!sawMutation) { + return; + } + // Reset mutation flag. + sawMutation = false; + + // A truthy result means we have a hit so we can stop observing. + let result = checkFn?.(); + if (result) { + cleanup(result); + } + }; + pollIntervalId = lazy.setInterval(intervalFn, lazy.pollingInterval); + + let cleanup = result => { + lazy.logConsole.debug("#promiseObserve cleanup", { + result, + observer, + cleanupTimeoutId: this.#observerCleanUpTimer, + pollIntervalId, + }); + + // Unregister the observer. + if (observer) { + observer.disconnect(); + observer = null; + } + + // Stop the polling checks. + if (pollIntervalId) { + lazy.clearInterval(pollIntervalId); + pollIntervalId = null; + } + + // Clear the cleanup timeout. This can happen when the actor gets + // destroyed before the cleanup timeout itself fires. + if (this.#observerCleanUpTimer) { + lazy.clearTimeout(this.#observerCleanUpTimer); + } + + this.#observerCleanUp = null; + resolve(result); + }; + + // The clean up function to clean unfinished observer and timer on timeout + // or when the actor destroys. + this.#observerCleanUp = () => { + cleanup(null); + }; + }); + } + + // Detecting if the banner is shown on the page. + async #detectBanner() { + if (!this.#clickRules?.length) { + return []; + } + lazy.logConsole.debug("Starting to detect the banner"); + + // Returns an array of rules for which a cookie banner exists for the + // current site. + let presenceDetector = () => { + lazy.logConsole.debug("presenceDetector start"); + let matchingRules = this.#clickRules.filter(rule => { + let { presence, skipPresenceVisibilityCheck } = rule; + + let banner = this.#querySelector(presence); + lazy.logConsole.debug("Testing banner el presence", { + result: banner, + rule, + presence, + }); + + if (!banner) { + return false; + } + if (skipPresenceVisibilityCheck) { + return true; + } + + let isVisible = this.#isVisible(banner); + // Store visibility of banner element to keep track of why detection + // failed. + this.#telemetryStatus.bannerVisibilityFail = !isVisible; + + return isVisible; + }); + + // For no rules matched return null explicitly so #promiseObserve knows we + // want to keep observing. + if (!matchingRules.length) { + return null; + } + return matchingRules; + }; + + lazy.logConsole.debug("Initial call to presenceDetector"); + let rules = presenceDetector(); + + // If we couldn't detect the banner at the beginning, we register an + // observer with the timeout to observe if the banner was shown within the + // timeout. + if (!rules?.length) { + lazy.logConsole.debug( + "Initial presenceDetector failed, registering MutationObserver", + rules + ); + this.#telemetryStatus.currentStage = "mutation_pre_load"; + rules = await this.#promiseObserve(presenceDetector); + } + + if (!rules?.length) { + lazy.logConsole.debug("Couldn't detect the banner", rules); + return []; + } + + lazy.logConsole.debug("Detected the banner for rules", rules); + + return rules; + } + + // Clicking the target button. + async #clickTarget(rules) { + lazy.logConsole.debug("Starting to detect the target button"); + + let targetEl; + for (let rule of rules) { + targetEl = this.#querySelector(rule.target); + if (targetEl) { + break; + } + } + + // The target button is not available. We register an observer to wait until + // it's ready. + if (!targetEl) { + targetEl = await this.#promiseObserve(() => { + for (let rule of rules) { + let el = this.#querySelector(rule.target); + + lazy.logConsole.debug("Testing button el presence", { + result: el, + rule, + target: rule.target, + }); + + if (el) { + lazy.logConsole.debug( + "Found button from rule", + rule, + rule.target, + el + ); + return el; + } + } + return null; + }); + + if (!targetEl) { + lazy.logConsole.debug("Cannot find the target button."); + return false; + } + } + + lazy.logConsole.debug("Found the target button, click it.", targetEl); + targetEl.click(); + return true; + } + + // The helper function to check if the given element if visible. + #isVisible(element) { + return element.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true, + }); + } + + /** + * Wrapper around document.querySelector calls which collects perf telemetry. + * @param {string} selectors - Selector list passed into document.querySelector. + * @returns document.querySelector result. + */ + #querySelector(selectors) { + let start = Cu.now(); + + let result = this.document.querySelector(selectors); + + this.#telemetryStatus.querySelectorTimeMS += Cu.now() - start; + this.#telemetryStatus.querySelectorCount += 1; + + return result; + } + + #maybeSendTestMessage() { + if (lazy.testing) { + let win = this.contentWindow; + + // Report the clicking is finished after the style has been flushed. + win.requestAnimationFrame(() => { + win.setTimeout(() => { + this.sendAsyncMessage("CookieBanner::Test-FinishClicking"); + }, 0); + }); + } + } +} |