diff --git a/toolkit/components/telemetry/TelemetryController.jsm b/toolkit/components/telemetry/TelemetryController.jsm index 7c5eabb8bcad..7788a8ae3294 100644 --- a/toolkit/components/telemetry/TelemetryController.jsm +++ b/toolkit/components/telemetry/TelemetryController.jsm @@ -87,6 +87,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession", "resource://gre/modules/TelemetrySession.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend", "resource://gre/modules/TelemetrySend.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy", + "resource://gre/modules/TelemetryReportingPolicy.jsm"); /** * Setup Telemetry logging. This function also gets called when loggin related @@ -651,6 +653,9 @@ let Impl = { this._sessionRecorder.onStartup(); } + // This will trigger displaying the datachoices infobar. + TelemetryReportingPolicy.setup(); + if (!this.enableTelemetryRecording()) { this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup."); return Promise.resolve(); @@ -726,6 +731,9 @@ let Impl = { // Now do an orderly shutdown. try { + // Stop the datachoices infobar display. + TelemetryReportingPolicy.shutdown(); + // Stop any ping sending. yield TelemetrySend.shutdown(); diff --git a/toolkit/components/telemetry/TelemetryReportingPolicy.jsm b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm new file mode 100644 index 000000000000..400155cbd7cf --- /dev/null +++ b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm @@ -0,0 +1,379 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "TelemetryReportingPolicy" +]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://services-common/observers.js", this); + +XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend", + "resource://gre/modules/TelemetrySend.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", + "resource://gre/modules/UpdateChannel.jsm"); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryReportingPolicy::"; + +// Oldest year to allow in date preferences. The FHR infobar was implemented in +// 2012 and no dates older than that should be encountered. +const OLDEST_ALLOWED_ACCEPTANCE_YEAR = 2012; + +const PREF_BRANCH = "datareporting.policy."; +// Indicates whether this is the first run or not. This is used to decide when to display +// the policy. +const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun"; +// Allows to skip the datachoices infobar. This should only be used in tests. +const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification"; +// The submission kill switch: if this preference is disable, no submission will ever take place. +const PREF_DATA_SUBMISSION_ENABLED = PREF_BRANCH + "dataSubmissionEnabled"; +// This preference holds the current policy version, which overrides +// DEFAULT_DATAREPORTING_POLICY_VERSION +const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion"; +// This indicates the minimum required policy version. If the accepted policy version +// is lower than this, the notification bar must be showed again. +const PREF_MINIMUM_POLICY_VERSION = PREF_BRANCH + "minimumPolicyVersion"; +// The version of the accepted policy. +const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion"; +// The date user accepted the policy. +const PREF_ACCEPTED_POLICY_DATE = PREF_BRANCH + "dataSubmissionPolicyNotifiedTime"; +// 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 + +/** + * 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. + */ +function NotifyPolicyRequest(aLog) { + this._log = aLog; +} + +NotifyPolicyRequest.prototype = Object.freeze({ + /** + * Called when the user is notified of the policy. + */ + onUserNotifyComplete: function () { + return TelemetryReportingPolicyImpl._infobarShownCallback(); + }, + + /** + * Called when there was an error notifying the user about the policy. + * + * @param error + * (Error) Explains what went wrong. + */ + onUserNotifyFailed: function (error) { + this._log.error("onUserNotifyFailed - " + error); + }, +}); + +this.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, + + /** + * Setup the policy. + */ + setup: function() { + return TelemetryReportingPolicyImpl.setup(); + }, + + /** + * Shutdown and clear the policy. + */ + shutdown: function() { + 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: function() { + return TelemetryReportingPolicyImpl.canUpload(); + }, +}; + +let 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, + + 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 = Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0); + let valueInteger = parseInt(prefString, 10); + + // If nothing or an invalid value is saved in the prefs, bail out. + if (Number.isNaN(valueInteger) || valueInteger == 0) { + 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_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_ACCEPTANCE_YEAR) { + this._log.error("set dataSubmissionPolicyNotifiedDate - Invalid notification date."); + return; + } + + Preferences.set(PREF_ACCEPTED_POLICY_DATE, 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 Preferences.get(PREF_DATA_SUBMISSION_ENABLED, true); + }, + + get currentPolicyVersion() { + return Preferences.get(PREF_CURRENT_POLICY_VERSION, + TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION); + }, + + /** + * The minimum policy version which for dataSubmissionPolicyAccepted to + * to be valid. + */ + get minimumPolicyVersion() { + const minPolicyVersion = Preferences.get(PREF_MINIMUM_POLICY_VERSION, 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 = UpdateChannel.get(false); + } catch(e) { + this._log.error("minimumPolicyVersion - Unable to retrieve the current channel."); + return minPolicyVersion; + } + const channelPref = PREF_MINIMUM_POLICY_VERSION + ".channel-" + channel; + return Preferences.get(channelPref, minPolicyVersion); + }, + + get dataSubmissionPolicyAcceptedVersion() { + return Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0); + }, + + set dataSubmissionPolicyAcceptedVersion(value) { + Preferences.set(PREF_ACCEPTED_POLICY_VERSION, 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; + }, + + /** + * Setup the policy. + */ + setup: function() { + this._log.trace("setup"); + + // Migrate the data choices infobar, if needed. + this._migratePreferences(); + + // Add the event observers. + Services.obs.addObserver(this, "sessionstore-windows-restored", false); + }, + + /** + * Clean up the reporting policy. + */ + shutdown: function() { + this._log.trace("shutdown"); + + this._detachObservers(); + + clearTimeout(this._startupNotificationTimerId); + }, + + /** + * Detach the observers that were attached during setup. + */ + _detachObservers: function() { + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + }, + + /** + * 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: function() { + // If data submission is disabled, there's no point in showing the infobar. Just + // forbid to upload. + if (!this.dataSubmissionEnabled) { + return false; + } + + // Make sure the user is notified of the current policy. If he isn't, don't try + // to upload anything. + if (!this._ensureUserNotified()) { + return false; + } + + // Submission is enabled and user is notified: upload is allowed. + return true; + }, + + /** + * Migrate the data policy preferences, if needed. + */ + _migratePreferences: function() { + // Current prefs are mostly the same than the old ones, except for some deprecated ones. + for (let pref of DEPRECATED_FHR_PREFS) { + Preferences.reset(pref); + } + }, + + /** + * Make sure the user is notified about the policy before allowing upload. + * @return {Boolean} True if the user was notified, false otherwise. + */ + _ensureUserNotified: function() { + const BYPASS_NOTIFICATION = Preferences.get(PREF_BYPASS_NOTIFICATION, false); + if (this.isUserNotifiedOfCurrentPolicy || BYPASS_NOTIFICATION) { + return true; + } + + this._log.trace("ensureUserNotified - User not notified, notifying now."); + if (this._notificationInProgress) { + this._log.trace("ensureUserNotified - User not notified, notification in progress."); + return false; + } + + this._notificationInProgress = true; + let request = new NotifyPolicyRequest(this._log); + Observers.notify("datareporting:notify-data-policy:request", request); + + return false; + }, + + /** + * Called when the user is notified with the infobar. + */ + _infobarShownCallback: function() { + this._log.trace("_infobarShownCallback"); + this._recordNotificationData(); + TelemetrySend.notifyCanUpload(); + }, + + /** + * Record date and the version of the accepted policy. + */ + _recordNotificationData: function() { + this._log.trace("_recordNotificationData"); + this.dataSubmissionPolicyNotifiedDate = new Date(); + this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion; + // The user was notified and the notification data saved: the notification + // is no longer in progress. + this._notificationInProgress = false; + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "sessionstore-windows-restored") { + return; + } + + const isFirstRun = Preferences.get(PREF_FIRST_RUN, true); + const delay = + isFirstRun ? NOTIFICATION_DELAY_FIRST_RUN_MSEC: NOTIFICATION_DELAY_NEXT_RUNS_MSEC; + + this._startupNotificationTimerId = setTimeout( + // Calling |canUpload| eventually shows the infobar, if needed. + () => this.canUpload(), delay); + // We performed at least a run, flip the firstRun preference. + Preferences.set(PREF_FIRST_RUN, false); + }, +}; diff --git a/toolkit/components/telemetry/TelemetrySend.jsm b/toolkit/components/telemetry/TelemetrySend.jsm index 4579f7815455..a2801737ddff 100644 --- a/toolkit/components/telemetry/TelemetrySend.jsm +++ b/toolkit/components/telemetry/TelemetrySend.jsm @@ -30,6 +30,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage", "resource://gre/modules/TelemetryStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy", + "resource://gre/modules/TelemetryReportingPolicy.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", "@mozilla.org/base/telemetry;1", "nsITelemetry"); @@ -198,6 +200,13 @@ this.TelemetrySend = { return TelemetrySendImpl.overduePingsCount; }, + /** + * Notify that we can start submitting data to the servers. + */ + notifyCanUpload: function() { + return TelemetrySendImpl.notifyCanUpload(); + }, + /** * Only used in tests. Used to reset the module data to emulate a restart. */ @@ -382,8 +391,8 @@ let SendScheduler = { let pending = TelemetryStorage.getPendingPingList(); let current = TelemetrySendImpl.getUnpersistedPings(); this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length); - pending = pending.filter(p => TelemetrySendImpl.canSend(p)); - current = current.filter(p => TelemetrySendImpl.canSend(p)); + pending = pending.filter(p => TelemetrySendImpl.sendingEnabled(p)); + current = current.filter(p => TelemetrySendImpl.sendingEnabled(p)); this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length); // Bail out if there is nothing to send. @@ -632,6 +641,15 @@ let TelemetrySendImpl = { return SendScheduler.reset(); }, + /** + * Notify that we can start submitting data to the servers. + */ + notifyCanUpload: function() { + // Let the scheduler trigger sending pings if possible. + SendScheduler.triggerSendingPings(true); + return this.promisePendingPingActivity(); + }, + observe: function(subject, topic, data) { switch(topic) { case TOPIC_IDLE_DAILY: @@ -643,15 +661,15 @@ let TelemetrySendImpl = { submitPing: function(ping) { this._log.trace("submitPing - ping id: " + ping.id); - if (!this.canSend(ping)) { + if (!this.sendingEnabled(ping)) { this._log.trace("submitPing - Telemetry is not allowed to send pings."); return Promise.resolve(); } - if (!this._sendingEnabled) { + if (!this.canSendNow) { // Sending is disabled or throttled, add this to the persisted pending pings. this._log.trace("submitPing - can't send ping now, persisting to disk - " + - "sendingEnabled: " + this._sendingEnabled); + "canSendNow: " + this.canSendNow); return TelemetryStorage.savePendingPing(ping); } @@ -806,7 +824,7 @@ let TelemetrySendImpl = { }, _doPing: function(ping, id, isPersisted) { - if (!this.canSend(ping)) { + if (!this.sendingEnabled(ping)) { // We can't send the pings to the server, so don't try to. this._log.trace("_doPing - Can't send ping " + ping.id); return Promise.resolve(); @@ -910,7 +928,21 @@ let TelemetrySendImpl = { }, /** - * Check if pings can be sent to the server. If FHR is not allowed to upload, + * Check if sending is temporarily disabled. + * @return {Boolean} True if we can send pings to the server right now, false if + * sending is temporarily disabled. + */ + get canSendNow() { + // If the reporting policy was not accepted yet, don't send pings. + if (!TelemetryReportingPolicy.canUpload()) { + return false; + } + + return this._sendingEnabled; + }, + + /** + * Check if sending is disabled. If FHR is not allowed to upload, * pings are not sent to the server (Telemetry is a sub-feature of FHR). If trying * to send a deletion ping, don't block it. * If unified telemetry is off, don't send pings if Telemetry is disabled. @@ -918,7 +950,7 @@ let TelemetrySendImpl = { * @param {Object} [ping=null] A ping to be checked. * @return {Boolean} True if pings can be send to the servers, false otherwise. */ - canSend: function(ping = null) { + sendingEnabled: function(ping = null) { // We only send pings from official builds, but allow overriding this for tests. if (!Telemetry.isOfficialTelemetry && !this._testMode) { return false; diff --git a/toolkit/components/telemetry/moz.build b/toolkit/components/telemetry/moz.build index 49d225069028..83609d887467 100644 --- a/toolkit/components/telemetry/moz.build +++ b/toolkit/components/telemetry/moz.build @@ -32,6 +32,7 @@ EXTRA_COMPONENTS += [ EXTRA_JS_MODULES += [ 'TelemetryArchive.jsm', 'TelemetryLog.jsm', + 'TelemetryReportingPolicy.jsm', 'TelemetrySend.jsm', 'TelemetryStopwatch.jsm', 'TelemetryStorage.jsm',