gecko-dev/toolkit/components/telemetry/TelemetryReportingPolicy.jsm

497 строки
16 KiB
JavaScript

/* 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, "UpdateUtils",
"resource://gre/modules/UpdateUtils.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";
// URL of privacy policy to be opened in a background tab on first run instead of showing the
// data choices infobar.
const PREF_FIRST_RUN_URL = PREF_BRANCH + "firstRunURL";
// 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
/**
* 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.
*/
var Policy = {
now: () => new Date(),
setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearShowInfobarTimeout: (id) => clearTimeout(id),
};
/**
* 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() {
return TelemetryReportingPolicyImpl._userNotified();
},
/**
* 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.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() {
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();
},
/**
* Test only method, restarts the policy.
*/
reset() {
return TelemetryReportingPolicyImpl.reset();
},
/**
* Test only method, used to check if user is notified of the policy in tests.
*/
testIsUserNotified() {
return TelemetryReportingPolicyImpl.isUserNotifiedOfCurrentPolicy;
},
/**
* Test only method, used to simulate the infobar being shown in xpcshell tests.
*/
testInfobarShown() {
return TelemetryReportingPolicyImpl._userNotified();
},
};
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,
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);
// 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_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 = UpdateUtils.getUpdateChannel(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;
},
/**
* Test only method, restarts the policy.
*/
reset() {
this.shutdown();
return this.setup();
},
/**
* Setup the policy.
*/
setup() {
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() {
this._log.trace("shutdown");
this._detachObservers();
Policy.clearShowInfobarTimeout(this._startupNotificationTimerId);
},
/**
* Detach the observers that were attached during setup.
*/
_detachObservers() {
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() {
// If data submission is disabled, there's no point in showing the infobar. Just
// forbid to upload.
if (!this.dataSubmissionEnabled) {
return false;
}
// Submission is enabled. We enable upload if user is notified or we need to bypass
// the policy.
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
return this.isUserNotifiedOfCurrentPolicy || bypassNotification;
},
/**
* 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) {
Preferences.reset(pref);
}
},
/**
* Show the data choices infobar if the user wasn't already notified and data submission
* is enabled.
*/
_showInfobar() {
if (!this.dataSubmissionEnabled) {
this._log.trace("_showInfobar - Data submission disabled by the policy.");
return;
}
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) {
this._log.trace("_showInfobar - User already notified or bypassing the policy.");
return;
}
if (this._notificationInProgress) {
this._log.trace("_showInfobar - User not notified, notification already in progress.");
return;
}
this._log.trace("_showInfobar - User not notified, notifying now.");
this._notificationInProgress = true;
let request = new NotifyPolicyRequest(this._log);
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();
TelemetrySend.notifyCanUpload();
},
/**
* Record date and the version of the accepted policy.
*/
_recordNotificationData() {
this._log.trace("_recordNotificationData");
this.dataSubmissionPolicyNotifiedDate = Policy.now();
this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion;
// 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.
*/
_openFirstRunPage() {
let firstRunPolicyURL = Preferences.get(PREF_FIRST_RUN_URL, "");
if (!firstRunPolicyURL) {
return false;
}
firstRunPolicyURL = Services.urlFormatter.formatURL(firstRunPolicyURL);
let win;
try {
const { RecentWindow } = Cu.import("resource:///modules/RecentWindow.jsm", {});
win = RecentWindow.getMostRecentBrowserWindow();
} catch (e) {}
if (!win) {
this._log.info("Couldn't find browser window to open first-run page. Falling back to infobar.");
return false;
}
// 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, aStatus) => {
if (aWebProgress.isTopLevel &&
tab &&
tab.linkedBrowser == aBrowser &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
let uri = aBrowser.documentURI;
if (uri && !/^about:(blank|neterror|certerror|blocked)/.test(uri.spec)) {
this._userNotified();
} else {
this._log.info("Failed to load first-run page. Falling back to infobar.");
this._showInfobar();
}
removeListeners();
}
};
let removeListeners = () => {
win.removeEventListener("unload", removeListeners);
win.gBrowser.removeTabsProgressListener(progressListener);
};
win.addEventListener("unload", removeListeners);
win.gBrowser.addTabsProgressListener(progressListener);
tab = win.gBrowser.loadOneTab(firstRunPolicyURL, { inBackground: true });
return true;
},
observe(aSubject, aTopic, aData) {
if (aTopic != "sessionstore-windows-restored") {
return;
}
const isFirstRun = Preferences.get(PREF_FIRST_RUN, true);
if (isFirstRun) {
// We're performing the first run, flip firstRun preference for subsequent runs.
Preferences.set(PREF_FIRST_RUN, false);
try {
if (this._openFirstRunPage()) {
return;
}
} catch (e) {
this._log.error("Failed to open privacy policy tab: " + e);
}
}
// Show the info bar.
const delay =
isFirstRun ? NOTIFICATION_DELAY_FIRST_RUN_MSEC : NOTIFICATION_DELAY_NEXT_RUNS_MSEC;
this._startupNotificationTimerId = Policy.setShowInfobarTimeout(
// Calling |canUpload| eventually shows the infobar, if needed.
() => this._showInfobar(), delay);
},
};