--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1258,16 +1258,17 @@ var gBrowserInit = {
TrackingProtection.init();
CaptivePortalWatcher.init();
ZoomUI.init(window);
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
mm.loadFrameScript("chrome://browser/content/content.js", true);
mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
+ mm.loadFrameScript("chrome://global/content/content-HybridContentTelemetry.js", true);
mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad);
if (!gMultiProcessBrowser) {
// There is a Content:Click message manually sent from content.
Services.els.addSystemEventListener(gBrowser, "click", contentAreaClick, true);
}
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -34,16 +34,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
DateTimePickerHelper: "resource://gre/modules/DateTimePickerHelper.jsm",
DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
Feeds: "resource:///modules/Feeds.jsm",
FileUtils: "resource://gre/modules/FileUtils.jsm",
FileSource: "resource://gre/modules/L10nRegistry.jsm",
FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
+ HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
Integration: "resource://gre/modules/Integration.jsm",
L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
NetUtil: "resource://gre/modules/NetUtil.jsm",
NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
@@ -3067,8 +3068,15 @@ this.NSGetFactory = XPCOMUtils.generateN
// Listen for UITour messages.
// Do it here instead of the UITour module itself so that the UITour module is lazy loaded
// when the first message is received.
var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
globalMM.addMessageListener("UITour:onPageEvent", function(aMessage) {
UITour.onPageEvent(aMessage, aMessage.data);
});
+
+// Listen for HybridContentTelemetry messages.
+// Do it here instead of HybridContentTelemetry.init() so that
+// the module can be lazily loaded on the first message.
+globalMM.addMessageListener("HybridContentTelemetry:onTelemetryMessage", aMessage => {
+ HybridContentTelemetry.onTelemetryMessage(aMessage, aMessage.data);
+});
--- a/toolkit/components/telemetry/TelemetryUtils.jsm
+++ b/toolkit/components/telemetry/TelemetryUtils.jsm
@@ -27,16 +27,17 @@ const IS_CONTENT_PROCESS = (function() {
this.TelemetryUtils = {
Preferences: Object.freeze({
// General Preferences
ArchiveEnabled: "toolkit.telemetry.archive.enabled",
CachedClientId: "toolkit.telemetry.cachedClientID",
FirstRun: "toolkit.telemetry.reportingpolicy.firstRun",
FirstShutdownPingEnabled: "toolkit.telemetry.firstShutdownPing.enabled",
HealthPingEnabled: "toolkit.telemetry.healthping.enabled",
+ HybridContentEnabled: "toolkit.telemetry.hybridContent.enabled",
OverrideOfficialCheck: "toolkit.telemetry.send.overrideOfficialCheck",
OverridePreRelease: "toolkit.telemetry.testing.overridePreRelease",
Server: "toolkit.telemetry.server",
ShutdownPingSender: "toolkit.telemetry.shutdownPingSender.enabled",
ShutdownPingSenderFirstSession: "toolkit.telemetry.shutdownPingSender.enabledFirstSession",
TelemetryEnabled: "toolkit.telemetry.enabled",
Unified: "toolkit.telemetry.unified",
UpdatePing: "toolkit.telemetry.updatePing.enabled",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/hybrid-content/HybridContentTelemetry-lib.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+if (typeof Mozilla == "undefined") {
+ var Mozilla = {};
+}
+
+(function($) {
+ "use strict";
+
+ var _canUpload = false;
+
+ if (typeof Mozilla.ContentTelemetry == "undefined") {
+ /**
+ * Library that exposes an event-based Web API for communicating with the
+ * desktop browser chrome. It can be used for recording Telemetry data from
+ * authorized web content pages.
+ *
+ * <p>For security/privacy reasons `Mozilla.ContentTelemetry` will only work
+ * on a list of allowed secure origins. The list of allowed origins can be
+ * found in
+ * {@link https://dxr.mozilla.org/mozilla-central/source/browser/app/permissions|
+ * browser/app/permissions}.</p>
+ *
+ * @since 59
+ * @namespace
+ */
+ Mozilla.ContentTelemetry = {};
+ }
+
+ function _sendMessageToChrome(name, data) {
+ var event = new CustomEvent("mozTelemetry", {
+ bubbles: true,
+ detail: {
+ name,
+ data: data || {}
+ }
+ });
+
+ document.dispatchEvent(event);
+ }
+
+ /**
+ * This internal function is used to register the policy handler. This is
+ * needed by pages that do not want to use Telemetry but still need to
+ * respect user Privacy choices.
+ */
+ function _registerInternalPolicyHandler() {
+ // Register the handler that will update the policy boolean.
+ function policyChangeHandler(updatedPref) {
+ if (!("detail" in updatedPref) ||
+ !("canUpload" in updatedPref.detail) ||
+ typeof updatedPref.detail.canUpload != "boolean") {
+ return;
+ }
+ _canUpload = updatedPref.detail.canUpload;
+ }
+ document.addEventListener("mozTelemetryPolicyChange", policyChangeHandler);
+
+ // Make sure the chrome is initialized.
+ _sendMessageToChrome("init");
+ }
+
+ Mozilla.ContentTelemetry.canUpload = function() {
+ return _canUpload;
+ };
+
+ Mozilla.ContentTelemetry.registerEvents = function(category, eventData) {
+ _sendMessageToChrome("registerEvents", { category, eventData });
+ };
+
+ Mozilla.ContentTelemetry.recordEvent = function(category, method, object, value, extra) {
+ _sendMessageToChrome("recordEvent", { category, method, object, value, extra });
+ };
+
+ // Register the policy handler so that |canUpload| is always up to date.
+ _registerInternalPolicyHandler();
+})();
+
+// Make this library Require-able.
+/* eslint-env commonjs */
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = Mozilla.ContentTelemetry;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/hybrid-content/HybridContentTelemetry.jsm
@@ -0,0 +1,124 @@
+/* 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 = ["HybridContentTelemetry"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let HybridContentTelemetry = {
+ _logger: null,
+ _observerInstalled: false,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger =
+ Log.repository.getLoggerWithMessagePrefix("Toolkit.Telemetry",
+ "HybridContentTelemetry::");
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Lazily initialized observer for the Telemetry upload preference. This is
+ * only ever executed if a page uses hybrid content telemetry and has enough
+ * privileges to run it.
+ */
+ _lazyObserverInit() {
+ if (this._observerInstalled) {
+ // We only want to install the observers once, if needed.
+ return;
+ }
+ this._log.trace("_lazyObserverInit - installing the pref observers.");
+ XPCOMUtils.defineLazyPreferenceGetter(this, "_uploadEnabled",
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false, /* aDefaultValue */
+ () => this._broadcastPolicyUpdate());
+ this._observerInstalled = true;
+ },
+
+ /**
+ * This is the handler for the async "HybridContentTelemetry:onTelemetryMessage"
+ * message. This function is getting called by the listener in nsBrowserGlue.js.
+ */
+ onTelemetryMessage(aMessage, aData) {
+ if (!this._hybridContentEnabled) {
+ this._log.trace("onTelemetryMessage - hybrid content telemetry is disabled.");
+ return;
+ }
+
+ this._log.trace("onTelemetryMessage - Message received, dispatching API call.");
+ if (!aData ||
+ !("data" in aData) ||
+ !("name" in aData) ||
+ typeof aData.name != "string" ||
+ typeof aData.data != "object") {
+ this._log.error("onTelemetryMessage - received a malformed message.");
+ return;
+ }
+ this._dispatchAPICall(aData.name, aData.data, aMessage);
+ },
+
+ /**
+ * Broadcast the upload policy state to the pages using hybrid
+ * content telemetry.
+ */
+ _broadcastPolicyUpdate() {
+ this._log.trace(`_broadcastPolicyUpdate - New value is ${this._uploadEnabled}.`);
+ Services.mm.broadcastAsyncMessage("HybridContentTelemetry:PolicyChanged",
+ {canUpload: this._uploadEnabled});
+ },
+
+ /**
+ * Dispatches the calls to the Telemetry service.
+ * @param {String} aEndpoint The name of the api endpoint to call.
+ * @param {Object} aData An object containing the data to pass to the API.
+ * @param {Object} aOriginalMessage The message object coming from the listener.
+ */
+ _dispatchAPICall(aEndpoint, aData, aOriginalMessage) {
+ this._log.info(`_dispatchAPICall - processing "${aEndpoint}".`);
+
+ // We don't really care too much about validating the passed parameters.
+ // Telemetry will take care of that for us so there is little gain from
+ // duplicating that logic here. Just make sure we don't throw and report
+ // any error.
+ try {
+ switch (aEndpoint) {
+ case "init":
+ this._lazyObserverInit();
+ this._broadcastPolicyUpdate();
+ break;
+ case "registerEvents":
+ Services.telemetry.registerEvents(aData.category, aData.eventData);
+ break;
+ case "recordEvent":
+ // Don't pass "undefined" for the optional |value| and |extra|:
+ // the Telemetry API expects them to be "null" if something is being
+ // passed.
+ let check = (data, key) => (key in data && typeof data[key] != "undefined");
+ Services.telemetry.recordEvent(aData.category,
+ aData.method,
+ aData.object,
+ check(aData, "value") ? aData.value : null,
+ check(aData, "extra") ? aData.extra : null);
+ break;
+ default:
+ this._log.error(`_dispatchAPICall - unknown "${aEndpoint}"" API call.`);
+ }
+ } catch (e) {
+ this._log.error(`_dispatchAPICall - error executing "${aEndpoint}".`, e);
+ }
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(HybridContentTelemetry, "_hybridContentEnabled",
+ TelemetryUtils.Preferences.HybridContentEnabled,
+ false /* aDefaultValue */);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/hybrid-content/content-HybridContentTelemetry.js
@@ -0,0 +1,171 @@
+/* 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-env mozilla/frame-script */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const TelemetryPrefs = TelemetryUtils.Preferences;
+
+// The permission to be granted to pages that want to use HCT.
+const HCT_PERMISSION = "hc_telemetry";
+// The message that chrome uses to communicate back to content, when the
+// upload policy changes.
+const HCT_POLICY_CHANGE_MSG = "HybridContentTelemetry:PolicyChanged";
+
+var HybridContentTelemetryListener = {
+ _logger: null,
+ _hasListener: false,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger =
+ Log.repository.getLoggerWithMessagePrefix("Toolkit.Telemetry",
+ "HybridContentTelemetryListener::");
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Verifies that the hybrid content telemetry request comes
+ * from a trusted origin.
+ * @oaram {nsIDomEvent} event Optional object containing the data for the event coming from
+ * the content. The "detail" section of this event holds the
+ * hybrid content specific payload. It looks like:
+ * {name: "apiEndpoint", data: { ... endpoint specific data ... }}
+ * @return {Boolean} true if we are on a trusted page, false otherwise.
+ */
+ isTrustedOrigin(aEvent) {
+ // Make sure that events only come from the toplevel frame. We need to check the
+ // event's target as |content| always represents the current top level window in
+ // the frame (or null). This means that a check like |content.top != content| will
+ // always be false. See nsIMessageManager.idl for more info.
+ if (aEvent &&
+ (!("ownerGlobal" in aEvent.target) ||
+ aEvent.target.ownerGlobal != content)) {
+ return false;
+ }
+
+ const principal =
+ aEvent ? aEvent.target.ownerGlobal.document.nodePrincipal : content.document.nodePrincipal;
+ if (principal.isSystemPrincipal) {
+ return true;
+ }
+
+ const allowedSchemes = ["https", "about"];
+ if (!allowedSchemes.includes(principal.URI.scheme)) {
+ return false;
+ }
+
+ // If this is not a system principal, it needs to have the
+ // HCT_PERMISSION to use this API.
+ let permission =
+ Services.perms.testPermissionFromPrincipal(principal, HCT_PERMISSION);
+ if (permission == Services.perms.ALLOW_ACTION) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Handles incoming events from the content library.
+ * This verifies that:
+ *
+ * - the hybrid content telemetry is on;
+ * - the page is trusted and allowed to use it;
+ * - the CustomEvent coming from the "content" has the expected format and
+ * is for a known API end-point.
+ *
+ * If the page is allowed to use the API and the event validates, the call
+ * is passed on to Telemetry.
+ *
+ * @param {nsIDomEvent} event An object containing the data for the event coming from
+ * the content. The "detail" section of this event holds the
+ * hybrid content specific payload. It looks like:
+ * {name: "apiEndpoint", data: { ... endpoint specific data ... }}
+ */
+ handleEvent(event) {
+ if (!this._hybridContentEnabled) {
+ this._log.trace("handleEvent - hybrid content telemetry is disabled.");
+ return;
+ }
+
+ if (!this.isTrustedOrigin(event)) {
+ this._log.warn("handleEvent - accessing telemetry from an untrusted origin.");
+ return;
+ }
+
+ if (!event ||
+ !("detail" in event) ||
+ !("name" in event.detail) ||
+ !("data" in event.detail) ||
+ typeof event.detail.name != "string" ||
+ typeof event.detail.data != "object") {
+ this._log.error("handleEvent - received a malformed message.");
+ return;
+ }
+
+ // We add the message listener here to guarantee it only gets added
+ // for trusted origins.
+ // Note that the name of the message must match the name of the one
+ // in HybridContentTelemetry.jsm.
+ if (!this._hasListener) {
+ addMessageListener(HCT_POLICY_CHANGE_MSG, this);
+ this._hasListener = true;
+ }
+
+ // Note that the name of the async message must match the name of
+ // the message in the related listener in nsBrowserGlue.js.
+ sendAsyncMessage("HybridContentTelemetry:onTelemetryMessage", {
+ name: event.detail.name,
+ data: event.detail.data,
+ });
+ },
+
+ /**
+ * Handles the HCT_POLICY_CHANGE_MSG message coming from chrome and
+ * passes it back to the content.
+ * @param {Object} aMessage The incoming message. See nsIMessageListener docs.
+ */
+ receiveMessage(aMessage) {
+ if (!this.isTrustedOrigin()) {
+ this._log.warn("receiveMessage - accessing telemetry from an untrusted origin.");
+ return;
+ }
+
+ if (aMessage.name != HCT_POLICY_CHANGE_MSG ||
+ !("data" in aMessage) ||
+ !("canUpload" in aMessage.data) ||
+ typeof aMessage.data.canUpload != "boolean") {
+ this._log.warn("receiveMessage - received an unexpected message.");
+ return;
+ }
+
+ // Finally send the message down to the page.
+ content.document.dispatchEvent(
+ new content.document.defaultView.CustomEvent("mozTelemetryPolicyChange", {
+ bubbles: true,
+ detail: {canUpload: aMessage.data.canUpload}
+ })
+ );
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(HybridContentTelemetryListener, "_hybridContentEnabled",
+ TelemetryUtils.Preferences.HybridContentEnabled,
+ false /* aDefaultValue */);
+
+// The following function installs the handler for "mozTelemetry"
+// events in the chrome. Please note that the name of the message (i.e.
+// "mozTelemetry") needs to match the one in HybridContentTelemetry-lib.js.
+addEventListener("mozTelemetry", HybridContentTelemetryListener, false, true);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/hybrid-content/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+toolkit.jar:
+ content/global/content-HybridContentTelemetry.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/hybrid-content/moz.build
@@ -0,0 +1,9 @@
+# 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/.
+
+EXTRA_JS_MODULES += [
+ 'HybridContentTelemetry.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -6,16 +6,17 @@
HAS_MISC_RULE = True
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
DIRS = [
+ 'hybrid-content',
'pingsender',
]
DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
LOCAL_INCLUDES += [
'/xpcom/build',
'/xpcom/threads',
--- a/toolkit/components/telemetry/tests/browser/browser.ini
+++ b/toolkit/components/telemetry/tests/browser/browser.ini
@@ -1,8 +1,12 @@
# 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/.
[browser_TelemetryGC.js]
[browser_UpdatePingSuccess.js]
[browser_DynamicScalars.js]
skip-if = !e10s # e10s specific test for definition broadcasting across processes.
+[browser_HybridContentTelemetry.js]
+support-files =
+ ../../hybrid-content/HybridContentTelemetry-lib.js
+ hybrid_content.html
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser_HybridContentTelemetry.js
@@ -0,0 +1,370 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+"use strict";
+
+const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+const { TelemetryUtils } = Cu.import("resource://gre/modules/TelemetryUtils.jsm", {});
+const { ObjectUtils } = Cu.import("resource://gre/modules/ObjectUtils.jsm", {});
+
+const HC_PERMISSION = "hc_telemetry";
+
+async function waitForProcessesEvents(aProcesses,
+ aAdditionalCondition = data => true) {
+ await ContentTaskUtils.waitForCondition(() => {
+ const events =
+ Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ return aProcesses.every(p => Object.keys(events).includes(p))
+ && aAdditionalCondition(events);
+ });
+}
+
+/**
+ * Wait for a specific event to appear in the given process data.
+ * @param {String} aProcess the name of the process we expect the event to appear.
+ * @param {Array} aEventData the event data to look for.
+ * @return {Promise} Resolved when the event is found or rejected if the search
+ * times out.
+ */
+async function waitForEvent(aProcess, aEventData) {
+ await waitForProcessesEvents([aProcess], events => {
+ let processEvents = events[aProcess].map(e => e.slice(1));
+ if (processEvents.length == 0) {
+ return false;
+ }
+
+ return processEvents.find(e => ObjectUtils.deepEqual(e, aEventData));
+ });
+}
+
+/**
+ * Remove the trailing null/undefined from an event definition.
+ * This is useful for comparing the sample events (that might
+ * contain null/undefined) to the data from the snapshot (which might
+ * filter them).
+ */
+function removeTrailingInvalidEntry(aEvent) {
+ while (aEvent[aEvent.length - 1] === undefined ||
+ aEvent[aEvent.length - 1] === null) {
+ aEvent.pop();
+ }
+ return aEvent;
+}
+
+add_task(async function test_setup() {
+ // Make sure the newly spawned content processes will have extended Telemetry and
+ // hybrid content telemetry enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TelemetryUtils.Preferences.OverridePreRelease, true],
+ [TelemetryUtils.Preferences.HybridContentEnabled, true],
+ [TelemetryUtils.Preferences.LogLevel, "Trace"]
+ ]
+ });
+ // And take care of the already initialized one as well.
+ let canRecordExtended = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
+});
+
+add_task(async function test_untrusted_http_origin() {
+ Services.telemetry.clearEvents();
+
+ // Install a custom handler that intercepts hybrid content telemetry messages
+ // and makes the test fail. We don't expect any message from non secure contexts.
+ const messageName = "HybridContentTelemetry:onTelemetryMessage";
+ let makeTestFail = () => ok(false, `Received an unexpected ${messageName}.`);
+ Services.mm.addMessageListener(messageName, makeTestFail);
+
+ // Try to use the API on a non-secure host.
+ const testHost = "http://example.org";
+ let testHttpUri = Services.io.newURI(testHost);
+ Services.perms.add(testHttpUri, HC_PERMISSION, Services.perms.ALLOW_ACTION);
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ url = url.replace("chrome://mochitests/content", testHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Try to use the API. Also record a content event from outside HCT: we'll
+ // use this event to know when we can stop waiting for the hybrid content data.
+ const TEST_CONTENT_EVENT = ["telemetry.test", "main_and_content", "object1"];
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", true);
+ await ContentTask.spawn(newTab.linkedBrowser, [TEST_CONTENT_EVENT],
+ ([testContentEvent]) => {
+ // Call the hybrid content telemetry API.
+ let contentWin = Components.utils.waiveXrays(content);
+ contentWin.testRegisterEvents(testContentEvent[0], JSON.stringify({}));
+ // Record from the usual telemetry API a "canary" event.
+ Services.telemetry.recordEvent(...testContentEvent);
+ });
+
+ // Let's support both e10s/non-e10s testing.
+ const processName = Services.appinfo.browserTabsRemoteAutostart ? "content" : "parent";
+ await waitForEvent(processName, TEST_CONTENT_EVENT);
+
+ // This is needed otherwise the test will fail due to missing test passes.
+ ok(true, "The untrusted HTTP page was not able to use the API.");
+
+ // Finally clean up the listener.
+ await BrowserTestUtils.removeTab(newTab);
+ Services.perms.remove(testHttpUri, HC_PERMISSION);
+ Services.mm.removeMessageListener(messageName, makeTestFail);
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", false);
+});
+
+add_task(async function test_secure_non_whitelisted_origin() {
+ Services.telemetry.clearEvents();
+
+ // Install a custom handler that intercepts hybrid content telemetry messages
+ // and makes the test fail. We don't expect any message from non whitelisted pages.
+ const messageName = "HybridContentTelemetry:onTelemetryMessage";
+ let makeTestFail = () => ok(false, `Received an unexpected ${messageName}.`);
+ Services.mm.addMessageListener(messageName, makeTestFail);
+
+ // Try to use the API on a secure host but don't give the page enough privileges.
+ const testHost = "https://example.org";
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ url = url.replace("chrome://mochitests/content", testHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Try to use the API. Also record a content event from outside HCT: we'll
+ // use this event to know when we can stop waiting for the hybrid content data.
+ const TEST_CONTENT_EVENT = ["telemetry.test", "main_and_content", "object1"];
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", true);
+ await ContentTask.spawn(newTab.linkedBrowser, [TEST_CONTENT_EVENT],
+ ([testContentEvent]) => {
+ // Call the hybrid content telemetry API.
+ let contentWin = Components.utils.waiveXrays(content);
+ contentWin.testRegisterEvents(testContentEvent[0], JSON.stringify({}));
+ // Record from the usual telemetry API a "canary" event.
+ Services.telemetry.recordEvent(...testContentEvent);
+ });
+
+ // Let's support both e10s/non-e10s testing.
+ const processName = Services.appinfo.browserTabsRemoteAutostart ? "content" : "parent";
+ await waitForEvent(processName, TEST_CONTENT_EVENT);
+
+ // This is needed otherwise the test will fail due to missing test passes.
+ ok(true, "The HTTPS page without permission was not able to use the API.");
+
+ // Finally clean up the listener.
+ await BrowserTestUtils.removeTab(newTab);
+ Services.mm.removeMessageListener(messageName, makeTestFail);
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", false);
+});
+
+add_task(async function test_trusted_disabled_hybrid_telemetry() {
+ Services.telemetry.clearEvents();
+
+ // This test requires hybrid content telemetry to be disabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [[TelemetryUtils.Preferences.HybridContentEnabled, false]]
+ });
+
+ // Install a custom handler that intercepts hybrid content telemetry messages
+ // and makes the test fail. We don't expect any message when the API is disabled.
+ const messageName = "HybridContentTelemetry:onTelemetryMessage";
+ let makeTestFail = () => ok(false, `Received an unexpected ${messageName}.`);
+ Services.mm.addMessageListener(messageName, makeTestFail);
+
+ // Try to use the API on a secure host.
+ const testHost = "https://example.org";
+ let testHttpsUri = Services.io.newURI(testHost);
+ Services.perms.add(testHttpsUri, HC_PERMISSION, Services.perms.ALLOW_ACTION);
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ url = url.replace("chrome://mochitests/content", testHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Try to use the API. Also record a content event from outside HCT: we'll
+ // use this event to know when we can stop waiting for the hybrid content data.
+ const TEST_CONTENT_EVENT = ["telemetry.test", "main_and_content", "object1"];
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", true);
+ await ContentTask.spawn(newTab.linkedBrowser, [TEST_CONTENT_EVENT],
+ ([testContentEvent]) => {
+ // Call the hybrid content telemetry API.
+ let contentWin = Components.utils.waiveXrays(content);
+ contentWin.testRegisterEvents(testContentEvent[0], JSON.stringify({}));
+ // Record from the usual telemetry API a "canary" event.
+ Services.telemetry.recordEvent(...testContentEvent);
+ });
+
+ // Let's support both e10s/non-e10s testing.
+ const processName = Services.appinfo.browserTabsRemoteAutostart ? "content" : "parent";
+ await waitForEvent(processName, TEST_CONTENT_EVENT);
+
+ // This is needed otherwise the test will fail due to missing test passes.
+ ok(true, "There were no unintended hybrid content API usages.");
+
+ // Finally clean up the listener.
+ await SpecialPowers.popPrefEnv();
+ await BrowserTestUtils.removeTab(newTab);
+ Services.perms.remove(testHttpsUri, HC_PERMISSION);
+ Services.mm.removeMessageListener(messageName, makeTestFail);
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", false);
+});
+
+add_task(async function test_hybrid_content_with_iframe() {
+ Services.telemetry.clearEvents();
+
+ // Open a trusted page that can use in the HCT in a new tab.
+ const testOuterPageHost = "https://example.com";
+ let testHttpsUri = Services.io.newURI(testOuterPageHost);
+ Services.perms.add(testHttpsUri, HC_PERMISSION, Services.perms.ALLOW_ACTION);
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ let outerUrl = url.replace("chrome://mochitests/content", testOuterPageHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, outerUrl);
+
+ // Install a custom handler that intercepts hybrid content telemetry messages
+ // and makes the test fail. This needs to be done after the tab is opened.
+ const messageName = "HybridContentTelemetry:onTelemetryMessage";
+ let makeTestFail = () => ok(false, `Received an unexpected ${messageName}.`);
+ Services.mm.addMessageListener(messageName, makeTestFail);
+
+ // Enable recording the canary event.
+ const TEST_CONTENT_EVENT = ["telemetry.test", "main_and_content", "object1"];
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ // Add an iframe to the test page. The URI in the iframe should not be able
+ // to use HCT to the the missing privileges.
+ const testHost = "https://example.org";
+ let iframeUrl = url.replace("chrome://mochitests/content", testHost);
+ await ContentTask.spawn(newTab.linkedBrowser,
+ [iframeUrl, TEST_CONTENT_EVENT],
+ async function([iframeUrl, testContentEvent]) {
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+ let promiseIframeLoaded = ContentTaskUtils.waitForEvent(iframe, "load", false);
+ iframe.src = iframeUrl;
+ doc.body.insertBefore(iframe, doc.body.firstChild);
+ await promiseIframeLoaded;
+
+ // Call the hybrid content telemetry API.
+ let contentWin = Components.utils.waiveXrays(iframe.contentWindow);
+ contentWin.testRegisterEvents(testContentEvent[0], JSON.stringify({}));
+
+ // Record from the usual telemetry API a "canary" event.
+ Services.telemetry.recordEvent(...testContentEvent);
+ });
+
+ // Let's support both e10s/non-e10s testing.
+ const processName = Services.appinfo.browserTabsRemoteAutostart ? "content" : "parent";
+ await waitForEvent(processName, TEST_CONTENT_EVENT);
+
+ // This is needed otherwise the test will fail due to missing test passes.
+ ok(true, "There were no unintended hybrid content API usages from the iframe.");
+
+ // Cleanup permissions and remove the tab.
+ await BrowserTestUtils.removeTab(newTab);
+ Services.mm.removeMessageListener(messageName, makeTestFail);
+ Services.perms.remove(testHttpsUri, HC_PERMISSION);
+ Services.telemetry.setEventRecordingEnabled("telemetry.test", false);
+});
+
+add_task(async function test_hybrid_content_recording() {
+ const testHost = "https://example.org";
+ const TEST_EVENT_CATEGORY = "telemetry.test.hct";
+ const RECORDED_TEST_EVENTS = [
+ [TEST_EVENT_CATEGORY, "test1", "object1"],
+ [TEST_EVENT_CATEGORY, "test2", "object1", null, {"key1": "foo", "key2": "bar"}],
+ [TEST_EVENT_CATEGORY, "test2", "object1", "some value"],
+ [TEST_EVENT_CATEGORY, "test1", "object1", null, null],
+ [TEST_EVENT_CATEGORY, "test1", "object1", "", null],
+ ];
+ const NON_RECORDED_TEST_EVENTS = [
+ [TEST_EVENT_CATEGORY, "unknown", "unknown"],
+ ];
+
+ Services.telemetry.clearEvents();
+
+ // Give the test host enough privileges to use the API and open the test page.
+ let testHttpsUri = Services.io.newURI(testHost);
+ Services.perms.add(testHttpsUri, HC_PERMISSION, Services.perms.ALLOW_ACTION);
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ url = url.replace("chrome://mochitests/content", testHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Register some events and record them in Telemetry.
+ await ContentTask.spawn(newTab.linkedBrowser,
+ [TEST_EVENT_CATEGORY, RECORDED_TEST_EVENTS, NON_RECORDED_TEST_EVENTS],
+ ([eventCategory, recordedTestEvents, nonRecordedTestEvents]) => {
+ let contentWin = Components.utils.waiveXrays(content);
+
+ // If we tried to call contentWin.Mozilla.ContentTelemetry.* functions
+ // and pass non-string parameters, |waiveXrays| would complain and not
+ // let us access them. To work around this, we generate test functions
+ // in the test HTML file and unwrap the passed JSON blob there.
+ contentWin.testRegisterEvents(eventCategory, JSON.stringify({
+ // Event with only required fields.
+ "test1": {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ "test2": {
+ methods: ["test2", "test2b"],
+ objects: ["object1"],
+ extra_keys: ["key1", "key2"],
+ },
+ }));
+
+ // Record some valid events.
+ recordedTestEvents.forEach(e => contentWin.testRecordEvents(JSON.stringify(e)));
+
+ // Test recording an unknown event. The Telemetry API itself is supposed to throw,
+ // but we catch that in hybrid content telemetry and log an error message.
+ nonRecordedTestEvents.forEach(e => contentWin.testRecordEvents(JSON.stringify(e)));
+ });
+
+ // Wait for the data to be in the snapshot, then get the Telemetry data.
+ await waitForProcessesEvents(["dynamic"]);
+ let snapshot =
+ Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ // Check that the dynamically register events made it to the snapshot.
+ ok("dynamic" in snapshot,
+ "The snapshot must contain the 'dynamic' process section");
+ let dynamicEvents = snapshot.dynamic.map(e => e.slice(1));
+ is(dynamicEvents.length, RECORDED_TEST_EVENTS.length, "Should match expected event count.");
+ for (let i = 0; i < RECORDED_TEST_EVENTS.length; ++i) {
+ SimpleTest.isDeeply(dynamicEvents[i],
+ removeTrailingInvalidEntry(RECORDED_TEST_EVENTS[i]),
+ "Should have recorded the expected event.");
+ }
+
+ // Cleanup permissions and remove the tab.
+ await BrowserTestUtils.removeTab(newTab);
+ Services.perms.remove(testHttpsUri, HC_PERMISSION);
+});
+
+add_task(async function test_can_upload() {
+ const testHost = "https://example.org";
+
+ await SpecialPowers.pushPrefEnv({set: [[TelemetryUtils.Preferences.FhrUploadEnabled, false]]});
+
+ // Give the test host enough privileges to use the API and open the test page.
+ let testHttpsUri = Services.io.newURI(testHost);
+ Services.perms.add(testHttpsUri, HC_PERMISSION, Services.perms.ALLOW_ACTION);
+ let url = getRootDirectory(gTestPath) + "hybrid_content.html";
+ url = url.replace("chrome://mochitests/content", testHost);
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Check that CanUpload reports the correct value.
+ await ContentTask.spawn(newTab.linkedBrowser, {}, () => {
+ let contentWin = Components.utils.waiveXrays(content);
+ // We don't need to pass any parameter, we can safely call Mozilla.ContentTelemetry.
+ let canUpload = contentWin.Mozilla.ContentTelemetry.canUpload();
+ ok(!canUpload, "CanUpload must report 'false' if the preference has that value.");
+ });
+
+ // Flip the pref and check again.
+ await SpecialPowers.pushPrefEnv({set: [[TelemetryUtils.Preferences.FhrUploadEnabled, true]]});
+ await ContentTask.spawn(newTab.linkedBrowser, {}, () => {
+ let contentWin = Components.utils.waiveXrays(content);
+ let canUpload = contentWin.Mozilla.ContentTelemetry.canUpload();
+ ok(canUpload, "CanUpload must report 'true' if the preference has that value.");
+ });
+
+ // Cleanup permissions and remove the tab.
+ await BrowserTestUtils.removeTab(newTab);
+ Services.perms.remove(testHttpsUri, HC_PERMISSION);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/hybrid_content.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Hybrid content telemetry test</title>
+ <script type="application/javascript" src="HybridContentTelemetry-lib.js">
+ </script>
+ <script type="application/javascript">
+ /**
+ * The following functions are simply wrapping API calls
+ * to make sure we don't have waiveXRays complaining about
+ * "same-origin" problems in tests.
+ */
+ function testRegisterEvents(category, dataAsString) {
+ // eslint-disable-next-line no-undef
+ Mozilla.ContentTelemetry.registerEvents(category, JSON.parse(dataAsString));
+ }
+
+ function testRecordEvents(paramsAsString) {
+ // eslint-disable-next-line no-undef
+ Mozilla.ContentTelemetry.recordEvent(...JSON.parse(paramsAsString));
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Hybrid Content Telemetry tests</h1>
+ <p>Because Firefox is...</p>
+ <p>Never gonna let you down</p>
+ <p>Never gonna give you up</p>
+ </body>
+</html>