diff --git a/.eslintignore b/.eslintignore
index 0e772fdd6d97..57ea5a029689 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -275,6 +275,7 @@ mobile/android/tests/browser/chrome/tp5/**
# Uses `#filter substitution`
mobile/android/app/mobile.js
+mobile/android/chrome/content/healthreport-prefs.js
# Uses `#expand`
mobile/android/chrome/content/about.js
diff --git a/browser/base/content/abouthealthreport/abouthealth.css b/browser/base/content/abouthealthreport/abouthealth.css
new file mode 100644
index 000000000000..3dd40fc24312
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.css
@@ -0,0 +1,15 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+#remote-report {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: flex;
+}
diff --git a/browser/base/content/abouthealthreport/abouthealth.js b/browser/base/content/abouthealthreport/abouthealth.js
new file mode 100644
index 000000000000..d5c09a691243
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.js
@@ -0,0 +1,172 @@
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_REPORTING_URL = "datareporting.healthreport.about.reportUrl";
+
+var healthReportWrapper = {
+ init() {
+ let iframe = document.getElementById("remote-report");
+ iframe.addEventListener("load", healthReportWrapper.initRemotePage);
+ iframe.src = this._getReportURI().spec;
+ XPCOMUtils.defineLazyPreferenceGetter(this, /* unused */ "_isUploadEnabled",
+ "datareporting.healthreport.uploadEnabled",
+ false, () => this.updatePrefState());
+ },
+
+ _getReportURI() {
+ let url = Services.urlFormatter.formatURLPref(PREF_REPORTING_URL);
+ return Services.io.newURI(url);
+ },
+
+ setDataSubmission(enabled) {
+ MozSelfSupport.healthReportDataSubmissionEnabled = enabled;
+ this.updatePrefState();
+ },
+
+ updatePrefState() {
+ try {
+ let prefsObj = {
+ enabled: MozSelfSupport.healthReportDataSubmissionEnabled,
+ };
+ healthReportWrapper.injectData("prefs", prefsObj);
+ } catch (ex) {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PREFS_FAILED);
+ }
+ },
+
+ sendTelemetryPingList() {
+ console.log("AboutHealthReport: Collecting Telemetry ping list.");
+ MozSelfSupport.getTelemetryPingList().then((list) => {
+ console.log("AboutHealthReport: Sending Telemetry ping list.");
+ this.injectData("telemetry-ping-list", list);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting ping list failed: " + ex);
+ });
+ },
+
+ sendTelemetryPingData(pingId) {
+ console.log("AboutHealthReport: Collecting Telemetry ping data.");
+ MozSelfSupport.getTelemetryPing(pingId).then((ping) => {
+ console.log("AboutHealthReport: Sending Telemetry ping data.");
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ pingData: ping,
+ });
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Loading ping data failed: " + ex);
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ error: "error-generic",
+ });
+ });
+ },
+
+ sendCurrentEnvironment() {
+ console.log("AboutHealthReport: Sending Telemetry environment data.");
+ MozSelfSupport.getCurrentTelemetryEnvironment().then((environment) => {
+ this.injectData("telemetry-current-environment-data", environment);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current environment data failed: " + ex);
+ });
+ },
+
+ sendCurrentPingData() {
+ console.log("AboutHealthReport: Sending current Telemetry ping data.");
+ MozSelfSupport.getCurrentTelemetrySubsessionPing().then((ping) => {
+ this.injectData("telemetry-current-ping-data", ping);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current ping data failed: " + ex);
+ });
+ },
+
+ injectData(type, content) {
+ let report = this._getReportURI();
+
+ // file URIs can't be used for targetOrigin, so we use "*" for this special case
+ // in all other cases, pass in the URL to the report so we properly restrict the message dispatch
+ let reportUrl = report.scheme == "file" ? "*" : report.spec;
+
+ let data = {
+ type,
+ content
+ };
+
+ let iframe = document.getElementById("remote-report");
+ iframe.contentWindow.postMessage(data, reportUrl);
+ },
+
+ handleRemoteCommand(evt) {
+ // Do an origin check to harden against the frame content being loaded from unexpected locations.
+ let allowedPrincipal = Services.scriptSecurityManager.createCodebasePrincipal(this._getReportURI(), {});
+ let targetPrincipal = evt.target.nodePrincipal;
+ if (!allowedPrincipal.equals(targetPrincipal)) {
+ Cu.reportError(`Origin check failed for message "${evt.detail.command}": ` +
+ `target origin is "${targetPrincipal.origin}", expected "${allowedPrincipal.origin}"`);
+ return;
+ }
+
+ switch (evt.detail.command) {
+ case "DisableDataSubmission":
+ this.setDataSubmission(false);
+ break;
+ case "EnableDataSubmission":
+ this.setDataSubmission(true);
+ break;
+ case "RequestCurrentPrefs":
+ this.updatePrefState();
+ break;
+ case "RequestTelemetryPingList":
+ this.sendTelemetryPingList();
+ break;
+ case "RequestTelemetryPingData":
+ this.sendTelemetryPingData(evt.detail.id);
+ break;
+ case "RequestCurrentEnvironment":
+ this.sendCurrentEnvironment();
+ break;
+ case "RequestCurrentPingData":
+ this.sendCurrentPingData();
+ break;
+ default:
+ Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+ break;
+ }
+ },
+
+ initRemotePage() {
+ let iframe = document.getElementById("remote-report").contentDocument;
+ iframe.addEventListener("RemoteHealthReportCommand",
+ function onCommand(e) { healthReportWrapper.handleRemoteCommand(e); });
+ healthReportWrapper.updatePrefState();
+ },
+
+ // error handling
+ ERROR_INIT_FAILED: 1,
+ ERROR_PAYLOAD_FAILED: 2,
+ ERROR_PREFS_FAILED: 3,
+
+ reportFailure(error) {
+ let details = {
+ errorType: error,
+ };
+ healthReportWrapper.injectData("error", details);
+ },
+
+ handleInitFailure() {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
+ },
+
+ handlePayloadFailure() {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
+ },
+};
+
+window.addEventListener("load", function() { healthReportWrapper.init(); });
diff --git a/browser/base/content/abouthealthreport/abouthealth.xhtml b/browser/base/content/abouthealthreport/abouthealth.xhtml
new file mode 100644
index 000000000000..ff57bd15d7e0
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.xhtml
@@ -0,0 +1,31 @@
+
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %securityPrefsDTD;
+
+ %aboutHealthReportDTD;
+]>
+
+
+
+ &abouthealth.pagetitle;
+
+
+
+
+
+
+
+
+
diff --git a/browser/base/content/baseMenuOverlay.xul b/browser/base/content/baseMenuOverlay.xul
index 98c9a60af93f..804bbdebb797 100644
--- a/browser/base/content/baseMenuOverlay.xul
+++ b/browser/base/content/baseMenuOverlay.xul
@@ -61,6 +61,13 @@
onclick="checkForMiddleClick(this, event);"
label="&helpKeyboardShortcuts.label;"
accesskey="&helpKeyboardShortcuts.accesskey;"/>
+#ifdef MOZ_SERVICES_HEALTHREPORT
+
+#endif