/* 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 { Log } from "resource://gre/modules/Log.sys.mjs"; import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { Observers } from "resource://services-common/observers.sys.mjs"; import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", OnboardingMessageProvider: "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs", WinTaskbarJumpList: "resource:///modules/WindowsJumpLists.sys.mjs", }); const LOGGER_NAME = "Toolkit.Telemetry"; const LOGGER_PREFIX = "TelemetryReportingPolicy::"; // Oldest year to allow in telemetry policy date preferences. The FHR infobar // was implemented in 2012 and no dates older than that should be encountered. const OLDEST_ALLOWED_TELEMETRY_POLICY_ACCEPTANCE_YEAR = 2012; // Oldest year to allow in terms of use date preferences. const OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR = 2025; const PREF_BRANCH = "datareporting.policy."; const TOU_ACCEPTED_VERSION_PREF = "termsofuse.acceptedVersion"; const TOU_CURRENT_VERSION_PREF = "termsofuse.currentVersion"; const TOU_MINIMUM_VERSION_PREF = "termsofuse.minimumVersion"; const TOU_ACCEPTED_DATE_PREF = "termsofuse.acceptedDate"; const TOU_BYPASS_NOTIFICATION_PREF = "termsofuse.bypassNotification"; const TOU_PREF_MIGRATION_CHECK = "browser.termsofuse.prefMigrationCheck"; // The following preferences are deprecated and will be purged during the preferences // migration process. const DEPRECATED_FHR_PREFS = [ PREF_BRANCH + "dataSubmissionPolicyAccepted", PREF_BRANCH + "dataSubmissionPolicyBypassAcceptance", PREF_BRANCH + "dataSubmissionPolicyResponseType", PREF_BRANCH + "dataSubmissionPolicyResponseTime", ]; // How much time until we display the data choices notification bar, on the first run. const NOTIFICATION_DELAY_FIRST_RUN_MSEC = 60 * 1000; // 60s // Same as above, for the next runs. const NOTIFICATION_DELAY_NEXT_RUNS_MSEC = 10 * 1000; // 10s const NOTIFICATION_TYPES = { DATAREPORTING_POLICY_TAB_OR_INFOBAR: 0, TERMS_OF_SERVICE_MODAL: 1, }; /** * This is a policy object used to override behavior within this module. * Tests override properties on this object to allow for control of behavior * that would otherwise be very hard to cover. */ export var Policy = { now: () => new Date(), setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs), clearShowInfobarTimeout: id => clearTimeout(id), // This test method fakes a session restore event which triggers // `_delayedSetup` and thus kicks off the reporting flow fakeSessionRestoreNotification: async () => { TelemetryReportingPolicyImpl.observe( null, "sessionstore-windows-restored", null ); }, showModal: async data => { return TelemetryReportingPolicyImpl._showModal(data); }, delayedSetup: async () => TelemetryReportingPolicyImpl._delayedSetup(), }; /** * Represents a request to display data policy. * * Receivers of these instances are expected to call one or more of the on* * functions when events occur. * * When one of these requests is received, the first thing a callee should do * is present notification to the user of the data policy. When the notice * is displayed to the user, the callee should call `onUserNotifyComplete`. * * If for whatever reason the callee could not display a notice, * it should call `onUserNotifyFailed`. * * @param {object} aLog The log object used to log the error in case of failures. * @param {function} aResolve Promise-like callback function, invoked with * `true` (complete) or `false` (error). */ function NotifyPolicyRequest(aLog, aResolve) { this._log = aLog; this._resolve = aResolve; } NotifyPolicyRequest.prototype = Object.freeze({ /** * Called when the user is notified of the policy. */ onUserNotifyComplete() { this._resolve(true); }, /** * Called when there was an error notifying the user about the policy. * * @param error * (Error) Explains what went wrong. */ onUserNotifyFailed(error) { this._log.error("onUserNotifyFailed - " + error); this._resolve(false); }, }); export var TelemetryReportingPolicy = { // The current policy version number. If the version number stored in the prefs // is smaller than this, data upload will be disabled until the user is re-notified // about the policy changes. DEFAULT_DATAREPORTING_POLICY_VERSION: 1, // We set the default version as 4 to distinguish it from version numbers used // in the original TOU experiments and rollouts DEFAULT_TERMS_OF_USE_POLICY_VERSION: 4, /** * This event notifies other parts of the system that it's safe to proceed with initialization. * Example: newtab waits to initialize until it receives this event. * * It is dispatched when: * 1. The user accepts the Terms of Use (ToU), or * 2. The user has previously accepted the ToU, or * 2. The user is not eligible to see the ToU. Example local builds and temporarily Linux. */ TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE: "telemetry-tou-accepted-or-ineligible", // Make this value accessible on TelemetryReportingPolicy OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR, TOU_ACCEPTED_DATE_PREF, /** * Setup the policy. */ setup() { return TelemetryReportingPolicyImpl.setup(); }, /** * Shutdown and clear the policy. */ shutdown() { return TelemetryReportingPolicyImpl.shutdown(); }, /** * Check if we are allowed to upload data. In order to submit data both these conditions * should be true: * - The data submission preference should be true. * - The datachoices infobar should have been displayed. * * @return {boolean} True if we are allowed to upload data, false otherwise. */ canUpload() { return TelemetryReportingPolicyImpl.canUpload(); }, /** * Check if this is the first time the browser ran. */ isFirstRun() { return TelemetryReportingPolicyImpl.isFirstRun(); }, /** * Test only method, restarts the policy. */ reset() { return TelemetryReportingPolicyImpl.reset(); }, /** * Test only method, used to check if the policy should notify in tests. */ testShouldNotify() { return TelemetryReportingPolicyImpl._shouldNotifyDataReportingPolicy(); }, /** * Test only method, used to check if user is notified of the legacy data reporting policy in tests. */ testIsUserNotifiedOfDataReportingPolicy() { return TelemetryReportingPolicyImpl.isUserNotifiedOfCurrentPolicy; }, /** * Used to check if user has accepted the TOU. */ userHasAcceptedTOU() { return TelemetryReportingPolicyImpl.hasUserAcceptedCurrentTOU; }, /** * Test only method, used to simulate the infobar being shown in xpcshell tests. */ testInfobarShown() { TelemetryReportingPolicyImpl._notificationType = NOTIFICATION_TYPES.DATAREPORTING_POLICY_TAB_OR_INFOBAR; return TelemetryReportingPolicyImpl._userNotified(); }, /** * Test only method, used to trigger an update of the "first run" state. */ testUpdateFirstRun() { TelemetryReportingPolicyImpl._isFirstRun = undefined; TelemetryReportingPolicyImpl.isFirstRun(); }, /** * Test only method, used simulate a notification being in-progress. */ testNotificationInProgress(inProgress) { TelemetryReportingPolicyImpl._notificationInProgress = inProgress; }, get termsOfUseAcceptedDate() { return TelemetryReportingPolicyImpl.termsOfUseAcceptedDate; }, /** * Test only method, used to get TOS on-train release dates by channel. */ get fullOnTrainReleaseDates() { return TelemetryReportingPolicyImpl.fullOnTrainReleaseDates; }, get minimumPolicyVersion() { return TelemetryReportingPolicyImpl.minimumPolicyVersion; }, async ensureUserIsNotified() { return TelemetryReportingPolicyImpl.ensureUserIsNotified(); }, }; var TelemetryReportingPolicyImpl = { _logger: null, // Keep track of the notification status if user wasn't notified already. _notificationInProgress: false, // The timer used to show the datachoices notification at startup. _startupNotificationTimerId: null, // Keep track of the first session state, as the related preference // is flipped right after the browser starts. _isFirstRun: undefined, // Ensure notification flow is idempotent. _ensureUserIsNotifiedPromise: undefined, // Nimbus `preonboarding` feature variables. Set in response to // `sessionstore-window-restored`; immutable there-after. _nimbusVariables: {}, // Record whether users were notified via the terms of use modal or the data // reporting policy tab / infobar _notificationType: null, _observerRegistered: false, get _log() { if (!this._logger) { this._logger = Log.repository.getLoggerWithMessagePrefix( LOGGER_NAME, LOGGER_PREFIX ); } return this._logger; }, /** * Get the date the policy was notified. * * @return {object} A date object or null on errors. */ get dataSubmissionPolicyNotifiedDate() { let prefString = Services.prefs.getStringPref( TelemetryUtils.Preferences.AcceptedPolicyDate, "0" ); let valueInteger = parseInt(prefString, 10); // Bail out if we didn't store any value yet. if (valueInteger == 0) { this._log.info( "get dataSubmissionPolicyNotifiedDate - No date stored yet." ); return null; } // If an invalid value is saved in the prefs, bail out too. if (Number.isNaN(valueInteger)) { this._log.error( "get dataSubmissionPolicyNotifiedDate - Invalid date stored." ); return null; } // Make sure the notification date is newer then the oldest allowed date. let date = new Date(valueInteger); if (date.getFullYear() < OLDEST_ALLOWED_TELEMETRY_POLICY_ACCEPTANCE_YEAR) { this._log.error( "get dataSubmissionPolicyNotifiedDate - The stored date is too old." ); return null; } return date; }, /** * Set the date the policy was notified. * * @param {object} aDate A valid date object. */ set dataSubmissionPolicyNotifiedDate(aDate) { this._log.trace("set dataSubmissionPolicyNotifiedDate - aDate: " + aDate); if ( !aDate || aDate.getFullYear() < OLDEST_ALLOWED_TELEMETRY_POLICY_ACCEPTANCE_YEAR ) { this._log.error( "set dataSubmissionPolicyNotifiedDate - Invalid notification date." ); return; } Services.prefs.setStringPref( TelemetryUtils.Preferences.AcceptedPolicyDate, aDate.getTime().toString() ); }, /** * Get the date the terms of use were accepted. * * @return {object} A date object or null on errors. */ get termsOfUseAcceptedDate() { // For consistency, we use the same method of parsing a stringified // timestamp as is used in the legacy data reporting policy flow let prefString = Services.prefs.getStringPref(TOU_ACCEPTED_DATE_PREF, "0"); let valueInteger = parseInt(prefString, 10); // Bail out if we didn't store any value yet. if (valueInteger == 0) { this._log.info("get termsOfUseAcceptedDate - No date stored yet."); return null; } // If an invalid value is saved in the prefs, bail out too. if (Number.isNaN(valueInteger)) { this._log.error("get termsOfUseAcceptedDate - Invalid date stored."); return null; } // Make sure the notification date is newer then the oldest allowed date. let date = new Date(valueInteger); if (date.getFullYear() < OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR) { this._log.error( "get termsOfUseAcceptedDate - The stored date is too old." ); return null; } return date; }, /** * Set the date the policy was notified. * * @param {object} aDate A valid date object. */ set termsOfUseAcceptedDate(aDate) { this._log.trace("set termsOfUseAcceptedDate - aDate: " + aDate); if (!aDate || aDate.getFullYear() < OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR) { this._log.error( "set termsOfUseAcceptedDate - Invalid notification date." ); return; } Services.prefs.setStringPref( TOU_ACCEPTED_DATE_PREF, aDate.getTime().toString() ); }, /** * Whether submission of data is allowed. * * This is the master switch for remote server communication. If it is * false, we never request upload or deletion. */ get dataSubmissionEnabled() { // Default is true because we are opt-out. return Services.prefs.getBoolPref( TelemetryUtils.Preferences.DataSubmissionEnabled, true ); }, get currentPolicyVersion() { return Services.prefs.getIntPref( TelemetryUtils.Preferences.CurrentPolicyVersion, TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION ); }, /** * The current terms of use version. * * @returns {number} * The integer value stored in `TOU_CURRENT_VERSION_PREF`. If that preference * is unset or cannot be parsed, falls back to * `TelemetryReportingPolicy.DEFAULT_TERMS_OF_USE_POLICY_VERSION`. * * Falling back to the default ensures we always have a valid version for * comparisons. */ get currentTermsOfUseVersion() { return Services.prefs.getIntPref( TOU_CURRENT_VERSION_PREF, TelemetryReportingPolicy.DEFAULT_TERMS_OF_USE_POLICY_VERSION ); }, /** * The minimum policy version which for dataSubmissionPolicyAccepted to * to be valid. */ get minimumPolicyVersion() { const minPolicyVersion = Services.prefs.getIntPref( TelemetryUtils.Preferences.MinimumPolicyVersion, 1 ); // First check if the current channel has a specific minimum policy version. If not, // use the general minimum policy version. let channel = ""; try { channel = TelemetryUtils.getUpdateChannel(); } catch (e) { this._log.error( "minimumPolicyVersion - Unable to retrieve the current channel." ); return minPolicyVersion; } const channelPref = TelemetryUtils.Preferences.MinimumPolicyVersion + ".channel-" + channel; return Services.prefs.getIntPref(channelPref, minPolicyVersion); }, /** * The minimum terms of use version the user needs to have accepted to not be * shown the terms of use modal. */ get minimumTermsOfUseVersion() { return ( this._nimbusVariables.minimumPolicyVersion || Services.prefs.getIntPref( TOU_MINIMUM_VERSION_PREF, TelemetryReportingPolicy.DEFAULT_TERMS_OF_USE_POLICY_VERSION ) ); }, get dataSubmissionPolicyAcceptedVersion() { return Services.prefs.getIntPref( TelemetryUtils.Preferences.AcceptedPolicyVersion, 0 ); }, set dataSubmissionPolicyAcceptedVersion(value) { Services.prefs.setIntPref( TelemetryUtils.Preferences.AcceptedPolicyVersion, value ); }, get termsOfUseAcceptedVersion() { return Services.prefs.getIntPref(TOU_ACCEPTED_VERSION_PREF, 0); }, set termsOfUseAcceptedVersion(value) { Services.prefs.setIntPref(TOU_ACCEPTED_VERSION_PREF, value); }, /** * Checks to see if the user has been notified about data submission * * @return {Bool} True if user has been notified and the notification is still valid, * false otherwise. */ get isUserNotifiedOfCurrentPolicy() { // If we don't have a sane notification date, the user was not notified yet. if ( !this.dataSubmissionPolicyNotifiedDate || this.dataSubmissionPolicyNotifiedDate.getTime() <= 0 ) { return false; } // The accepted policy version should not be less than the minimum policy version. if (this.dataSubmissionPolicyAcceptedVersion < this.minimumPolicyVersion) { return false; } // Otherwise the user was already notified. return true; }, /** * Checks to see if the user has accepted the current terms of use * * @return {Bool} True if user has accepted and the acceptance is still valid, * false otherwise. */ get hasUserAcceptedCurrentTOU() { // If we don't have a sane notification date, the user was not notified yet. if (!this.termsOfUseAcceptedDate) { return false; } // The accepted terms of use version should not be less than the minimum // terms of use version. if (this.termsOfUseAcceptedVersion < this.minimumTermsOfUseVersion) { return false; } // Otherwise the user has already accepted. return true; }, /** * Reset a preference completely back to its built-in default value. * * Calling clearUserPref only removes the user-set override. By also clearing * the default branch, we ensure all overrides, both user and runtime changes, * are removed so the pref truly returns to the value declared in all.js. */ clearPref(prefName) { Services.prefs.clearUserPref(prefName); Services.prefs.getDefaultBranch("").clearUserPref(prefName); }, maybeMigrateLegacyTOUDatePref() { const hasTOUAcceptedDate = this.termsOfUseAcceptedDate && this.termsOfUseAcceptedDate.getTime() > 0; if (hasTOUAcceptedDate) { return; } Services.prefs.setStringPref( TOU_ACCEPTED_DATE_PREF, Services.prefs.getStringPref( TelemetryUtils.Preferences.AcceptedPolicyDate, "0" ) ); this.clearPref(TelemetryUtils.Preferences.AcceptedPolicyDate); this._log.trace( "maybeMigrateLegacyTOUDatePref - migrated data reporting policy accepted date to TOU accepted date." ); }, // If needed, migrate user's prefs from initial TOU experiments and rollouts // to a set of consistent values. maybeMigrateLegacyTOUVersionPref() { const LEGACY_ACCEPTED_TOU_VERSION = TelemetryReportingPolicy.DEFAULT_TERMS_OF_USE_POLICY_VERSION; if (this.termsOfUseAcceptedVersion) { return; } Services.prefs.setIntPref( TOU_ACCEPTED_VERSION_PREF, LEGACY_ACCEPTED_TOU_VERSION ); this.clearPref(TelemetryUtils.Preferences.AcceptedPolicyVersion); // Clear legacy current and minimum versions which may have been set by TOU // experiments or rollouts this.clearPref(TelemetryUtils.Preferences.CurrentPolicyVersion); this.clearPref(TelemetryUtils.Preferences.MinimumPolicyVersion); // clear the channel override for minimumPolicyVersion: let channel = TelemetryUtils.getUpdateChannel(); let channelMin = TelemetryUtils.Preferences.MinimumPolicyVersion + ".channel-" + channel; Services.prefs.clearUserPref(channelMin); this._log.trace( "maybeMigrateLegacyTOUVersionPref - migrated data reporting policy accepted version to TOU accepted version." ); }, // See comment in `acceptedTOUDuringExperimentationPhase` for more details. fullOnTrainReleaseDates: { // Note that months are expected to be zero-indexed for Date.UTC // May 27, 2025, 13:00 UTC when 139.0 went live on Release release: String(Date.UTC(2025, 4, 27, 13, 0, 0)), // May 2, 2025, 13:00 UTC when 139.0b3 went live on Beta and Aurora beta: String(Date.UTC(2025, 4, 2, 13, 0, 0)), aurora: String(Date.UTC(2025, 4, 2, 13, 0, 0)), // May 2, 2025, 11:45 UTC approximate timestamp for Nightly and Default // roll-out nightly: String(Date.UTC(2025, 4, 2, 11, 45, 0)), default: String(Date.UTC(2025, 4, 2, 11, 45, 0)), // "esr" channel unaffected }, get acceptedTOUDuringExperimentationPhase() { let channel = TelemetryUtils.getUpdateChannel(); let releaseDateOnChannel = this.fullOnTrainReleaseDates[channel] || "0"; const legacyAcceptedVersion = this.dataSubmissionPolicyAcceptedVersion; const legacyAcceptedTime = parseInt( Services.prefs.getStringPref( TelemetryUtils.Preferences.AcceptedPolicyDate, "0" ), 10 ); // Due to a bug in the patch landed in Bug 1959542, the legacy accepted // version pref for users who accepted the TOU via the preonboarding modal // in after TOU was enabled by default on-train (Bug 1952000) was recorded // as 2 instead of 3. "browser.preonboarding.enrolledInOnTrainRollout" always // being set to true when `_configureFromNimbus` ran and a user was not // enrolled in a nimbus experiment. So, we also need to check to see if the // user accepted after the full rollout went live on the relevent channel to // confirm that they accepted the TOU and not the legacy data reporting // flow. const legacyFullRolloutVersion = 2; const enrolled = Services.prefs.getBoolPref( "browser.preonboarding.enrolledInOnTrainRollout", false ); // TOS is current disabled for Linux users, who see the legacy data // reporting flow instead (see Bug 1964180). const TOSEnabled = Services.prefs.getBoolPref( "browser.preonboarding.enabled", true ); const acceptedAfterTOULandedAsDefault = TOSEnabled && enrolled && legacyAcceptedVersion >= legacyFullRolloutVersion && legacyAcceptedTime >= parseInt(releaseDateOnChannel, 10); // Users who accepted TOU in the initial Nimbus experiments or partial // on-train rollouts (Bug 1952000), have an accepted version of 3. const acceptedInExperimentOrOnTrainRollout = Boolean( legacyAcceptedVersion === 3 ); return ( (acceptedAfterTOULandedAsDefault || acceptedInExperimentOrOnTrainRollout) && this.dataSubmissionPolicyNotifiedDate ); }, /** * Update terms of use prefs for users who accepted during initial Nimbus * experiments or the experimental on-train rollout * * The pref migration logic can be removed when the TOU moves to the next * major version (see Bug 1971184). */ updateTOUPrefsForLegacyUsers() { if ( this.termsOfUseAcceptedVersion >= TelemetryReportingPolicy.DEFAULT_TERMS_OF_USE_POLICY_VERSION ) { return; } // If a user previously opted to bypass notification, opt to do so via the // new TOU bypass pref as well. if ( Services.prefs.getBoolPref( TelemetryUtils.Preferences.BypassNotification, false ) ) { Services.prefs.setBoolPref(TOU_BYPASS_NOTIFICATION_PREF, true); } // The experimentation phase was the first phase of asking users to accept // the TOU, the legacy data reporting flow marks users as being notified of // the telemetry reporting policy. So, we do only migrate pref values for // users who accepted TOU, not those who only accepted the legacy data // reporting flow as the latter still need to agree to the TOU. if (!this.acceptedTOUDuringExperimentationPhase) { this._log.trace( "updateTOUPrefsForLegacyUsers - did not accept TOU during initial experimentation phase, no action required." ); Services.prefs.setBoolPref(TOU_PREF_MIGRATION_CHECK, true); return; } this.maybeMigrateLegacyTOUDatePref(); this.maybeMigrateLegacyTOUVersionPref(); Services.prefs.setBoolPref(TOU_PREF_MIGRATION_CHECK, true); }, /** * Test only method, restarts the policy. */ reset() { this.shutdown(); this._isFirstRun = undefined; this._ensureUserIsNotifiedPromise = undefined; this._nimbusVariables = {}; return this.setup(); }, /** * Setup the policy. */ setup() { this._log.trace("setup"); // Migrate the data choices infobar, if needed. this._migratePreferences(); // Update TOU metrics with current state so that its sent in the next // metrics ping even if the values don't change. this._recordTOUDateTelemetry(); this._recordTOUVersionTelemetry(); // Watch for TOU pref changes so our metrics always stay in sync. Services.prefs.addObserver(TOU_ACCEPTED_DATE_PREF, this); Services.prefs.addObserver(TOU_ACCEPTED_VERSION_PREF, this); // Add the event observers. if (!this._observerRegistered) { Services.obs.addObserver(this, "sessionstore-windows-restored"); this._observerRegistered = true; } }, /** * Clean up the reporting policy. */ shutdown() { this._log.trace("shutdown"); this._detachObservers(); Policy.clearShowInfobarTimeout(this._startupNotificationTimerId); }, /** * Detach the observers that were attached during setup. */ _detachObservers() { if (this._observerRegistered) { Services.obs.removeObserver(this, "sessionstore-windows-restored"); this._observerRegistered = false; } Services.prefs.removeObserver(TOU_ACCEPTED_DATE_PREF, this); Services.prefs.removeObserver(TOU_ACCEPTED_VERSION_PREF, this); }, /** * Check if we are allowed to upload data. * Prerequisite: data submission is enabled (this.dataSubmissionEnabled). * * If a notification is currently in progress, the user should not be allowed * to upload data. * * Otherwise, for upload to be allowed from a data reporting standpoint, the * user should not qualify to see the legacy policy notification flow and also * not qualify to see the Terms of Use acceptance flow. * * @return {boolean} True if we are allowed to upload data, false otherwise. */ canUpload() { // If data submission is disabled, there's no point in showing the infobar. Just // forbid to upload. if (!this.dataSubmissionEnabled) { return false; } // If a notification is in progress, such as when the TOU modal is currently // showing and user has not yet accepted, do not allow upload. The legacy // flow doesn't require interaction, so the notification is only considered // to be in progress for the brief period between when the infobar or // privacy notice tab is shown and when the notification is recorded. if (this._notificationInProgress) { return false; } return !this._shouldNotifyDataReportingPolicy() && !this._shouldShowTOU(); }, isFirstRun() { if (this._isFirstRun === undefined) { this._isFirstRun = Services.prefs.getBoolPref( TelemetryUtils.Preferences.FirstRun, true ); } return this._isFirstRun; }, /** * Migrate the data policy preferences, if needed. */ _migratePreferences() { // Current prefs are mostly the same than the old ones, except for some deprecated ones. for (let pref of DEPRECATED_FHR_PREFS) { Services.prefs.clearUserPref(pref); } }, /** * Determine whether the user should be notified of telemetry policy. */ _shouldNotifyDataReportingPolicy() { if (!this.dataSubmissionEnabled) { this._log.trace( "_shouldNotifyDataReportingPolicy - Data submission disabled by the policy." ); return false; } // If user already accepted the TOU, we don't need to show the data reporting policy notification. if (this.hasUserAcceptedCurrentTOU) { this._log.trace( "_shouldNotifyDataReportingPolicy - TOU already accepted, no need to notify via legacy data reporting policy." ); return false; } const bypassNotification = Services.prefs.getBoolPref( TelemetryUtils.Preferences.BypassNotification, false ); if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) { this._log.trace( "_shouldNotifyDataReportingPolicy - User already notified or bypassing the policy." ); return false; } if (this._notificationInProgress) { this._log.trace( "_shouldNotifyDataReportingPolicy - User not notified, notification already in progress." ); return false; } return true; }, /** * Determine whether the user should be shown the terms of use. */ _shouldShowTOU() { // In some cases, _shouldShowTOU can be called before the Nimbus variables // are set. When this happens, we call _configureFromNimbus to initialize // these variables before evaluating them. This ensures we have accurate // data regarding whether preonboarding is enabled for the user. When // preonboarding is explicitly disabled, it should be treated the same the // bypassing the TOU flow via the bypass pref. if ( !this._nimbusVariables || (typeof this._nimbusVariables === "object" && Object.keys(this._nimbusVariables).length === 0) ) { this._configureFromNimbus(); } if (!this._nimbusVariables.enabled || !this._nimbusVariables.screens) { this._log.trace( "_shouldShowTOU - TOU not enabled or no screens configured." ); return false; } // If the user was already notified via the legacy data-reporting flow, we // should not show them the new user Terms of Use modal flow. if (this.isUserNotifiedOfCurrentPolicy) { this._log.trace( "_shouldShowTOU - User already saw legacy datareporting flow." ); return false; } const bypassNotification = Services.prefs.getBoolPref( TOU_BYPASS_NOTIFICATION_PREF, false ); if (bypassNotification) { this._log.trace("_shouldShowTOU - User bypassing the policy."); return false; } if (this.hasUserAcceptedCurrentTOU) { this._log.trace("_shouldShowTOU - User already accepted TOU."); return false; } if (this._notificationInProgress) { this._log.trace( "_shouldShowTOU - User not notified, notification already in progress." ); return false; } return true; }, /** * Show the data choices infobar if needed. * * @param {function} resolve invoked with `true` if user was notified, `false` * if user was not notified. */ _showInfobar(resolve) { if (!this._shouldNotifyDataReportingPolicy()) { this._log.trace("_showInfobar - User already notified, nothing to do."); resolve(false); return; } this._log.trace("_showInfobar - User not notified, notifying now."); this._notificationInProgress = true; let request = new NotifyPolicyRequest(this._log, resolve); Observers.notify("datareporting:notify-data-policy:request", request); }, /** * Called when the user is notified with the infobar or otherwise. */ _userNotified() { this._log.trace("_userNotified"); this._recordNotificationData(); lazy.TelemetrySend.notifyCanUpload(); }, _recordTOUDateTelemetry() { const date = this.termsOfUseAcceptedDate; let formattedDate; try { // only set the ping if date is a real Date if (date instanceof Date) { // PRTime/microseconds expected formattedDate = date.getTime() * 1000; Glean.termsofuse.date.set(formattedDate); } } catch (e) { this._log.error("Failed to record TOU Glean metrics", e); } }, _recordTOUVersionTelemetry() { const version = this.termsOfUseAcceptedVersion; try { Glean.termsofuse.version.set(version); } catch (e) { this._log.error("Failed to record TOU Version Glean metric", e); } }, /** * Record date and the version of the accepted policy. */ _recordNotificationData() { this._log.trace("_recordNotificationData"); switch (this._notificationType) { case NOTIFICATION_TYPES.DATAREPORTING_POLICY_TAB_OR_INFOBAR: this.dataSubmissionPolicyNotifiedDate = Policy.now(); this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion; break; case NOTIFICATION_TYPES.TERMS_OF_SERVICE_MODAL: this.termsOfUseAcceptedDate = Policy.now(); this.termsOfUseAcceptedVersion = this.currentTermsOfUseVersion; break; } // The user was notified and the notification data saved: the notification // is no longer in progress. this._notificationInProgress = false; }, /** * Try to open the privacy policy in a background tab instead of showing the infobar. * * @return {Promise} Resolves to `true` if the user was notified via * background tab, `false` if we should fallback to * an infobar. */ async _openFirstRunPage() { if (!this._shouldNotifyDataReportingPolicy()) { return false; } let firstRunPolicyURL = Services.prefs.getStringPref( TelemetryUtils.Preferences.FirstRunURL, "" ); if (!firstRunPolicyURL) { return false; } firstRunPolicyURL = Services.urlFormatter.formatURL(firstRunPolicyURL); const { BrowserWindowTracker } = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ); let win = BrowserWindowTracker.getTopWindow({ allowFromInactiveWorkspace: true, }); if (!win) { this._log.info( "Couldn't find browser window to open first-run page. Falling back to infobar." ); return false; } return new Promise(resolve => { // We'll consider the user notified once the privacy policy has been loaded // in a background tab even if that tab hasn't been selected. let tab; let progressListener = {}; progressListener.onStateChange = ( aBrowser, aWebProgress, aRequest, aStateFlags ) => { if ( aWebProgress.isTopLevel && tab && tab.linkedBrowser == aBrowser && aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK ) { removeListeners(); let uri = aBrowser.documentURI; if ( uri && !/^about:(blank|neterror|certerror|blocked)/.test(uri.spec) ) { resolve(true); } else { this._log.info("Failed to load first-run page."); resolve(false); } } }; let removeListeners = () => { win.removeEventListener("unload", removeListeners); win.gBrowser.removeTabsProgressListener(progressListener); }; win.addEventListener("unload", removeListeners); win.gBrowser.addTabsProgressListener(progressListener); tab = win.gBrowser.addTab(firstRunPolicyURL, { inBackground: true, triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), }); }); }, observe(aSubject, aTopic, aData) { if (aTopic == "sessionstore-windows-restored") { this._delayedSetup(); } if (aTopic === "nsPref:changed") { if (aData === TOU_ACCEPTED_DATE_PREF) { this._recordTOUDateTelemetry(); } if (aData === TOU_ACCEPTED_VERSION_PREF) { this._recordTOUVersionTelemetry(); } } }, /** * Handle initialization/configuration that happens at * `sessionstore-windows-restored` time. */ async _delayedSetup() { // We're ready to make decisions about how to notify the user. We only read // the Nimbus features once, so that Nimbus features changing doesn't yield // inconsistent results. We also configure the datareporting policy Gecko // preferences based on the Nimbus `preonboarding` feature variables. This // makes sense because we don't have support for re-notifying a user // _during_ the Firefox process lifetime; right now, we only notify the user // at Firefox startup. this.updateTOUPrefsForLegacyUsers(); this._configureFromNimbus(); if (this.isFirstRun()) { // We're performing the first run, flip firstRun preference for subsequent runs. Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false); } if (!this._shouldShowTOU() && !this._shouldNotifyDataReportingPolicy()) { this._log.trace( `_delayedSetup: neither TOU or legacy data reporting policy will show, no further action required` ); Services.obs.notifyObservers( null, TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE ); return; } this.ensureUserIsNotified().then(() => { this._log.debug("_delayedSetup: marking user notified"); Services.obs.notifyObservers( null, TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE ); this._userNotified(); }); }, async _waitForUserIsNotified() { // We're about to show the user the TOU modal dialog or legacy data // reporting flow. Make sure Glean won't initialize on shutdown, in case the // user never interacts with the modal or isn't notified. By default // `telemetry.fog.init_on_shutdown` is true, but we delay it here to avoid // recording data before the user makes their choice in the TOU modal or is // notified via the legacy data reporting flow (see Bug D239753). // // This pref will be reset to true only after the notification flow // completes (see TelemetryReportingPolicyImpl.ensureUserIsNotified). Services.prefs.setBoolPref("telemetry.fog.init_on_shutdown", false); if (!this._shouldShowTOU()) { this._log.trace(`_waitForUserIsNotified: will not show TOU`); } else if (await this._requestAndAwaitUserResponseViaFxMS()) { this._log.trace( `_waitForUserIsNotified: user notified via Messaging System` ); this._notificationType = NOTIFICATION_TYPES.TERMS_OF_SERVICE_MODAL; return; } // If the user is in the legacy flow, they were not eligible for ToU. Services.obs.notifyObservers( null, TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE ); this._log.trace( `_waitForUserIsNotified: user not shown TOU modal, falling back to legacy notification` ); if (!this._shouldNotifyDataReportingPolicy()) { this._log.trace( `_waitForUserIsNotified: will not show data reporting policy flow, no further action required` ); return; } await this._notifyUserViaTabOrInfobar(); this._notificationType = NOTIFICATION_TYPES.DATAREPORTING_POLICY_TAB_OR_INFOBAR; }, /** * Notify user of privacy policy via background tab or (possibly after falling * back) via infobar. The user is considered notified after the background * tab is loaded without error, or after the user has been shown (but not * necessarily interacted with) an infobar. * * @return {Promise} Resolves after user is notified. */ async _notifyUserViaTabOrInfobar() { if (this.isFirstRun()) { try { if (await this._openFirstRunPage()) { this._log.trace( `_notifyUserViaTabOrInfobar: user notified via browser tab` ); return; } } catch (e) { this._log.error("Failed to open privacy policy tab: " + e); } } // Show the info bar. const delay = this.isFirstRun() ? NOTIFICATION_DELAY_FIRST_RUN_MSEC : NOTIFICATION_DELAY_NEXT_RUNS_MSEC; this._log.trace( `_notifyUserViaTabOrInfobar: notifying user via infobar after ${delay} milliseconds` ); let p = new Promise(resolve => { this._startupNotificationTimerId = Policy.setShowInfobarTimeout( // Calling |canUpload| eventually shows the infobar, if needed. () => this._showInfobar(resolve), delay ); }); await p; }, /** * If the preonboarding feature does not require interaction, resolve * immediately. If the preonboarding feature does require interaction and the * required interaction has been completed, resolve immediately. Otherwise, * wait until the required interaction is completed. * * @return Promise * @throws {Error} when called before `sessionstore-windows-restored` notification. */ async ensureUserIsNotified() { if (!this._ensureUserIsNotifiedPromise) { this._ensureUserIsNotifiedPromise = this._waitForUserIsNotified(); } return this._ensureUserIsNotifiedPromise.then(() => { // The user has been notified and interacted with the modal. // Glean can now init on shutdown if necessary. Services.prefs.setBoolPref("telemetry.fog.init_on_shutdown", true); }); }, /** * Capture Nimbus configuration: record feature variables for future use and * set Gecko preferences based on values. */ _configureFromNimbus() { if (AppConstants.MOZ_BUILD_APP != "browser") { // OnboardingMessageProvider is browser/ only return; } this._nimbusVariables = lazy.NimbusFeatures.preonboarding.getAllVariables(); if (this._nimbusVariables.enabled === null) { const preonboardingMessage = lazy.OnboardingMessageProvider.getPreonboardingMessages().find( m => m.id === "NEW_USER_TOU_ONBOARDING" ); // Use default message variables, overriding with values from any set // fallback prefs. this._nimbusVariables = { ...preonboardingMessage, ...Object.fromEntries( Object.entries(this._nimbusVariables).filter( ([_, value]) => value !== null ) ), }; this._log.trace( `_configureFromNimbus: using default preonboarding message` ); } if (this._nimbusVariables.enabled) { if ("currentVersion" in this._nimbusVariables) { this._log.trace( `_configureFromNimbus: setting currentPolicyVersion from Nimbus feature (${this._nimbusVariables.currentVersion})` ); Services.prefs.setIntPref( TOU_CURRENT_VERSION_PREF, this._nimbusVariables.currentVersion ); } if ("minimumVersion" in this._nimbusVariables) { this._log.trace( `_configureFromNimbus: setting minimumPolicyVersion from Nimbus feature (${this._nimbusVariables.minimumVersion})` ); Services.prefs.setIntPref( TOU_MINIMUM_VERSION_PREF, this._nimbusVariables.minimumVersion ); } if ("firstRunURL" in this._nimbusVariables) { this._log.trace( `_configureFromNimbus: setting firstRunURL from Nimbus feature ('${this._nimbusVariables.firstRunURL}')` ); Services.prefs.setCharPref( TelemetryUtils.Preferences.FirstRunURL, this._nimbusVariables.firstRunURL ); } } else { this._log.trace( `_configureFromNimbus: Nimbus feature disabled, not setting preferences` ); } }, async _showModal(data) { const { BrowserWindowTracker } = ChromeUtils.importESModule( "resource:///modules/BrowserWindowTracker.sys.mjs" ); const { SpecialMessageActions } = ChromeUtils.importESModule( "resource://messaging-system/lib/SpecialMessageActions.sys.mjs" ); let win = BrowserWindowTracker.getTopWindow({ allowFromInactiveWorkspace: true, }); const config = { type: "SHOW_SPOTLIGHT", data: { content: { template: "multistage", id: data?.id || "PRE_ONBOARDING_MODAL", backdrop: data?.backdrop, screens: data.screens, UTMTerm: data?.UTMTerm, disableEscClose: data?.requireAction, // displayed as a window modal by default }, }, }; SpecialMessageActions.handleAction(config, win); this._notificationInProgress = true; return true; }, /** * Notify the user via the Firefox Messaging System (e.g., a modal dialog) and * wait for user interaction. * * User interaction is signaled by the * `termsofuse:interacted` observer notification. * * @return {Promise} `true` if user was notified, `false` to fallback * to legacy tab/infobar notification. */ async _requestAndAwaitUserResponseViaFxMS() { let p = lazy.BrowserUtils.promiseObserved("termsofuse:interacted"); if (!(await Policy.showModal(this._nimbusVariables))) { this._log.trace( "_notifyUserViaModal: notification was not displayed, falling back to legacy notification" ); return false; } this._log.trace( "_notifyUserViaModal: modal displayed, waiting for user interaction" ); // On Windows, clear the jump list to limit opening new windows while the // modal is displayed if (AppConstants.platform === "win") { lazy.WinTaskbarJumpList.blockJumpList(p); } await p; this._log.trace("_notifyUserViaModal: user interacted with modal"); return true; }, };