From e72ef92b5bdc43cd2584198e2e54e951b70299e8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 03:32:58 -0500 Subject: Add Basilisk --- application/basilisk/modules/AboutHome.jsm | 192 +++ application/basilisk/modules/AboutNewTab.jsm | 43 + application/basilisk/modules/AttributionCode.jsm | 123 ++ .../basilisk/modules/BrowserUITelemetry.jsm | 902 ++++++++++++++ .../basilisk/modules/BrowserUsageTelemetry.jsm | 561 +++++++++ application/basilisk/modules/CastingApps.jsm | 164 +++ application/basilisk/modules/ContentClick.jsm | 97 ++ .../basilisk/modules/ContentCrashHandlers.jsm | 921 +++++++++++++++ .../basilisk/modules/ContentLinkHandler.jsm | 146 +++ application/basilisk/modules/ContentObservers.jsm | 55 + application/basilisk/modules/ContentSearch.jsm | 562 +++++++++ application/basilisk/modules/ContentWebRTC.jsm | 405 +++++++ .../basilisk/modules/DirectoryLinksProvider.jsm | 1243 ++++++++++++++++++++ application/basilisk/modules/E10SUtils.jsm | 206 ++++ application/basilisk/modules/ExtensionsUI.jsm | 351 ++++++ application/basilisk/modules/Feeds.jsm | 103 ++ .../basilisk/modules/FormSubmitObserver.jsm | 240 ++++ .../basilisk/modules/FormValidationHandler.jsm | 157 +++ application/basilisk/modules/HiddenFrame.jsm | 86 ++ application/basilisk/modules/LaterRun.jsm | 167 +++ .../basilisk/modules/NetworkPrioritizer.jsm | 194 +++ application/basilisk/modules/PermissionUI.jsm | 601 ++++++++++ application/basilisk/modules/PluginContent.jsm | 1182 +++++++++++++++++++ .../basilisk/modules/ProcessHangMonitor.jsm | 398 +++++++ application/basilisk/modules/QuotaManager.jsm | 45 + application/basilisk/modules/ReaderParent.jsm | 186 +++ application/basilisk/modules/RecentWindow.jsm | 65 + application/basilisk/modules/RemotePrompt.jsm | 110 ++ application/basilisk/modules/Sanitizer.jsm | 22 + .../basilisk/modules/SelfSupportBackend.jsm | 331 ++++++ application/basilisk/modules/SitePermissions.jsm | 616 ++++++++++ application/basilisk/modules/TransientPrefs.jsm | 24 + application/basilisk/modules/URLBarZoom.jsm | 75 ++ .../basilisk/modules/Windows8WindowFrameColor.jsm | 53 + application/basilisk/modules/WindowsJumpLists.jsm | 577 +++++++++ .../basilisk/modules/WindowsPreviewPerTab.jsm | 863 ++++++++++++++ application/basilisk/modules/moz.build | 54 + application/basilisk/modules/offlineAppCache.jsm | 20 + application/basilisk/modules/webrtcUI.jsm | 1087 +++++++++++++++++ 39 files changed, 13227 insertions(+) create mode 100644 application/basilisk/modules/AboutHome.jsm create mode 100644 application/basilisk/modules/AboutNewTab.jsm create mode 100644 application/basilisk/modules/AttributionCode.jsm create mode 100644 application/basilisk/modules/BrowserUITelemetry.jsm create mode 100644 application/basilisk/modules/BrowserUsageTelemetry.jsm create mode 100644 application/basilisk/modules/CastingApps.jsm create mode 100644 application/basilisk/modules/ContentClick.jsm create mode 100644 application/basilisk/modules/ContentCrashHandlers.jsm create mode 100644 application/basilisk/modules/ContentLinkHandler.jsm create mode 100644 application/basilisk/modules/ContentObservers.jsm create mode 100644 application/basilisk/modules/ContentSearch.jsm create mode 100644 application/basilisk/modules/ContentWebRTC.jsm create mode 100644 application/basilisk/modules/DirectoryLinksProvider.jsm create mode 100644 application/basilisk/modules/E10SUtils.jsm create mode 100644 application/basilisk/modules/ExtensionsUI.jsm create mode 100644 application/basilisk/modules/Feeds.jsm create mode 100644 application/basilisk/modules/FormSubmitObserver.jsm create mode 100644 application/basilisk/modules/FormValidationHandler.jsm create mode 100644 application/basilisk/modules/HiddenFrame.jsm create mode 100644 application/basilisk/modules/LaterRun.jsm create mode 100644 application/basilisk/modules/NetworkPrioritizer.jsm create mode 100644 application/basilisk/modules/PermissionUI.jsm create mode 100644 application/basilisk/modules/PluginContent.jsm create mode 100644 application/basilisk/modules/ProcessHangMonitor.jsm create mode 100644 application/basilisk/modules/QuotaManager.jsm create mode 100644 application/basilisk/modules/ReaderParent.jsm create mode 100644 application/basilisk/modules/RecentWindow.jsm create mode 100644 application/basilisk/modules/RemotePrompt.jsm create mode 100644 application/basilisk/modules/Sanitizer.jsm create mode 100644 application/basilisk/modules/SelfSupportBackend.jsm create mode 100644 application/basilisk/modules/SitePermissions.jsm create mode 100644 application/basilisk/modules/TransientPrefs.jsm create mode 100644 application/basilisk/modules/URLBarZoom.jsm create mode 100644 application/basilisk/modules/Windows8WindowFrameColor.jsm create mode 100644 application/basilisk/modules/WindowsJumpLists.jsm create mode 100644 application/basilisk/modules/WindowsPreviewPerTab.jsm create mode 100644 application/basilisk/modules/moz.build create mode 100644 application/basilisk/modules/offlineAppCache.jsm create mode 100644 application/basilisk/modules/webrtcUI.jsm (limited to 'application/basilisk/modules') diff --git a/application/basilisk/modules/AboutHome.jsm b/application/basilisk/modules/AboutHome.jsm new file mode 100644 index 000000000..6af429153 --- /dev/null +++ b/application/basilisk/modules/AboutHome.jsm @@ -0,0 +1,192 @@ +/* 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"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "AboutHomeUtils", "AboutHome" ]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate", + "resource:///modules/AutoMigrate.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", + "resource://gre/modules/FxAccounts.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +// Url to fetch snippets, in the urlFormatter service format. +const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl"; + +// Should be bumped up if the snippets content format changes. +const STARTPAGE_VERSION = 4; + +this.AboutHomeUtils = { + get snippetsVersion() { + return STARTPAGE_VERSION; + }, + + /* + * showKnowYourRights - Determines if the user should be shown the + * about:rights notification. The notification should *not* be shown if + * we've already shown the current version, or if the override pref says to + * never show it. The notification *should* be shown if it's never been seen + * before, if a newer version is available, or if the override pref says to + * always show it. + */ + get showKnowYourRights() { + // Look for an unconditional override pref. If set, do what it says. + // (true --> never show, false --> always show) + try { + return !Services.prefs.getBoolPref("browser.rights.override"); + } catch (e) { } + // Ditto, for the legacy EULA pref. + try { + return !Services.prefs.getBoolPref("browser.EULA.override"); + } catch (e) { } + +#ifndef MOZILLA_OFFICIAL + // Non-official builds shouldn't show the notification. + return false; +#else + // Look to see if the user has seen the current version or not. + var currentVersion = Services.prefs.getIntPref("browser.rights.version"); + try { + return !Services.prefs.getBoolPref("browser.rights." + currentVersion + ".shown"); + } catch (e) { } + + // Legacy: If the user accepted a EULA, we won't annoy them with the + // equivalent about:rights page until the version changes. + try { + return !Services.prefs.getBoolPref("browser.EULA." + currentVersion + ".accepted"); + } catch (e) { } + + // We haven't shown the notification before, so do so now. + return true; +#endif + } +}; + +/** + * Returns the URL to fetch snippets from, in the urlFormatter service format. + */ +XPCOMUtils.defineLazyGetter(AboutHomeUtils, "snippetsURL", function() { + let updateURL = Services.prefs + .getCharPref(SNIPPETS_URL_PREF) + .replace("%STARTPAGE_VERSION%", STARTPAGE_VERSION); + return Services.urlFormatter.formatURL(updateURL); +}); + +/** + * This code provides services to the about:home page. Whenever + * about:home needs to do something chrome-privileged, it sends a + * message that's handled here. + */ +var AboutHome = { + MESSAGES: [ + "AboutHome:RestorePreviousSession", + "AboutHome:Downloads", + "AboutHome:Bookmarks", + "AboutHome:History", + "AboutHome:Addons", + "AboutHome:Sync", + "AboutHome:Settings", + "AboutHome:RequestUpdate", + "AboutHome:MaybeShowAutoMigrationUndoNotification", + ], + + init() { + let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + + for (let msg of this.MESSAGES) { + mm.addMessageListener(msg, this); + } + }, + + receiveMessage(aMessage) { + let window = aMessage.target.ownerGlobal; + + switch (aMessage.name) { + case "AboutHome:RestorePreviousSession": + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.canRestoreLastSession) { + ss.restoreLastSession(); + } + break; + + case "AboutHome:Downloads": + window.BrowserDownloadsUI(); + break; + + case "AboutHome:Bookmarks": + window.PlacesCommandHook.showPlacesOrganizer("UnfiledBookmarks"); + break; + + case "AboutHome:History": + window.PlacesCommandHook.showPlacesOrganizer("History"); + break; + + case "AboutHome:Addons": + window.BrowserOpenAddonsMgr(); + break; + + case "AboutHome:Sync": + window.openPreferences("paneSync", { urlParams: { entrypoint: "abouthome" } }); + break; + + case "AboutHome:Settings": + window.openPreferences(); + break; + + case "AboutHome:RequestUpdate": + this.sendAboutHomeData(aMessage.target); + break; + + case "AboutHome:MaybeShowAutoMigrationUndoNotification": + AutoMigrate.maybeShowUndoNotification(aMessage.target); + break; + } + }, + + // Send all the chrome-privileged data needed by about:home. This + // gets re-sent when the search engine changes. + sendAboutHomeData(target) { + let wrapper = {}; + Components.utils.import("resource:///modules/sessionstore/SessionStore.jsm", + wrapper); + let ss = wrapper.SessionStore; + + ss.promiseInitialized.then(function() { + let data = { + showRestoreLastSession: ss.canRestoreLastSession, + snippetsURL: AboutHomeUtils.snippetsURL, + showKnowYourRights: AboutHomeUtils.showKnowYourRights, + snippetsVersion: AboutHomeUtils.snippetsVersion, + }; + + if (AboutHomeUtils.showKnowYourRights) { + // Set pref to indicate we've shown the notification. + let currentVersion = Services.prefs.getIntPref("browser.rights.version"); + Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true); + } + + if (target && target.messageManager) { + target.messageManager.sendAsyncMessage("AboutHome:Update", data); + } else { + let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + mm.broadcastAsyncMessage("AboutHome:Update", data); + } + }).then(null, function onError(x) { + Cu.reportError("Error in AboutHome.sendAboutHomeData: " + x); + }); + }, + +}; diff --git a/application/basilisk/modules/AboutNewTab.jsm b/application/basilisk/modules/AboutNewTab.jsm new file mode 100644 index 000000000..f93ddfadc --- /dev/null +++ b/application/basilisk/modules/AboutNewTab.jsm @@ -0,0 +1,43 @@ +/* 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"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "AboutNewTab" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate", + "resource:///modules/AutoMigrate.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", + "resource://gre/modules/NewTabUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemotePages", + "resource://gre/modules/RemotePageManager.jsm"); + +var AboutNewTab = { + + pageListener: null, + + init() { + this.pageListener = new RemotePages("about:newtab"); + this.pageListener.addMessageListener("NewTab:Customize", this.customize.bind(this)); + this.pageListener.addMessageListener("NewTab:MaybeShowAutoMigrationUndoNotification", + (msg) => AutoMigrate.maybeShowUndoNotification(msg.target.browser)); + }, + + customize(message) { + NewTabUtils.allPages.enabled = message.data.enabled; + NewTabUtils.allPages.enhanced = message.data.enhanced; + }, + + uninit() { + this.pageListener.destroy(); + this.pageListener = null; + }, +}; diff --git a/application/basilisk/modules/AttributionCode.jsm b/application/basilisk/modules/AttributionCode.jsm new file mode 100644 index 000000000..243e60dba --- /dev/null +++ b/application/basilisk/modules/AttributionCode.jsm @@ -0,0 +1,123 @@ +/* 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"; + +#filter substitution + +this.EXPORTED_SYMBOLS = ["AttributionCode"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const ATTR_CODE_MAX_LENGTH = 200; +const ATTR_CODE_KEYS_REGEX = /^source|medium|campaign|content$/; +const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/; +const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded & +const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded = + +let gCachedAttrData = null; + +/** + * Returns an nsIFile for the file containing the attribution data. + */ +function getAttributionFile() { + let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile); + // appinfo does not exist in xpcshell, so we need defaults. + file.append(Services.appinfo.vendor || "mozilla"); + file.append("@MOZ_APP_NAME@"); + file.append("postSigningData"); + return file; +} + +/** + * Returns an object containing a key-value pair for each piece of attribution + * data included in the passed-in attribution code string. + * If the string isn't a valid attribution code, returns an empty object. + */ +function parseAttributionCode(code) { + if (code.length > ATTR_CODE_MAX_LENGTH) { + return {}; + } + + let isValid = true; + let parsed = {}; + for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) { + let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2); + if (key && ATTR_CODE_KEYS_REGEX.test(key)) { + if (value && ATTR_CODE_VALUE_REGEX.test(value)) { + parsed[key] = value; + } + } else { + isValid = false; + break; + } + } + return isValid ? parsed : {}; +} + +var AttributionCode = { + /** + * Reads the attribution code, either from disk or a cached version. + * Returns a promise that fulfills with an object containing the parsed + * attribution data if the code could be read and is valid, + * or an empty object otherwise. + */ + getAttrDataAsync() { + return Task.spawn(function*() { + if (gCachedAttrData != null) { + return gCachedAttrData; + } + + let code = ""; + try { + let bytes = yield OS.File.read(getAttributionFile().path); + let decoder = new TextDecoder(); + code = decoder.decode(bytes); + } catch (ex) { + // The attribution file may already have been deleted, + // or it may have never been installed at all; + // failure to open or read it isn't an error. + } + + gCachedAttrData = parseAttributionCode(code); + return gCachedAttrData; + }); + }, + + /** + * Deletes the attribution data file. + * Returns a promise that resolves when the file is deleted, + * or if the file couldn't be deleted (the promise is never rejected). + */ + deleteFileAsync() { + return Task.spawn(function*() { + try { + yield OS.File.remove(getAttributionFile().path); + } catch (ex) { + // The attribution file may already have been deleted, + // or it may have never been installed at all; + // failure to delete it isn't an error. + } + }); + }, + + /** + * Clears the cached attribution code value, if any. + * Does nothing if called from outside of an xpcshell test. + */ + _clearCache() { + let env = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + if (env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + gCachedAttrData = null; + } + }, +}; diff --git a/application/basilisk/modules/BrowserUITelemetry.jsm b/application/basilisk/modules/BrowserUITelemetry.jsm new file mode 100644 index 000000000..11b7b8fef --- /dev/null +++ b/application/basilisk/modules/BrowserUITelemetry.jsm @@ -0,0 +1,902 @@ +// 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 = ["BrowserUITelemetry"]; + +const {interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", + "resource://gre/modules/UITelemetry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", + "resource:///modules/CustomizableUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UITour", + "resource:///modules/UITour.jsm"); +XPCOMUtils.defineLazyGetter(this, "Timer", function() { + let timer = {}; + Cu.import("resource://gre/modules/Timer.jsm", timer); + return timer; +}); + +const MS_SECOND = 1000; +const MS_MINUTE = MS_SECOND * 60; +const MS_HOUR = MS_MINUTE * 60; + +XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() { + let result = { + "PanelUI-contents": [ + "edit-controls", + "zoom-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "sync-button", + "developer-button", + ], + "nav-bar": [ + "urlbar-container", + "search-container", + "bookmarks-menu-button", + "pocket-button", + "downloads-button", + "home-button", + "social-share-button", + ], + // It's true that toolbar-menubar is not visible + // on OS X, but the XUL node is definitely present + // in the document. + "toolbar-menubar": [ + "menubar-items", + ], + "TabsToolbar": [ + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + ], + "PersonalToolbar": [ + "personal-bookmarks", + ], + }; + + let showCharacterEncoding = Services.prefs.getComplexValue( + "browser.menu.showCharacterEncoding", + Ci.nsIPrefLocalizedString + ).data; + if (showCharacterEncoding == "true") { + result["PanelUI-contents"].push("characterencoding-button"); + } + +#ifdef NIGHTLY_BUILD + if (Services.prefs.getBoolPref("extensions.webcompat-reporter.enabled")) { + result["PanelUI-contents"].push("webcompat-reporter-button"); + } +#endif + + return result; +}); + +XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() { + return Object.keys(DEFAULT_AREA_PLACEMENTS); +}); + +XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() { + let result = [ + "open-file-button", + "developer-button", + "feed-button", + "email-link-button", + "containers-panelmenu", + ]; + + let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"]; + if (panelPlacements.indexOf("characterencoding-button") == -1) { + result.push("characterencoding-button"); + } + + if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) { + result.push("panic-button"); + } + + return result; +}); + +XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() { + let result = []; + for (let [, buttons] of Object.entries(DEFAULT_AREA_PLACEMENTS)) { + result = result.concat(buttons); + } + return result; +}); + +XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() { + // These special cases are for click events on built-in items that are + // contained within customizable items (like the navigation widget). + const SPECIAL_CASES = [ + "back-button", + "forward-button", + "urlbar-stop-button", + "urlbar-go-button", + "urlbar-reload-button", + "searchbar", + "cut-button", + "copy-button", + "paste-button", + "zoom-out-button", + "zoom-reset-button", + "zoom-in-button", + "BMB_bookmarksPopup", + "BMB_unsortedBookmarksPopup", + "BMB_bookmarksToolbarPopup", + "search-go-button", + "soundplaying-icon", + ] + return DEFAULT_ITEMS.concat(PALETTE_ITEMS) + .concat(SPECIAL_CASES); +}); + +const OTHER_MOUSEUP_MONITORED_ITEMS = [ + "PlacesChevron", + "PlacesToolbarItems", + "menubar-items", +]; + +// Items that open arrow panels will often be overlapped by +// the panel that they're opening by the time the mouseup +// event is fired, so for these items, we monitor mousedown. +const MOUSEDOWN_MONITORED_ITEMS = [ + "PanelUI-menu-button", +]; + +// Weakly maps browser windows to objects whose keys are relative +// timestamps for when some kind of session started. For example, +// when a customization session started. That way, when the window +// exits customization mode, we can determine how long the session +// lasted. +const WINDOW_DURATION_MAP = new WeakMap(); + +// Default bucket name, when no other bucket is active. +const BUCKET_DEFAULT = "__DEFAULT__"; +// Bucket prefix, for named buckets. +const BUCKET_PREFIX = "bucket_"; +// Standard separator to use between different parts of a bucket name, such +// as primary name and the time step string. +const BUCKET_SEPARATOR = "|"; + +this.BrowserUITelemetry = { + init() { + UITelemetry.addSimpleMeasureFunction("toolbars", + this.getToolbarMeasures.bind(this)); + UITelemetry.addSimpleMeasureFunction("contextmenu", + this.getContextMenuInfo.bind(this)); + // Ensure that UITour.jsm remains lazy-loaded, yet always registers its + // simple measure function with UITelemetry. + UITelemetry.addSimpleMeasureFunction("UITour", + () => UITour.getTelemetry()); + + UITelemetry.addSimpleMeasureFunction("syncstate", + this.getSyncState.bind(this)); + + Services.obs.addObserver(this, "sessionstore-windows-restored", false); + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); + Services.obs.addObserver(this, "autocomplete-did-enter-text", false); + CustomizableUI.addListener(this); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "sessionstore-windows-restored": + this._gatherFirstWindowMeasurements(); + break; + case "browser-delayed-startup-finished": + this._registerWindow(aSubject); + break; + case "autocomplete-did-enter-text": + let input = aSubject.QueryInterface(Ci.nsIAutoCompleteInput); + if (input && input.id == "urlbar" && !input.inPrivateContext && + input.popup.selectedIndex != -1) { + this._logAwesomeBarSearchResult(input.textValue); + } + break; + } + }, + + /** + * For the _countableEvents object, constructs a chain of + * Javascript Objects with the keys in aKeys, with the final + * key getting the value in aEndWith. If the final key already + * exists in the final object, its value is not set. In either + * case, a reference to the second last object in the chain is + * returned. + * + * Example - suppose I want to store: + * _countableEvents: { + * a: { + * b: { + * c: 0 + * } + * } + * } + * + * And then increment the "c" value by 1, you could call this + * function like this: + * + * let example = this._ensureObjectChain([a, b, c], 0); + * example["c"]++; + * + * Subsequent repetitions of these last two lines would + * simply result in the c value being incremented again + * and again. + * + * @param aKeys the Array of keys to chain Objects together with. + * @param aEndWith the value to assign to the last key. + * @param aRoot the root object onto which we create/get the object chain + * designated by aKeys. + * @returns a reference to the second last object in the chain - + * so in our example, that'd be "b". + */ + _ensureObjectChain(aKeys, aEndWith, aRoot) { + let current = aRoot; + let parent = null; + aKeys.unshift(this._bucket); + for (let [i, key] of aKeys.entries()) { + if (!(key in current)) { + if (i == aKeys.length - 1) { + current[key] = aEndWith; + } else { + current[key] = {}; + } + } + parent = current; + current = current[key]; + } + return parent; + }, + + _countableEvents: {}, + _countEvent(aKeyArray, root = this._countableEvents) { + let countObject = this._ensureObjectChain(aKeyArray, 0, root); + let lastItemKey = aKeyArray[aKeyArray.length - 1]; + countObject[lastItemKey]++; + }, + + _countMouseUpEvent(aCategory, aAction, aButton) { + const BUTTONS = ["left", "middle", "right"]; + let buttonKey = BUTTONS[aButton]; + if (buttonKey) { + this._countEvent([aCategory, aAction, buttonKey]); + } + }, + + _firstWindowMeasurements: null, + _gatherFirstWindowMeasurements() { + // We'll gather measurements as soon as the session has restored. + // We do this here instead of waiting for UITelemetry to ask for + // our measurements because at that point all browser windows have + // probably been closed, since the vast majority of saved-session + // pings are gathered during shutdown. + let win = RecentWindow.getMostRecentBrowserWindow({ + private: false, + allowPopups: false, + }); + + Services.search.init(rv => { + // If there are no such windows (or we've just about found one + // but it's closed already), we're out of luck. :( + let hasWindow = win && !win.closed; + this._firstWindowMeasurements = hasWindow ? this._getWindowMeasurements(win, rv) + : {}; + }); + }, + + _registerWindow(aWindow) { + aWindow.addEventListener("unload", this); + let document = aWindow.document; + + for (let areaID of CustomizableUI.areas) { + let areaNode = document.getElementById(areaID); + if (areaNode) { + (areaNode.customizationTarget || areaNode).addEventListener("mouseup", this); + } + } + + for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { + let item = document.getElementById(itemID); + if (item) { + item.addEventListener("mouseup", this); + } + } + + for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { + let item = document.getElementById(itemID); + if (item) { + item.addEventListener("mousedown", this); + } + } + + WINDOW_DURATION_MAP.set(aWindow, {}); + }, + + _unregisterWindow(aWindow) { + aWindow.removeEventListener("unload", this); + let document = aWindow.document; + + for (let areaID of CustomizableUI.areas) { + let areaNode = document.getElementById(areaID); + if (areaNode) { + (areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this); + } + } + + for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { + let item = document.getElementById(itemID); + if (item) { + item.removeEventListener("mouseup", this); + } + } + + for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { + let item = document.getElementById(itemID); + if (item) { + item.removeEventListener("mousedown", this); + } + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "unload": + this._unregisterWindow(aEvent.currentTarget); + break; + case "mouseup": + this._handleMouseUp(aEvent); + break; + case "mousedown": + this._handleMouseDown(aEvent); + break; + } + }, + + _handleMouseUp(aEvent) { + let targetID = aEvent.currentTarget.id; + + switch (targetID) { + case "PlacesToolbarItems": + this._PlacesToolbarItemsMouseUp(aEvent); + break; + case "PlacesChevron": + this._PlacesChevronMouseUp(aEvent); + break; + case "menubar-items": + this._menubarMouseUp(aEvent); + break; + default: + this._checkForBuiltinItem(aEvent); + } + }, + + _handleMouseDown(aEvent) { + if (aEvent.currentTarget.id == "PanelUI-menu-button") { + // _countMouseUpEvent expects a detail for the second argument, + // but we don't really have any details to give. Just passing in + // "button" is probably simpler than trying to modify + // _countMouseUpEvent for this particular case. + this._countMouseUpEvent("click-menu-button", "button", aEvent.button); + } + }, + + _PlacesChevronMouseUp(aEvent) { + let target = aEvent.originalTarget; + let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item"; + this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); + }, + + _PlacesToolbarItemsMouseUp(aEvent) { + let target = aEvent.originalTarget; + // If this isn't a bookmark-item, we don't care about it. + if (!target.classList.contains("bookmark-item")) { + return; + } + + let result = target.hasAttribute("container") ? "container" : "item"; + this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); + }, + + _menubarMouseUp(aEvent) { + let target = aEvent.originalTarget; + let tag = target.localName + let result = (tag == "menu" || tag == "menuitem") ? tag : "other"; + this._countMouseUpEvent("click-menubar", result, aEvent.button); + }, + + _bookmarksMenuButtonMouseUp(aEvent) { + let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button"); + if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) { + // In the menu panel, only the star is visible, and that opens up the + // bookmarks subview. + this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel", + aEvent.button); + } else { + let clickedItem = aEvent.originalTarget; + // Did we click on the star, or the dropmarker? The star + // has an anonid of "button". If we don't find that, we'll + // assume we clicked on the dropmarker. + let action = "menu"; + if (clickedItem.getAttribute("anonid") == "button") { + // We clicked on the star - now we just need to record + // whether or not we're adding a bookmark or editing an + // existing one. + let bookmarksMenuNode = + bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node; + action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add"; + } + this._countMouseUpEvent("click-bookmarks-menu-button", action, + aEvent.button); + } + }, + + _checkForBuiltinItem(aEvent) { + let item = aEvent.originalTarget; + + // We don't want to count clicks on the private browsing + // button for privacy reasons. See bug 1176391. + if (item.id == "privatebrowsing-button") { + return; + } + + // We special-case the bookmarks-menu-button, since we want to + // monitor more than just clicks on it. + if (item.id == "bookmarks-menu-button" || + getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") { + this._bookmarksMenuButtonMouseUp(aEvent); + return; + } + + // Perhaps we're seeing one of the default toolbar items + // being clicked. + if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { + // Base case - we clicked directly on one of our built-in items, + // and we can go ahead and register that click. + this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); + return; + } + + // If not, we need to check if the item's anonid is in our list + // of built-in items to check. + if (ALL_BUILTIN_ITEMS.indexOf(item.getAttribute("anonid")) != -1) { + this._countMouseUpEvent("click-builtin-item", item.getAttribute("anonid"), aEvent.button); + return; + } + + // If not, we need to check if one of the ancestors of the clicked + // item is in our list of built-in items to check. + let candidate = getIDBasedOnFirstIDedAncestor(item); + if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) { + this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button); + } + }, + + _getWindowMeasurements(aWindow, searchResult) { + let document = aWindow.document; + let result = {}; + + // Determine if the window is in the maximized, normal or + // fullscreen state. + result.sizemode = document.documentElement.getAttribute("sizemode"); + + // Determine if the Bookmarks bar is currently visible + let bookmarksBar = document.getElementById("PersonalToolbar"); + result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed; + + // Determine if the menubar is currently visible. On OS X, the menubar + // is never shown, despite not having the collapsed attribute set. + let menuBar = document.getElementById("toolbar-menubar"); + result.menuBarEnabled = + menuBar && Services.appinfo.OS != "Darwin" + && menuBar.getAttribute("autohide") != "true"; + + // Determine if the titlebar is currently visible. + result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar"); + + // Examine all customizable areas and see what default items + // are present and missing. + let defaultKept = []; + let defaultMoved = []; + let nondefaultAdded = []; + + for (let areaID of CustomizableUI.areas) { + let items = CustomizableUI.getWidgetIdsInArea(areaID); + for (let item of items) { + // Is this a default item? + if (DEFAULT_ITEMS.indexOf(item) != -1) { + // Ok, it's a default item - but is it in its default + // toolbar? We use Array.isArray instead of checking for + // toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might + // be clever and give itself the id of "toString" or something. + if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) && + DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) { + // The item is in its default toolbar + defaultKept.push(item); + } else { + defaultMoved.push(item); + } + } else if (PALETTE_ITEMS.indexOf(item) != -1) { + // It's a palette item that's been moved into a toolbar + nondefaultAdded.push(item); + } + // else, it's provided by an add-on, and we won't record it. + } + } + + // Now go through the items in the palette to see what default + // items are in there. + let paletteItems = + CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette); + let defaultRemoved = []; + for (let item of paletteItems) { + if (DEFAULT_ITEMS.indexOf(item.id) != -1) { + defaultRemoved.push(item.id); + } + } + + result.defaultKept = defaultKept; + result.defaultMoved = defaultMoved; + result.nondefaultAdded = nondefaultAdded; + result.defaultRemoved = defaultRemoved; + + // Next, determine how many add-on provided toolbars exist. + let addonToolbars = 0; + let toolbars = document.querySelectorAll("toolbar[customizable=true]"); + for (let toolbar of toolbars) { + if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) { + addonToolbars++; + } + } + result.addonToolbars = addonToolbars; + + // Find out how many open tabs we have in each window + let winEnumerator = Services.wm.getEnumerator("navigator:browser"); + let visibleTabs = []; + let hiddenTabs = []; + while (winEnumerator.hasMoreElements()) { + let someWin = winEnumerator.getNext(); + if (someWin.gBrowser) { + let visibleTabsNum = someWin.gBrowser.visibleTabs.length; + visibleTabs.push(visibleTabsNum); + hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum); + } + } + result.visibleTabs = visibleTabs; + result.hiddenTabs = hiddenTabs; + + if (Components.isSuccessCode(searchResult)) { + result.currentSearchEngine = Services.search.currentEngine.name; + } + + return result; + }, + + getToolbarMeasures() { + let result = this._firstWindowMeasurements || {}; + result.countableEvents = this._countableEvents; + result.durations = this._durations; + return result; + }, + + getSyncState() { + let result = {}; + for (let sub of ["desktop", "mobile"]) { + let count = 0; + try { + count = Services.prefs.getIntPref("services.sync.clients.devices." + sub); + } catch (ex) {} + result[sub] = count; + } + return result; + }, + + countCustomizationEvent(aEventType) { + this._countEvent(["customize", aEventType]); + }, + + countSearchEvent(source, query, selection) { + this._countEvent(["search", source]); + if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) { + this._countEvent(["search", "urlbar-keyword"]); + } + if (selection) { + this._countEvent(["search", "selection", source, selection.index, selection.kind]); + } + }, + + countOneoffSearchEvent(id, type, where) { + this._countEvent(["search-oneoff", id, type, where]); + }, + + countSearchSettingsEvent(source) { + this._countEvent(["click-builtin-item", source, "search-settings"]); + }, + + countPanicEvent(timeId) { + this._countEvent(["forget-button", timeId]); + }, + + countTabMutingEvent(action, reason) { + this._countEvent(["tab-audio-control", action, reason || "no reason given"]); + }, + + countSyncedTabEvent(what, where) { + // "what" will be, eg, "open" + // "where" will be "toolbarbutton-subview" or "sidebar" + this._countEvent(["synced-tabs", what, where]); + }, + + countSidebarEvent(sidebarID, action) { + // sidebarID is the ID of the sidebar (duh!) + // action will be "hide" or "show" + this._countEvent(["sidebar", sidebarID, action]); + }, + + _logAwesomeBarSearchResult(url) { + let spec = Services.search.parseSubmissionURL(url); + if (spec.engine) { + let matchedEngine = "default"; + if (spec.engine.name !== Services.search.currentEngine.name) { + matchedEngine = "other"; + } + this.countSearchEvent("autocomplete-" + matchedEngine); + } + }, + + _durations: { + customization: [], + }, + + onCustomizeStart(aWindow) { + this._countEvent(["customize", "start"]); + let durationMap = WINDOW_DURATION_MAP.get(aWindow); + if (!durationMap) { + durationMap = {}; + WINDOW_DURATION_MAP.set(aWindow, durationMap); + } + + durationMap.customization = { + start: aWindow.performance.now(), + bucket: this._bucket, + }; + }, + + onCustomizeEnd(aWindow) { + let durationMap = WINDOW_DURATION_MAP.get(aWindow); + if (durationMap && "customization" in durationMap) { + let duration = aWindow.performance.now() - durationMap.customization.start; + this._durations.customization.push({ + duration, + bucket: durationMap.customization.bucket, + }); + delete durationMap.customization; + } + }, + + _contextMenuItemWhitelist: new Set([ + "close-without-interaction", // for closing the menu without clicking it. + "custom-page-item", // The ID we use for page-provided items + "unknown", // The bucket for stuff with no id. + // Everything we know of so far (which will exclude add-on items): + "navigation", "back", "forward", "reload", "stop", "bookmarkpage", + "spell-no-suggestions", "spell-add-to-dictionary", + "spell-undo-add-to-dictionary", "openlinkincurrent", "openlinkintab", + "openlink", + // "openlinkprivate" intentionally omitted for privacy reasons. See bug 1176391. + "bookmarklink", "savelink", + "marklinkMenu", "copyemail", "copylink", "media-play", "media-pause", + "media-mute", "media-unmute", "media-playbackrate", + "media-playbackrate-050x", "media-playbackrate-100x", + "media-playbackrate-125x", "media-playbackrate-150x", "media-playbackrate-200x", + "media-showcontrols", "media-hidecontrols", + "video-fullscreen", "leave-dom-fullscreen", + "reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage", + "copyvideourl", "copyaudiourl", "saveimage", "sendimage", + "setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo", + "saveaudio", "video-saveimage", "sendvideo", "sendaudio", + "ctp-play", "ctp-hide", "savepage", "pocket", "markpageMenu", + "viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall", + "keywordfield", "searchselect", "frame", "showonlythisframe", + "openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe", + "printframe", "viewframesource", "viewframeinfo", + "viewpartialsource-selection", "viewpartialsource-mathml", + "viewsource", "viewinfo", "spell-check-enabled", + "spell-add-dictionaries-main", "spell-dictionaries", + "spell-dictionaries-menu", "spell-add-dictionaries", + "bidi-text-direction-toggle", "bidi-page-direction-toggle", "inspect", + "media-eme-learn-more" + ]), + + _contextMenuInteractions: {}, + + registerContextMenuInteraction(keys, itemID) { + if (itemID) { + if (itemID == "openlinkprivate") { + // Don't record anything, not even an other-item count + // if the user chose to open in a private window. See + // bug 1176391. + return; + } + + if (!this._contextMenuItemWhitelist.has(itemID)) { + itemID = "other-item"; + } + keys.push(itemID); + } + + this._countEvent(keys, this._contextMenuInteractions); + }, + + getContextMenuInfo() { + return this._contextMenuInteractions; + }, + + _bucket: BUCKET_DEFAULT, + _bucketTimer: null, + + /** + * Default bucket name, when no other bucket is active. + */ + get BUCKET_DEFAULT() { + return BUCKET_DEFAULT; + }, + + /** + * Bucket prefix, for named buckets. + */ + get BUCKET_PREFIX() { + return BUCKET_PREFIX; + }, + + /** + * Standard separator to use between different parts of a bucket name, such + * as primary name and the time step string. + */ + get BUCKET_SEPARATOR() { + return BUCKET_SEPARATOR; + }, + + get currentBucket() { + return this._bucket; + }, + + /** + * Sets a named bucket for all countable events and select durections to be + * put into. + * + * @param aName Name of bucket, or null for default bucket name (__DEFAULT__) + */ + setBucket(aName) { + if (this._bucketTimer) { + Timer.clearTimeout(this._bucketTimer); + this._bucketTimer = null; + } + + if (aName) + this._bucket = BUCKET_PREFIX + aName; + else + this._bucket = BUCKET_DEFAULT; + }, + + /** + * Sets a bucket that expires at the rate of a given series of time steps. + * Once the bucket expires, the current bucket will automatically revert to + * the default bucket. While the bucket is expiring, it's name is postfixed + * by '|' followed by a short string representation of the time step it's + * currently in. + * If any other bucket (expiring or normal) is set while an expiring bucket is + * still expiring, the old expiring bucket stops expiring and the new bucket + * immediately takes over. + * + * @param aName Name of bucket. + * @param aTimeSteps An array of times in milliseconds to count up to before + * reverting back to the default bucket. The array of times + * is expected to be pre-sorted in ascending order. + * For example, given a bucket name of 'bucket', the times: + * [60000, 300000, 600000] + * will result in the following buckets: + * * bucket|1m - for the first 1 minute + * * bucket|5m - for the following 4 minutes + * (until 5 minutes after the start) + * * bucket|10m - for the following 5 minutes + * (until 10 minutes after the start) + * * __DEFAULT__ - until a new bucket is set + * @param aTimeOffset Time offset, in milliseconds, from which to start + * counting. For example, if the first time step is 1000ms, + * and the time offset is 300ms, then the next time step + * will become active after 700ms. This affects all + * following time steps also, meaning they will also all be + * timed as though they started expiring 300ms before + * setExpiringBucket was called. + */ + setExpiringBucket(aName, aTimeSteps, aTimeOffset = 0) { + if (aTimeSteps.length === 0) { + this.setBucket(null); + return; + } + + if (this._bucketTimer) { + Timer.clearTimeout(this._bucketTimer); + this._bucketTimer = null; + } + + // Make a copy of the time steps array, so we can safely modify it without + // modifying the original array that external code has passed to us. + let steps = [...aTimeSteps]; + let msec = steps.shift(); + let postfix = this._toTimeStr(msec); + this.setBucket(aName + BUCKET_SEPARATOR + postfix); + + this._bucketTimer = Timer.setTimeout(() => { + this._bucketTimer = null; + this.setExpiringBucket(aName, steps, aTimeOffset + msec); + }, msec - aTimeOffset); + }, + + /** + * Formats a time interval, in milliseconds, to a minimal non-localized string + * representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds, + * 'ms' for milliseconds. + * Examples: + * 65 => 65ms + * 1000 => 1s + * 60000 => 1m + * 61000 => 1m01s + * + * @param aTimeMS Time in milliseconds + * + * @return Minimal string representation. + */ + _toTimeStr(aTimeMS) { + let timeStr = ""; + + function reduce(aUnitLength, aSymbol) { + if (aTimeMS >= aUnitLength) { + let units = Math.floor(aTimeMS / aUnitLength); + aTimeMS = aTimeMS - (units * aUnitLength) + timeStr += units + aSymbol; + } + } + + reduce(MS_HOUR, "h"); + reduce(MS_MINUTE, "m"); + reduce(MS_SECOND, "s"); + reduce(1, "ms"); + + return timeStr; + }, +}; + +/** + * Returns the id of the first ancestor of aNode that has an id. If aNode + * has no parent, or no ancestor has an id, returns null. + * + * @param aNode the node to find the first ID'd ancestor of + */ +function getIDBasedOnFirstIDedAncestor(aNode) { + while (!aNode.id) { + aNode = aNode.parentNode; + if (!aNode) { + return null; + } + } + + return aNode.id; +} diff --git a/application/basilisk/modules/BrowserUsageTelemetry.jsm b/application/basilisk/modules/BrowserUsageTelemetry.jsm new file mode 100644 index 000000000..194ee0100 --- /dev/null +++ b/application/basilisk/modules/BrowserUsageTelemetry.jsm @@ -0,0 +1,561 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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 = ["BrowserUsageTelemetry", "URLBAR_SELECTED_RESULT_TYPES"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +// The upper bound for the count of the visited unique domain names. +const MAX_UNIQUE_VISITED_DOMAINS = 100; + +// Observed topic names. +const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored"; +const TAB_RESTORING_TOPIC = "SSTabRestoring"; +const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split"; +const DOMWINDOW_OPENED_TOPIC = "domwindowopened"; +const AUTOCOMPLETE_ENTER_TEXT_TOPIC = "autocomplete-did-enter-text"; + +// Probe names. +const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count"; +const MAX_WINDOW_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_window_count"; +const TAB_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.tab_open_event_count"; +const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.window_open_event_count"; +const UNIQUE_DOMAINS_COUNT_SCALAR_NAME = "browser.engagement.unique_domains_count"; +const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count"; +const UNFILTERED_URI_COUNT_SCALAR_NAME = "browser.engagement.unfiltered_uri_count"; + +// A list of known search origins. +const KNOWN_SEARCH_SOURCES = [ + "abouthome", + "contextmenu", + "newtab", + "searchbar", + "urlbar", +]; + +const KNOWN_ONEOFF_SOURCES = [ + "oneoff-urlbar", + "oneoff-searchbar", + "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7). +]; + +/** + * The buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE + * histogram. + */ +const URLBAR_SELECTED_RESULT_TYPES = { + autofill: 0, + bookmark: 1, + history: 2, + keyword: 3, + searchengine: 4, + searchsuggestion: 5, + switchtab: 6, + tag: 7, + visiturl: 8, + remotetab: 9, + extension: 10, +}; + +function getOpenTabsAndWinsCounts() { + let tabCount = 0; + let winCount = 0; + + let browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements()) { + let win = browserEnum.getNext(); + winCount++; + tabCount += win.gBrowser.tabs.length; + } + + return { tabCount, winCount }; +} + +function getSearchEngineId(engine) { + if (engine) { + if (engine.identifier) { + return engine.identifier; + } + // Due to bug 1222070, we can't directly check Services.telemetry.canRecordExtended + // here. + const extendedTelemetry = Services.prefs.getBoolPref("toolkit.telemetry.enabled"); + if (engine.name && extendedTelemetry) { + // If it's a custom search engine only report the engine name + // if extended Telemetry is enabled. + return "other-" + engine.name; + } + } + return "other"; +} + +let URICountListener = { + // A set containing the visited domains, see bug 1271310. + _domainSet: new Set(), + // A map to keep track of the URIs loaded from the restored tabs. + _restoredURIsMap: new WeakMap(), + + isHttpURI(uri) { + // Only consider http(s) schemas. + return uri.schemeIs("http") || uri.schemeIs("https"); + }, + + addRestoredURI(browser, uri) { + if (!this.isHttpURI(uri)) { + return; + } + + this._restoredURIsMap.set(browser, uri.spec); + }, + + onLocationChange(browser, webProgress, request, uri, flags) { + // Don't count this URI if it's an error page. + if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { + return; + } + + // We only care about top level loads. + if (!webProgress.isTopLevel) { + return; + } + + // The SessionStore sets the URI of a tab first, firing onLocationChange the + // first time, then manages content loading using its scheduler. Once content + // loads, we will hit onLocationChange again. + // We can catch the first case by checking for null requests: be advised that + // this can also happen when navigating page fragments, so account for it. + if (!request && + !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { + return; + } + + // Track URI loads, even if they're not http(s). + let uriSpec = null; + try { + uriSpec = uri.spec; + } catch (e) { + // If we have troubles parsing the spec, still count this as + // an unfiltered URI. + Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1); + return; + } + + + // Don't count about:blank and similar pages, as they would artificially + // inflate the counts. + if (browser.ownerDocument.defaultView.gInitialPages.includes(uriSpec)) { + return; + } + + // If the URI we're loading is in the _restoredURIsMap, then it comes from a + // restored tab. If so, let's skip it and remove it from the map as we want to + // count page refreshes. + if (this._restoredURIsMap.get(browser) === uriSpec) { + this._restoredURIsMap.delete(browser); + return; + } + + // The URI wasn't from a restored tab. Count it among the unfiltered URIs. + // If this is an http(s) URI, this also gets counted by the "total_uri_count" + // probe. + Services.telemetry.scalarAdd(UNFILTERED_URI_COUNT_SCALAR_NAME, 1); + + if (!this.isHttpURI(uri)) { + return; + } + + // Update the URI counts. + Services.telemetry.scalarAdd(TOTAL_URI_COUNT_SCALAR_NAME, 1); + + // We only want to count the unique domains up to MAX_UNIQUE_VISITED_DOMAINS. + if (this._domainSet.size == MAX_UNIQUE_VISITED_DOMAINS) { + return; + } + + // Unique domains should be aggregated by (eTLD + 1): x.test.com and y.test.com + // are counted once as test.com. + try { + // Even if only considering http(s) URIs, |getBaseDomain| could still throw + // due to the URI containing invalid characters or the domain actually being + // an ipv4 or ipv6 address. + this._domainSet.add(Services.eTLD.getBaseDomain(uri)); + } catch (e) { + return; + } + + Services.telemetry.scalarSet(UNIQUE_DOMAINS_COUNT_SCALAR_NAME, this._domainSet.size); + }, + + /** + * Reset the counts. This should be called when breaking a session in Telemetry. + */ + reset() { + this._domainSet.clear(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), +}; + +let urlbarListener = { + init() { + Services.obs.addObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC, true); + }, + + uninit() { + Services.obs.removeObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC); + }, + + observe(subject, topic, data) { + switch (topic) { + case AUTOCOMPLETE_ENTER_TEXT_TOPIC: + this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput)); + break; + } + }, + + /** + * Used to log telemetry when the user enters text in the urlbar. + * + * @param {nsIAutoCompleteInput} input The autocomplete element where the + * text was entered. + */ + _handleURLBarTelemetry(input) { + if (!input || + input.id != "urlbar" || + input.inPrivateContext || + input.popup.selectedIndex < 0) { + return; + } + let controller = + input.popup.view.QueryInterface(Ci.nsIAutoCompleteController); + let idx = input.popup.selectedIndex; + let value = controller.getValueAt(idx); + let action = input._parseActionUrl(value); + let actionType; + if (action) { + actionType = + action.type == "searchengine" && action.params.searchSuggestion ? + "searchsuggestion" : + action.type; + } + if (!actionType) { + let styles = new Set(controller.getStyleAt(idx).split(/\s+/)); + let style = ["autofill", "tag", "bookmark"].find(s => styles.has(s)); + actionType = style || "history"; + } + + Services.telemetry + .getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX") + .add(idx); + + // Ideally this would be a keyed histogram and we'd just add(actionType), + // but keyed histograms aren't currently shown on the telemetry dashboard + // (bug 1151756). + // + // You can add values but don't change any of the existing values. + // Otherwise you'll break our data. + if (actionType in URLBAR_SELECTED_RESULT_TYPES) { + Services.telemetry + .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE") + .add(URLBAR_SELECTED_RESULT_TYPES[actionType]); + } else { + Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " + + actionType); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), +}; + +let BrowserUsageTelemetry = { + init() { + Services.obs.addObserver(this, WINDOWS_RESTORED_TOPIC, false); + urlbarListener.init(); + }, + + /** + * Handle subsession splits in the parent process. + */ + afterSubsessionSplit() { + // Scalars just got cleared due to a subsession split. We need to set the maximum + // concurrent tab and window counts so that they reflect the correct value for the + // new subsession. + const counts = getOpenTabsAndWinsCounts(); + Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, counts.tabCount); + Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount); + + // Reset the URI counter. + URICountListener.reset(); + }, + + uninit() { + Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC); + Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC); + Services.obs.removeObserver(this, WINDOWS_RESTORED_TOPIC); + urlbarListener.uninit(); + }, + + observe(subject, topic, data) { + switch (topic) { + case WINDOWS_RESTORED_TOPIC: + this._setupAfterRestore(); + break; + case DOMWINDOW_OPENED_TOPIC: + this._onWindowOpen(subject); + break; + case TELEMETRY_SUBSESSIONSPLIT_TOPIC: + this.afterSubsessionSplit(); + break; + } + }, + + handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._onTabOpen(); + break; + case "unload": + this._unregisterWindow(event.target); + break; + case TAB_RESTORING_TOPIC: + // We're restoring a new tab from a previous or crashed session. + // We don't want to track the URIs from these tabs, so let + // |URICountListener| know about them. + let browser = event.target.linkedBrowser; + URICountListener.addRestoredURI(browser, browser.currentURI); + break; + } + }, + + /** + * The main entry point for recording search related Telemetry. This includes + * search counts and engagement measurements. + * + * Telemetry records only search counts per engine and action origin, but + * nothing pertaining to the search contents themselves. + * + * @param {nsISearchEngine} engine + * The engine handling the search. + * @param {String} source + * Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed + * values. + * @param {Object} [details] Options object. + * @param {Boolean} [details.isOneOff=false] + * true if this event was generated by a one-off search. + * @param {Boolean} [details.isSuggestion=false] + * true if this event was generated by a suggested search. + * @param {Boolean} [details.isAlias=false] + * true if this event was generated by a search using an alias. + * @param {Object} [details.type=null] + * The object describing the event that triggered the search. + * @throws if source is not in the known sources list. + */ + recordSearch(engine, source, details = {}) { + const isOneOff = !!details.isOneOff; + const countId = getSearchEngineId(engine) + "." + source; + + if (isOneOff) { + if (!KNOWN_ONEOFF_SOURCES.includes(source)) { + // Silently drop the error if this bogus call + // came from 'urlbar' or 'searchbar'. They're + // calling |recordSearch| twice from two different + // code paths because they want to record the search + // in SEARCH_COUNTS. + if (["urlbar", "searchbar"].includes(source)) { + Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId); + return; + } + throw new Error("Unknown source for one-off search: " + source); + } + } else { + if (!KNOWN_SEARCH_SOURCES.includes(source)) { + throw new Error("Unknown source for search: " + source); + } + Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId); + } + + // Dispatch the search signal to other handlers. + this._handleSearchAction(engine, source, details); + }, + + _recordSearch(engine, source, action = null) { + let scalarKey = action ? "search_" + action : "search"; + Services.telemetry.keyedScalarAdd("browser.engagement.navigation." + source, + scalarKey, 1); + Services.telemetry.recordEvent("navigation", "search", source, action, + { engine: getSearchEngineId(engine) }); + }, + + _handleSearchAction(engine, source, details) { + switch (source) { + case "urlbar": + case "oneoff-urlbar": + case "searchbar": + case "oneoff-searchbar": + case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7). + this._handleSearchAndUrlbar(engine, source, details); + break; + case "abouthome": + this._recordSearch(engine, "about_home", "enter"); + break; + case "newtab": + this._recordSearch(engine, "about_newtab", "enter"); + break; + case "contextmenu": + this._recordSearch(engine, "contextmenu"); + break; + } + }, + + /** + * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and + * "searchbar-oneoff" sources. + */ + _handleSearchAndUrlbar(engine, source, details) { + // We want "urlbar" and "urlbar-oneoff" (and similar cases) to go in the same + // scalar, but in a different key. + + // When using one-offs in the searchbar we get an "unknown" source. See bug + // 1195733 comment 7 for the context. Fix-up the label here. + const sourceName = + (source === "unknown") ? "searchbar" : source.replace("oneoff-", ""); + + const isOneOff = !!details.isOneOff; + if (isOneOff) { + // We will receive a signal from the "urlbar"/"searchbar" even when the + // search came from "oneoff-urlbar". That's because both signals + // are propagated from search.xml. Skip it if that's the case. + // Moreover, we skip the "unknown" source that comes from the searchbar + // when performing searches from the default search engine. See bug 1195733 + // comment 7 for context. + if (["urlbar", "searchbar", "unknown"].includes(source)) { + return; + } + + // If that's a legit one-off search signal, record it using the relative key. + this._recordSearch(engine, sourceName, "oneoff"); + return; + } + + // The search was not a one-off. It was a search with the default search engine. + if (details.isSuggestion) { + // It came from a suggested search, so count it as such. + this._recordSearch(engine, sourceName, "suggestion"); + return; + } else if (details.isAlias) { + // This one came from a search that used an alias. + this._recordSearch(engine, sourceName, "alias"); + return; + } + + // The search signal was generated by typing something and pressing enter. + this._recordSearch(engine, sourceName, "enter"); + }, + + /** + * This gets called shortly after the SessionStore has finished restoring + * windows and tabs. It counts the open tabs and adds listeners to all the + * windows. + */ + _setupAfterRestore() { + // Make sure to catch new chrome windows and subsession splits. + Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, false); + Services.obs.addObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false); + + // Attach the tabopen handlers to the existing Windows. + let browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements()) { + this._registerWindow(browserEnum.getNext()); + } + + // Get the initial tab and windows max counts. + const counts = getOpenTabsAndWinsCounts(); + Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, counts.tabCount); + Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount); + }, + + /** + * Adds listeners to a single chrome window. + */ + _registerWindow(win) { + win.addEventListener("unload", this); + win.addEventListener("TabOpen", this, true); + + // Don't include URI and domain counts when in private mode. + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + return; + } + win.gBrowser.tabContainer.addEventListener(TAB_RESTORING_TOPIC, this); + win.gBrowser.addTabsProgressListener(URICountListener); + }, + + /** + * Removes listeners from a single chrome window. + */ + _unregisterWindow(win) { + win.removeEventListener("unload", this); + win.removeEventListener("TabOpen", this, true); + + // Don't include URI and domain counts when in private mode. + if (PrivateBrowsingUtils.isWindowPrivate(win.defaultView)) { + return; + } + win.defaultView.gBrowser.tabContainer.removeEventListener(TAB_RESTORING_TOPIC, this); + win.defaultView.gBrowser.removeTabsProgressListener(URICountListener); + }, + + /** + * Updates the tab counts. + * @param {Number} [newTabCount=0] The count of the opened tabs across all windows. This + * is computed manually if not provided. + */ + _onTabOpen(tabCount = 0) { + // Use the provided tab count if available. Otherwise, go on and compute it. + tabCount = tabCount || getOpenTabsAndWinsCounts().tabCount; + // Update the "tab opened" count and its maximum. + Services.telemetry.scalarAdd(TAB_OPEN_EVENT_COUNT_SCALAR_NAME, 1); + Services.telemetry.scalarSetMaximum(MAX_TAB_COUNT_SCALAR_NAME, tabCount); + }, + + /** + * Tracks the window count and registers the listeners for the tab count. + * @param{Object} win The window object. + */ + _onWindowOpen(win) { + // Make sure to have a |nsIDOMWindow|. + if (!(win instanceof Ci.nsIDOMWindow)) { + return; + } + + let onLoad = () => { + win.removeEventListener("load", onLoad); + + // Ignore non browser windows. + if (win.document.documentElement.getAttribute("windowtype") != "navigator:browser") { + return; + } + + this._registerWindow(win); + // Track the window open event and check the maximum. + const counts = getOpenTabsAndWinsCounts(); + Services.telemetry.scalarAdd(WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME, 1); + Services.telemetry.scalarSetMaximum(MAX_WINDOW_COUNT_SCALAR_NAME, counts.winCount); + + // We won't receive the "TabOpen" event for the first tab within a new window. + // Account for that. + this._onTabOpen(counts.tabCount); + }; + win.addEventListener("load", onLoad); + }, +}; diff --git a/application/basilisk/modules/CastingApps.jsm b/application/basilisk/modules/CastingApps.jsm new file mode 100644 index 000000000..3d4f6f5b0 --- /dev/null +++ b/application/basilisk/modules/CastingApps.jsm @@ -0,0 +1,164 @@ +// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* 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 = ["CastingApps"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm"); + + +var CastingApps = { + _sendEventToVideo(element, data) { + let event = element.ownerDocument.createEvent("CustomEvent"); + event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(data)); + element.dispatchEvent(event); + }, + + makeURI(url, charset, baseURI) { + return Services.io.newURI(url, charset, baseURI); + }, + + getVideo(element) { + if (!element) { + return null; + } + + let extensions = SimpleServiceDiscovery.getSupportedExtensions(); + let types = SimpleServiceDiscovery.getSupportedMimeTypes(); + + // Grab the poster attribute from the