зеркало из https://github.com/mozilla/gecko-dev.git
535 строки
16 KiB
JavaScript
535 строки
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/. */
|
|
|
|
/* eslint semi: error */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["SavantShieldStudy"];
|
|
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm"
|
|
});
|
|
|
|
// See LOG_LEVELS in Console.jsm. Examples: "all", "info", "warn", & "error".
|
|
const PREF_LOG_LEVEL = "shield.savant.loglevel";
|
|
|
|
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
|
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
|
let consoleOptions = {
|
|
maxLogLevelPref: PREF_LOG_LEVEL,
|
|
prefix: "SavantShieldStudy",
|
|
};
|
|
return new ConsoleAPI(consoleOptions);
|
|
});
|
|
|
|
class SavantShieldStudyClass {
|
|
constructor() {
|
|
this.STUDY_PREF = "shield.savant.enabled";
|
|
this.STUDY_TELEMETRY_CATEGORY = "savant";
|
|
this.ALWAYS_PRIVATE_BROWSING_PREF = "browser.privatebrowsing.autostart";
|
|
this.STUDY_DURATION_OVERRIDE_PREF = "shield.savant.duration_override";
|
|
this.STUDY_EXPIRATION_DATE_PREF = "shield.savant.expiration_date";
|
|
// ms = 'x' weeks * 7 days/week * 24 hours/day * 60 minutes/hour
|
|
// * 60 seconds/minute * 1000 milliseconds/second
|
|
this.DEFAULT_STUDY_DURATION_MS = 4 * 7 * 24 * 60 * 60 * 1000;
|
|
// If on startupStudy(), user is ineligible or study has expired,
|
|
// no probe listeners from this module have been added yet
|
|
this.shouldRemoveListeners = true;
|
|
}
|
|
|
|
init() {
|
|
this.telemetryEvents = new TelemetryEvents(this.STUDY_TELEMETRY_CATEGORY);
|
|
this.addonListener = new AddonListener(this.STUDY_TELEMETRY_CATEGORY);
|
|
this.bookmarkObserver = new BookmarkObserver(this.STUDY_TELEMETRY_CATEGORY);
|
|
this.menuListener = new MenuListener(this.STUDY_TELEMETRY_CATEGORY);
|
|
|
|
// check the pref in case Normandy flipped it on before we could add the pref listener
|
|
this.shouldCollect = Services.prefs.getBoolPref(this.STUDY_PREF);
|
|
if (this.shouldCollect) {
|
|
this.startupStudy();
|
|
}
|
|
Services.prefs.addObserver(this.STUDY_PREF, this);
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic === "nsPref:changed" && data === this.STUDY_PREF) {
|
|
// toggle state of the pref
|
|
this.shouldCollect = !this.shouldCollect;
|
|
if (this.shouldCollect) {
|
|
this.startupStudy();
|
|
} else {
|
|
// The pref has been turned off
|
|
this.endStudy("study_disable");
|
|
}
|
|
}
|
|
}
|
|
|
|
startupStudy() {
|
|
// enable before any possible calls to endStudy, since it sends an 'end_study' event
|
|
this.telemetryEvents.enableCollection();
|
|
|
|
if (!this.isEligible()) {
|
|
this.shouldRemoveListeners = false;
|
|
this.endStudy("ineligible");
|
|
return;
|
|
}
|
|
|
|
this.initStudyDuration();
|
|
|
|
if (this.isStudyExpired()) {
|
|
log.debug("Study expired in between this and the previous session.");
|
|
this.shouldRemoveListeners = false;
|
|
this.endStudy("expired");
|
|
}
|
|
|
|
this.addonListener.init();
|
|
this.bookmarkObserver.init();
|
|
this.menuListener.init();
|
|
}
|
|
|
|
isEligible() {
|
|
const isAlwaysPrivateBrowsing = Services.prefs.getBoolPref(this.ALWAYS_PRIVATE_BROWSING_PREF);
|
|
if (isAlwaysPrivateBrowsing) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
initStudyDuration() {
|
|
if (Services.prefs.getStringPref(this.STUDY_EXPIRATION_DATE_PREF, "")) {
|
|
return;
|
|
}
|
|
Services.prefs.setStringPref(
|
|
this.STUDY_EXPIRATION_DATE_PREF,
|
|
this.getExpirationDateString()
|
|
);
|
|
}
|
|
|
|
getDurationFromPref() {
|
|
return Services.prefs.getIntPref(this.STUDY_DURATION_OVERRIDE_PREF, 0);
|
|
}
|
|
|
|
getExpirationDateString() {
|
|
const now = Date.now();
|
|
const studyDurationInMs =
|
|
this.getDurationFromPref()
|
|
|| this.DEFAULT_STUDY_DURATION_MS;
|
|
const expirationDateInt = now + studyDurationInMs;
|
|
return new Date(expirationDateInt).toISOString();
|
|
}
|
|
|
|
isStudyExpired() {
|
|
const expirationDateInt =
|
|
Date.parse(Services.prefs.getStringPref(
|
|
this.STUDY_EXPIRATION_DATE_PREF,
|
|
this.getExpirationDateString()
|
|
));
|
|
|
|
if (isNaN(expirationDateInt)) {
|
|
log.error(
|
|
`The value for the preference ${this.STUDY_EXPIRATION_DATE_PREF} is invalid.`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (Date.now() > expirationDateInt) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
endStudy(reason) {
|
|
log.debug(`Ending the study due to reason: ${ reason }`);
|
|
const isStudyEnding = true;
|
|
Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, "end_study", reason, null,
|
|
{ subcategory: "shield" });
|
|
this.telemetryEvents.disableCollection();
|
|
this.uninit(isStudyEnding);
|
|
// These prefs needs to persist between restarts, so only reset on endStudy
|
|
Services.prefs.clearUserPref(this.STUDY_PREF);
|
|
Services.prefs.clearUserPref(this.STUDY_EXPIRATION_DATE_PREF);
|
|
}
|
|
|
|
// Called on every Firefox shutdown and endStudy
|
|
uninit(isStudyEnding = false) {
|
|
// if just shutting down, check for expiration, so the endStudy event can
|
|
// be sent along with this session's main ping.
|
|
if (!isStudyEnding && this.isStudyExpired()) {
|
|
log.debug("Study expired during this session.");
|
|
this.endStudy("expired");
|
|
return;
|
|
}
|
|
|
|
this.addonListener.uninit();
|
|
this.bookmarkObserver.uninit();
|
|
this.menuListener.uninit();
|
|
|
|
Services.prefs.removeObserver(this.ALWAYS_PRIVATE_BROWSING_PREF, this);
|
|
Services.prefs.removeObserver(this.STUDY_PREF, this);
|
|
Services.prefs.removeObserver(this.STUDY_DURATION_OVERRIDE_PREF, this);
|
|
Services.prefs.clearUserPref(PREF_LOG_LEVEL);
|
|
Services.prefs.clearUserPref(this.STUDY_DURATION_OVERRIDE_PREF);
|
|
}
|
|
}
|
|
|
|
// References:
|
|
// - https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/normandy/lib/TelemetryEvents.jsm
|
|
// - https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/normandy/lib/PreferenceExperiments.jsm#l357
|
|
class TelemetryEvents {
|
|
constructor(studyCategory) {
|
|
this.STUDY_TELEMETRY_CATEGORY = studyCategory;
|
|
}
|
|
|
|
enableCollection() {
|
|
log.debug("Study has been enabled; turning ON data collection.");
|
|
Services.telemetry.setEventRecordingEnabled(this.STUDY_TELEMETRY_CATEGORY, true);
|
|
}
|
|
|
|
disableCollection() {
|
|
log.debug("Study has been disabled; turning OFF data collection.");
|
|
Services.telemetry.setEventRecordingEnabled(this.STUDY_TELEMETRY_CATEGORY, false);
|
|
}
|
|
}
|
|
|
|
class AddonListener {
|
|
constructor(studyCategory) {
|
|
this.STUDY_TELEMETRY_CATEGORY = studyCategory;
|
|
this.METHOD = "addon";
|
|
this.EXTRA_SUBCATEGORY = "customize";
|
|
}
|
|
|
|
init() {
|
|
this.listener = {
|
|
onInstalling: (addon, needsRestart) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("install_start", addon_id);
|
|
},
|
|
|
|
onInstalled: (addon) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("install_finish", addon_id);
|
|
},
|
|
|
|
onEnabled: (addon) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("enable", addon_id);
|
|
},
|
|
|
|
onDisabled: (addon) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("disable", addon_id);
|
|
},
|
|
|
|
onUninstalling: (addon, needsRestart) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("remove_start", addon_id);
|
|
},
|
|
|
|
onUninstalled: (addon) => {
|
|
const addon_id = addon.id;
|
|
this.recordEvent("remove_finish", addon_id);
|
|
}
|
|
};
|
|
this.addListeners();
|
|
}
|
|
|
|
addListeners() {
|
|
AddonManager.addAddonListener(this.listener);
|
|
}
|
|
|
|
recordEvent(event, addon_id) {
|
|
log.debug(`Addon ID: ${addon_id}; event: ${ event }`);
|
|
Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY,
|
|
this.METHOD,
|
|
event,
|
|
addon_id,
|
|
{ subcategory: this.EXTRA_SUBCATEGORY });
|
|
}
|
|
|
|
removeListeners() {
|
|
AddonManager.removeAddonListener(this.listener);
|
|
}
|
|
|
|
uninit() {
|
|
if (SavantShieldStudy.shouldRemoveListeners) {
|
|
this.removeListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
class BookmarkObserver {
|
|
constructor(studyCategory) {
|
|
this.STUDY_TELEMETRY_CATEGORY = studyCategory;
|
|
// there are two probes: bookmark and follow_bookmark
|
|
this.METHOD_1 = "bookmark";
|
|
this.EXTRA_SUBCATEGORY_1 = "feature";
|
|
this.METHOD_2 = "follow_bookmark";
|
|
this.EXTRA_SUBCATEGORY_2 = "navigation";
|
|
this.TYPE_BOOKMARK = Ci.nsINavBookmarksService.TYPE_BOOKMARK;
|
|
// Ignore "fake" bookmarks created for bookmark tags
|
|
this.skipTags = true;
|
|
}
|
|
|
|
init() {
|
|
this.addObservers();
|
|
}
|
|
|
|
addObservers() {
|
|
PlacesUtils.bookmarks.addObserver(this);
|
|
}
|
|
|
|
onItemAdded(itemID, parentID, index, itemType, uri, title, dateAdded, guid, parentGUID, source) {
|
|
this.handleItemAddRemove(itemType, uri, source, "save");
|
|
}
|
|
|
|
onItemRemoved(itemID, parentID, index, itemType, uri, guid, parentGUID, source) {
|
|
this.handleItemAddRemove(itemType, uri, source, "remove");
|
|
}
|
|
|
|
handleItemAddRemove(itemType, uri, source, event) {
|
|
/*
|
|
* "place:query" uris are used to create containers like Most Visited or
|
|
* Recently Bookmarked. These are added as default bookmarks.
|
|
*/
|
|
if (itemType === this.TYPE_BOOKMARK && !uri.schemeIs("place")
|
|
&& source === PlacesUtils.bookmarks.SOURCE_DEFAULT) {
|
|
const isBookmarkProbe = true;
|
|
this.recordEvent(event, isBookmarkProbe);
|
|
}
|
|
}
|
|
|
|
// This observer is only fired for TYPE_BOOKMARK items.
|
|
onItemVisited(itemID, visitID, time, transitionType, uri, parentID, guid, parentGUID) {
|
|
const isBookmarkProbe = false;
|
|
this.recordEvent("open", isBookmarkProbe);
|
|
}
|
|
|
|
recordEvent(event, isBookmarkProbe) {
|
|
const method = isBookmarkProbe ? this.METHOD_1 : this.METHOD_2;
|
|
const subcategory = isBookmarkProbe ? this.EXTRA_SUBCATEGORY_1 : this.EXTRA_SUBCATEGORY_2;
|
|
Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, method, event, null,
|
|
{
|
|
subcategory
|
|
});
|
|
}
|
|
|
|
removeObservers() {
|
|
PlacesUtils.bookmarks.removeObserver(this);
|
|
}
|
|
|
|
uninit() {
|
|
if (SavantShieldStudy.shouldRemoveListeners) {
|
|
this.removeObservers();
|
|
}
|
|
}
|
|
}
|
|
|
|
class MenuListener {
|
|
constructor(studyCategory) {
|
|
this.STUDY_TELEMETRY_CATEGORY = studyCategory;
|
|
this.NAVIGATOR_TOOLBOX_ID = "navigator-toolbox";
|
|
this.OVERFLOW_PANEL_ID = "widget-overflow";
|
|
this.LIBRARY_PANELVIEW_ID = "appMenu-libraryView";
|
|
this.HAMBURGER_PANEL_ID = "appMenu-popup";
|
|
this.DOTDOTDOT_PANEL_ID = "pageActionPanel";
|
|
this.windowWatcher = new WindowWatcher();
|
|
}
|
|
|
|
init() {
|
|
this.windowWatcher.init(this.loadIntoWindow.bind(this),
|
|
this.unloadFromWindow.bind(this),
|
|
this.onWindowError.bind(this));
|
|
}
|
|
|
|
loadIntoWindow(win) {
|
|
this.addListeners(win);
|
|
}
|
|
|
|
unloadFromWindow(win) {
|
|
this.removeListeners(win);
|
|
}
|
|
|
|
onWindowError(msg) {
|
|
log.error(msg);
|
|
}
|
|
|
|
addListeners(win) {
|
|
const doc = win.document;
|
|
const navToolbox = doc.getElementById(this.NAVIGATOR_TOOLBOX_ID);
|
|
const overflowPanel = doc.getElementById(this.OVERFLOW_PANEL_ID);
|
|
const hamburgerPanel = doc.getElementById(this.HAMBURGER_PANEL_ID);
|
|
const dotdotdotPanel = doc.getElementById(this.DOTDOTDOT_PANEL_ID);
|
|
|
|
/*
|
|
* the library menu "ViewShowing" event bubbles up on the navToolbox in its
|
|
* default location. A separate listener is needed if it is moved to the
|
|
* overflow panel via Hamburger > Customize
|
|
*/
|
|
navToolbox.addEventListener("ViewShowing", this);
|
|
overflowPanel.addEventListener("ViewShowing", this);
|
|
hamburgerPanel.addEventListener("popupshown", this);
|
|
dotdotdotPanel.addEventListener("popupshown", this);
|
|
}
|
|
|
|
handleEvent(evt) {
|
|
switch (evt.type) {
|
|
case "ViewShowing":
|
|
if (evt.target.id === this.LIBRARY_PANELVIEW_ID) {
|
|
log.debug("Library panel opened.");
|
|
this.recordEvent("library_menu");
|
|
}
|
|
break;
|
|
case "popupshown":
|
|
switch (evt.target.id) {
|
|
case this.HAMBURGER_PANEL_ID:
|
|
log.debug("Hamburger panel opened.");
|
|
this.recordEvent("hamburger_menu");
|
|
break;
|
|
case this.DOTDOTDOT_PANEL_ID:
|
|
log.debug("Dotdotdot panel opened.");
|
|
this.recordEvent("dotdotdot_menu");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
recordEvent(method) {
|
|
Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, method, "open", null,
|
|
{ subcategory: "menu" });
|
|
}
|
|
|
|
removeListeners(win) {
|
|
const doc = win.document;
|
|
const navToolbox = doc.getElementById(this.NAVIGATOR_TOOLBOX_ID);
|
|
const overflowPanel = doc.getElementById(this.OVERFLOW_PANEL_ID);
|
|
const hamburgerPanel = doc.getElementById(this.HAMBURGER_PANEL_ID);
|
|
const dotdotdotPanel = doc.getElementById(this.DOTDOTDOT_PANEL_ID);
|
|
|
|
try {
|
|
navToolbox.removeEventListener("ViewShowing", this);
|
|
overflowPanel.removeEventListener("ViewShowing", this);
|
|
hamburgerPanel.removeEventListener("popupshown", this);
|
|
dotdotdotPanel.removeEventListener("popupshown", this);
|
|
} catch (err) {
|
|
// Firefox is shutting down; elements have already been removed.
|
|
}
|
|
}
|
|
|
|
uninit() {
|
|
if (SavantShieldStudy.shouldRemoveListeners) {
|
|
this.windowWatcher.uninit();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The WindowWatcher is used to add/remove listeners from MenuListener
|
|
* to/from all windows.
|
|
*/
|
|
class WindowWatcher {
|
|
constructor() {
|
|
this._isActive = false;
|
|
this._loadCallback = null;
|
|
this._unloadCallback = null;
|
|
this._errorCallback = null;
|
|
}
|
|
|
|
// It is expected that loadCallback, unloadCallback, and errorCallback are bound
|
|
// to a `this` value.
|
|
init(loadCallback, unloadCallback, errorCallback) {
|
|
if (this._isActive) {
|
|
errorCallback("Called init, but WindowWatcher was already running");
|
|
return;
|
|
}
|
|
|
|
this._isActive = true;
|
|
this._loadCallback = loadCallback;
|
|
this._unloadCallback = unloadCallback;
|
|
this._errorCallback = errorCallback;
|
|
|
|
// Add loadCallback to existing windows
|
|
const windows = Services.wm.getEnumerator("navigator:browser");
|
|
while (windows.hasMoreElements()) {
|
|
const win = windows.getNext();
|
|
try {
|
|
this._loadCallback(win);
|
|
} catch (ex) {
|
|
this._errorCallback(`WindowWatcher code loading callback failed: ${ ex }`);
|
|
}
|
|
}
|
|
|
|
// Add loadCallback to future windows
|
|
// This will call the observe method on WindowWatcher
|
|
Services.ww.registerNotification(this);
|
|
}
|
|
|
|
uninit() {
|
|
if (!this._isActive) {
|
|
this._errorCallback("Called uninit, but WindowWatcher was already uninited");
|
|
return;
|
|
}
|
|
|
|
const windows = Services.wm.getEnumerator("navigator:browser");
|
|
while (windows.hasMoreElements()) {
|
|
const win = windows.getNext();
|
|
try {
|
|
this._unloadCallback(win);
|
|
} catch (ex) {
|
|
this._errorCallback(`WindowWatcher code unloading callback failed: ${ ex }`);
|
|
}
|
|
}
|
|
|
|
Services.ww.unregisterNotification(this);
|
|
|
|
this._loadCallback = null;
|
|
this._unloadCallback = null;
|
|
this._errorCallback = null;
|
|
this._isActive = false;
|
|
}
|
|
|
|
observe(win, topic) {
|
|
switch (topic) {
|
|
case "domwindowopened":
|
|
this._onWindowOpened(win);
|
|
break;
|
|
case "domwindowclosed":
|
|
this._onWindowClosed(win);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
_onWindowOpened(win) {
|
|
win.addEventListener("load", this, { once: true });
|
|
}
|
|
|
|
// only one event type expected: "load"
|
|
handleEvent(evt) {
|
|
const win = evt.target.ownerGlobal;
|
|
|
|
// make sure we only add window listeners to a DOMWindow (browser.xul)
|
|
const winType = win.document.documentElement.getAttribute("windowtype");
|
|
if (winType === "navigator:browser") {
|
|
this._loadCallback(win);
|
|
}
|
|
}
|
|
|
|
_onWindowClosed(win) {
|
|
this._unloadCallback(win);
|
|
}
|
|
}
|
|
|
|
const SavantShieldStudy = new SavantShieldStudyClass();
|